mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0
This commit is contained in:
@@ -55,7 +55,6 @@ Those marked with an asterisk are part of IfcOpenShell.
|
||||
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
|
||||
| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later |
|
||||
| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later |
|
||||
| ifccobie | Extract IFC data for COBie handover requirements | LGPL-3.0-or-later |
|
||||
| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* |
|
||||
| ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later |
|
||||
| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later |
|
||||
|
||||
+67
-80
@@ -36,35 +36,49 @@ endif()
|
||||
set(EXTRA_VERSION "-alpha.3")
|
||||
|
||||
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
|
||||
option(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
|
||||
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
|
||||
option(WASM_BUILD "Build a WebAssembly binary." OFF)
|
||||
|
||||
option(ENABLE_BUILD_OPTIMIZATIONS "Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds." OFF)
|
||||
option(BUILD_PACKAGE "" OFF)
|
||||
option(BUILD_IFCGEOM "Build IfcGeom." ON)
|
||||
option(BUILD_DOCUMENTATION "Build IfcOpenShell Documentation." OFF)
|
||||
option(BUILD_IFCPYTHON "Build IfcPython." ON)
|
||||
option(BUILD_EXAMPLES "Build example applications." ON)
|
||||
option(BUILD_GEOMSERVER "Build IfcGeomServer executable." ON)
|
||||
option(BUILD_CONVERT "Build IfcConvert executable." ON)
|
||||
option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
|
||||
option(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only." OFF)
|
||||
option(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
|
||||
option(BUILD_SHARED_LIBS "Build IfcParse and IfcGeom as shared libs (SO/DLL)." OFF)
|
||||
option(MSVC_PARALLEL_BUILD "Multi-threaded compilation in Microsoft Visual Studio (/MP)" OFF)
|
||||
|
||||
option(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only." OFF)
|
||||
option(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
|
||||
option(NO_WARN "Disable all warnings" OFF)
|
||||
option(WASM_BUILD OFF)
|
||||
|
||||
if(${BUILD_CONVERT})
|
||||
option(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF)
|
||||
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
|
||||
endif()
|
||||
option(BUILD_IFCGEOM "Build IfcGeom." ON)
|
||||
option(BUILD_IFCPYTHON "Build IfcPython." ON)
|
||||
option(BUILD_CONVERT "Build IfcConvert executable." ON)
|
||||
option(BUILD_DOCUMENTATION "Build IfcOpenShell Documentation." OFF)
|
||||
option(BUILD_EXAMPLES "Build example applications." ON)
|
||||
option(BUILD_GEOMSERVER "Build IfcGeomServer executable." ON)
|
||||
option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
|
||||
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6
|
||||
option(BUILD_PACKAGE "" OFF)
|
||||
|
||||
option(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
|
||||
option(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF)
|
||||
option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
|
||||
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
|
||||
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
|
||||
|
||||
option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
|
||||
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, warning results in many rebuilds, requires git" OFF)
|
||||
option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
|
||||
# QtViewer requires Qt6
|
||||
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF)
|
||||
|
||||
if(MINIMAL_BUILD)
|
||||
message(STATUS "Setting options for minimal build")
|
||||
set(BUILD_GEOMSERVER OFF)
|
||||
set(BUILD_IFCPYTHON OFF)
|
||||
set(COLLADA_SUPPORT OFF)
|
||||
set(GLTF_SUPPORT OFF)
|
||||
set(HDF5_SUPPORT OFF)
|
||||
set(IFCXML_SUPPORT OFF)
|
||||
set(USD_SUPPORT OFF)
|
||||
endif()
|
||||
|
||||
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM))
|
||||
message(STATUS "'IfcGeom' is required with current outputs")
|
||||
set(BUILD_IFCGEOM ON)
|
||||
endif()
|
||||
|
||||
if(MSVC AND MSVC_PARALLEL_BUILD)
|
||||
add_definitions("/MP")
|
||||
@@ -80,50 +94,37 @@ endif()
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM))
|
||||
message(STATUS "'IfcGeom' is required with current outputs")
|
||||
set(BUILD_IFCGEOM ON)
|
||||
endif()
|
||||
|
||||
# Specify where to install files
|
||||
# Specify paths to install files
|
||||
if(NOT BINDIR)
|
||||
set(BINDIR bin)
|
||||
endif()
|
||||
|
||||
if(NOT IS_ABSOLUTE ${BINDIR})
|
||||
set(BINDIR ${CMAKE_INSTALL_BINDIR})
|
||||
endif()
|
||||
|
||||
message(STATUS "BINDIR: ${BINDIR}")
|
||||
|
||||
if(NOT INCLUDEDIR)
|
||||
set(INCLUDEDIR include)
|
||||
endif()
|
||||
|
||||
if(NOT IS_ABSOLUTE ${INCLUDEDIR})
|
||||
set(INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
endif()
|
||||
|
||||
message(STATUS "INCLUDEDIR: ${INCLUDEDIR}")
|
||||
|
||||
if(NOT LIBDIR)
|
||||
set(LIBDIR lib)
|
||||
endif()
|
||||
|
||||
if(NOT IS_ABSOLUTE ${LIBDIR})
|
||||
set(LIBDIR ${CMAKE_INSTALL_LIBDIR})
|
||||
endif()
|
||||
|
||||
message(STATUS "LIBDIR: ${LIBDIR}")
|
||||
|
||||
set(IFCOPENSHELL_LIBRARY_DIR "") # for *nix rpaths
|
||||
|
||||
if(BUILD_SHARED_LIBS)
|
||||
add_definitions(-DIFC_SHARED_BUILD)
|
||||
|
||||
if(MSVC)
|
||||
message(WARNING "Building DLLs against the static VC run-time. This is not recommended if the DLLs are to be redistributed.")
|
||||
|
||||
# C4521: 'identifier' : class 'type' needs to have dll-interface to be used by clients of class 'type2'
|
||||
# There will be couple hundreds of these so suppress them away, https://msdn.microsoft.com/en-us/library/esew7y1w.aspx
|
||||
add_definitions(-wd4251)
|
||||
@@ -170,7 +171,7 @@ if(WASM_BUILD)
|
||||
set(CMAKE_FIND_ROOT_PATH "")
|
||||
endif()
|
||||
|
||||
if(NOT MINIMAL_BUILD AND GLTF_SUPPORT AND BUILD_CONVERT)
|
||||
if(GLTF_SUPPORT AND BUILD_CONVERT)
|
||||
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
|
||||
find_file(json_hpp "json.hpp" ${JSON_INCLUDE_DIR}/nlohmann)
|
||||
|
||||
@@ -185,7 +186,7 @@ if(NOT MINIMAL_BUILD AND GLTF_SUPPORT AND BUILD_CONVERT)
|
||||
endif()
|
||||
|
||||
# Add USD support to serializers
|
||||
if(NOT MINIMAL_BUILD AND USD_SUPPORT)
|
||||
if(USD_SUPPORT)
|
||||
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
|
||||
|
||||
@@ -303,7 +304,7 @@ if(NOT MINIMAL_BUILD)
|
||||
find_package(LibXml2 REQUIRED)
|
||||
endif()
|
||||
|
||||
if(NOT MINIMAL_BUILD AND IFCXML_SUPPORT)
|
||||
if(IFCXML_SUPPORT)
|
||||
add_definitions(-DWITH_IFCXML)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_IFCXML)
|
||||
endif()
|
||||
@@ -452,7 +453,7 @@ if(BUILD_IFCGEOM)
|
||||
|
||||
endif(BUILD_IFCGEOM)
|
||||
|
||||
if(NOT MINIMAL_BUILD AND COLLADA_SUPPORT)
|
||||
if(COLLADA_SUPPORT)
|
||||
# Find OpenCOLLADA
|
||||
if("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
|
||||
message(STATUS "No OpenCOLLADA include directory specified")
|
||||
@@ -536,7 +537,7 @@ if(NOT MINIMAL_BUILD AND COLLADA_SUPPORT)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT MINIMAL_BUILD AND HDF5_SUPPORT)
|
||||
if(HDF5_SUPPORT)
|
||||
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 include directory specified")
|
||||
else()
|
||||
@@ -876,20 +877,19 @@ else()
|
||||
target_link_libraries(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
endif()
|
||||
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
# IfcGeom
|
||||
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/*.h ../src/ifcgeom/*.i)
|
||||
file(GLOB IFCGEOM_CPP_FILES ../src/ifcgeom/*.cpp)
|
||||
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES})
|
||||
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
add_library(IfcGeom_ifc${schema} STATIC ${IFCGEOM_FILES})
|
||||
set_target_properties(IfcGeom_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||
if (NOT WASM_BUILD)
|
||||
target_link_libraries(IfcGeom_ifc${schema} IfcParse ${OPENCASCADE_LIBRARIES})
|
||||
endif()
|
||||
endforeach()
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
add_library(IfcGeom_ifc${schema} STATIC ${IFCGEOM_FILES})
|
||||
set_target_properties(IfcGeom_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||
if (NOT WASM_BUILD)
|
||||
target_link_libraries(IfcGeom_ifc${schema} IfcParse ${OPENCASCADE_LIBRARIES})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# IfcGeom (schema agnostic)
|
||||
file(GLOB SCHEMA_AGNOSTIC_H_FILES ../src/ifcgeom_schema_agnostic/*.h)
|
||||
@@ -903,11 +903,11 @@ endforeach()
|
||||
find_package(Threads)
|
||||
endif()
|
||||
|
||||
if (WASM_BUILD)
|
||||
target_link_libraries(IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
target_link_libraries(IfcGeom IfcParse ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
if (WASM_BUILD)
|
||||
target_link_libraries(IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
target_link_libraries(IfcGeom IfcParse ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
|
||||
endif(BUILD_IFCGEOM)
|
||||
|
||||
@@ -920,16 +920,16 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
file(GLOB SERIALIZERS_S_CPP_FILES ../src/serializers/schema_dependent/*.cpp)
|
||||
set(SERIALIZERS_S_FILES ${SERIALIZERS_S_H_FILES} ${SERIALIZERS_S_CPP_FILES})
|
||||
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
add_library(Serializers_ifc${schema} STATIC ${SERIALIZERS_S_FILES})
|
||||
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||
|
||||
if (WASM_BUILD)
|
||||
target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(Serializers_ifc${schema} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
|
||||
endif()
|
||||
endforeach()
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
add_library(Serializers_ifc${schema} STATIC ${SERIALIZERS_S_FILES})
|
||||
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||
|
||||
if (WASM_BUILD)
|
||||
target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(Serializers_ifc${schema} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_library(Serializers ${SERIALIZERS_FILES})
|
||||
set_target_properties(Serializers PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS" VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
@@ -962,7 +962,7 @@ if(BUILD_CONVERT)
|
||||
endif(BUILD_CONVERT)
|
||||
|
||||
# IfcGeomServer
|
||||
if(NOT MINIMAL_BUILD AND BUILD_GEOMSERVER)
|
||||
if(BUILD_GEOMSERVER)
|
||||
file(GLOB CPP_FILES ../src/ifcgeomserver/*.cpp)
|
||||
file(GLOB H_FILES ../src/ifcgeomserver/*.h)
|
||||
set(SOURCE_FILES ${CPP_FILES} ${H_FILES})
|
||||
@@ -1019,7 +1019,7 @@ if(BUILD_DOCUMENTATION)
|
||||
add_subdirectory(../docs docs)
|
||||
endif()
|
||||
|
||||
if(NOT MINIMAL_BUILD AND BUILD_IFCPYTHON)
|
||||
if(BUILD_IFCPYTHON)
|
||||
add_subdirectory(../src/ifcwrap ifcwrap)
|
||||
endif()
|
||||
|
||||
@@ -1027,21 +1027,8 @@ if(BUILD_EXAMPLES)
|
||||
add_subdirectory(../src/examples examples)
|
||||
endif()
|
||||
|
||||
if(NOT MINIMAL_BUILD AND BUILD_IFCMAX)
|
||||
foreach(max_year RANGE 2014 2030)
|
||||
set(max_sdk "$ENV{ADSK_3DSMAX_SDK_${max_year}}")
|
||||
|
||||
if(NOT "${max_sdk}" STREQUAL "")
|
||||
message(STATUS "Autodesk 3ds Max SDK found at ${max_sdk}")
|
||||
set(HAS_MAX TRUE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(HAS_MAX)
|
||||
add_subdirectory(../src/ifcmax ifcmax)
|
||||
else()
|
||||
message(STATUS "Autodesk 3ds Max SDK not found, is required to build IFCMax.")
|
||||
endif()
|
||||
if(BUILD_IFCMAX)
|
||||
add_subdirectory(../src/ifcmax ifcmax)
|
||||
endif()
|
||||
|
||||
if(NOT MINIMAL_BUILD)
|
||||
|
||||
+3
-82
@@ -1,84 +1,5 @@
|
||||
# bcf
|
||||
|
||||
A simple Python implementation of BCF.
|
||||
Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API
|
||||
is available via `bcfapi.py`.
|
||||
|
||||
It tries to support BCF-XML version 2.1 and 3.0, and BCF-API 3.0.
|
||||
|
||||
## bcfxml
|
||||
|
||||
The `bcfxml.load` function lets you read a BCF-XML file.
|
||||
It takes care of using the right version based on the "bcf.version" file contained in the BCF package.
|
||||
|
||||
The BCF files are extracted and parsed on-demand, and edits are stored in memory until you call the `save` method.
|
||||
|
||||
```python
|
||||
from bcf.bcfxml import load
|
||||
|
||||
# Load a project
|
||||
with load("/path/to/file.bcf") as bcfxml:
|
||||
project = bcfxml.project
|
||||
print(project.name)
|
||||
|
||||
# To edit a project, just modify the object directly
|
||||
bcfxml.project.name = "New name"
|
||||
|
||||
# Get a dictionary of topics
|
||||
topics = bcfxml.topics
|
||||
|
||||
for topic_guid, topic_handler in bcfxml.topics.items():
|
||||
topic = topic_handler.topic
|
||||
print("Topic guid is", topic.guid)
|
||||
print("Topic title is", topic.title)
|
||||
|
||||
# Fetch extra data about a topic
|
||||
header = topic_handler.header
|
||||
comments = topic_handler.comments
|
||||
viewpoints = topic_handler.viewpoints
|
||||
|
||||
for comment in comments:
|
||||
print(comment.guid)
|
||||
print(comment.comment)
|
||||
print(comment.author)
|
||||
|
||||
# Get a particular topic
|
||||
topic = bcfxml.get_topic(guid)
|
||||
|
||||
# Modify a topic
|
||||
topic.title = "New title"
|
||||
|
||||
bcfxml.save()
|
||||
```
|
||||
|
||||
## bcfapi
|
||||
|
||||
The `bcfapi` module lets you interact with the BCF-API standard.
|
||||
|
||||
```python
|
||||
from bcf.v3.bcfapi import FoundationClient, BcfClient
|
||||
|
||||
foundation_client = FoundationClient("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET", "OPENCDE_BASEURL")
|
||||
auth_methods = foundation_client.get_auth_methods()
|
||||
|
||||
# Our library currently only implements the authorization_code flow
|
||||
if "authorization_code" in auth_methods:
|
||||
foundation_client.login()
|
||||
|
||||
bcf_client = BcfClient(foundation_client)
|
||||
|
||||
versions = foundation_client.get_versions()
|
||||
for version in versions:
|
||||
if "3.0" in versions:
|
||||
if version["api_id"] == "bcf" and version["version_id"] == "3.0":
|
||||
bcf_client.set_version(version)
|
||||
|
||||
data = bcf_client.get_projects()
|
||||
print(data)
|
||||
project_id = data[0]["project_id"]
|
||||
print(project_id)
|
||||
data = bcf_client.get_project(project_id)
|
||||
print(data)
|
||||
data = bcf_client.get_extensions(project_id)
|
||||
print(data)
|
||||
```
|
||||
A simple Python implementation of the BCF standard. Manipulation of BCF-XML is
|
||||
available via `bcfxml.py` and manipulation of BCF-API is available via
|
||||
`bcfapi.py`.
|
||||
|
||||
+4
-10
@@ -199,8 +199,10 @@ endif
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcbimtester/bimtester dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCTester functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifctester/ifctester dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCCOBie functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifccobie/* dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCFM functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcfm/ifcfm dist/blenderbim/libs/site/packages/
|
||||
# Provides bSDD functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/bsdd/* dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCDiff functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcdiff/* dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCCSV functionality
|
||||
@@ -592,14 +594,6 @@ endif
|
||||
cd dist/working/parse_type-0.5.2/ && cp -r parse_type ../../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by IFCCOBie for XLSX support
|
||||
# TODO: see if we can replace this with openpyxl which does both read/write
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/0c/bc/82d6783f83f65f56d8b77d052773c4a2f952fa86385f0cd54e1e006658d7/XlsxWriter-1.2.9.tar.gz
|
||||
cd dist/working && tar -xzvf XlsxWriter*
|
||||
cd dist/working/XlsxWriter-1.2.9/ && cp -r xlsxwriter ../../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by augin
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/76/b4/b7baffbda025efd5dc8fcd8d2e953e3aa939c236a484084fa8f4c3588ee9/boto3-1.17.17.tar.gz
|
||||
|
||||
@@ -30,6 +30,7 @@ modules = {
|
||||
"project": None,
|
||||
"search": None,
|
||||
"bcf": None,
|
||||
"bsdd": None,
|
||||
"root": None,
|
||||
"unit": None,
|
||||
"model": None,
|
||||
@@ -44,7 +45,7 @@ modules = {
|
||||
"void": None,
|
||||
"aggregate": None,
|
||||
"geometry": None,
|
||||
"cobie": None,
|
||||
"fm": None,
|
||||
"resource": None,
|
||||
"cost": None,
|
||||
"sequence": None,
|
||||
@@ -77,6 +78,7 @@ modules = {
|
||||
"augin": None,
|
||||
"debug": None,
|
||||
"ifcgit": None,
|
||||
"covering": None,
|
||||
# Uncomment this line to enable loading of the demo module. Happy hacking!
|
||||
# The name "demo" must correlate to a folder name in `bim/module/`.
|
||||
# "demo": None,
|
||||
|
||||
@@ -47,6 +47,13 @@
|
||||
<g id="dot">
|
||||
<circle r="1" fill="black" stroke="black" style="stroke-width: 0.25;" />
|
||||
</g>
|
||||
<g id="setout-tag">
|
||||
<circle fill="white" stroke="black" r="2.5" style="stroke-width: 0.5"/>
|
||||
<path d="M 0 0 L -2.5 0 A 2.5 2.5 0 0 1 0 -2.5 Z" fill="black"/>
|
||||
<path d="M 0 0 L 2.5 0 A 2.5 2.5 0 0 1 0 2.5 Z" fill="black"/>
|
||||
<text x="3" y="-8" class="regular" text-anchor="start" dominant-baseline="auto" data-type="text-template"></text>
|
||||
<text x="3" y="-4" class="regular" text-anchor="start" dominant-baseline="auto" data-type="text-template"></text>
|
||||
</g>
|
||||
<g id="setout-point">
|
||||
<circle fill="white" stroke="black" r="2.5" style="stroke-width: 0.5"/>
|
||||
<path d="M 0 0 L -2.5 0 A 2.5 2.5 0 0 1 0 -2.5 Z" fill="black"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -1,17 +1,17 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
|
||||
FILE_NAME('/dev/null','2023-09-02T16:09:02+10:00',(),(),'IfcOpenShell v0.7.0-fbd8ea1ed','IfcOpenShell v0.7.0-fbd8ea1ed','Nobody');
|
||||
FILE_NAME('/dev/null','2023-09-07T13:40:06+10:00',(),(),'IfcOpenShell v0.7.0-fbd8ea1ed','IfcOpenShell v0.7.0-fbd8ea1ed','Nobody');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROJECT('1UlLTVwDzDvR$QNEAxFx6F',$,'BlenderBIM Demo',$,$,$,$,(#12,#16),#7);
|
||||
#2=IFCPROJECTLIBRARY('3QN02XEh1FQh0fii364VEi',$,'BlenderBIM Demo Library',$,$,$,$,$,$);
|
||||
#3=IFCRELDECLARES('0aiEyU5lL7mAdB5xwdXbUN',$,$,$,#1,(#2));
|
||||
#1=IFCPROJECT('1arVsMni92iPUaaryGTUz2',$,'BlenderBIM Demo',$,$,$,$,(#12,#16),#7);
|
||||
#2=IFCPROJECTLIBRARY('1FfDG5b21DuBDOO4eUE9rF',$,'BlenderBIM Demo Library',$,$,$,$,$,$);
|
||||
#3=IFCRELDECLARES('3tfDIC3Tf9wP8EUOoFeycK',$,$,$,#1,(#2));
|
||||
#4=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#5=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#6=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#7=IFCUNITASSIGNMENT((#6,#4,#5));
|
||||
#7=IFCUNITASSIGNMENT((#5,#6,#4));
|
||||
#8=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#9=IFCDIRECTION((0.,0.,1.));
|
||||
#10=IFCDIRECTION((1.,0.,0.));
|
||||
@@ -25,82 +25,82 @@ DATA;
|
||||
#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#16,$,.PLAN_VIEW.,$);
|
||||
#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#16,$,.PLAN_VIEW.,$);
|
||||
#20=IFCMATERIAL('Unknown',$,$);
|
||||
#21=IFCWALLTYPE('0Ir6HewafB2hnPRigxrUnM',$,'WAL50',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#21=IFCWALLTYPE('2o8NqQQbHA5BeErrgwfr7d',$,'WAL50',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#22=IFCMATERIALLAYERSET((#24),$,$);
|
||||
#23=IFCRELASSOCIATESMATERIAL('3S$YbWQCnDCA2tmoikSnrh',$,$,$,(#21),#22);
|
||||
#23=IFCRELASSOCIATESMATERIAL('1Q$5DZKr98n82Eitz2dWey',$,$,$,(#21),#22);
|
||||
#24=IFCMATERIALLAYER(#20,0.05,$,$,$,$,$);
|
||||
#25=IFCRELDECLARES('1eLCJqssP2kvQIdmPlRK19',$,$,$,#2,(#38,#82,#77,#42,#26,#48,#72,#59,#92,#30,#96,#63,#54,#87,#153,#34,#67,#21,#210));
|
||||
#26=IFCWALLTYPE('2oUFJayonDlBTcbJtxYFSv',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#25=IFCRELDECLARES('1KXKUEn8z0g8lvyEsCTWD3',$,$,$,#2,(#38,#82,#77,#42,#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#153,#34,#67,#210));
|
||||
#26=IFCWALLTYPE('3KlIOv_P9A79tkt8D_jq_m',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#27=IFCMATERIALLAYERSET((#29),$,$);
|
||||
#28=IFCRELASSOCIATESMATERIAL('0n3vPKxufDgO48f4yfGtUd',$,$,$,(#26),#27);
|
||||
#28=IFCRELASSOCIATESMATERIAL('32B9Wx8Z1FxhIN7m77MiHG',$,$,$,(#26),#27);
|
||||
#29=IFCMATERIALLAYER(#20,0.1,$,$,$,$,$);
|
||||
#30=IFCWALLTYPE('0UDhWxnIXDMOxZ2yPDCUMP',$,'WAL200',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#30=IFCWALLTYPE('2DqrSeel1AGw6h3b$ujafZ',$,'WAL200',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#31=IFCMATERIALLAYERSET((#33),$,$);
|
||||
#32=IFCRELASSOCIATESMATERIAL('2Boxz3uaP0LOy6luKau2R0',$,$,$,(#30),#31);
|
||||
#32=IFCRELASSOCIATESMATERIAL('2FeYUpt3D4tAvwgGmwXWZn',$,$,$,(#30),#31);
|
||||
#33=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
|
||||
#34=IFCWALLTYPE('20fPutWQrFyeMIiKxc$MkU',$,'WAL300',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#34=IFCWALLTYPE('2jNdEOFIP1eQb3WZePlFGY',$,'WAL300',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#35=IFCMATERIALLAYERSET((#37),$,$);
|
||||
#36=IFCRELASSOCIATESMATERIAL('2qAjlTTxjBkvdp80A9XdMZ',$,$,$,(#34),#35);
|
||||
#36=IFCRELASSOCIATESMATERIAL('2bFeM6xevAwucuwMlauX77',$,$,$,(#34),#35);
|
||||
#37=IFCMATERIALLAYER(#20,0.3,$,$,$,$,$);
|
||||
#38=IFCCOVERINGTYPE('100hdtcxzBdQATomAi0tv5',$,'COV10',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#38=IFCCOVERINGTYPE('3310gCgH59w9tu$DfmiCEN',$,'COV10',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#39=IFCMATERIALLAYERSET((#41),$,$);
|
||||
#40=IFCRELASSOCIATESMATERIAL('3tO1xRn9b06utMa1TZC$WW',$,$,$,(#38),#39);
|
||||
#40=IFCRELASSOCIATESMATERIAL('3pTNmWd_jBoOWyHc8Yf4qI',$,$,$,(#38),#39);
|
||||
#41=IFCMATERIALLAYER(#20,0.01,$,$,$,$,$);
|
||||
#42=IFCCOVERINGTYPE('1sO51oul95ABWezHr7P7Dk',$,'COV20',$,$,(#46),$,$,$,.NOTDEFINED.);
|
||||
#42=IFCCOVERINGTYPE('2sksWFuPzCgeNk1ofXS6PZ',$,'COV20',$,$,(#46),$,$,$,.NOTDEFINED.);
|
||||
#43=IFCMATERIALLAYERSET((#45),$,$);
|
||||
#44=IFCRELASSOCIATESMATERIAL('0w6lexCg19TuiqqTxo91zc',$,$,$,(#42),#43);
|
||||
#44=IFCRELASSOCIATESMATERIAL('36rl5NddT1ux6ejkQAkT2z',$,$,$,(#42),#43);
|
||||
#45=IFCMATERIALLAYER(#20,0.02,$,$,$,$,$);
|
||||
#46=IFCPROPERTYSET('3ytgFxH0571uBCG4mBNXI9',$,'EPset_Parametric',$,(#47));
|
||||
#46=IFCPROPERTYSET('2VZJ9xfdn1NeOY6uEmnnYW',$,'EPset_Parametric',$,(#47));
|
||||
#47=IFCPROPERTYSINGLEVALUE('LayerSetDirection',$,IFCLABEL('AXIS2'),$);
|
||||
#48=IFCCOVERINGTYPE('09GKC1aO56qwEPF9halcxJ',$,'COV30',$,$,(#52),$,$,$,.NOTDEFINED.);
|
||||
#48=IFCCOVERINGTYPE('1uU6rxt95AC91lZ0jTRG6G',$,'COV30',$,$,(#52),$,$,$,.NOTDEFINED.);
|
||||
#49=IFCMATERIALLAYERSET((#51),$,$);
|
||||
#50=IFCRELASSOCIATESMATERIAL('0rDrnpiX58DgQhcURj_WlB',$,$,$,(#48),#49);
|
||||
#50=IFCRELASSOCIATESMATERIAL('2lFv4xQYr5bwtiSsMTVCgz',$,$,$,(#48),#49);
|
||||
#51=IFCMATERIALLAYER(#20,0.03,$,$,$,$,$);
|
||||
#52=IFCPROPERTYSET('14u6OUyPL5y8O$QkEnB6Rr',$,'EPset_Parametric',$,(#53));
|
||||
#52=IFCPROPERTYSET('0HnrRhcd16Yfbgtci4sqCG',$,'EPset_Parametric',$,(#53));
|
||||
#53=IFCPROPERTYSINGLEVALUE('LayerSetDirection',$,IFCLABEL('AXIS3'),$);
|
||||
#54=IFCRAMPTYPE('2G4JrZ99j2B9SkIwWDk0sC',$,'RAM200',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#54=IFCRAMPTYPE('3nonXCVn58jhVV08N8c97B',$,'RAM200',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#55=IFCMATERIALLAYERSET((#57),$,$);
|
||||
#56=IFCRELASSOCIATESMATERIAL('2nNlFzNPj0_h6_NwdVJvW$',$,$,$,(#54),#55);
|
||||
#56=IFCRELASSOCIATESMATERIAL('2NdIrmbUP6bh4x$p_RY31C',$,$,$,(#54),#55);
|
||||
#57=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
|
||||
#58=IFCCIRCLEPROFILEDEF(.AREA.,$,$,0.3);
|
||||
#59=IFCPILETYPE('3uIc5iHjP5Lel3X6KXyN8W',$,'P1',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#59=IFCPILETYPE('2uuf5kq5TDDfO9jGN7XIHh',$,'P1',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#60=IFCMATERIALPROFILESET($,$,(#62),$);
|
||||
#61=IFCRELASSOCIATESMATERIAL('13buLvs1r5vAyhgbabb2g1',$,$,$,(#59),#60);
|
||||
#61=IFCRELASSOCIATESMATERIAL('3hi821Ffj8YR9yFjp226LU',$,$,$,(#59),#60);
|
||||
#62=IFCMATERIALPROFILE($,$,#20,#58,$,$);
|
||||
#63=IFCSLABTYPE('2CbahfxvnFpPoHyMBpGTQm',$,'FLR150',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#63=IFCSLABTYPE('2janmz3nH4P9IHaOVvGY_B',$,'FLR150',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#64=IFCMATERIALLAYERSET((#66),$,$);
|
||||
#65=IFCRELASSOCIATESMATERIAL('2Irb7$9xL8YOXW99sF0SOp',$,$,$,(#63),#64);
|
||||
#65=IFCRELASSOCIATESMATERIAL('0og5Zg4ib67g6XWgOgqBWy',$,$,$,(#63),#64);
|
||||
#66=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
|
||||
#67=IFCSLABTYPE('04dx2Vg4vEp9ZOKGI1ifWb',$,'FLR250',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#67=IFCSLABTYPE('26UKpEaTb9fh4qrqi3Ymzj',$,'FLR250',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#68=IFCMATERIALLAYERSET((#70),$,$);
|
||||
#69=IFCRELASSOCIATESMATERIAL('04tl3Me6T6xOb$WEvv_dL0',$,$,$,(#67),#68);
|
||||
#69=IFCRELASSOCIATESMATERIAL('1BeBXPqen9TQEE42SxzhjF',$,$,$,(#67),#68);
|
||||
#70=IFCMATERIALLAYER(#20,0.3,$,$,$,$,$);
|
||||
#71=IFCRECTANGLEPROFILEDEF(.AREA.,'500x600',$,0.5,0.6);
|
||||
#72=IFCCOLUMNTYPE('13rk_6wmDApeVQWfrArKbl',$,'C1',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#72=IFCCOLUMNTYPE('1KnWzBBoLCNfJ3_J79LT7d',$,'C1',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#73=IFCMATERIALPROFILESET($,$,(#75),$);
|
||||
#74=IFCRELASSOCIATESMATERIAL('0X3OUScT9E1BjOvJWiUrv3',$,$,$,(#72),#73);
|
||||
#74=IFCRELASSOCIATESMATERIAL('1W8OWQjEL05gH5hEpve6Lh',$,$,$,(#72),#73);
|
||||
#75=IFCMATERIALPROFILE($,$,#20,#71,$,$);
|
||||
#76=IFCCIRCLEHOLLOWPROFILEDEF(.AREA.,'500.0x5.0 CHS',$,0.25,0.005);
|
||||
#77=IFCCOLUMNTYPE('0IaS5T1Uv6DQu$BJ5I3bks',$,'C2',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#77=IFCCOLUMNTYPE('33jPFERfL9bfTeWfTL5kZ7',$,'C2',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#78=IFCMATERIALPROFILESET($,$,(#80),$);
|
||||
#79=IFCRELASSOCIATESMATERIAL('1HWcDe7a17WwPeKCHnckid',$,$,$,(#77),#78);
|
||||
#79=IFCRELASSOCIATESMATERIAL('3czOrQw2v9BfS0PeQCrzWq',$,$,$,(#77),#78);
|
||||
#80=IFCMATERIALPROFILE($,$,#20,#76,$,$);
|
||||
#81=IFCRECTANGLEHOLLOWPROFILEDEF(.AREA.,'150x75x2.0 RHS',$,0.075,0.15,0.002,0.005,0.005);
|
||||
#82=IFCCOLUMNTYPE('13p6$J2Jv84RFI5QAp6FTQ',$,'C3',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#82=IFCCOLUMNTYPE('21YcNdKNj2$95U55yNx1Nw',$,'C3',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#83=IFCMATERIALPROFILESET($,$,(#85),$);
|
||||
#84=IFCRELASSOCIATESMATERIAL('1GWCzHY657$9kUJrD0AEir',$,$,$,(#82),#83);
|
||||
#84=IFCRELASSOCIATESMATERIAL('3n6Cru3xr6jvDptU9u7QEF',$,$,$,(#82),#83);
|
||||
#85=IFCMATERIALPROFILE($,$,#20,#81,$,$);
|
||||
#86=IFCISHAPEPROFILEDEF(.AREA.,'DEMO-I',$,0.1,0.2,0.005,0.01,0.005,$,$);
|
||||
#87=IFCBEAMTYPE('1Y_PtOyJP9YfZkU59r$vs1',$,'B1',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#87=IFCBEAMTYPE('2CsGpD$6nDCxWv9jXTtvq9',$,'B1',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#88=IFCMATERIALPROFILESET($,$,(#90),$);
|
||||
#89=IFCRELASSOCIATESMATERIAL('2CfmfKJ0X45xijxsDUHZ85',$,$,$,(#87),#88);
|
||||
#89=IFCRELASSOCIATESMATERIAL('2LJExc74j9rgBSBx51HDPK',$,$,$,(#87),#88);
|
||||
#90=IFCMATERIALPROFILE($,$,#20,#86,$,$);
|
||||
#91=IFCCSHAPEPROFILEDEF(.AREA.,'DEMO-C',$,0.2,0.1,0.0015,0.03,0.005);
|
||||
#92=IFCBEAMTYPE('2qogXGNrb9EANilnZjnGjq',$,'B2',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#92=IFCBEAMTYPE('32pHN2b0P0awGw$U8PpneO',$,'B2',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#93=IFCMATERIALPROFILESET($,$,(#95),$);
|
||||
#94=IFCRELASSOCIATESMATERIAL('3$1pGIrVLA2RTOmJIsbelu',$,$,$,(#92),#93);
|
||||
#94=IFCRELASSOCIATESMATERIAL('2wg8R1Drb2xhQ2Y_pRSh3c',$,$,$,(#92),#93);
|
||||
#95=IFCMATERIALPROFILE($,$,#20,#91,$,$);
|
||||
#96=IFCWINDOWTYPE('1Dc9pAExL3fxL9jScEo2EF',$,'WT01',$,$,$,(#135,#152),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
|
||||
#96=IFCWINDOWTYPE('1Gg9HfZEr69u8SdWfDs2J3',$,'WT01',$,$,$,(#135,#152),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
|
||||
#97=IFCINDEXEDPOLYGONALFACE((13,17,18,14));
|
||||
#98=IFCINDEXEDPOLYGONALFACE((5,6,3,4));
|
||||
#99=IFCINDEXEDPOLYGONALFACE((7,8,2,1));
|
||||
@@ -157,7 +157,7 @@ DATA;
|
||||
#150=IFCDIRECTION((0.,0.,1.));
|
||||
#151=IFCAXIS2PLACEMENT3D(#148,#150,#149);
|
||||
#152=IFCREPRESENTATIONMAP(#151,#147);
|
||||
#153=IFCDOORTYPE('05Z1DhJjj11wczPRkvXSPE',$,'DT01',$,$,$,(#196,#209),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
|
||||
#153=IFCDOORTYPE('0NBUmPKyT9WecsIeYJrEqg',$,'DT01',$,$,$,(#196,#209),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
|
||||
#154=IFCINDEXEDPOLYGONALFACE((17,16,15,14));
|
||||
#155=IFCINDEXEDPOLYGONALFACE((2,3,29,28));
|
||||
#156=IFCINDEXEDPOLYGONALFACE((27,28,29,30,32,31));
|
||||
@@ -214,7 +214,7 @@ DATA;
|
||||
#207=IFCDIRECTION((0.,0.,1.));
|
||||
#208=IFCAXIS2PLACEMENT3D(#205,#207,#206);
|
||||
#209=IFCREPRESENTATIONMAP(#208,#204);
|
||||
#210=IFCFURNITURETYPE('3ZMmSB_rfBnhrBckPTFKvP',$,'BUN01',$,$,$,(#931,#952),$,$,.NOTDEFINED.,.NOTDEFINED.);
|
||||
#210=IFCFURNITURETYPE('3Kyc6IyarAUw3_8fkNtXIg',$,'BUN01',$,$,$,(#931,#952),$,$,.NOTDEFINED.,.NOTDEFINED.);
|
||||
#211=IFCINDEXEDPOLYGONALFACE((187,278,44));
|
||||
#212=IFCINDEXEDPOLYGONALFACE((21,52,60));
|
||||
#213=IFCINDEXEDPOLYGONALFACE((91,100,31));
|
||||
@@ -957,140 +957,161 @@ DATA;
|
||||
#950=IFCDIRECTION((0.,0.,1.));
|
||||
#951=IFCAXIS2PLACEMENT3D(#948,#950,#949);
|
||||
#952=IFCREPRESENTATIONMAP(#951,#947);
|
||||
#953=IFCTYPEPRODUCT('1hmWolVQb9buX2GhHwaVFB',$,'SETOUT-POINT',$,'IfcAnnotation/SYMBOL',(#954),$,$);
|
||||
#954=IFCPROPERTYSET('2YNCFr62b8vhuhHGspQl4$',$,'EPset_Annotation',$,(#955));
|
||||
#953=IFCTYPEPRODUCT('0TBMBnD_b66QLUWKD9HLsc',$,'SETOUT-POINT',$,'IfcAnnotation/SYMBOL',(#954),$,$);
|
||||
#954=IFCPROPERTYSET('2dQzX_K4r1Xet6dz9zBlgs',$,'EPset_Annotation',$,(#955));
|
||||
#955=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('setout-point'),$);
|
||||
#956=IFCTYPEPRODUCT('3H4oE1a_9AwQE8OFsCjdfh',$,'CONTROL-POINT',$,'IfcAnnotation/SYMBOL',(#957),$,$);
|
||||
#957=IFCPROPERTYSET('0ARWSOH$z3kQDRkpEPHWCt',$,'EPset_Annotation',$,(#958));
|
||||
#956=IFCTYPEPRODUCT('0vo_7PU3H9ygTnrIipqPRI',$,'CONTROL-POINT',$,'IfcAnnotation/SYMBOL',(#957),$,$);
|
||||
#957=IFCPROPERTYSET('3APQOw$FP1ivxp08UxHsl6',$,'EPset_Annotation',$,(#958));
|
||||
#958=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('control-point'),$);
|
||||
#959=IFCTYPEPRODUCT('3YDPnyrWn2wRWgRzb0V7Kx',$,'TRAVERSE-POINT',$,'IfcAnnotation/SYMBOL',(#960),$,$);
|
||||
#960=IFCPROPERTYSET('1v$MzHU1b82hDqHdtz4e5y',$,'EPset_Annotation',$,(#961));
|
||||
#959=IFCTYPEPRODUCT('32V27G3$T1OO3TbXg6948F',$,'TRAVERSE-POINT',$,'IfcAnnotation/SYMBOL',(#960),$,$);
|
||||
#960=IFCPROPERTYSET('0cTSYXMc9B$PTXVDrhBYYW',$,'EPset_Annotation',$,(#961));
|
||||
#961=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('traverse-point'),$);
|
||||
#962=IFCTYPEPRODUCT('31Em8$VNL0Mh$9EZ3pXjpL',$,'DASHED',$,'IfcAnnotation/LINEWORK',(#963),$,$);
|
||||
#963=IFCPROPERTYSET('0K84$piRb31PmkAwZfP5kd',$,'EPset_Annotation',$,(#964));
|
||||
#962=IFCTYPEPRODUCT('2PXIC7Bg914gJhRh2XeGnN',$,'DASHED',$,'IfcAnnotation/LINEWORK',(#963),$,$);
|
||||
#963=IFCPROPERTYSET('0RiYsOxp529gXnCkw44wF8',$,'EPset_Annotation',$,(#964));
|
||||
#964=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('dashed'),$);
|
||||
#965=IFCTYPEPRODUCT('3P0K0gDxP4NgwnuLGqlyW6',$,'FINE',$,'IfcAnnotation/LINEWORK',(#966),$,$);
|
||||
#966=IFCPROPERTYSET('179vwKcVv9uet4_kI4NT9G',$,'EPset_Annotation',$,(#967));
|
||||
#965=IFCTYPEPRODUCT('1Ou1kA3Vb4HhuNBvM0uGe0',$,'FINE',$,'IfcAnnotation/LINEWORK',(#966),$,$);
|
||||
#966=IFCPROPERTYSET('2gzly9D0L1qPK2MExeXgRO',$,'EPset_Annotation',$,(#967));
|
||||
#967=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('fine'),$);
|
||||
#968=IFCTYPEPRODUCT('0Rh30iszn41xBCgQJl2w2c',$,'THIN',$,'IfcAnnotation/LINEWORK',(#969),$,$);
|
||||
#969=IFCPROPERTYSET('0bZV_Tby121uj5O3n08l1l',$,'EPset_Annotation',$,(#970));
|
||||
#968=IFCTYPEPRODUCT('3lx$KQPRbEZwbQ7Xdfm5gw',$,'THIN',$,'IfcAnnotation/LINEWORK',(#969),$,$);
|
||||
#969=IFCPROPERTYSET('0vsVpqs6zArvA2bfBtvQew',$,'EPset_Annotation',$,(#970));
|
||||
#970=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('thin'),$);
|
||||
#971=IFCTYPEPRODUCT('3ti_6xztr7qPp9dASz9vP4',$,'MEDIUM',$,'IfcAnnotation/LINEWORK',(#972),$,$);
|
||||
#972=IFCPROPERTYSET('19ZhF3bxL07evL8gi27Kei',$,'EPset_Annotation',$,(#973));
|
||||
#971=IFCTYPEPRODUCT('00j$y97p903w2HOb35lAQ2',$,'MEDIUM',$,'IfcAnnotation/LINEWORK',(#972),$,$);
|
||||
#972=IFCPROPERTYSET('240rEBDOn8PAvfnm_43s55',$,'EPset_Annotation',$,(#973));
|
||||
#973=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('medium'),$);
|
||||
#974=IFCTYPEPRODUCT('2hlcxjzxv8zf0gh1md23xY',$,'THICK',$,'IfcAnnotation/LINEWORK',(#975),$,$);
|
||||
#975=IFCPROPERTYSET('0wRtpbInzE2AkqX8RIreQr',$,'EPset_Annotation',$,(#976));
|
||||
#974=IFCTYPEPRODUCT('2tVdFGorj6dfrL4uA1kyW8',$,'THICK',$,'IfcAnnotation/LINEWORK',(#975),$,$);
|
||||
#975=IFCPROPERTYSET('2IEGHncr1D$R8fKA9zCEJP',$,'EPset_Annotation',$,(#976));
|
||||
#976=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('thick'),$);
|
||||
#977=IFCTYPEPRODUCT('1pefU$PA1ENw7kZG9T3mjr',$,'STRONG',$,'IfcAnnotation/LINEWORK',(#978),$,$);
|
||||
#978=IFCPROPERTYSET('1RpJZBysn8shqevvAquPQ8',$,'EPset_Annotation',$,(#979));
|
||||
#977=IFCTYPEPRODUCT('1T5C$$ONTBB8A9a7vv0Gn6',$,'STRONG',$,'IfcAnnotation/LINEWORK',(#978),$,$);
|
||||
#978=IFCPROPERTYSET('1gRomAS1L2WguA51ZI0E40',$,'EPset_Annotation',$,(#979));
|
||||
#979=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('strong'),$);
|
||||
#980=IFCTYPEPRODUCT('1HKC8zCIv5_ef3dEfa$L5N',$,'DOOR-TAG',$,'IfcAnnotation/TEXT',(#981),(#1000),$);
|
||||
#981=IFCPROPERTYSET('0aC8JrqFX6HhKUhjdLw4mN',$,'EPset_Annotation',$,(#982));
|
||||
#982=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('door-tag'),$);
|
||||
#980=IFCTYPEPRODUCT('2LmKYrAEr2K8EwJyCAsyc4',$,'SETOUT-TAG',$,'IfcAnnotation/TEXT',(#981),(#1000),$);
|
||||
#981=IFCPROPERTYSET('0nF9du8qDAF9aldqjCUsbX',$,'EPset_Annotation',$,(#982));
|
||||
#982=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('setout-tag'),$);
|
||||
#983=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#984=IFCDIRECTION((0.,0.,1.));
|
||||
#985=IFCDIRECTION((1.,0.,0.));
|
||||
#986=IFCAXIS2PLACEMENT3D(#983,#984,#985);
|
||||
#987=IFCPLANAREXTENT(1000.,1000.);
|
||||
#988=IFCTEXTLITERALWITHEXTENT('{{type.Name}}',#986,.RIGHT.,#987,'center');
|
||||
#988=IFCTEXTLITERALWITHEXTENT('E ``round({{easting}}, 0.001)``',#986,.RIGHT.,#987,'center');
|
||||
#989=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#990=IFCDIRECTION((0.,0.,1.));
|
||||
#991=IFCDIRECTION((1.,0.,0.));
|
||||
#992=IFCAXIS2PLACEMENT3D(#989,#990,#991);
|
||||
#993=IFCPLANAREXTENT(1000.,1000.);
|
||||
#994=IFCTEXTLITERALWITHEXTENT('{{Name}}',#992,.RIGHT.,#993,'center');
|
||||
#994=IFCTEXTLITERALWITHEXTENT('N ``round({{northing}}, 0.001)``',#992,.RIGHT.,#993,'center');
|
||||
#995=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#988,#994));
|
||||
#996=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#997=IFCDIRECTION((1.,0.,0.));
|
||||
#998=IFCDIRECTION((0.,0.,1.));
|
||||
#999=IFCAXIS2PLACEMENT3D(#996,#998,#997);
|
||||
#1000=IFCREPRESENTATIONMAP(#999,#995);
|
||||
#1001=IFCTYPEPRODUCT('2Jy_wl$FX8h9mgYT1$Y9oc',$,'WINDOW-TAG',$,'IfcAnnotation/TEXT',(#1002),(#1015),$);
|
||||
#1002=IFCPROPERTYSET('2ncCyO$LPCzv7kqXZK_Omh',$,'EPset_Annotation',$,(#1003));
|
||||
#1003=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('window-tag'),$);
|
||||
#1001=IFCTYPEPRODUCT('1cdhVmqEPF6e13_TFOdlQw',$,'DOOR-TAG',$,'IfcAnnotation/TEXT',(#1002),(#1021),$);
|
||||
#1002=IFCPROPERTYSET('1MUPGQolnDEgp0NxcMN56E',$,'EPset_Annotation',$,(#1003));
|
||||
#1003=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('door-tag'),$);
|
||||
#1004=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1005=IFCDIRECTION((0.,0.,1.));
|
||||
#1006=IFCDIRECTION((1.,0.,0.));
|
||||
#1007=IFCAXIS2PLACEMENT3D(#1004,#1005,#1006);
|
||||
#1008=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1009=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1007,.RIGHT.,#1008,'center');
|
||||
#1010=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1009));
|
||||
#1011=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1009=IFCTEXTLITERALWITHEXTENT('{{type.Name}}',#1007,.RIGHT.,#1008,'center');
|
||||
#1010=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1011=IFCDIRECTION((0.,0.,1.));
|
||||
#1012=IFCDIRECTION((1.,0.,0.));
|
||||
#1013=IFCDIRECTION((0.,0.,1.));
|
||||
#1014=IFCAXIS2PLACEMENT3D(#1011,#1013,#1012);
|
||||
#1015=IFCREPRESENTATIONMAP(#1014,#1010);
|
||||
#1016=IFCTYPEPRODUCT('1LTLrGzxH4qhuWGNeTlTRi',$,'SPACE-TAG',$,'IfcAnnotation/TEXT',(#1017),(#1042),$);
|
||||
#1017=IFCPROPERTYSET('2JpTIsSZf3zQUWK48RLIcL',$,'EPset_Annotation',$,(#1018));
|
||||
#1018=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('space-tag'),$);
|
||||
#1019=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1020=IFCDIRECTION((0.,0.,1.));
|
||||
#1021=IFCDIRECTION((1.,0.,0.));
|
||||
#1022=IFCAXIS2PLACEMENT3D(#1019,#1020,#1021);
|
||||
#1023=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1024=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1022,.RIGHT.,#1023,'center');
|
||||
#1013=IFCAXIS2PLACEMENT3D(#1010,#1011,#1012);
|
||||
#1014=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1015=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1013,.RIGHT.,#1014,'center');
|
||||
#1016=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1009,#1015));
|
||||
#1017=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1018=IFCDIRECTION((1.,0.,0.));
|
||||
#1019=IFCDIRECTION((0.,0.,1.));
|
||||
#1020=IFCAXIS2PLACEMENT3D(#1017,#1019,#1018);
|
||||
#1021=IFCREPRESENTATIONMAP(#1020,#1016);
|
||||
#1022=IFCTYPEPRODUCT('0OuDi3gRH2CxW9mrtE0vXw',$,'WINDOW-TAG',$,'IfcAnnotation/TEXT',(#1023),(#1036),$);
|
||||
#1023=IFCPROPERTYSET('2X7dAo_1P5ruRnlKA4kIl6',$,'EPset_Annotation',$,(#1024));
|
||||
#1024=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('window-tag'),$);
|
||||
#1025=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1026=IFCDIRECTION((0.,0.,1.));
|
||||
#1027=IFCDIRECTION((1.,0.,0.));
|
||||
#1028=IFCAXIS2PLACEMENT3D(#1025,#1026,#1027);
|
||||
#1029=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1030=IFCTEXTLITERALWITHEXTENT('{{Description}}',#1028,.RIGHT.,#1029,'center');
|
||||
#1031=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1032=IFCDIRECTION((0.,0.,1.));
|
||||
#1030=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1028,.RIGHT.,#1029,'center');
|
||||
#1031=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1030));
|
||||
#1032=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1033=IFCDIRECTION((1.,0.,0.));
|
||||
#1034=IFCAXIS2PLACEMENT3D(#1031,#1032,#1033);
|
||||
#1035=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1036=IFCTEXTLITERALWITHEXTENT('``round({{Qto_SpaceBaseQuantities.NetFloorArea}} or 0., 2)``',#1034,.RIGHT.,#1035,'center');
|
||||
#1037=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1024,#1030,#1036));
|
||||
#1038=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1039=IFCDIRECTION((1.,0.,0.));
|
||||
#1040=IFCDIRECTION((0.,0.,1.));
|
||||
#1041=IFCAXIS2PLACEMENT3D(#1038,#1040,#1039);
|
||||
#1042=IFCREPRESENTATIONMAP(#1041,#1037);
|
||||
#1043=IFCTYPEPRODUCT('3Or$DhaVf6ygGBUpRb2PMs',$,'MATERIAL-TAG',$,'IfcAnnotation/TEXT',(#1044),(#1057),$);
|
||||
#1044=IFCPROPERTYSET('2YfvIc6dPDdwWjERHZZlof',$,'EPset_Annotation',$,(#1045));
|
||||
#1045=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('rectangle-tag'),$);
|
||||
#1034=IFCDIRECTION((0.,0.,1.));
|
||||
#1035=IFCAXIS2PLACEMENT3D(#1032,#1034,#1033);
|
||||
#1036=IFCREPRESENTATIONMAP(#1035,#1031);
|
||||
#1037=IFCTYPEPRODUCT('3WEV_9wQn6AQTESgjW36PH',$,'SPACE-TAG',$,'IfcAnnotation/TEXT',(#1038),(#1063),$);
|
||||
#1038=IFCPROPERTYSET('02FAX8dIzAlunWD5ak$6sU',$,'EPset_Annotation',$,(#1039));
|
||||
#1039=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('space-tag'),$);
|
||||
#1040=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1041=IFCDIRECTION((0.,0.,1.));
|
||||
#1042=IFCDIRECTION((1.,0.,0.));
|
||||
#1043=IFCAXIS2PLACEMENT3D(#1040,#1041,#1042);
|
||||
#1044=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1045=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1043,.RIGHT.,#1044,'center');
|
||||
#1046=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1047=IFCDIRECTION((0.,0.,1.));
|
||||
#1048=IFCDIRECTION((1.,0.,0.));
|
||||
#1049=IFCAXIS2PLACEMENT3D(#1046,#1047,#1048);
|
||||
#1050=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1051=IFCTEXTLITERALWITHEXTENT('{{material.Name}}',#1049,.RIGHT.,#1050,'center');
|
||||
#1052=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1051));
|
||||
#1053=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1051=IFCTEXTLITERALWITHEXTENT('{{Description}}',#1049,.RIGHT.,#1050,'center');
|
||||
#1052=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1053=IFCDIRECTION((0.,0.,1.));
|
||||
#1054=IFCDIRECTION((1.,0.,0.));
|
||||
#1055=IFCDIRECTION((0.,0.,1.));
|
||||
#1056=IFCAXIS2PLACEMENT3D(#1053,#1055,#1054);
|
||||
#1057=IFCREPRESENTATIONMAP(#1056,#1052);
|
||||
#1058=IFCTYPEPRODUCT('01Hhk_DbjDROhyr91Hp$aV',$,'TYPE-TAG',$,'IfcAnnotation/TEXT',(#1059),(#1072),$);
|
||||
#1059=IFCPROPERTYSET('0LyRD7lHr17uJo7XNzCvZq',$,'EPset_Annotation',$,(#1060));
|
||||
#1060=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
|
||||
#1061=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1062=IFCDIRECTION((0.,0.,1.));
|
||||
#1063=IFCDIRECTION((1.,0.,0.));
|
||||
#1064=IFCAXIS2PLACEMENT3D(#1061,#1062,#1063);
|
||||
#1065=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1066=IFCTEXTLITERALWITHEXTENT('{{type.Name}}',#1064,.RIGHT.,#1065,'center');
|
||||
#1067=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1066));
|
||||
#1068=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1055=IFCAXIS2PLACEMENT3D(#1052,#1053,#1054);
|
||||
#1056=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1057=IFCTEXTLITERALWITHEXTENT('``round({{Qto_SpaceBaseQuantities.NetFloorArea}}, 0.01)``',#1055,.RIGHT.,#1056,'center');
|
||||
#1058=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1045,#1051,#1057));
|
||||
#1059=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1060=IFCDIRECTION((1.,0.,0.));
|
||||
#1061=IFCDIRECTION((0.,0.,1.));
|
||||
#1062=IFCAXIS2PLACEMENT3D(#1059,#1061,#1060);
|
||||
#1063=IFCREPRESENTATIONMAP(#1062,#1058);
|
||||
#1064=IFCTYPEPRODUCT('1evqvQLLL1zxdsIWqEn0lK',$,'MATERIAL-TAG',$,'IfcAnnotation/TEXT',(#1065),(#1078),$);
|
||||
#1065=IFCPROPERTYSET('2KPJlGyer0UBmphfR7952k',$,'EPset_Annotation',$,(#1066));
|
||||
#1066=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('rectangle-tag'),$);
|
||||
#1067=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1068=IFCDIRECTION((0.,0.,1.));
|
||||
#1069=IFCDIRECTION((1.,0.,0.));
|
||||
#1070=IFCDIRECTION((0.,0.,1.));
|
||||
#1071=IFCAXIS2PLACEMENT3D(#1068,#1070,#1069);
|
||||
#1072=IFCREPRESENTATIONMAP(#1071,#1067);
|
||||
#1073=IFCTYPEPRODUCT('0a8MHfTUzA5AV8H6p1bqC9',$,'NAME-TAG',$,'IfcAnnotation/TEXT',(#1074),(#1087),$);
|
||||
#1074=IFCPROPERTYSET('2EboL9u1j68Q_Mp8dBa7ux',$,'EPset_Annotation',$,(#1075));
|
||||
#1075=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
|
||||
#1076=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1077=IFCDIRECTION((0.,0.,1.));
|
||||
#1078=IFCDIRECTION((1.,0.,0.));
|
||||
#1079=IFCAXIS2PLACEMENT3D(#1076,#1077,#1078);
|
||||
#1080=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1081=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1079,.RIGHT.,#1080,'center');
|
||||
#1082=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1081));
|
||||
#1083=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1070=IFCAXIS2PLACEMENT3D(#1067,#1068,#1069);
|
||||
#1071=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1072=IFCTEXTLITERALWITHEXTENT('{{material.Name}}',#1070,.RIGHT.,#1071,'center');
|
||||
#1073=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1072));
|
||||
#1074=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1075=IFCDIRECTION((1.,0.,0.));
|
||||
#1076=IFCDIRECTION((0.,0.,1.));
|
||||
#1077=IFCAXIS2PLACEMENT3D(#1074,#1076,#1075);
|
||||
#1078=IFCREPRESENTATIONMAP(#1077,#1073);
|
||||
#1079=IFCTYPEPRODUCT('3Nem6d4xX87O3deyWDi3AW',$,'TYPE-TAG',$,'IfcAnnotation/TEXT',(#1080),(#1093),$);
|
||||
#1080=IFCPROPERTYSET('15bspgnA9CEuFox6xFjoTL',$,'EPset_Annotation',$,(#1081));
|
||||
#1081=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
|
||||
#1082=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1083=IFCDIRECTION((0.,0.,1.));
|
||||
#1084=IFCDIRECTION((1.,0.,0.));
|
||||
#1085=IFCDIRECTION((0.,0.,1.));
|
||||
#1086=IFCAXIS2PLACEMENT3D(#1083,#1085,#1084);
|
||||
#1087=IFCREPRESENTATIONMAP(#1086,#1082);
|
||||
#1085=IFCAXIS2PLACEMENT3D(#1082,#1083,#1084);
|
||||
#1086=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1087=IFCTEXTLITERALWITHEXTENT('{{type.Name}}',#1085,.RIGHT.,#1086,'center');
|
||||
#1088=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1087));
|
||||
#1089=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1090=IFCDIRECTION((1.,0.,0.));
|
||||
#1091=IFCDIRECTION((0.,0.,1.));
|
||||
#1092=IFCAXIS2PLACEMENT3D(#1089,#1091,#1090);
|
||||
#1093=IFCREPRESENTATIONMAP(#1092,#1088);
|
||||
#1094=IFCTYPEPRODUCT('0klFX9AjnEnPBkdIURv8XD',$,'NAME-TAG',$,'IfcAnnotation/TEXT',(#1095),(#1108),$);
|
||||
#1095=IFCPROPERTYSET('1d53tifbv2rwcoFFJosiHf',$,'EPset_Annotation',$,(#1096));
|
||||
#1096=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
|
||||
#1097=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1098=IFCDIRECTION((0.,0.,1.));
|
||||
#1099=IFCDIRECTION((1.,0.,0.));
|
||||
#1100=IFCAXIS2PLACEMENT3D(#1097,#1098,#1099);
|
||||
#1101=IFCPLANAREXTENT(1000.,1000.);
|
||||
#1102=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1100,.RIGHT.,#1101,'center');
|
||||
#1103=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1102));
|
||||
#1104=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#1105=IFCDIRECTION((1.,0.,0.));
|
||||
#1106=IFCDIRECTION((0.,0.,1.));
|
||||
#1107=IFCAXIS2PLACEMENT3D(#1104,#1106,#1105);
|
||||
#1108=IFCREPRESENTATIONMAP(#1107,#1103);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -20,10 +20,9 @@ import os
|
||||
import re
|
||||
import bpy
|
||||
import time
|
||||
import json
|
||||
import bmesh
|
||||
import shutil
|
||||
import logging
|
||||
import threading
|
||||
import mathutils
|
||||
import numpy as np
|
||||
import multiprocessing
|
||||
@@ -31,7 +30,6 @@ import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.selector
|
||||
import ifcopenshell.util.geolocation
|
||||
import blenderbim.tool as tool
|
||||
from itertools import chain, accumulate
|
||||
@@ -39,16 +37,6 @@ from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
|
||||
|
||||
|
||||
class FileCopy(threading.Thread):
|
||||
def __init__(self, file_path, destination):
|
||||
threading.Thread.__init__(self)
|
||||
self.file_path = file_path
|
||||
self.destination = destination
|
||||
|
||||
def run(self):
|
||||
shutil.copy(self.file_path, self.destination)
|
||||
|
||||
|
||||
class MaterialCreator:
|
||||
def __init__(self, ifc_import_settings, ifc_importer):
|
||||
self.mesh = None
|
||||
@@ -158,12 +146,15 @@ class MaterialCreator:
|
||||
faces_remap = None
|
||||
texture_map = None
|
||||
if coordinates.is_a("IfcIndexedPolygonalTextureMap"):
|
||||
faces_remap = [[coordinates_remap[i-1] for i in tex_coord_index.TexCoordsOf.CoordIndex]
|
||||
for tex_coord_index in coordinates.TexCoordIndices]
|
||||
faces_remap = [
|
||||
[coordinates_remap[i - 1] for i in tex_coord_index.TexCoordsOf.CoordIndex]
|
||||
for tex_coord_index in coordinates.TexCoordIndices
|
||||
]
|
||||
texture_map = [tex_coord_index.TexCoordIndex for tex_coord_index in coordinates.TexCoordIndices]
|
||||
elif coordinates.is_a("IfcIndexedTriangleTextureMap"):
|
||||
faces_remap = [[coordinates_remap[i-1] for i in triangle_face]
|
||||
for triangle_face in coordinates.MappedTo.CoordIndex]
|
||||
faces_remap = [
|
||||
[coordinates_remap[i - 1] for i in triangle_face] for triangle_face in coordinates.MappedTo.CoordIndex
|
||||
]
|
||||
texture_map = coordinates.TexCoordIndex
|
||||
|
||||
# apply uv to each face
|
||||
@@ -178,7 +169,7 @@ class MaterialCreator:
|
||||
)
|
||||
# apply uv to each loop
|
||||
for loop, i in zip(bface.loops, texCoordIndex):
|
||||
loop[uv_layer].uv = coordinates.TexCoords.TexCoordsList[i-1]
|
||||
loop[uv_layer].uv = coordinates.TexCoords.TexCoordsList[i - 1]
|
||||
|
||||
# Finish up, write the bmesh back to the mesh
|
||||
bm.to_mesh(self.mesh)
|
||||
@@ -275,6 +266,8 @@ class IfcImporter:
|
||||
self.profile_code("Calculate unit scale")
|
||||
self.calculate_model_offset()
|
||||
self.profile_code("Calculate model offset")
|
||||
self.predict_dense_mesh()
|
||||
self.profile_code("Predict dense mesh")
|
||||
self.set_units()
|
||||
self.profile_code("Set units")
|
||||
self.create_project()
|
||||
@@ -318,6 +311,8 @@ class IfcImporter:
|
||||
self.profile_code("Merging by colour")
|
||||
self.set_default_context()
|
||||
self.profile_code("Setting default context")
|
||||
self.setup_viewport_camera()
|
||||
self.setup_arrays()
|
||||
self.update_progress(100)
|
||||
bpy.context.window_manager.progress_end()
|
||||
|
||||
@@ -358,6 +353,7 @@ class IfcImporter:
|
||||
)
|
||||
if self.body_contexts:
|
||||
self.settings.set_context_ids(self.body_contexts)
|
||||
self.settings_body_2d.set_context_ids(self.body_contexts)
|
||||
# Annotation ContextType is to accommodate broken Revit files
|
||||
# See https://github.com/Autodesk/revit-ifc/issues/187
|
||||
self.plan_contexts = [
|
||||
@@ -508,6 +504,26 @@ class IfcImporter:
|
||||
products.extend(self.get_products_from_shape_representation(inverse_element))
|
||||
return products
|
||||
|
||||
def predict_dense_mesh(self):
|
||||
threshold = 10000 # Just from experience.
|
||||
|
||||
faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")]
|
||||
if faces and max(faces) > threshold:
|
||||
self.ifc_import_settings.should_use_native_meshes = True
|
||||
return
|
||||
|
||||
if self.file.schema == "IFC2X3":
|
||||
return
|
||||
|
||||
faces = [len(e.Faces) for e in self.file.by_type("IfcPolygonalFaceSet")]
|
||||
if faces and max(faces) > threshold:
|
||||
self.ifc_import_settings.should_use_native_meshes = True
|
||||
return
|
||||
|
||||
faces = [len(e.CoordIndex) for e in self.file.by_type("IfcTriangulatedFaceSet")]
|
||||
if faces and max(faces) > threshold:
|
||||
self.ifc_import_settings.should_use_native_meshes = True
|
||||
|
||||
def calculate_model_offset(self):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
if props.has_blender_offset:
|
||||
@@ -1913,6 +1929,22 @@ class IfcImporter:
|
||||
obj.matrix_world = matrix_world
|
||||
tool.Geometry.record_object_position(obj)
|
||||
|
||||
def setup_viewport_camera(self):
|
||||
context_override = tool.Blender.get_viewport_context()
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.view3d.view_selected()
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
def setup_arrays(self):
|
||||
for element in self.file.by_type("IfcElement"):
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not pset_data or not pset_data.get("Data", None): # skip array children
|
||||
continue
|
||||
for i in range(len(json.loads(pset_data["Data"]))):
|
||||
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
|
||||
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
|
||||
|
||||
|
||||
class IfcImportSettings:
|
||||
def __init__(self):
|
||||
|
||||
@@ -202,6 +202,7 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if aggregate_obj in context.selectable_objects:
|
||||
aggregate_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.GetBSDDClassificationProperties,
|
||||
operator.LoadBSDDDomains,
|
||||
operator.SearchBSDDClassifications,
|
||||
operator.SetActiveBSDDDomain,
|
||||
prop.BSDDDomain,
|
||||
prop.BSDDClassification,
|
||||
prop.BSDDPset,
|
||||
prop.BIMBSDDProperties,
|
||||
ui.BIM_UL_bsdd_domains,
|
||||
ui.BIM_UL_bsdd_classifications,
|
||||
ui.BIM_PT_bsdd,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMBSDDProperties = bpy.props.PointerProperty(type=prop.BIMBSDDProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMBSDDProperties
|
||||
@@ -0,0 +1,132 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import bsdd
|
||||
import json
|
||||
import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class LoadBSDDDomains(bpy.types.Operator):
|
||||
bl_idname = "bim.load_bsdd_domains"
|
||||
bl_label = "Load bSDD Domains"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
props.domains.clear()
|
||||
client = bsdd.Client()
|
||||
for domain in sorted(client.Domain(), key=lambda x: x["name"]):
|
||||
new = props.domains.add()
|
||||
new.name = domain["name"]
|
||||
new.namespace_uri = domain["namespaceUri"]
|
||||
new.default_language_code = domain["defaultLanguageCode"]
|
||||
new.organization_name_owner = domain["organizationNameOwner"]
|
||||
new.status = domain["status"]
|
||||
new.version = domain["version"]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetActiveBSDDDomain(bpy.types.Operator):
|
||||
bl_idname = "bim.set_active_bsdd_domain"
|
||||
bl_label = "Load bSDD Domains"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.StringProperty()
|
||||
uri: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
props.active_domain = self.name
|
||||
props.active_uri = self.uri
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SearchBSDDClassifications(bpy.types.Operator):
|
||||
bl_idname = "bim.search_bsdd_classifications"
|
||||
bl_label = "Search bSDD Classifications"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
props.classifications.clear()
|
||||
client = bsdd.Client()
|
||||
related_ifc_entities = []
|
||||
if len(props.keyword) < 3:
|
||||
return {"FINISHED"}
|
||||
if props.should_filter_ifc_class and context.active_object:
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if element:
|
||||
related_ifc_entities = [element.is_a()]
|
||||
results = client.ClassificationSearchOpen(props.keyword, DomainNamespaceUris=[props.active_uri], RelatedIfcEntities=related_ifc_entities)
|
||||
for result in sorted(results["classifications"], key=lambda x: x["referenceCode"]):
|
||||
new = props.classifications.add()
|
||||
new.name = result["name"]
|
||||
new.reference_code = result["referenceCode"]
|
||||
new.description = result.get("description", "")
|
||||
new.namespace_uri = result["namespaceUri"]
|
||||
new.domain_name = result["domainName"]
|
||||
new.domain_namespace_uri = result["domainNamespaceUri"]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GetBSDDClassificationProperties(bpy.types.Operator):
|
||||
bl_idname = "bim.get_bsdd_classification_properties"
|
||||
bl_label = "Search bSDD Classifications"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
bprops = context.scene.BIMBSDDProperties
|
||||
bprops.classification_psets.clear()
|
||||
bsdd_classification = bprops.classifications[bprops.active_classification_index]
|
||||
client = bsdd.Client()
|
||||
data = client.Classification(bsdd_classification.namespace_uri)
|
||||
|
||||
properties = data.get("classificationProperties", None)
|
||||
if not properties:
|
||||
return {"FINISHED"}
|
||||
|
||||
psets = {}
|
||||
|
||||
for prop in properties:
|
||||
if prop.get("propertyDomainName") != "IFC":
|
||||
continue
|
||||
pset = prop.get("propertySet", None)
|
||||
if not pset:
|
||||
continue
|
||||
psets.setdefault(pset, {})
|
||||
|
||||
predefined_value = prop.get("predefinedValue")
|
||||
if predefined_value:
|
||||
possible_values = [predefined_value]
|
||||
else:
|
||||
possible_values = prop.get("possibleValues", []) or []
|
||||
possible_values = [v["value"] for v in possible_values]
|
||||
|
||||
psets[pset][prop["name"]] = possible_values
|
||||
|
||||
for pset_name, pset in psets.items():
|
||||
new = bprops.classification_psets.add()
|
||||
new.name = pset_name
|
||||
for name, values in pset.items():
|
||||
new2 = new.properties.add()
|
||||
new2.name = name
|
||||
new2.enum_items = json.dumps(values)
|
||||
new2.data_type = "enum"
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,66 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from blenderbim.bim.prop import Attribute, StrProperty
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
class BSDDDomain(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
namespace_uri: StringProperty(name="URI")
|
||||
default_language_code: StringProperty(name="Language")
|
||||
organization_name_owner: StringProperty(name="Organization")
|
||||
status: StringProperty(name="Status")
|
||||
version: StringProperty(name="Version")
|
||||
|
||||
|
||||
class BSDDClassification(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
reference_code: StringProperty(name="Reference Code")
|
||||
description: StringProperty(name="Description")
|
||||
namespace_uri: StringProperty(name="Namespace URI")
|
||||
domain_name: StringProperty(name="Domain Name")
|
||||
domain_namespace_uri: StringProperty(name="Domain Namespace URI")
|
||||
|
||||
|
||||
class BSDDPset(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
properties: CollectionProperty(name="Properties", type=Attribute)
|
||||
|
||||
|
||||
class BIMBSDDProperties(PropertyGroup):
|
||||
active_domain: StringProperty(name="Active Domain")
|
||||
active_uri: StringProperty(name="Active URI")
|
||||
domains: CollectionProperty(name="Domains", type=BSDDDomain)
|
||||
active_domain_index: IntProperty(name="Active Domain Index")
|
||||
classifications: CollectionProperty(name="Classifications", type=BSDDClassification)
|
||||
active_classification_index: IntProperty(name="Active Classification Index")
|
||||
keyword: StringProperty(name="Keyword")
|
||||
should_filter_ifc_class: BoolProperty(name="Filter Active IFC Class", default=True)
|
||||
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
|
||||
@@ -0,0 +1,71 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class BIM_PT_bsdd(Panel):
|
||||
bl_label = "buildingSMART Data Dictionary"
|
||||
bl_idname = "BIM_PT_bsdd"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
if props.active_domain:
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + props.active_domain, icon="URL")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
|
||||
if len(props.domains):
|
||||
self.layout.template_list(
|
||||
"BIM_UL_bsdd_domains",
|
||||
"",
|
||||
props,
|
||||
"domains",
|
||||
props,
|
||||
"active_domain_index",
|
||||
)
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_bsdd_domains")
|
||||
|
||||
|
||||
class BIM_UL_bsdd_domains(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner})")
|
||||
op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.name = item.name
|
||||
op.uri = item.namespace_uri
|
||||
|
||||
|
||||
class BIM_UL_bsdd_classifications(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.reference_code)
|
||||
row.label(text=item.name)
|
||||
@@ -336,8 +336,15 @@ class SelectIfcClashResults(bpy.types.Operator):
|
||||
else:
|
||||
element_file = self.file
|
||||
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if element.GlobalId in global_ids:
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
except:
|
||||
continue
|
||||
|
||||
global_id = getattr(element, "GlobalId", None)
|
||||
if not global_id:
|
||||
continue
|
||||
if global_id in global_ids:
|
||||
obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.AddClassification,
|
||||
operator.AddClassificationFromBSDD,
|
||||
operator.AddClassificationReference,
|
||||
operator.AddClassificationReferenceFromBSDD,
|
||||
operator.ChangeClassificationLevel,
|
||||
operator.DisableEditingClassification,
|
||||
operator.DisableEditingClassificationReference,
|
||||
|
||||
@@ -54,6 +54,25 @@ class AddClassification(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_classification_from_bsdd"
|
||||
bl_label = "Add Classification From bSDD"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
domain = [d for d in props.domains if d.name == props.active_domain][0]
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if element.Name == props.active_domain or element.Location == domain.namespace_uri:
|
||||
return
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification=props.active_domain
|
||||
)
|
||||
classification.Source = domain.organization_name_owner
|
||||
classification.Location = domain.namespace_uri
|
||||
classification.Edition = domain.version
|
||||
|
||||
|
||||
class EnableEditingClassification(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_classification"
|
||||
bl_label = "Enable Editing Classification"
|
||||
@@ -254,6 +273,70 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_classification_reference_from_bsdd"
|
||||
bl_label = "Add Classification Reference From bSDD"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj_type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
if self.obj_type == "Object":
|
||||
if context.selected_objects:
|
||||
objects = [o.name for o in context.selected_objects]
|
||||
else:
|
||||
objects = [context.active_object.name]
|
||||
else:
|
||||
objects = [self.obj]
|
||||
props = context.scene.BIMClassificationProperties
|
||||
bprops = context.scene.BIMBSDDProperties
|
||||
|
||||
bsdd_classification = bprops.classifications[bprops.active_classification_index]
|
||||
|
||||
classification = None
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if (
|
||||
element.Name == bsdd_classification.domain_name
|
||||
or element.Location == bsdd_classification.domain_namespace_uri
|
||||
):
|
||||
classification = element
|
||||
break
|
||||
|
||||
if not classification:
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name
|
||||
)
|
||||
classification.Location = bsdd_classification.domain_namespace_uri
|
||||
|
||||
for obj in objects:
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
|
||||
if not ifc_definition_id:
|
||||
continue
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
reference = ifcopenshell.api.run(
|
||||
"classification.add_reference",
|
||||
tool.Ifc.get(),
|
||||
product=element,
|
||||
classification=classification,
|
||||
identification=bsdd_classification.reference_code,
|
||||
name=bsdd_classification.name,
|
||||
)
|
||||
reference.Location = bsdd_classification.namespace_uri
|
||||
|
||||
for classification_pset in bprops.classification_psets:
|
||||
pset = ifcopenshell.util.element.get_pset(element, classification_pset.name)
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=element, name=classification_pset.name
|
||||
)
|
||||
properties = {}
|
||||
for prop in classification_pset.properties:
|
||||
properties[prop.name] = prop.get_value()
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=properties)
|
||||
|
||||
|
||||
class ChangeClassificationLevel(bpy.types.Operator):
|
||||
bl_idname = "bim.change_classification_level"
|
||||
bl_label = "Change Classification Level"
|
||||
|
||||
@@ -48,6 +48,15 @@ class ClassificationReference(PropertyGroup):
|
||||
|
||||
|
||||
class BIMClassificationProperties(PropertyGroup):
|
||||
classification_source: EnumProperty(
|
||||
items=[
|
||||
("FILE", "IFC File", ""),
|
||||
("BSDD", "buildingSMART Data Dictionary", ""),
|
||||
("MANUAL", "Manual Entry", ""),
|
||||
],
|
||||
name="Classification Source",
|
||||
default="FILE",
|
||||
)
|
||||
available_classifications: EnumProperty(items=get_available_classifications, name="Available Classifications")
|
||||
classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute)
|
||||
active_classification_id: IntProperty(name="Active Classification Id")
|
||||
|
||||
@@ -48,6 +48,42 @@ class BIM_PT_classifications(Panel):
|
||||
|
||||
self.props = context.scene.BIMClassificationProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Source", icon="OUTLINER")
|
||||
row.prop(self.props, "classification_source", text="")
|
||||
|
||||
if self.props.classification_source == "FILE":
|
||||
self.draw_add_file_ui(context)
|
||||
elif self.props.classification_source == "BSDD":
|
||||
self.draw_add_bsdd_ui(context)
|
||||
elif self.props.classification_source == "MANUAL":
|
||||
self.draw_add_manual_ui(context)
|
||||
|
||||
for classification in ClassificationsData.data["classifications"]:
|
||||
if self.props.active_classification_id == classification["id"]:
|
||||
self.draw_editable_ui()
|
||||
else:
|
||||
self.draw_ui(classification)
|
||||
|
||||
def draw_add_manual_ui(self, context):
|
||||
row = self.layout.row()
|
||||
row.label(text="TODO", icon="ERROR")
|
||||
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
self.bprops = context.scene.BIMBSDDProperties
|
||||
|
||||
if not self.bprops.active_domain:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
|
||||
|
||||
row = self.layout.row()
|
||||
row.operator("bim.add_classification_from_bsdd", icon="ADD")
|
||||
|
||||
def draw_add_file_ui(self, context):
|
||||
if ClassificationsData.data["has_classification_file"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "available_classifications", text="")
|
||||
@@ -58,12 +94,6 @@ class BIM_PT_classifications(Panel):
|
||||
row.label(text="No Active Classification Library")
|
||||
row.operator("bim.load_classification_library", text="", icon="IMPORT")
|
||||
|
||||
for classification in ClassificationsData.data["classifications"]:
|
||||
if self.props.active_classification_id == classification["id"]:
|
||||
self.draw_editable_ui()
|
||||
else:
|
||||
self.draw_ui(classification)
|
||||
|
||||
def draw_editable_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_classification", text="Save changes", icon="CHECKMARK")
|
||||
@@ -84,6 +114,7 @@ class ReferenceUI:
|
||||
obj = context.active_object
|
||||
self.oprops = obj.BIMObjectProperties
|
||||
self.sprops = context.scene.BIMClassificationProperties
|
||||
self.bprops = context.scene.BIMBSDDProperties
|
||||
self.props = obj.BIMClassificationReferenceProperties
|
||||
self.file = IfcStore.get_file()
|
||||
|
||||
@@ -100,14 +131,76 @@ class ReferenceUI:
|
||||
self.draw_reference_ui(reference)
|
||||
|
||||
def draw_add_ui(self, context):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Source", icon="OUTLINER")
|
||||
row.prop(self.sprops, "classification_source", text="")
|
||||
|
||||
if self.sprops.classification_source == "FILE":
|
||||
self.draw_add_file_ui(context)
|
||||
elif self.sprops.classification_source == "BSDD":
|
||||
self.draw_add_bsdd_ui(context)
|
||||
elif self.sprops.classification_source == "MANUAL":
|
||||
self.draw_add_manual_ui(context)
|
||||
|
||||
def draw_add_manual_ui(self, context):
|
||||
row = self.layout.row()
|
||||
row.label(text="TODO", icon="ERROR")
|
||||
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
if not self.bprops.active_domain:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.bprops, "keyword", text="")
|
||||
row.operator("bim.search_bsdd_classifications", text="", icon="VIEWZOOM")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(self.bprops, "should_filter_ifc_class")
|
||||
|
||||
if len(self.bprops.classifications):
|
||||
self.layout.template_list(
|
||||
"BIM_UL_bsdd_classifications",
|
||||
"",
|
||||
self.bprops,
|
||||
"classifications",
|
||||
self.bprops,
|
||||
"active_classification_index",
|
||||
)
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Search Results")
|
||||
|
||||
if self.bprops.active_classification_index < len(self.bprops.classifications):
|
||||
row = self.layout.row(align=True)
|
||||
op = row.operator(
|
||||
"bim.add_classification_reference_from_bsdd", text="Add Classification Reference", icon="ADD"
|
||||
)
|
||||
op.obj = self.obj
|
||||
op.obj_type = self.obj_type
|
||||
row.operator("bim.get_bsdd_classification_properties", text="", icon="COPY_ID")
|
||||
|
||||
if len(self.bprops.classification_psets):
|
||||
for pset in self.bprops.classification_psets:
|
||||
box = self.layout.box()
|
||||
row = box.row()
|
||||
row.label(text=pset.name, icon="COPY_ID")
|
||||
blenderbim.bim.helper.draw_attributes(pset.properties, box)
|
||||
|
||||
def draw_add_file_ui(self, context):
|
||||
if not self.data.data["active_classification_library"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No Active Classification Library")
|
||||
row.label(text="No Active Classification Library", icon="ERROR")
|
||||
row.operator("bim.load_classification_library", text="", icon="IMPORT")
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"Active Classification Library: {self.data.data['active_classification_library']}")
|
||||
#row.prop(self.sprops, "available_classifications", text="")
|
||||
# row.prop(self.sprops, "available_classifications", text="")
|
||||
if not self.sprops.available_library_references:
|
||||
op = row.operator("bim.change_classification_level", text="", icon="GREASEPENCIL")
|
||||
op.parent_id = int(self.sprops.available_classifications)
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import os
|
||||
import logging
|
||||
import ifcopenshell
|
||||
import json
|
||||
import webbrowser
|
||||
import tempfile
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class SelectCobieIfcFile(bpy.types.Operator):
|
||||
bl_idname = "bim.select_cobie_ifc_file"
|
||||
bl_label = "Select COBie IFC File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.COBieProperties.cobie_ifc_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectCobieJsonFile(bpy.types.Operator):
|
||||
bl_idname = "bim.select_cobie_json_file"
|
||||
bl_label = "Select COBie JSON File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.COBieProperties.cobie_json_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExecuteIfcCobie(bpy.types.Operator):
|
||||
bl_idname = "bim.execute_ifc_cobie"
|
||||
bl_label = "Execute IFCCOBie"
|
||||
file_format: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.COBieProperties
|
||||
return props.should_load_from_memory or props.cobie_ifc_file
|
||||
|
||||
def execute(self, context):
|
||||
from cobie import IfcCobieParser
|
||||
|
||||
props = context.scene.COBieProperties
|
||||
|
||||
if props.should_load_from_memory:
|
||||
output_dir = tempfile.gettempdir()
|
||||
else:
|
||||
output_dir = os.path.dirname(props.cobie_ifc_file)
|
||||
|
||||
output = os.path.join(output_dir, "output")
|
||||
logger = logging.getLogger("IFCtoCOBie")
|
||||
fh = logging.FileHandler(os.path.join(output_dir, "cobie.log"))
|
||||
fh.setLevel(logging.DEBUG)
|
||||
fh.setFormatter(logging.Formatter("%(asctime)s : %(levelname)s : %(message)s"))
|
||||
logger = logging.getLogger("IFCtoCOBie")
|
||||
logger.addHandler(fh)
|
||||
selector = ifcopenshell.util.selector.Selector()
|
||||
if props.cobie_json_file:
|
||||
with open(props.cobie_json_file, "r") as f:
|
||||
custom_data = json.load(f)
|
||||
else:
|
||||
custom_data = {}
|
||||
parser = IfcCobieParser(logger, selector)
|
||||
|
||||
ifc_file = IfcStore.get_file()
|
||||
|
||||
if not (ifc_file and props.should_load_from_memory):
|
||||
ifc_file = props.cobie_ifc_file
|
||||
|
||||
parser.parse(
|
||||
ifc_file,
|
||||
props.cobie_types,
|
||||
props.cobie_components,
|
||||
custom_data,
|
||||
)
|
||||
if self.file_format == "xlsx":
|
||||
from cobie import CobieXlsWriter
|
||||
|
||||
writer = CobieXlsWriter(parser, output)
|
||||
writer.write()
|
||||
webbrowser.open("file://" + output + "." + self.file_format)
|
||||
elif self.file_format == "ods":
|
||||
from cobie import CobieOdsWriter
|
||||
|
||||
writer = CobieOdsWriter(parser, output)
|
||||
writer.write()
|
||||
webbrowser.open("file://" + output + "." + self.file_format)
|
||||
else:
|
||||
from cobie import CobieCsvWriter
|
||||
|
||||
writer = CobieCsvWriter(parser, output_dir)
|
||||
writer.write()
|
||||
webbrowser.open("file://" + output_dir)
|
||||
webbrowser.open("file://" + output_dir + "/cobie.log")
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,41 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import prop, workspace
|
||||
|
||||
classes = (
|
||||
prop.BIMCoveringProperties,
|
||||
workspace.Hotkey,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.register_tool(workspace.CoveringTool, after={"bim.structural_tool"}, separator=False, group=False)
|
||||
bpy.types.Scene.BIMCoveringProperties = bpy.props.PointerProperty(type=prop.BIMCoveringProperties)
|
||||
# bpy.types.Object.BIMObjectSpatialProperties = bpy.props.PointerProperty(type=prop.BIMObjectSpatialProperties)
|
||||
# bpy.types.Scene.BIMSpatialManagerProperties = bpy.props.PointerProperty(type=prop.BIMSpatialManagerProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.unregister_tool(workspace.CoveringTool)
|
||||
del bpy.types.Scene.BIMCoveringProperties
|
||||
# del bpy.types.Object.BIMObjectSpatialProperties
|
||||
# del bpy.types.Scene.BIMSpatialManagerProperties
|
||||
Binary file not shown.
+8
-6
@@ -17,6 +17,8 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
#from blenderbim.bim.module.spatial.data import SpatialData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -28,11 +30,11 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.geometry
|
||||
import ifcopenshell
|
||||
|
||||
|
||||
class COBieProperties(PropertyGroup):
|
||||
cobie_ifc_file: StringProperty(default="", name="COBie IFC File")
|
||||
cobie_types: StringProperty(default=".COBieType", name="COBie Types")
|
||||
cobie_components: StringProperty(default=".COBie", name="COBie Components")
|
||||
cobie_json_file: StringProperty(default="", name="COBie JSON File")
|
||||
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
|
||||
class BIMCoveringProperties(PropertyGroup):
|
||||
pass
|
||||
# depth: bpy.props.FloatProperty(name="Depth", default=0.1, subtype="DISTANCE", description="Flooring depth")
|
||||
@@ -0,0 +1,166 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 @Andrej730
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.bim.handler
|
||||
|
||||
|
||||
# declaring it here to avoid circular import problems
|
||||
class Operator:
|
||||
def execute(self, context):
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
class CoveringTool(WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "bim.covering_tool"
|
||||
bl_label = "Covering Tool"
|
||||
bl_description = "Create and edit coverings"
|
||||
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.covering")
|
||||
bl_widget = None
|
||||
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
|
||||
("bim.covering_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def draw_settings(cls, context, layout, ws_tool):
|
||||
CoveringToolUI.draw(context, layout, ifc_element_type="IfcCoveringType")
|
||||
|
||||
def add_layout_hotkey(layout, text, hotkey, description):
|
||||
args = ["covering", layout, text, hotkey, description]
|
||||
tool.Blender.add_layout_hotkey_operator(*args)
|
||||
|
||||
class CoveringToolUI:
|
||||
@classmethod
|
||||
def draw(cls, context, layout, ifc_element_type = None):
|
||||
cls.layout = layout
|
||||
cls.props = context.scene.BIMModelProperties
|
||||
cls.covering_props = context.scene.BIMCoveringProperties
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
if not tool.Ifc.get():
|
||||
row.label(text="No IFC Project", icon="ERROR")
|
||||
return
|
||||
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load(ifc_element_type)
|
||||
elif AuthoringData.data["ifc_element_type"] != ifc_element_type:
|
||||
AuthoringData.load(ifc_element_type)
|
||||
|
||||
|
||||
if context.region.type == "TOOL_HEADER":
|
||||
cls.draw_header_interface()
|
||||
elif context.region.type in ("UI", "WINDOW"):
|
||||
cls.draw_basic_bim_tool_interface()
|
||||
|
||||
cls.draw_default_interface()
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def draw_header_interface(cls):
|
||||
cls.draw_type_selection_interface()
|
||||
|
||||
@classmethod
|
||||
def draw_default_interface(cls):
|
||||
if AuthoringData.data["ifc_classes"]:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
|
||||
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
|
||||
else:
|
||||
op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
op.from_invoke = True
|
||||
if cls.props.relating_type_id.isnumeric():
|
||||
op.relating_type_id = int(cls.props.relating_type_id)
|
||||
|
||||
@classmethod
|
||||
def draw_type_selection_interface(cls):
|
||||
# shared by both sidebar and header
|
||||
row = cls.layout.row(align=True)
|
||||
if AuthoringData.data["ifc_classes"]:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="FILE_3D")
|
||||
prop_with_search(row, cls.props, "relating_type_id", text="")
|
||||
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
|
||||
else:
|
||||
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
|
||||
row = cls.layout.row()
|
||||
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
|
||||
|
||||
@classmethod
|
||||
def draw_basic_bim_tool_interface(cls):
|
||||
cls.draw_type_selection_interface()
|
||||
|
||||
if AuthoringData.data["ifc_classes"]:
|
||||
if cls.props.ifc_class:
|
||||
box = cls.layout.box()
|
||||
if AuthoringData.data["type_thumbnail"]:
|
||||
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
|
||||
else:
|
||||
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
|
||||
op.ifc_class = cls.props.ifc_class
|
||||
|
||||
|
||||
class Hotkey(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.covering_hotkey"
|
||||
bl_label = "Hotkey"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, operator):
|
||||
return operator.description or ""
|
||||
|
||||
def _execute(self, context):
|
||||
# self.props = context.scene.BIMCoveringProperties
|
||||
getattr(self, f"hotkey_{self.hotkey}")()
|
||||
|
||||
def invoke(self, context, event):
|
||||
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
|
||||
# self.props = context.scene.BIMSpatialProperties
|
||||
return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
|
||||
def hotkey_S_A(self):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
|
||||
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
|
||||
else:
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
|
||||
@@ -22,11 +22,10 @@ import json
|
||||
import ifccsv
|
||||
import logging
|
||||
import tempfile
|
||||
import webbrowser
|
||||
import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.handler import purge_module_data
|
||||
from blenderbim.bim.handler import refresh_ui_data
|
||||
|
||||
|
||||
class AddCsvAttribute(bpy.types.Operator):
|
||||
@@ -205,6 +204,7 @@ class ExportIfcCsv(bpy.types.Operator):
|
||||
delimiter=sep,
|
||||
include_global_id=props.include_global_id,
|
||||
null=props.null_value,
|
||||
empty=props.empty_value,
|
||||
bool_true=props.true_value,
|
||||
bool_false=props.false_value,
|
||||
sort=sort,
|
||||
@@ -244,12 +244,13 @@ class ImportIfcCsv(bpy.types.Operator):
|
||||
attributes=attributes,
|
||||
delimiter=sep,
|
||||
null=props.null_value,
|
||||
empty=props.empty_value,
|
||||
bool_true=props.true_value,
|
||||
bool_false=props.false_value,
|
||||
)
|
||||
if not props.should_load_from_memory:
|
||||
ifc_file.write(props.csv_ifc_file)
|
||||
purge_module_data()
|
||||
refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ class CsvProperties(PropertyGroup):
|
||||
should_preserve_existing: BoolProperty(default=False, name="Preserve Existing")
|
||||
include_global_id: BoolProperty(default=True, name="Include GlobalId")
|
||||
null_value: StringProperty(default="N/A", name="Null Value")
|
||||
empty_value: StringProperty(default="-", name="Empty String Value")
|
||||
true_value: StringProperty(default="YES", name="True Value")
|
||||
false_value: StringProperty(default="NO", name="False Value")
|
||||
csv_delimiter: EnumProperty(
|
||||
|
||||
@@ -74,6 +74,8 @@ class BIM_PT_ifccsv(Panel):
|
||||
row = layout.row()
|
||||
row.prop(props, "null_value")
|
||||
row = layout.row()
|
||||
row.prop(props, "empty_value")
|
||||
row = layout.row()
|
||||
row.prop(props, "true_value")
|
||||
row = layout.row()
|
||||
row.prop(props, "false_value")
|
||||
|
||||
@@ -20,23 +20,26 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.ConvertToBlender,
|
||||
operator.CopyDebugInformation,
|
||||
operator.CreateAllShapes,
|
||||
operator.CreateShapeFromStepId,
|
||||
operator.InspectFromObject,
|
||||
operator.InspectFromStepId,
|
||||
operator.OverrideDisplayType,
|
||||
operator.ParseExpress,
|
||||
operator.PrintIfcFile,
|
||||
operator.PrintObjectPlacement,
|
||||
operator.ProfileImportIFC,
|
||||
operator.PurgeHdf5Cache,
|
||||
operator.PurgeIfcLinks,
|
||||
operator.ConvertToBlender,
|
||||
operator.RewindInspector,
|
||||
operator.SelectExpressFile,
|
||||
operator.SelectHighPolygonMeshes,
|
||||
operator.SelectHighestPolygonMeshes,
|
||||
operator.ValidateIfcFile,
|
||||
operator.PrintUnusedElementStats,
|
||||
operator.PurgeUnusedElementsByClass,
|
||||
prop.BIMDebugProperties,
|
||||
ui.BIM_PT_debug,
|
||||
)
|
||||
|
||||
@@ -116,7 +116,7 @@ class PurgeIfcLinks(bpy.types.Operator):
|
||||
context.scene.BIMProperties.ifc_file = ""
|
||||
context.scene.BIMDebugProperties.attributes.clear()
|
||||
IfcStore.purge()
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ class ConvertToBlender(bpy.types.Operator):
|
||||
m.BIMMaterialProperties.ifc_style_id = False
|
||||
bpy.context.scene.BIMProperties.ifc_file = ""
|
||||
IfcStore.purge()
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -443,3 +443,55 @@ class PurgeHdf5Cache(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
core.purge_hdf5_cache(tool.Debug)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OverrideDisplayType(bpy.types.Operator):
|
||||
bl_idname = "bim.override_display_type"
|
||||
bl_label = "Override Display Type"
|
||||
display: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
obj.display_type = self.display
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class PrintUnusedElementStats(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.print_unused_elements_stats"
|
||||
bl_label = "Purge Unused Elements Stats"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Print all unused elements in current IFC project in system console, not limited to the selected class"
|
||||
)
|
||||
|
||||
ignore_contexts: bpy.props.BoolProperty(name="Ignore Contexts", default=True)
|
||||
ignore_relationships: bpy.props.BoolProperty(name="Ignore Relationships", default=True)
|
||||
ignore_types: bpy.props.BoolProperty(name="Ignore Types", default=True)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMDebugProperties
|
||||
# ignore some classes that could have zero 0 inverse references by their nature
|
||||
ignore_classes = []
|
||||
if self.ignore_contexts:
|
||||
ignore_classes += ["IfcRepresentationContext"]
|
||||
if self.ignore_relationships:
|
||||
ignore_classes += ["IfcRelationship"]
|
||||
if self.ignore_types:
|
||||
ignore_classes += ["IfcTypeProduct"]
|
||||
|
||||
unused_elements = tool.Debug.print_unused_elements_stats(props.ifc_class_purge, ignore_classes)
|
||||
self.report({"INFO"}, f"{unused_elements} unused elements found, check the system console for the details.")
|
||||
|
||||
|
||||
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.purge_unused_elements_by_class"
|
||||
bl_label = "Purge Unused Elements By Class"
|
||||
bl_description = (
|
||||
"Will find all elements of class that have no inverse refernces and will remove them, use very carefully."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMDebugProperties
|
||||
purged_elements = core.purge_unused_elements(tool.Ifc, tool.Debug, props.ifc_class_purge)
|
||||
self.report({"INFO"}, f"{purged_elements} unused elements found and removed.")
|
||||
|
||||
@@ -40,3 +40,14 @@ class BIMDebugProperties(PropertyGroup):
|
||||
inverse_attributes: CollectionProperty(name="Inverse Attributes", type=Attribute)
|
||||
inverse_references: CollectionProperty(name="Inverse References", type=Attribute)
|
||||
express_file: StringProperty(name="Express File")
|
||||
display_type: EnumProperty(
|
||||
items=[
|
||||
("BOUNDS", "Bounds", ""),
|
||||
("WIRE", "Wire", ""),
|
||||
("SOLID", "Solid", ""),
|
||||
("TEXTURED", "Textured", ""),
|
||||
],
|
||||
name="Display Type",
|
||||
default="BOUNDS",
|
||||
)
|
||||
ifc_class_purge: StringProperty(name="Unused Elements IFC Class", default="")
|
||||
|
||||
@@ -83,6 +83,10 @@ class BIM_PT_debug(Panel):
|
||||
).percentile = context.scene.BIMDebugProperties.percentile_of_polygons
|
||||
row.prop(props, "percentile_of_polygons", text="")
|
||||
|
||||
row = layout.split(factor=0.5, align=True)
|
||||
row.prop(props, "display_type", text="")
|
||||
row.operator("bim.override_display_type").display = context.scene.BIMDebugProperties.display_type
|
||||
|
||||
if context.active_object and context.active_object.data:
|
||||
mprops = context.active_object.data.BIMMeshProperties
|
||||
row = layout.row()
|
||||
|
||||
@@ -22,6 +22,7 @@ import ifccsv
|
||||
import ifcopenshell
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class SelectDiffJsonFile(bpy.types.Operator):
|
||||
@@ -51,8 +52,27 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
diff = json.load(file)
|
||||
for obj in context.visible_objects:
|
||||
obj.color = (1.0, 1.0, 1.0, 1.0)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
|
||||
ifc_file = ""
|
||||
for scene in obj.users_scene:
|
||||
if scene.BIMProperties.ifc_file:
|
||||
ifc_file = scene.BIMProperties.ifc_file
|
||||
if scene.library:
|
||||
break
|
||||
|
||||
if ifc_file:
|
||||
if ifc_file not in IfcStore.session_files:
|
||||
IfcStore.session_files[ifc_file] = ifcopenshell.open(ifc_file)
|
||||
element_file = IfcStore.session_files[ifc_file]
|
||||
else:
|
||||
element_file = ifc_file
|
||||
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
except:
|
||||
continue
|
||||
global_id = getattr(element, "GlobalId", None)
|
||||
if not global_id:
|
||||
|
||||
@@ -52,15 +52,20 @@ class profile_consequential:
|
||||
cls.test_name = test_name
|
||||
|
||||
@classmethod
|
||||
def log(cls):
|
||||
def log(cls, args=[]):
|
||||
if cls.start_time is not None:
|
||||
cls.lines.append(f"{cls.test_name}\t{timer() - cls.start_time:.10f}")
|
||||
args = "" if not args else "\t" + "\t".join(args)
|
||||
cls.lines.append(f"{cls.test_name}\t{timer() - cls.start_time:.10f}{args}")
|
||||
|
||||
@classmethod
|
||||
def stop(cls):
|
||||
cls.log()
|
||||
cls.start_time = None
|
||||
print("\n".join(cls.lines))
|
||||
lines = "\n".join(cls.lines)
|
||||
print(lines)
|
||||
import pyperclip
|
||||
|
||||
pyperclip.copy(lines)
|
||||
cls.lines = []
|
||||
|
||||
|
||||
@@ -1926,7 +1931,7 @@ class DecorationsHandler:
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
# NOTE: we USE POST_PIXEL here so that we can draw use both 3D_POLYLINE_UNIFORM_COLOR
|
||||
# NOTE: we USE POST_PIXEL here so that we can use both 3D_POLYLINE_UNIFORM_COLOR
|
||||
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
|
||||
|
||||
@@ -1944,6 +1949,7 @@ class DecorationsHandler:
|
||||
self.decorators[object_type] = self.decorators["FALL"]
|
||||
|
||||
def get_objects_and_decorators(self, collection):
|
||||
# TODO: do it in data instead of the handler for performance?
|
||||
results = []
|
||||
|
||||
for obj in collection.all_objects:
|
||||
|
||||
@@ -180,10 +180,6 @@ def format_distance(
|
||||
frac = 0
|
||||
inches += 1
|
||||
|
||||
# Check values and compose string
|
||||
if inches == 12:
|
||||
feet += 1
|
||||
inches = 0
|
||||
|
||||
if not isArea:
|
||||
add_inches = bool(inches) or not suppress_zero_inches
|
||||
|
||||
@@ -26,7 +26,6 @@ import shutil
|
||||
import hashlib
|
||||
import shapely
|
||||
import subprocess
|
||||
import webbrowser
|
||||
import numpy as np
|
||||
import multiprocessing
|
||||
import ifcopenshell
|
||||
|
||||
@@ -402,12 +402,27 @@ class Scheduler:
|
||||
else:
|
||||
wrapped_lines = text_lines
|
||||
|
||||
for line_number, text_line in enumerate(wrapped_lines[::-1]):
|
||||
if box_alignment.startswith("top"):
|
||||
dy_dir = 1
|
||||
line_number = 0
|
||||
elif box_alignment.startswith("bot"):
|
||||
dy_dir = -1
|
||||
line_number = 0
|
||||
else: # middle row
|
||||
# the idea is that the middle row should always stay in the center
|
||||
# e.g. dy offset for 3 lines is: 1, 0, -1
|
||||
# for 2 lines: 0.5, -0.5
|
||||
dy_dir = -1
|
||||
line_number = (len(wrapped_lines) - 1) / 2
|
||||
|
||||
for text_line in wrapped_lines[::dy_dir]:
|
||||
# position has to be inserted at tspan to avoid x offset between tspans
|
||||
tspan = self.svg.tspan(text_line, insert=(x, y), **text_params)
|
||||
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
|
||||
tspan.update({"dy": f"-{line_number}em"})
|
||||
tspan.update({"dy": f"{line_number}em"})
|
||||
text_tag.add(tspan)
|
||||
line_number += dy_dir
|
||||
|
||||
self.svg.add(text_tag)
|
||||
|
||||
def convert_to_mm(self, value):
|
||||
|
||||
@@ -345,8 +345,12 @@ class SheetBuilder:
|
||||
def build_drawings(self, root, sheet):
|
||||
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
|
||||
drawing_id = int(view.attrib["data-id"])
|
||||
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
|
||||
drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"])
|
||||
try:
|
||||
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
|
||||
drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"])
|
||||
except:
|
||||
# Perhaps the SVG has outdated content or is edited externally which we cannot control.
|
||||
continue
|
||||
|
||||
images = view.findall("{http://www.w3.org/2000/svg}image")
|
||||
|
||||
@@ -387,8 +391,12 @@ class SheetBuilder:
|
||||
|
||||
def build_schedules(self, root, sheet):
|
||||
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'):
|
||||
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
|
||||
schedule = tool.Ifc.get().by_id(int(view.attrib["data-schedule"]))
|
||||
try:
|
||||
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
|
||||
schedule = tool.Ifc.get().by_id(int(view.attrib["data-schedule"]))
|
||||
except:
|
||||
# Perhaps the SVG has outdated content or is edited externally which we cannot control.
|
||||
continue
|
||||
|
||||
images = view.findall("{http://www.w3.org/2000/svg}image")
|
||||
|
||||
|
||||
@@ -831,7 +831,9 @@ class SvgWriter:
|
||||
symbol_xml.attrib.pop("id")
|
||||
# NOTE: zip makes sure that we iterate over the shortest list
|
||||
for field, text_literal in zip(template_text_fields, text_literals):
|
||||
field.text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
|
||||
field.text = tool.Drawing.replace_text_literal_variables(
|
||||
text_literal.Literal, product or element
|
||||
)
|
||||
field.attrib["class"] = classes_str
|
||||
|
||||
if fill_bg:
|
||||
@@ -849,7 +851,7 @@ class SvgWriter:
|
||||
|
||||
line_number = 0
|
||||
for text_literal in text_literals:
|
||||
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
|
||||
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product or element)
|
||||
text_tags = self.create_text_tag(
|
||||
text,
|
||||
text_position_svg,
|
||||
@@ -883,7 +885,6 @@ class SvgWriter:
|
||||
symbol_position_svg = point * self.svg_scale
|
||||
self.svg.add(self.svg.use(f"#{svg_id}", insert=symbol_position_svg))
|
||||
|
||||
|
||||
def draw_point_annotation(self, obj, classes):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
|
||||
+7
-8
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -20,17 +20,16 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.SelectCobieIfcFile,
|
||||
operator.SelectCobieJsonFile,
|
||||
operator.ExecuteIfcCobie,
|
||||
prop.COBieProperties,
|
||||
ui.BIM_PT_cobie,
|
||||
operator.SelectFMIfcFile,
|
||||
operator.ExecuteIfcFM,
|
||||
prop.BIMFMProperties,
|
||||
ui.BIM_PT_fm,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.COBieProperties = bpy.props.PointerProperty(type=prop.COBieProperties)
|
||||
bpy.types.Scene.BIMFMProperties = bpy.props.PointerProperty(type=prop.BIMFMProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.COBieProperties
|
||||
del bpy.types.Scene.BIMFMProperties
|
||||
@@ -0,0 +1,82 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import json
|
||||
import logging
|
||||
import tempfile
|
||||
import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class SelectFMIfcFile(bpy.types.Operator):
|
||||
bl_idname = "bim.select_fm_ifc_file"
|
||||
bl_label = "Select FM IFC File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".ifc"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMFMProperties.ifc_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExecuteIfcFM(bpy.types.Operator):
|
||||
bl_idname = "bim.execute_ifcfm"
|
||||
bl_label = "Execute IfcFM"
|
||||
file_format: bpy.props.StringProperty()
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
return props.should_load_from_memory or props.ifc_file
|
||||
|
||||
def invoke(self, context, event):
|
||||
props = context.scene.BIMFMProperties
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
import ifcfm
|
||||
|
||||
props = context.scene.BIMFMProperties
|
||||
ifc_file = tool.Ifc.get()
|
||||
if not (ifc_file and props.should_load_from_memory):
|
||||
ifc_file = ifcopenshell.open(props.ifc_file)
|
||||
|
||||
parser = ifcfm.Parser(preset=props.engine)
|
||||
parser.parse(ifc_file)
|
||||
writer = ifcfm.Writer(parser)
|
||||
writer.write()
|
||||
if props.format == "csv":
|
||||
writer.write_csv('tmp/')
|
||||
elif props.format == "ods":
|
||||
writer.write_ods(self.filepath)
|
||||
elif props.format == "xlsx":
|
||||
writer.write_xlsx(self.filepath)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,54 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
class BIMFMProperties(PropertyGroup):
|
||||
ifc_file: StringProperty(default="", name="IFC File")
|
||||
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
|
||||
engine: EnumProperty(
|
||||
items=[
|
||||
("aohbsem", "AOH-BSEM", ""),
|
||||
("basic", "Basic", ""),
|
||||
("cobie24", "COBie 2.4", ""),
|
||||
("cobie3", "COBie 3", ""),
|
||||
],
|
||||
name="Engine",
|
||||
default="cobie24",
|
||||
)
|
||||
format: EnumProperty(
|
||||
items=[
|
||||
("csv", "csv", ""),
|
||||
("xlsx", "xlsx", ""),
|
||||
("ods", "ods", ""),
|
||||
],
|
||||
name="Format",
|
||||
default="ods",
|
||||
)
|
||||
+10
-19
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -21,9 +21,9 @@ from bpy.types import Panel
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class BIM_PT_cobie(Panel):
|
||||
bl_label = "COBie"
|
||||
bl_idname = "BIM_PT_cobie"
|
||||
class BIM_PT_fm(Panel):
|
||||
bl_label = "Facility Management"
|
||||
bl_idname = "BIM_PT_fm"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
@@ -34,7 +34,7 @@ class BIM_PT_cobie(Panel):
|
||||
layout.use_property_split = True
|
||||
|
||||
scene = context.scene
|
||||
props = scene.COBieProperties
|
||||
props = scene.BIMFMProperties
|
||||
|
||||
if IfcStore.get_file():
|
||||
row = layout.row()
|
||||
@@ -42,22 +42,13 @@ class BIM_PT_cobie(Panel):
|
||||
|
||||
if not IfcStore.get_file() or not props.should_load_from_memory:
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "cobie_ifc_file")
|
||||
row.operator("bim.select_cobie_ifc_file", icon="FILE_FOLDER", text="")
|
||||
row.prop(props, "ifc_file")
|
||||
row.operator("bim.select_fm_ifc_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "cobie_types")
|
||||
row.prop(props, "engine")
|
||||
row = layout.row()
|
||||
row.prop(props, "cobie_components")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "cobie_json_file")
|
||||
row.operator("bim.select_cobie_json_file", icon="FILE_FOLDER", text="")
|
||||
row.prop(props, "format")
|
||||
|
||||
row = layout.row()
|
||||
op = row.operator("bim.execute_ifc_cobie", text="CSV")
|
||||
op.file_format = "csv"
|
||||
op = row.operator("bim.execute_ifc_cobie", text="ODS")
|
||||
op.file_format = "ods"
|
||||
op = row.operator("bim.execute_ifc_cobie", text="XLSX")
|
||||
op.file_format = "xlsx"
|
||||
op = row.operator("bim.execute_ifcfm", text="Convert To Spreadsheet")
|
||||
@@ -32,6 +32,7 @@ classes = (
|
||||
operator.OverrideDuplicateMoveLinkedMacro,
|
||||
operator.OverrideDuplicateMoveMacro,
|
||||
operator.OverrideJoin,
|
||||
operator.OverrideMeshSeparate,
|
||||
operator.OverrideModeSetEdit,
|
||||
operator.OverrideModeSetObject,
|
||||
operator.OverrideOriginSet,
|
||||
@@ -53,6 +54,7 @@ classes = (
|
||||
ui.BIM_PT_mesh,
|
||||
ui.BIM_PT_workarounds,
|
||||
ui.BIM_MT_object_set_origin,
|
||||
ui.BIM_MT_separate,
|
||||
)
|
||||
|
||||
|
||||
@@ -73,6 +75,7 @@ def register():
|
||||
bpy.types.VIEW3D_MT_object.append(ui.object_menu)
|
||||
bpy.types.OUTLINER_MT_object.append(ui.outliner_menu)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(ui.object_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.append(ui.edit_mesh_menu)
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
|
||||
@@ -116,6 +119,7 @@ def unregister():
|
||||
bpy.types.OBJECT_PT_transform.remove(ui.BIM_PT_transform)
|
||||
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.remove(ui.edit_mesh_menu)
|
||||
del bpy.types.Scene.BIMGeometryProperties
|
||||
del bpy.types.Object.BIMGeometryProperties
|
||||
wm = bpy.context.window_manager
|
||||
|
||||
@@ -57,6 +57,39 @@ class EditObjectPlacement(bpy.types.Operator, Operator):
|
||||
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
|
||||
class OverrideMeshSeparate(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.override_mesh_separate"
|
||||
bl_label = "IFC Mesh Separate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
|
||||
# You cannot separate meshes if the representation is mapped.
|
||||
relating_type = tool.Root.get_element_type(tool.Ifc.get_entity(obj))
|
||||
if relating_type and tool.Root.does_type_have_representations(relating_type):
|
||||
# We toggle edit mode to ensure that once representations are
|
||||
# unmapped, our Blender mesh only has a single user.
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
bpy.ops.bim.unassign_type(related_object=obj.name)
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
|
||||
selected_objects = context.selected_objects
|
||||
bpy.ops.mesh.separate(type=self.type)
|
||||
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
|
||||
new_objs = [obj]
|
||||
for new_obj in context.selected_objects:
|
||||
if new_obj == obj:
|
||||
continue
|
||||
# This is not very efficient, it needlessly copies the representations first.
|
||||
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
new_objs.append(new_obj)
|
||||
for new_obj in new_objs:
|
||||
bpy.ops.bim.update_representation(obj=new_obj.name)
|
||||
|
||||
|
||||
class OverrideOriginSet(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.override_origin_set"
|
||||
bl_label = "IFC Origin Set"
|
||||
@@ -88,8 +121,15 @@ class AddRepresentation(bpy.types.Operator, Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
representation_conversion_method: bpy.props.EnumProperty(
|
||||
items=[
|
||||
("OUTLINE", "Trace Outline", ""),
|
||||
("BOX", "Bounding Box", ""),
|
||||
("OUTLINE", "Trace Outline", "Traces outline by local XY axes, for Profile - by local XZ axes."),
|
||||
(
|
||||
"BOX",
|
||||
"Bounding Box",
|
||||
"Creates a bounding box representation.\n"
|
||||
"For Plan context - 2D bounding box by local XY axes,\n"
|
||||
"for Profile - 2D bounding box by local XZ axes.\n"
|
||||
"For other contexts - bounding box is 3d.",
|
||||
),
|
||||
("PROJECT", "Full Representation", ""),
|
||||
],
|
||||
name="Representation Conversion Method",
|
||||
@@ -216,7 +256,8 @@ class PurgeUnusedRepresentations(bpy.types.Operator, Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.purge_unused_representations(tool.Ifc, tool.Geometry)
|
||||
purged_representations = core.purge_unused_representations(tool.Ifc, tool.Geometry)
|
||||
self.report({"INFO"}, f"{purged_representations} representations were purged.")
|
||||
|
||||
|
||||
class UpdateRepresentation(bpy.types.Operator, Operator):
|
||||
@@ -272,7 +313,8 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
|
||||
if self.ifc_representation_class == "IfcTessellatedFaceSet":
|
||||
# We are explicitly casting to a tessellation, so remove all parametric materials.
|
||||
element_type = ifcopenshell.util.element.get_type(product)
|
||||
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=element_type)
|
||||
if element_type: # Some invalid IFCs use material sets without a type.
|
||||
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=element_type)
|
||||
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=product)
|
||||
else:
|
||||
# These objects are parametrically based on an axis and should not be modified as a mesh
|
||||
@@ -498,6 +540,8 @@ class OverrideDelete(bpy.types.Operator):
|
||||
def _execute(self, context):
|
||||
if self.is_batch:
|
||||
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
|
||||
|
||||
self.process_arrays(context)
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
@@ -507,6 +551,7 @@ class OverrideDelete(bpy.types.Operator):
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
else:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
if self.is_batch:
|
||||
old_file = tool.Ifc.get()
|
||||
old_file.end_transaction()
|
||||
@@ -527,6 +572,30 @@ class OverrideDelete(bpy.types.Operator):
|
||||
data["old_file"].redo()
|
||||
tool.Ifc.set(data["new_file"])
|
||||
|
||||
def process_arrays(self, context):
|
||||
selected_objects = set(context.selected_objects)
|
||||
array_parents = set()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not pset:
|
||||
continue
|
||||
array_parents.add(tool.Ifc.get().by_guid(pset["Parent"]))
|
||||
|
||||
for array_parent in array_parents:
|
||||
array_parent_obj = tool.Ifc.get_object(array_parent)
|
||||
data = [(i, data) for i, data in enumerate(tool.Blender.Modifier.Array.get_modifiers_data(array_parent))]
|
||||
# NOTE: there is a way to remove arrays more precisely but it's more complex
|
||||
for i, modifier_data in reversed(data):
|
||||
children = set(tool.Blender.Modifier.Array.get_children_objects(modifier_data))
|
||||
if children.issubset(selected_objects):
|
||||
with context.temp_override(active_object=array_parent_obj):
|
||||
bpy.ops.bim.remove_array(item=i)
|
||||
else:
|
||||
break # allows to remove only n last layers of an array
|
||||
|
||||
|
||||
class OverrideOutlinerDelete(bpy.types.Operator):
|
||||
bl_idname = "bim.override_outliner_delete"
|
||||
@@ -537,7 +606,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_ids) > 0
|
||||
return len(getattr(context, "selected_ids", [])) > 0
|
||||
|
||||
def execute(self, context):
|
||||
# In this override, we don't check self.hierarchy. This effectively
|
||||
@@ -652,6 +721,13 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
return len(context.selected_objects) > 0
|
||||
|
||||
def execute(self, context):
|
||||
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
|
||||
|
||||
def _execute(self, context):
|
||||
return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context)
|
||||
|
||||
@staticmethod
|
||||
def execute_duplicate_operator(self, context, linked=False):
|
||||
# Deep magick from the dawn of time
|
||||
if IfcStore.get_file():
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
@@ -662,7 +738,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
new_active_obj = None
|
||||
for obj in context.selected_objects:
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
if linked and obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
if obj == context.active_object:
|
||||
new_active_obj = new_obj
|
||||
@@ -674,44 +750,111 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
@staticmethod
|
||||
def execute_ifc_duplicate_operator(self, context, linked=False):
|
||||
objects_to_duplicate = set(context.selected_objects)
|
||||
|
||||
# handle arrays
|
||||
arrays_to_duplicate, array_children = OverrideDuplicateMove.process_arrays(self, context)
|
||||
objects_to_duplicate -= array_children
|
||||
for child in array_children:
|
||||
child.select_set(False)
|
||||
|
||||
self.new_active_obj = None
|
||||
# Track decompositions so they can be recreated after the operation
|
||||
relationships = tool.Root.get_decomposition_relationships(context.selected_objects)
|
||||
relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate)
|
||||
old_to_new = {}
|
||||
for obj in context.selected_objects:
|
||||
|
||||
for obj in objects_to_duplicate:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
continue # For now, don't copy drawings until we stabilise a bit more. It's tricky.
|
||||
|
||||
linked_non_ifc_object = linked and not element
|
||||
|
||||
# Prior to duplicating, sync the object placement to make decomposition recreation more stable.
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
temp_data = None
|
||||
|
||||
if obj.data and not linked_non_ifc_object:
|
||||
# assure root.copy_class won't replace the previous mesh globally
|
||||
temp_data = obj.data.copy()
|
||||
new_obj.data = temp_data
|
||||
|
||||
if obj == context.active_object:
|
||||
self.new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.link(new_obj)
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
|
||||
if linked_non_ifc_object:
|
||||
continue
|
||||
|
||||
# clear object's collection so it will be able to have it's own
|
||||
new_obj.BIMObjectProperties.collection = None
|
||||
# Copy the actual class
|
||||
# copy the actual class
|
||||
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
|
||||
# clean up the orphaned mesh with ifc id of the original object to avoid confusion
|
||||
if new and temp_data:
|
||||
tool.Blender.remove_data_block(temp_data)
|
||||
|
||||
if new:
|
||||
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
|
||||
if array_pset:
|
||||
array_pset = tool.Ifc.get().by_id(array_pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = [new]
|
||||
# TODO: handle array data for other cases of duplication
|
||||
array_data = arrays_to_duplicate.get(obj, None)
|
||||
tool.Model.handle_array_on_copied_element(new, array_data)
|
||||
if array_data:
|
||||
for child in tool.Blender.Modifier.Array.get_all_children_objects(new):
|
||||
child.select_set(True)
|
||||
|
||||
# TODO: add new array children to recreate their decomposition too
|
||||
old_to_new[element] = [new]
|
||||
if new.is_a("IfcRelSpaceBoundary"):
|
||||
tool.Boundary.decorate_boundary(new_obj)
|
||||
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
@staticmethod
|
||||
def process_arrays(self, context):
|
||||
selected_objects = set(context.selected_objects)
|
||||
array_parents = set()
|
||||
arrays_to_create = dict()
|
||||
array_children = set() # will be ignored during the duplication
|
||||
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not pset:
|
||||
continue
|
||||
array_parents.add(tool.Ifc.get().by_guid(pset["Parent"]))
|
||||
|
||||
for array_parent in array_parents:
|
||||
array_parent_obj = tool.Ifc.get_object(array_parent)
|
||||
if array_parent_obj not in selected_objects:
|
||||
continue
|
||||
|
||||
array_data = []
|
||||
for modifier_data in tool.Blender.Modifier.Array.get_modifiers_data(array_parent):
|
||||
children = set(tool.Blender.Modifier.Array.get_children_objects(modifier_data))
|
||||
if children.issubset(selected_objects):
|
||||
modifier_data["children"] = []
|
||||
array_data.append(modifier_data)
|
||||
array_children.update(children)
|
||||
else:
|
||||
break # allows to duplicate only n first layers of an array
|
||||
|
||||
if array_data:
|
||||
arrays_to_create[array_parent_obj] = array_data
|
||||
|
||||
return arrays_to_create, array_children
|
||||
|
||||
|
||||
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
|
||||
@@ -730,57 +873,10 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
return len(context.selected_objects) > 0
|
||||
|
||||
def execute(self, context):
|
||||
# Deep magick from the dawn of time
|
||||
if IfcStore.get_file():
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
if self.new_active_obj:
|
||||
context.view_layer.objects.active = self.new_active_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
new_active_obj = None
|
||||
for obj in context.selected_objects:
|
||||
new_obj = obj.copy()
|
||||
if obj == context.active_object:
|
||||
new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.link(new_obj)
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
if new_active_obj:
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
return {"FINISHED"}
|
||||
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=True)
|
||||
|
||||
def _execute(self, context):
|
||||
self.new_active_obj = None
|
||||
# Track decompositions so they can be recreated after the operation
|
||||
relationships = tool.Root.get_decomposition_relationships(context.selected_objects)
|
||||
old_to_new = {}
|
||||
for obj in context.selected_objects:
|
||||
# Prior to duplicating, sync the object placement to make decomposition recreation more stable.
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
if obj == context.active_object:
|
||||
self.new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.link(new_obj)
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
# Copy the actual class
|
||||
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
if new:
|
||||
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
|
||||
if array_pset:
|
||||
array_pset = tool.Ifc.get().by_id(array_pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = new
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
return {"FINISHED"}
|
||||
return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
|
||||
class OverrideDuplicateMoveAggregateMacro(bpy.types.Macro):
|
||||
@@ -800,28 +896,7 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
return len(context.selected_objects) > 0
|
||||
|
||||
def execute(self, context):
|
||||
# Deep magick from the dawn of time
|
||||
if IfcStore.get_file():
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
if self.new_active_obj:
|
||||
context.view_layer.objects.active = self.new_active_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
new_active_obj = None
|
||||
|
||||
for obj in context.selected_objects:
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
if obj == context.active_object:
|
||||
new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.link(new_obj)
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
if new_active_obj:
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
return {"FINISHED"}
|
||||
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
|
||||
|
||||
def _execute(self, context):
|
||||
self.new_active_obj = None
|
||||
@@ -1019,12 +1094,7 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
)
|
||||
|
||||
if new_entity:
|
||||
# Checks if the object belongs to an Ifc Array
|
||||
array_pset = ifcopenshell.util.element.get_pset(new_entity, "BBIM_Array")
|
||||
if array_pset:
|
||||
array_pset = tool.Ifc.get().by_id(array_pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new_entity, pset=array_pset)
|
||||
|
||||
tool.Model.handle_array_on_copied_element(new_entity)
|
||||
blenderbim.core.aggregate.unassign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
@@ -1059,6 +1129,17 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
|
||||
recreate_data_structure(new_root_entity)
|
||||
|
||||
# Remove connections with old objects
|
||||
for new in old_to_new.values():
|
||||
for connection in new[0].ConnectedTo:
|
||||
entity = connection.RelatedElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
for connection in new[0].ConnectedFrom:
|
||||
entity = connection.RelatingElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
|
||||
old_objs = []
|
||||
for old, new in old_to_new.items():
|
||||
old_objs.append(tool.Ifc.get_object(old))
|
||||
@@ -1067,7 +1148,7 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1152,12 +1233,7 @@ class RefreshAggregate(bpy.types.Operator):
|
||||
)
|
||||
|
||||
if new_entity:
|
||||
# Checks if the object belongs to an Ifc Array
|
||||
array_pset = ifcopenshell.util.element.get_pset(new_entity, "BBIM_Array")
|
||||
if array_pset:
|
||||
array_pset = tool.Ifc.get().by_id(array_pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new_entity, pset=array_pset)
|
||||
|
||||
tool.Model.handle_array_on_copied_element(new_entity)
|
||||
blenderbim.core.aggregate.unassign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
@@ -1171,22 +1247,33 @@ class RefreshAggregate(bpy.types.Operator):
|
||||
return new_entity
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
self.report({"INFO"}, "Only 1 object need to be selected.")
|
||||
return {"FINISHED"}
|
||||
|
||||
selected_root_obj = context.selected_objects[0]
|
||||
selected_root_entity = tool.Ifc.get_entity(selected_root_obj)
|
||||
|
||||
if not selected_root_entity.is_a("IfcElementAssembly"):
|
||||
if selected_root_entity.is_a("IfcElementAssembly"):
|
||||
pass
|
||||
elif selected_root_entity.Decomposes:
|
||||
if selected_root_entity.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
selected_root_entity = selected_root_entity.Decomposes[0].RelatingObject
|
||||
selected_root_obj = tool.Ifc.get_object(selected_root_entity)
|
||||
else:
|
||||
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data")
|
||||
if not pset:
|
||||
self.report({"INFO"}, "Object is not part of an assembly aggregate.")
|
||||
return {"FINISHED"}
|
||||
|
||||
pset_data = json.loads(pset["Data"])[0]
|
||||
instance_of = pset_data["instance_of"][0]
|
||||
original_root_entity = tool.Ifc.get().by_guid(instance_of)
|
||||
if original_root_entity == selected_root_entity:
|
||||
self.report({"INFO"}, "Cannot refresh original assembly. Select an assembly instance.")
|
||||
return {"FINISHED"}
|
||||
|
||||
parents = remove_objects(selected_root_entity)
|
||||
@@ -1199,13 +1286,25 @@ class RefreshAggregate(bpy.types.Operator):
|
||||
for parent in parents:
|
||||
duplicate_children(parent)
|
||||
|
||||
|
||||
# Remove connections with old objects
|
||||
for new in old_to_new.values():
|
||||
for connection in new[0].ConnectedTo:
|
||||
entity = connection.RelatedElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
for connection in new[0].ConnectedFrom:
|
||||
entity = connection.RelatingElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
|
||||
old_objs = []
|
||||
for old, new in old_to_new.items():
|
||||
old_objs.append(tool.Ifc.get_object(old))
|
||||
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
|
||||
matrix_diff = new_obj.matrix_world @ original_matrix
|
||||
matrix_diff = Matrix.inverted(original_matrix) @ new_obj.matrix_world
|
||||
new_matrix = selected_matrix @ matrix_diff
|
||||
|
||||
new_obj.matrix_world = new_matrix
|
||||
@@ -1214,7 +1313,7 @@ class RefreshAggregate(bpy.types.Operator):
|
||||
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1407,6 +1506,7 @@ class OverrideModeSetEdit(bpy.types.Operator):
|
||||
continue
|
||||
|
||||
if tool.Geometry.is_meshlike(representation):
|
||||
tool.Ifc.edit(obj)
|
||||
if getattr(element, "HasOpenings", None):
|
||||
# Mesh elements with openings must disable openings
|
||||
# so that you can edit the original topology.
|
||||
@@ -1425,7 +1525,9 @@ class OverrideModeSetEdit(bpy.types.Operator):
|
||||
else:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
if len(selected_objs) > 1 and (not context.selected_objects or len(context.selected_objects) != len(selected_objs)):
|
||||
if len(selected_objs) > 1 and (
|
||||
not context.selected_objects or len(context.selected_objects) != len(selected_objs)
|
||||
):
|
||||
# We are trying to edit at least one non-mesh-like object : Display a hint to the user
|
||||
self.report({"INFO"}, "Only mesh-compatible representations may be edited concurrently in edit mode.")
|
||||
|
||||
@@ -1537,6 +1639,9 @@ class OverrideModeSetObject(bpy.types.Operator):
|
||||
bpy.ops.bim.finish_editing_roof_path()
|
||||
elif tool.Model.get_usage_type(element) == "PROFILE":
|
||||
bpy.ops.bim.edit_extrusion_axis()
|
||||
# if in the process of editing arbitrary profile
|
||||
elif context.scene.BIMProfileProperties.active_arbitrary_profile_id:
|
||||
bpy.ops.bim.edit_arbitrary_profile()
|
||||
else:
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
return self.execute(context)
|
||||
@@ -1551,6 +1656,8 @@ class OverrideModeSetObject(bpy.types.Operator):
|
||||
self.edited_objs.append(obj)
|
||||
elif getattr(element, "HasOpenings", None):
|
||||
self.unchanged_objs_with_openings.append(obj)
|
||||
else:
|
||||
tool.Ifc.finish_edit(obj)
|
||||
|
||||
if self.edited_objs:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
@@ -32,6 +32,21 @@ def object_menu(self, context):
|
||||
self.layout.menu("BIM_MT_object_set_origin", icon="PLUGIN")
|
||||
|
||||
|
||||
def edit_mesh_menu(self, context):
|
||||
self.layout.separator()
|
||||
self.layout.menu("BIM_MT_separate", icon="PLUGIN")
|
||||
|
||||
|
||||
class BIM_MT_separate(Menu):
|
||||
bl_idname = "BIM_MT_separate"
|
||||
bl_label = "IFC Separate"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC Selection").type = "SELECTED"
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Material").type = "MATERIAL"
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Loose Parts").type = "LOOSE"
|
||||
|
||||
|
||||
class BIM_MT_object_set_origin(Menu):
|
||||
bl_idname = "BIM_MT_object_set_origin"
|
||||
bl_label = "IFC Set Origin"
|
||||
|
||||
@@ -22,6 +22,7 @@ import ifcopenshell
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.misc as core
|
||||
import blenderbim.core.geometry as core_geometry
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from mathutils import Vector, Matrix, Euler
|
||||
|
||||
@@ -140,7 +141,51 @@ class SplitAlongEdge(bpy.types.Operator, Operator):
|
||||
return context.selected_objects and tool.Ifc.get()
|
||||
|
||||
def _execute(self, context):
|
||||
core.split_along_edge(tool.Misc, cutter=context.active_object, objs=context.selected_objects)
|
||||
cutter = context.active_object
|
||||
objs = [o for o in context.selected_objects if o != cutter]
|
||||
|
||||
# Splitting only works on meshes
|
||||
for obj in objs:
|
||||
# You cannot split meshes if the representation is mapped.
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
relating_type = tool.Root.get_element_type(element)
|
||||
if relating_type and tool.Root.does_type_have_representations(relating_type):
|
||||
bpy.ops.bim.unassign_type(related_object=obj.name)
|
||||
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
core_geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=False,
|
||||
)
|
||||
|
||||
if not tool.Geometry.is_meshlike(representation):
|
||||
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="IfcTessellatedFaceSet")
|
||||
|
||||
new_objs = tool.Misc.split_objects_with_cutter(objs, cutter)
|
||||
for obj in new_objs:
|
||||
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
for obj in objs:
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
core_geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=True,
|
||||
)
|
||||
|
||||
|
||||
class GetConnectedSystemElements(bpy.types.Operator, Operator):
|
||||
@@ -205,46 +250,50 @@ class DrawSystemArrows(bpy.types.Operator, Operator):
|
||||
return context.selected_objects and tool.Ifc.get()
|
||||
|
||||
def _execute(self, context):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
curve = bpy.data.objects.new("System Arrows", bpy.data.curves.new("System Arrows", "CURVE"))
|
||||
curve.data.dimensions = "3D"
|
||||
context.scene.collection.objects.link(curve)
|
||||
sinks = []
|
||||
sources = []
|
||||
|
||||
for obj in bpy.context.selected_objects:
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
sources = []
|
||||
sinks = []
|
||||
for rel in getattr(element, "HasPorts", []) or []:
|
||||
if rel.RelatingPort.FlowDirection == "SOURCE":
|
||||
sources.append(
|
||||
self.get_absolute_matrix(
|
||||
ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement)
|
||||
)
|
||||
)
|
||||
elif rel.RelatingPort.FlowDirection == "SINK":
|
||||
sinks.append(
|
||||
self.get_absolute_matrix(
|
||||
ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement)
|
||||
)
|
||||
)
|
||||
sources_current = []
|
||||
sinks_current = []
|
||||
|
||||
for port in tool.System.get_ports(element):
|
||||
local_placement = ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement)
|
||||
m = self.get_absolute_matrix(local_placement)
|
||||
if port.FlowDirection == "SOURCE":
|
||||
sources_current.append(m)
|
||||
elif port.FlowDirection == "SINK":
|
||||
sinks_current.append(m)
|
||||
else:
|
||||
sources.append(
|
||||
self.get_absolute_matrix(
|
||||
ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement)
|
||||
)
|
||||
)
|
||||
sinks.append(
|
||||
self.get_absolute_matrix(
|
||||
ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement)
|
||||
)
|
||||
)
|
||||
for sink in sinks:
|
||||
for source in sources:
|
||||
sources_current.append(m)
|
||||
sinks_current.append(m)
|
||||
|
||||
if sinks_current or sources_current:
|
||||
sinks.append(sinks_current)
|
||||
sources.append(sources_current)
|
||||
|
||||
if not sinks:
|
||||
self.report({"INFO"}, "No sinks/sources found for selected objects.")
|
||||
return {"FINISHED"}
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
curve = bpy.data.objects.new("System Arrows", bpy.data.curves.new("System Arrows", "CURVE"))
|
||||
curve.data.dimensions = "3D"
|
||||
curve.show_in_front = True
|
||||
context.scene.collection.objects.link(curve)
|
||||
|
||||
for i in range(len(sinks)):
|
||||
for sink in sinks[i]:
|
||||
for source in sources[i]:
|
||||
polyline = curve.data.splines.new("POLY")
|
||||
polyline.points.add(1)
|
||||
polyline.points[0].co = (Matrix(sink).translation * unit_scale).to_4d()
|
||||
polyline.points[1].co = (Matrix(source).translation * unit_scale).to_4d()
|
||||
tool.Blender.select_and_activate_single_object(context, curve)
|
||||
|
||||
def get_absolute_matrix(self, matrix):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
|
||||
@@ -28,6 +28,7 @@ from . import (
|
||||
roof,
|
||||
slab,
|
||||
space,
|
||||
covering,
|
||||
stair,
|
||||
window,
|
||||
opening,
|
||||
@@ -106,6 +107,7 @@ classes = (
|
||||
slab.SetArcIndex,
|
||||
space.GenerateSpace,
|
||||
space.GenerateSpacesFromWalls,
|
||||
covering.AddInstanceFlooringCoveringsFromWalls,
|
||||
space.ToggleSpaceVisibility,
|
||||
mep.FitFlowSegments,
|
||||
mep.RegenerateDistributionElement,
|
||||
@@ -179,6 +181,7 @@ classes = (
|
||||
roof.SetGableRoofEdgeAngle,
|
||||
mep.MEPAddObstruction,
|
||||
mep.MEPAddTransition,
|
||||
mep.MEPAddBend,
|
||||
)
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
@@ -206,15 +206,29 @@ class SelectArrayParent(bpy.types.Operator):
|
||||
bl_idname = "bim.select_array_parent"
|
||||
bl_label = "Select Array Parent"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
parent: bpy.props.StringProperty(description="Parent Element GUID")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
cls.poll_message_set("No active object selected")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
element = tool.Ifc.get().by_guid(self.parent)
|
||||
except:
|
||||
self.report({"ERROR"}, f"Couldn't find array parent by guid '{self.parent}'")
|
||||
object = context.active_object
|
||||
element = tool.Ifc.get_entity(object)
|
||||
array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not array_pset:
|
||||
self.report({"ERROR"}, f"Object is not part of an array.")
|
||||
return {"CANCELLED"}
|
||||
obj = tool.Ifc.get_object(element)
|
||||
|
||||
try:
|
||||
parent_element = tool.Ifc.get().by_guid(array_pset["Parent"])
|
||||
except:
|
||||
self.report({"ERROR"}, f"Couldn't find array parent by guid '{array_pset['Parent']}'")
|
||||
return {"CANCELLED"}
|
||||
|
||||
obj = tool.Ifc.get_object(parent_element)
|
||||
if obj:
|
||||
tool.Blender.select_and_activate_single_object(context, active_object=obj)
|
||||
return {"FINISHED"}
|
||||
@@ -224,13 +238,26 @@ class SelectAllArrayObjects(bpy.types.Operator):
|
||||
bl_idname = "bim.select_all_array_objects"
|
||||
bl_label = "Select All Array Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
parent: bpy.props.StringProperty(description="Parent Element GUID")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
cls.poll_message_set("No active object selected")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
object = context.active_object
|
||||
element = tool.Ifc.get_entity(object)
|
||||
array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not array_pset:
|
||||
self.report({"ERROR"}, f"Object is not part of an array.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
try:
|
||||
parent_element = tool.Ifc.get().by_guid(self.parent)
|
||||
parent_element = tool.Ifc.get().by_guid(array_pset["Parent"])
|
||||
except RuntimeError:
|
||||
self.report({"ERROR"}, f"Couldn't find array parent by guid '{self.parent}'")
|
||||
self.report({"ERROR"}, f"Couldn't find array parent by guid '{array_pset['Parent']}'")
|
||||
return {"CANCELLED"}
|
||||
|
||||
array_objects = tool.Blender.Modifier.Array.get_all_objects(parent_element)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.covering as core
|
||||
|
||||
|
||||
class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_instance_flooring_coverings_from_walls"
|
||||
bl_label = "Add Typed Covering From Walls"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add instance flooring coverings from selected walls. The active object must be a wall and layered vertically"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element:
|
||||
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
# walls (i.e. prismatic) in the active object storey.
|
||||
# In order to run, the active object must be a wall and
|
||||
# there must be selected walls
|
||||
|
||||
active_obj = bpy.context.active_object
|
||||
if not active_obj:
|
||||
self.report({"ERROR"}, "No active object. Please select a wall")
|
||||
return
|
||||
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element and not element.is_a("IfcWall"):
|
||||
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if not container:
|
||||
self.report({"ERROR"}, "The wall is not contained.")
|
||||
|
||||
if not bpy.context.selected_objects:
|
||||
self.report({"ERROR"}, "No selected objects found. Please select walls.")
|
||||
return
|
||||
|
||||
core.add_instance_flooring_coverings_from_walls(tool.Ifc, tool.Spatial, tool.Collector, tool.Geometry)
|
||||
|
||||
@@ -68,6 +68,14 @@ def update_door_modifier_representation(context, obj):
|
||||
},
|
||||
}
|
||||
|
||||
def get_active_representation_context(obj):
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
if active_representation:
|
||||
return active_representation.ContextOfItems
|
||||
return ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
previously_active_context = get_active_representation_context(obj)
|
||||
|
||||
# ELEVATION_VIEW representation
|
||||
profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW")
|
||||
if profile:
|
||||
@@ -78,6 +86,7 @@ def update_door_modifier_representation(context, obj):
|
||||
tool.Model.replace_object_ifc_representation(profile, obj, elevation_representation)
|
||||
|
||||
# MODEL_VIEW representation
|
||||
# (Model/Body defined only BEFORE Plan/Body to prevent #2744)
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
representation_data["context"] = body
|
||||
model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
|
||||
@@ -113,14 +122,34 @@ def update_door_modifier_representation(context, obj):
|
||||
)
|
||||
tool.Model.replace_object_ifc_representation(plan_annotation, obj, plan_representation)
|
||||
|
||||
if plan_body or plan_annotation:
|
||||
# adding switch representation at the end instead of changing order of representations
|
||||
# to prevent #2744
|
||||
core.switch_representation(
|
||||
# adding switch representation at the end instead of changing order of representations
|
||||
# to prevent #2744
|
||||
if get_active_representation_context(obj) != previously_active_context:
|
||||
previously_active_representation = ifcopenshell.util.representation.get_representation(
|
||||
element,
|
||||
previously_active_context.ContextType,
|
||||
previously_active_context.ContextIdentifier,
|
||||
previously_active_context.TargetView,
|
||||
)
|
||||
|
||||
if not previously_active_representation:
|
||||
# we assume there is no representation because it was
|
||||
# Plan/Annotation/PLAN_VIEW
|
||||
previously_active_context = ifcopenshell.util.representation.get_context(
|
||||
ifc_file, "Plan", "Body", "PLAN_VIEW"
|
||||
)
|
||||
previously_active_representation = ifcopenshell.util.representation.get_representation(
|
||||
element,
|
||||
previously_active_context.ContextType,
|
||||
previously_active_context.ContextIdentifier,
|
||||
previously_active_context.TargetView,
|
||||
)
|
||||
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=model_representation,
|
||||
representation=previously_active_representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=True,
|
||||
|
||||
@@ -35,11 +35,12 @@ import blenderbim.core.type
|
||||
import blenderbim.core.root
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.tool as tool
|
||||
from math import pi, degrees, radians
|
||||
from math import pi, degrees, radians, sin, cos, asin, tan
|
||||
from copy import copy
|
||||
from mathutils import Vector, Matrix
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
from blenderbim.bim.module.model.profile import DumbProfileJoiner
|
||||
from blenderbim.tool.cad import VTX_PRECISION
|
||||
|
||||
V = lambda *x: Vector([float(i) for i in x])
|
||||
|
||||
@@ -47,7 +48,8 @@ V = lambda *x: Vector([float(i) for i in x])
|
||||
class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.regenerate_distribution_element"
|
||||
bl_description = (
|
||||
"Regenerates the positions and segment lengths of a distribution element and all connected elements."
|
||||
"Regenerates the positions and segment lengths of a distribution element and all connected elements.\n"
|
||||
"Will try to adjust as less elements as possible, never rotate them. Segments will also try to change their length to fit"
|
||||
)
|
||||
bl_label = "Regenerate Distribution Element"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -201,6 +203,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
|
||||
is_on_axis2 = tool.Cad.is_point_on_edge(intersect2, axis2)
|
||||
if not is_on_axis1 and not is_on_axis2:
|
||||
fitting_type = "BEND"
|
||||
bpy.ops.bim.mep_add_bend()
|
||||
elif is_on_axis1 and is_on_axis2:
|
||||
fitting_type = "CROSS"
|
||||
else:
|
||||
@@ -334,7 +337,7 @@ class MEPGenerator:
|
||||
class_name = "".join(split_camel_case(element.is_a())[:-1] + [mep_class_type])
|
||||
return class_name
|
||||
|
||||
def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type):
|
||||
def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type, bbim_data=None):
|
||||
"""
|
||||
returns a dict of compatible fitting_type and start_port_match flag to correctly place the fitting.
|
||||
|
||||
@@ -347,9 +350,12 @@ class MEPGenerator:
|
||||
|
||||
There lies the problem that it won't be
|
||||
able to identify the fittings that were not yet connected to any segments yet.
|
||||
|
||||
|
||||
`bbim_data` is used to find compatible fitting build with BBIM parametrically (BBIM_Fitting pset).
|
||||
All data in `bbim_data` supposed to be in project units.
|
||||
"""
|
||||
|
||||
# TODO: check angle, start, end and offset for transitions
|
||||
if not isinstance(segment_or_segments, collections.abc.Iterable):
|
||||
segments = [segment_or_segments]
|
||||
ports = [port_or_ports]
|
||||
@@ -357,6 +363,12 @@ class MEPGenerator:
|
||||
segments = segment_or_segments
|
||||
ports = port_or_ports
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
precision = VTX_PRECISION / si_conversion
|
||||
angle_precision = degrees(precision)
|
||||
start_port_match = True
|
||||
|
||||
segments_data = []
|
||||
for segment, port in zip(segments, ports, strict=True):
|
||||
segment_type = ifcopenshell.util.element.get_type(segment)
|
||||
@@ -365,6 +377,53 @@ class MEPGenerator:
|
||||
return
|
||||
segments_data.append((segment_type, port.PredefinedType, port.SystemType))
|
||||
|
||||
def compatible_with_bbim_data(fitting_type):
|
||||
nonlocal start_port_match
|
||||
start_port_match = True
|
||||
if not bbim_data:
|
||||
return True
|
||||
fitting_type_obj = tool.Ifc.get_object(fitting_type)
|
||||
fitting_bbim_data = tool.Model.get_modeling_bbim_pset_data(fitting_type_obj, "BBIM_Fitting")
|
||||
if not fitting_bbim_data:
|
||||
return False
|
||||
|
||||
fitting_bbim_data = fitting_bbim_data["data_dict"]
|
||||
|
||||
def compare_value(key, second_key=None):
|
||||
second_key = second_key or key
|
||||
requested_value = bbim_data[key]
|
||||
fitting_value = fitting_bbim_data[second_key]
|
||||
|
||||
if isinstance(requested_value, float):
|
||||
compare_precision = angle_precision if key == "angle" else precision
|
||||
compare = tool.Cad.is_x(requested_value, fitting_value, compare_precision)
|
||||
elif isinstance(fitting_value, list):
|
||||
compare = tool.Cad.are_vectors_equal(requested_value, Vector(fitting_value), precision)
|
||||
return compare
|
||||
|
||||
ignore_keys = []
|
||||
if predefined_type == "BEND":
|
||||
ignore_keys.extend(("start_length", "end_length"))
|
||||
# for bends there is a special case when lengths might not match
|
||||
# but fitting is still compatible if we flip it
|
||||
# since bend connects segments of the same type
|
||||
default_lengths_match = compare_value("start_length") and compare_value("end_length")
|
||||
if not default_lengths_match:
|
||||
switched_lengths_match = compare_value("start_length", "end_length") and compare_value(
|
||||
"end_length", "start_length"
|
||||
)
|
||||
if switched_lengths_match:
|
||||
start_port_match = False
|
||||
else:
|
||||
return False
|
||||
|
||||
for key in bbim_data:
|
||||
if key in ignore_keys:
|
||||
continue
|
||||
if not compare_value(key):
|
||||
return False
|
||||
return True
|
||||
|
||||
def are_connected_elements_compatible(segments_data, fitting_data):
|
||||
# prevent arguments mutation, not using deepcopy because of the errors with ifc elements
|
||||
segments_data = [copy(i) for i in segments_data]
|
||||
@@ -391,7 +450,7 @@ class MEPGenerator:
|
||||
|
||||
# NOTE: I have a feeling that there are cases where order
|
||||
# in which we're checking the segments is important
|
||||
# but I couldn't pin it down exact cases
|
||||
# but I couldn't pin it down to exact cases
|
||||
for test_segment_data in fitting_data[:]:
|
||||
for base_segment_data in segments_data:
|
||||
if not are_segments_compatible(test_segment_data, base_segment_data):
|
||||
@@ -406,17 +465,17 @@ class MEPGenerator:
|
||||
|
||||
if predefined_type == "OBSTRUCTION":
|
||||
return packed_data
|
||||
|
||||
|
||||
for port in ports:
|
||||
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
|
||||
if tool.Cad.is_x(port_local_position.length, 0.0):
|
||||
start_port = port
|
||||
break
|
||||
|
||||
|
||||
connected_port = tool.System.get_connected_port(start_port)
|
||||
connected_element = tool.System.get_port_relating_element(connected_port)
|
||||
element_type = ifcopenshell.util.element.get_type(connected_element)
|
||||
packed_data["start_port_match"] = element_type == segments_data[0][0]
|
||||
packed_data["start_port_match"] = element_type == segments_data[0][0] and start_port_match
|
||||
|
||||
return packed_data
|
||||
|
||||
@@ -450,12 +509,14 @@ class MEPGenerator:
|
||||
|
||||
fitting_data.append((element_type, port.PredefinedType, port.SystemType))
|
||||
|
||||
# if we skipped the occurrence we still can other occurrences
|
||||
# if we skipped the occurrence we still need to check other occurrences
|
||||
# otherwise checking 1 occurrence is enough
|
||||
if not skipped_the_occurrence:
|
||||
if are_connected_elements_compatible(segments_data, fitting_data):
|
||||
if compatible_with_bbim_data(fitting_type) and are_connected_elements_compatible(
|
||||
segments_data, fitting_data
|
||||
):
|
||||
return pack_return_data(fitting_type, ports, segments_data)
|
||||
return
|
||||
break
|
||||
|
||||
def create_obstruction_type(self, segment):
|
||||
# code is very similar to "bim.add_type"
|
||||
@@ -525,6 +586,8 @@ class MEPGenerator:
|
||||
obstruction_obj.matrix_world = segment_matrix
|
||||
|
||||
profile_joiner.set_depth(obstruction_obj, length)
|
||||
# NOTE: we add ports to the obstruction occurence and not to the type
|
||||
# since it's material profile based like segments
|
||||
obstruction_port = tool.System.add_ports(
|
||||
obstruction_obj,
|
||||
add_start_port=not at_segment_start,
|
||||
@@ -590,10 +653,13 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
start_length: bpy.props.FloatProperty(
|
||||
name="Start Length", description="Transition start length in SI units", default=0.1, subtype="DISTANCE"
|
||||
name="Start Length", description="Transition start length in SI units", default=0.1, subtype="DISTANCE", min=0
|
||||
)
|
||||
end_length: bpy.props.FloatProperty(
|
||||
name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE"
|
||||
name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE", min=0
|
||||
)
|
||||
angle: bpy.props.FloatProperty(
|
||||
name="Transition Angle", description="Transition angle in degrees", default=pi / 6, subtype="ANGLE", min=0
|
||||
)
|
||||
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
|
||||
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
|
||||
@@ -639,63 +705,69 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
start_object_z_basis = start_object_rotation.to_matrix().col[2] # z basis vector
|
||||
keep_only_z_axis = lambda p_ws: p_ws.dot(start_object_z_basis) * start_object_z_basis
|
||||
|
||||
# TODO: support cases when segments are partially or completely overlapping each other
|
||||
if not tool.Cad.are_edges_parallel(start_axis, end_axis):
|
||||
self.report({"ERROR"}, f"Failed to add transition - segments are not parallel.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# TODO: support different profiles rotation by local Z
|
||||
# check rotation difference
|
||||
end_object_rotation = end_object.matrix_world.to_quaternion()
|
||||
rotation_difference_z = (
|
||||
start_object.matrix_world.to_quaternion().rotation_difference(end_object_rotation).to_euler().z
|
||||
)
|
||||
|
||||
def is_multiple_of_pi(value):
|
||||
n = round(value / pi)
|
||||
return tool.Cad.is_x(abs(value - n * pi), 0)
|
||||
|
||||
if not is_multiple_of_pi(rotation_difference_z):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"There is some rotation difference between profiles by local Z axis: "
|
||||
f"{round(degrees(rotation_difference_z))} deg, this kind of transition is not yet supported.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# setup start / end points
|
||||
start_segment_data = MEPGenerator().get_segment_data(start_element)
|
||||
end_segment_data = MEPGenerator().get_segment_data(end_element)
|
||||
end_port = end_segment_data["start_port"]
|
||||
start_port = start_segment_data["end_port"]
|
||||
|
||||
points_ports_map = {
|
||||
start_segment_data["start_point"]: start_segment_data["start_port"],
|
||||
start_segment_data["end_point"]: start_segment_data["end_port"],
|
||||
end_segment_data["start_point"]: end_segment_data["start_port"],
|
||||
end_segment_data["end_point"]: end_segment_data["end_port"],
|
||||
}
|
||||
|
||||
# transition points
|
||||
start_point, end_point = tool.Cad.closest_points(
|
||||
(start_point, end_point), (first_segment_start, second_segment_end) = tool.Cad.closest_points(
|
||||
(start_segment_data["start_point"], start_segment_data["end_point"]),
|
||||
(end_segment_data["start_point"], end_segment_data["end_point"]),
|
||||
)
|
||||
start_port = points_ports_map[start_point]
|
||||
end_port = points_ports_map[end_point]
|
||||
start_point_on_origin = start_point == start_segment_data["start_point"]
|
||||
start_connection = "ATSTART" if start_point_on_origin else "ATEND"
|
||||
start_segment_sign = -1 if start_point_on_origin else 1
|
||||
|
||||
end_point_on_origin = end_point == end_segment_data["start_point"]
|
||||
end_connection = "ATSTART" if end_point_on_origin else "ATEND"
|
||||
|
||||
# figure profile offset
|
||||
base_transition_dir = keep_only_z_axis(end_point - start_point).normalized()
|
||||
flip_profile_offset = base_transition_dir.dot(start_object_z_basis) < 0
|
||||
|
||||
if tool.Cad.are_edges_collinear(start_axis, end_axis):
|
||||
profile_offset = None
|
||||
profile_offset = V(0, 0)
|
||||
else:
|
||||
to_start_object_space = start_object_rotation.inverted()
|
||||
profile_offset = (
|
||||
(to_start_object_space @ end_object.location) - (to_start_object_space @ start_object.location)
|
||||
).xy
|
||||
if tool.Cad.is_x(profile_offset.length_squared, 0):
|
||||
profile_offset = None
|
||||
else:
|
||||
profile_offset = profile_offset / si_conversion
|
||||
if flip_profile_offset:
|
||||
profile_offset *= V(1, -1)
|
||||
profile_offset = profile_offset / si_conversion
|
||||
if flip_profile_offset:
|
||||
profile_offset *= V(1, -1)
|
||||
|
||||
# world space profile offset
|
||||
profile_offset_ws = (
|
||||
start_object_rotation @ (profile_offset * si_conversion).to_3d() if profile_offset else V(0, 0, 0)
|
||||
)
|
||||
|
||||
# will need entire_length to check that transition length fill fit
|
||||
first_segment_start, second_segment_end = [
|
||||
p
|
||||
for p in (
|
||||
start_segment_data["start_point"],
|
||||
start_segment_data["end_point"],
|
||||
end_segment_data["start_point"],
|
||||
end_segment_data["end_point"],
|
||||
)
|
||||
if p not in (start_point, end_point)
|
||||
]
|
||||
profile_offset_ws = start_object_rotation @ (profile_offset * si_conversion).to_3d()
|
||||
|
||||
def get_segments_length():
|
||||
start_dir = (start_point - first_segment_start).normalized()
|
||||
@@ -707,8 +779,6 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# can't rely on (end_point-start_point) here because
|
||||
# transition might change the segments length and therefore direction will be changed
|
||||
segments_dir = (start_point - first_segment_start).normalized()
|
||||
start_port = points_ports_map[start_point]
|
||||
end_port = points_ports_map[end_point]
|
||||
|
||||
# add transition representation
|
||||
builder = ShapeBuilder(ifc_file)
|
||||
@@ -717,6 +787,7 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
end_element,
|
||||
self.start_length / si_conversion,
|
||||
self.end_length / si_conversion,
|
||||
angle=degrees(self.angle),
|
||||
profile_offset=profile_offset,
|
||||
)
|
||||
|
||||
@@ -741,31 +812,30 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
transition_dir = keep_only_z_axis(end_segment_extend_point - start_segment_extend_point).normalized()
|
||||
|
||||
# adjust the segments
|
||||
end_object_rotation = end_object.matrix_world.to_quaternion()
|
||||
end_object_z_basis = end_object_rotation.to_matrix().col[2] # z basis vector
|
||||
if tool.Cad.is_x(start_object_z_basis.dot(transition_dir), 1):
|
||||
start_connection = "ATEND"
|
||||
else:
|
||||
start_connection = "ATSTART"
|
||||
if tool.Cad.is_x(end_object_z_basis.dot(transition_dir), 1):
|
||||
end_connection = "ATSTART"
|
||||
else:
|
||||
end_connection = "ATEND"
|
||||
DumbProfileJoiner().join_E(start_object, start_segment_extend_point, start_connection)
|
||||
DumbProfileJoiner().join_E(end_object, end_segment_extend_point, end_connection)
|
||||
|
||||
# For bbim transitions, there is small convention that:
|
||||
# - start_length segment positioned at the start of the transition's Z-axis.
|
||||
# - end_length segment positioned at the of it.
|
||||
# this is why we sort the lengths in parametric data too
|
||||
parametric_data = {
|
||||
"start_length": (self.start_length if start_segment_sign == 1 else self.end_length) / si_conversion,
|
||||
"end_length": (self.end_length if start_segment_sign == 1 else self.start_length) / si_conversion,
|
||||
"profile_offset": profile_offset,
|
||||
"angle": degrees(self.angle),
|
||||
}
|
||||
|
||||
# find the compatible fitting type
|
||||
fitting_data = MEPGenerator().get_compatible_fitting_type(
|
||||
[start_element, end_element], [start_port, end_port], "TRANSITION"
|
||||
[start_element, end_element], [start_port, end_port], "TRANSITION", bbim_data=parametric_data
|
||||
)
|
||||
transition_type = fitting_data["fitting_type"] if fitting_data else None
|
||||
start_port_match = fitting_data["start_port_match"] if fitting_data else True
|
||||
if transition_type:
|
||||
# TODO: handle the case without creating a representation in the first place?
|
||||
ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=rep)
|
||||
start_port_match = fitting_data["start_port_match"] if fitting_data else True
|
||||
|
||||
# create new fitting type if nothing is compatible
|
||||
if not transition_type:
|
||||
else: # create new fitting type if nothing is compatible
|
||||
mesh = bpy.data.meshes.new("Transition")
|
||||
obj = bpy.data.objects.new("Transition", mesh)
|
||||
transition_type = blenderbim.core.root.assign_class(
|
||||
@@ -779,6 +849,7 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
tool.Model.replace_object_ifc_representation(body, obj, rep)
|
||||
tool.Blender.remove_data_block(mesh)
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=transition_type, name="BBIM_Fitting")
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
@@ -786,6 +857,7 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset=pset,
|
||||
properties={"Data": json.dumps(transition_data, default=list)},
|
||||
)
|
||||
tool.System.add_ports(obj, offset_end_port=profile_offset_ws)
|
||||
|
||||
# NOTE: at this point we loose current blender objects selection
|
||||
# create transition element
|
||||
@@ -809,10 +881,374 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
transition_obj.location = start_segment_extend_point if start_port_match else end_segment_extend_point
|
||||
|
||||
# add ports and connect them
|
||||
ports = tool.System.add_ports(transition_obj, offset_end_port=profile_offset_ws)
|
||||
ports = tool.System.get_ports(tool.Ifc.get_entity(transition_obj))
|
||||
if not start_port_match:
|
||||
start_port, end_port = end_port, start_port
|
||||
tool.Ifc.run("system.connect_port", port1=ports[0], port2=start_port, direction="NOTDEFINED")
|
||||
tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.mep_add_bend"
|
||||
bl_label = "Add Bend"
|
||||
bl_description = "Adds a bend between two MEP elements. Elements are either provided by ID or selected in Blender"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
start_length: bpy.props.FloatProperty(
|
||||
name="Start Length", description="Bend start length in SI units", default=0.1, subtype="DISTANCE", min=0
|
||||
)
|
||||
end_length: bpy.props.FloatProperty(
|
||||
name="End Length", description="Bend end length in SI units", default=0.1, subtype="DISTANCE", min=0
|
||||
)
|
||||
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
|
||||
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
|
||||
radius: bpy.props.FloatProperty(
|
||||
"Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
start_element, end_element = None, None
|
||||
ifc_file = tool.Ifc.get()
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
|
||||
if self.start_segment_id and self.end_segment_id:
|
||||
start_element = ifc_file.by_id(self.start_segment_id)
|
||||
end_element = ifc_file.by_id(self.end_segment_id)
|
||||
start_object = tool.Ifc.get_object(start_element)
|
||||
end_object = tool.Ifc.get_object(end_element)
|
||||
|
||||
elif len(context.selected_objects) == 2:
|
||||
start_object = context.active_object
|
||||
end_object = next(o for o in context.selected_objects if o != context.active_object)
|
||||
start_element = tool.Ifc.get_entity(start_object)
|
||||
end_element = tool.Ifc.get_entity(end_object)
|
||||
if not start_element or not end_element:
|
||||
self.report({"ERROR"}, f"Two IFC elements should be selected for the bend.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
else:
|
||||
self.report({"ERROR"}, f"Two IFC elements should be provided for the bend.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# check rotation difference
|
||||
def rotation_difference_check():
|
||||
end_object_rotation = end_object.matrix_world.to_quaternion()
|
||||
rotation_difference = (
|
||||
start_object.matrix_world.to_quaternion().rotation_difference(end_object_rotation).to_euler()
|
||||
)
|
||||
|
||||
def is_multiple_of_pi(value):
|
||||
n = round(value / pi)
|
||||
return tool.Cad.is_x(abs(value - n * pi), 0)
|
||||
|
||||
if not is_multiple_of_pi(rotation_difference.z):
|
||||
error_msg = (
|
||||
"There is some rotation difference between profiles by local Z axis: "
|
||||
f"{round(degrees(rotation_difference.z))} deg, adding a bend is not possible."
|
||||
)
|
||||
return error_msg
|
||||
|
||||
if error_msg := rotation_difference_check():
|
||||
self.report({"ERROR"}, error_msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# check segments types
|
||||
def types_check():
|
||||
start_type = ifcopenshell.util.element.get_type(start_element)
|
||||
end_type = ifcopenshell.util.element.get_type(end_element)
|
||||
if not start_type or not end_type:
|
||||
return False
|
||||
return start_type == end_type
|
||||
|
||||
if not types_check():
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Segments types do not match or one of the segments doesn't have type which is required for a bend.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
profile = tool.Model.get_flow_segment_profile(start_element)
|
||||
if not profile.is_a("IfcRectangleProfileDef") and not profile.is_a("IfcCircleProfileDef"):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"For now Only IfcRectangleProfileDef/IfcCircleProfileDef profiles supported for a bend, "
|
||||
f"the segments are {profile.is_a()}",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
def get_dim(profile):
|
||||
if profile.is_a("IfcRectangleProfileDef"):
|
||||
return V(profile.XDim / 2, profile.YDim / 2)
|
||||
elif profile.is_a("IfcCircleProfileDef"):
|
||||
return V(profile.Radius, profile.Radius)
|
||||
return None
|
||||
|
||||
# setup start / end points
|
||||
start_object_rotation = start_object.matrix_world.to_quaternion().to_matrix()
|
||||
start_segment_data = MEPGenerator().get_segment_data(start_element)
|
||||
end_segment_data = MEPGenerator().get_segment_data(end_element)
|
||||
points_ports_map = {
|
||||
start_segment_data["start_point"]: start_segment_data["start_port"],
|
||||
start_segment_data["end_point"]: start_segment_data["end_port"],
|
||||
end_segment_data["start_point"]: end_segment_data["start_port"],
|
||||
end_segment_data["end_point"]: end_segment_data["end_port"],
|
||||
}
|
||||
|
||||
get_z_basis = lambda o: tool.Cad.get_basis_vector(o, 2)
|
||||
segments_intersection_ws = tool.Cad.intersect_edges(
|
||||
(start_object.location, start_object.location + get_z_basis(start_object)),
|
||||
(end_object.location, end_object.location + get_z_basis(end_object)),
|
||||
)[0]
|
||||
|
||||
start_point, first_segment_start = tool.Cad.closest_and_furthest_vectors(
|
||||
segments_intersection_ws, (start_segment_data["start_point"], start_segment_data["end_point"])
|
||||
)
|
||||
end_point, second_segment_end = tool.Cad.closest_and_furthest_vectors(
|
||||
segments_intersection_ws, (end_segment_data["start_point"], end_segment_data["end_point"])
|
||||
)
|
||||
|
||||
start_port = points_ports_map[start_point]
|
||||
end_port = points_ports_map[end_point]
|
||||
start_point_on_origin = start_point == start_segment_data["start_point"]
|
||||
start_connection = "ATSTART" if start_point_on_origin else "ATEND"
|
||||
start_segment_sign = -1 if start_point_on_origin else 1
|
||||
|
||||
end_point_on_origin = end_point == end_segment_data["start_point"]
|
||||
end_connection = "ATSTART" if end_point_on_origin else "ATEND"
|
||||
end_segment_sign = -1 if end_point_on_origin else 1
|
||||
|
||||
profile_dim = get_dim(profile) * si_conversion
|
||||
|
||||
# TODO: profile offset may need to be flipped (check transition code)
|
||||
to_start_object_space = start_object_rotation.inverted()
|
||||
profile_offset = (to_start_object_space @ end_point) - (to_start_object_space @ start_point)
|
||||
|
||||
def check_for_double_bends():
|
||||
# The theory is To avoid double bends, the profile offset should occur along only two axes:
|
||||
# 1) The local Z-axis of the start segment
|
||||
# 2) One of the lateral axes (either X or Y)
|
||||
#
|
||||
# Double bend required when:
|
||||
# - there are 2 or 0 lateral axes involved
|
||||
# - offset appear by the non-lateral axis
|
||||
#
|
||||
# NOTE: some double bends are only possible for square profiles:
|
||||
# https://i.imgur.com/ZhdGbEp.png
|
||||
|
||||
z_axis_end_object = end_object.matrix_world.col[2].normalized().to_3d()
|
||||
z_axis_end_object_local = to_start_object_space @ z_axis_end_object
|
||||
lateral_axes = [i for i in range(2) if not tool.Cad.is_x(z_axis_end_object_local[i], 0)]
|
||||
|
||||
if len(lateral_axes) != 1:
|
||||
return (
|
||||
None,
|
||||
f"For now only one lateral axis is supported for a bend (double bends not supported). Found lateral axes: {len(lateral_axes)}.",
|
||||
)
|
||||
|
||||
non_lateral_axis = 0 if lateral_axes[0] == 1 else 1
|
||||
non_lateral_axis_offset = profile_offset[non_lateral_axis]
|
||||
if not tool.Cad.is_x(non_lateral_axis_offset, 0):
|
||||
return (
|
||||
None,
|
||||
"For now offset by non-lateral axis is not supported for a bend (double bends not supported).\n"
|
||||
f"Detected an offset of {round(non_lateral_axis_offset, 5)} along the local axis {'XY'[non_lateral_axis]} when lateral axis is {'XY'[lateral_axes[0]]}.",
|
||||
)
|
||||
|
||||
return lateral_axes[0], None
|
||||
|
||||
lateral_axis, error_msg = check_for_double_bends()
|
||||
if error_msg:
|
||||
self.report({"ERROR"}, error_msg)
|
||||
return {"CANCELLED"}
|
||||
non_lateral_axis = 0 if lateral_axis == 1 else 1
|
||||
|
||||
def get_bend_rotation():
|
||||
O = V(0, 0, 0)
|
||||
edge1 = (get_z_basis(start_object) * start_segment_sign, O)
|
||||
edge2 = (get_z_basis(end_object) * end_segment_sign, O)
|
||||
angle = pi - tool.Cad.angle_edges(edge1, edge2)
|
||||
axis = (edge2[1] - edge2[0]).cross(edge1[1] - edge1[0])
|
||||
return angle, axis
|
||||
|
||||
angle, rotation_axis = get_bend_rotation()
|
||||
|
||||
lateral_sign = tool.Cad.sign(profile_offset[lateral_axis])
|
||||
radial_offset = V(0, 0, 0)
|
||||
ref_point_radius = self.radius + profile_dim[lateral_axis]
|
||||
radial_offset[lateral_axis] = ref_point_radius * (1 - cos(angle)) * lateral_sign
|
||||
radial_offset.z = ref_point_radius * sin(angle)
|
||||
|
||||
def get_segments_extend():
|
||||
segments_intersection = segments_intersection_ws - start_point
|
||||
segments_intersection = to_start_object_space @ segments_intersection
|
||||
|
||||
# since tangent segments are equal
|
||||
# if drawn for the circle from the same point
|
||||
required_offset = ref_point_radius * tan(angle / 2)
|
||||
|
||||
current_start_offset = segments_intersection.length
|
||||
current_end_offset = (segments_intersection - profile_offset).length
|
||||
|
||||
start_extend = current_start_offset - (required_offset + self.start_length)
|
||||
end_extend = current_end_offset - (required_offset + self.end_length)
|
||||
|
||||
return start_extend, end_extend
|
||||
|
||||
def check_new_segment_length(start_point, end_point, extend_point):
|
||||
"""Check if segment is placed too near to the bend point.
|
||||
|
||||
The idea is that we can either extend segment toward the bend
|
||||
but we can shrink it only until it's start.
|
||||
|
||||
If the segment is too near it will return offset to fix the problem,
|
||||
otherwise returns `None`.
|
||||
|
||||
"""
|
||||
base_edge = end_point - start_point
|
||||
new_edge = extend_point - start_point
|
||||
projection = new_edge.dot(base_edge.normalized())
|
||||
if projection < 0 or tool.Cad.is_x(projection, 0):
|
||||
return projection
|
||||
return None
|
||||
|
||||
# adjust segments to fit the radius and angle
|
||||
start_segment_extend, end_segment_extend = get_segments_extend()
|
||||
start_segment_extend_point = start_point + start_segment_sign * start_segment_extend * get_z_basis(start_object)
|
||||
projection = check_new_segment_length(first_segment_start, start_point, start_segment_extend_point)
|
||||
if projection is not None:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Start segment starts too near to the bend, need to offset it atleast by {round(projection, 3)} m.",
|
||||
)
|
||||
return {"ERROR"}
|
||||
|
||||
end_segment_extend_point = end_point + end_segment_sign * end_segment_extend * get_z_basis(end_object)
|
||||
projection = check_new_segment_length(second_segment_end, end_point, end_segment_extend_point)
|
||||
if projection is not None:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"End segment starts too near to the bend, need to offset it atleast by {round(projection, 3)} m.",
|
||||
)
|
||||
return {"ERROR"}
|
||||
|
||||
DumbProfileJoiner().join_E(start_object, start_segment_extend_point, start_connection)
|
||||
DumbProfileJoiner().join_E(end_object, end_segment_extend_point, end_connection)
|
||||
|
||||
context.view_layer.update() # update matrices
|
||||
|
||||
builder = ShapeBuilder(ifc_file)
|
||||
rep, bend_data = builder.mep_bend_shape(
|
||||
start_element,
|
||||
self.start_length / si_conversion,
|
||||
self.end_length / si_conversion,
|
||||
angle,
|
||||
self.radius / si_conversion,
|
||||
profile_offset / si_conversion,
|
||||
flip_z_axis=start_segment_sign == -1,
|
||||
)
|
||||
|
||||
parametric_data = {
|
||||
"start_length": self.start_length / si_conversion,
|
||||
"end_length": self.end_length / si_conversion,
|
||||
"radius": self.radius / si_conversion,
|
||||
"angle": degrees(angle),
|
||||
"main_profile_dimension": profile_dim[lateral_axis] / si_conversion,
|
||||
}
|
||||
# find the compatible fitting type
|
||||
fitting_data = MEPGenerator().get_compatible_fitting_type(
|
||||
[start_element, end_element], [start_port, end_port], "BEND", bbim_data=parametric_data
|
||||
)
|
||||
bend_type = fitting_data["fitting_type"] if fitting_data else None
|
||||
start_port_match = fitting_data["start_port_match"] if fitting_data else True
|
||||
|
||||
# use current segments axes if no fitting type found
|
||||
lateral_axis_type = lateral_axis
|
||||
lateral_sign_type = lateral_sign
|
||||
z_sign_type = start_segment_sign
|
||||
non_lateral_axis_type = non_lateral_axis
|
||||
if bend_type:
|
||||
bend_obj = tool.Ifc.get_object(bend_type)
|
||||
bbim_data = tool.Model.get_modeling_bbim_pset_data(bend_obj, "BBIM_Fitting")["data_dict"]
|
||||
lateral_axis_type, lateral_sign_type = bbim_data["lateral_axis"], bbim_data["lateral_sign"]
|
||||
non_lateral_axis_type = 0 if lateral_axis_type == 1 else 1
|
||||
z_sign_type = bbim_data.get("z_axis_sign", None)
|
||||
# TODO: drop flip_z_axis a bit later
|
||||
if z_sign_type is None:
|
||||
z_sign_type = -1 if bbim_data["flip_z_axis"] else 1
|
||||
|
||||
# TODO: handle the case without creating a representation in the first place?
|
||||
ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=rep)
|
||||
else: # create new fitting type if nothing is compatible
|
||||
mesh = bpy.data.meshes.new("Bend")
|
||||
obj = bpy.data.objects.new("Bend", mesh)
|
||||
bend_type = blenderbim.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Root,
|
||||
obj=obj,
|
||||
ifc_class=MEPGenerator().get_mep_element_class_name(start_element, "FittingType"),
|
||||
predefined_type="BEND",
|
||||
should_add_representation=False,
|
||||
)
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
tool.Model.replace_object_ifc_representation(body, obj, rep)
|
||||
tool.Blender.remove_data_block(mesh)
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=bend_type, name="BBIM_Fitting")
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Data": json.dumps(bend_data, default=list)},
|
||||
)
|
||||
tool.System.add_ports(obj, offset_end_port=start_object_rotation @ (radial_offset * V(1, 1, 0)))
|
||||
|
||||
# NOTE: at this point we loose current blender objects selection
|
||||
# create transition element
|
||||
bpy.ops.bim.add_constr_type_instance(relating_type_id=bend_type.id())
|
||||
fitting_obj = bpy.context.active_object
|
||||
|
||||
# adjust fitting object rotation and location
|
||||
# required since we'll base our `fitting_obj_dir` on this
|
||||
fitting_obj.matrix_world = start_object.matrix_world
|
||||
context.view_layer.update()
|
||||
|
||||
# depending on bend direction we may need to rotate it to match
|
||||
# we just calculate the matrix basises - it's simpler than describing all possible conditions
|
||||
def get_fitting_matrix():
|
||||
matrix = Matrix.Identity(3)
|
||||
start_object_z_basis = tool.Cad.get_basis_vector(start_object, 2)
|
||||
start_object_lateral_basis = tool.Cad.get_basis_vector(start_object, lateral_axis)
|
||||
|
||||
def axis_direction(current_axis_sign, type_axis_sign):
|
||||
return -1 if current_axis_sign != type_axis_sign else 1
|
||||
|
||||
matrix.col[2] = start_object_z_basis * axis_direction(start_segment_sign, z_sign_type)
|
||||
matrix.col[lateral_axis_type] = start_object_lateral_basis * axis_direction(lateral_sign, lateral_sign_type)
|
||||
if not start_port_match:
|
||||
matrix.col[2] *= -1
|
||||
|
||||
if non_lateral_axis_type == 0:
|
||||
non_lateral_axis = matrix.col[lateral_axis_type].cross(matrix.col[2])
|
||||
else:
|
||||
non_lateral_axis = matrix.col[2].cross(matrix.col[lateral_axis_type])
|
||||
matrix.col[non_lateral_axis_type] = non_lateral_axis
|
||||
|
||||
if not start_port_match:
|
||||
angle_sign = np.sign(rotation_axis.dot(non_lateral_axis))
|
||||
matrix = matrix @ Matrix.Rotation(angle * angle_sign, 3, "XY"[non_lateral_axis_type])
|
||||
|
||||
matrix = matrix.to_4x4()
|
||||
matrix.translation = start_segment_extend_point if start_port_match else end_segment_extend_point
|
||||
return matrix
|
||||
|
||||
fitting_obj.matrix_world = get_fitting_matrix()
|
||||
|
||||
# add ports and connect them
|
||||
ports = tool.System.get_ports(tool.Ifc.get_entity(fitting_obj))
|
||||
if not start_port_match:
|
||||
start_port, end_port = end_port, start_port
|
||||
tool.Ifc.run("system.connect_port", port1=ports[0], port2=start_port, direction="NOTDEFINED")
|
||||
tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED")
|
||||
|
||||
self.report({"INFO"}, f"Success!.. kind of. The angle was {round(bend_data['angle'])}")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -274,9 +274,7 @@ class FilledOpeningGenerator:
|
||||
get_curve_2d_from_3d(profile),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=Vector([0.0, -thickness * 0.5 / unit_scale, 0.0]),
|
||||
position_x_axis=Vector((1, 0, 0)),
|
||||
position_z_axis=Vector((0, -1, 0)),
|
||||
extrusion_vector=Vector((0, 0, -1)),
|
||||
**shape_builder.extrude_kwargs("Y")
|
||||
)
|
||||
return shape_builder.get_representation(context, [extrusion])
|
||||
|
||||
@@ -309,9 +307,7 @@ class FilledOpeningGenerator:
|
||||
shape_builder.rectangle(size=opening_size),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=opening_position,
|
||||
position_z_axis=Vector((0.0, -1.0, 0.0)),
|
||||
position_x_axis=Vector((1.0, 0.0, 0.0)),
|
||||
extrusion_vector=Vector((0.0, 0.0, -1.0)),
|
||||
**shape_builder.extrude_kwargs("Y")
|
||||
)
|
||||
|
||||
return shape_builder.get_representation(context, [extrusion])
|
||||
|
||||
@@ -185,8 +185,13 @@ class AddConstrTypeInstance(bpy.types.Operator):
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
|
||||
tool.Blender.remove_data_block(mesh) # Remove "Instance" mesh
|
||||
|
||||
mesh_data = obj.data
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
|
||||
if obj.data != mesh_data: # remove orphaned mesh from "bim.assign_class"
|
||||
tool.Blender.remove_data_block(mesh_data)
|
||||
|
||||
# Update required as core.type.assign_type may change obj.data
|
||||
# TODO: This is inefficient. It literally creates a mesh, then potentially removes it.
|
||||
|
||||
@@ -132,6 +132,7 @@ class DumbProfileGenerator:
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Blender.remove_data_block(mesh)
|
||||
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbProfile"})
|
||||
|
||||
@@ -132,7 +132,7 @@ class BIMModelProperties(PropertyGroup):
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.5, subtype="DISTANCE", description="Size by Y axis for the opening")
|
||||
z: bpy.props.FloatProperty(name="Z", default=0.5, subtype="DISTANCE", description="Size by Z axis for the opening")
|
||||
# Used for things like walls, doors, flooring, skirting, etc
|
||||
rl1: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for walls")
|
||||
rl1: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for walls")
|
||||
# Used for things like windows, other hosted furniture, and MEP
|
||||
rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows")
|
||||
# Used for plan calculation points such as in room generation
|
||||
|
||||
@@ -196,6 +196,7 @@ class DumbSlabGenerator:
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Blender.remove_data_block(mesh)
|
||||
|
||||
if self.footprint_context:
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
|
||||
@@ -23,6 +23,7 @@ import shapely
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.spatial as core
|
||||
import blenderbim.core.type
|
||||
from math import pi
|
||||
from mathutils import Vector, Matrix
|
||||
@@ -230,182 +231,9 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
# walls (i.e. prismatic) in the active object storey.
|
||||
# In order to run, the active objct must be a wall and
|
||||
# have to be selected walls
|
||||
props = context.scene.BIMModelProperties
|
||||
active_obj = bpy.context.active_object
|
||||
|
||||
if not active_obj:
|
||||
self.report({"ERROR"}, "No active object. Please select a wall")
|
||||
return
|
||||
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element and not element.is_a("IfcWall"):
|
||||
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if not container:
|
||||
self.report({"ERROR"}, "The wall is not contained.")
|
||||
|
||||
if not bpy.context.selected_objects:
|
||||
self.report({"ERROR"}, "No selected objects found. Please select walls.")
|
||||
return
|
||||
|
||||
x, y, z = active_obj.matrix_world.translation.xyz
|
||||
mat = active_obj.matrix_world
|
||||
h = active_obj.dimensions.z
|
||||
selected_objects = bpy.context.selected_objects
|
||||
|
||||
boundary_elements = self.get_boundary_elements(selected_objects)
|
||||
|
||||
polys = self.get_polygons(boundary_elements)
|
||||
|
||||
converted_tolerance = self.get_converted_tolerance(tolerance=0.03)
|
||||
|
||||
union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style=2, join_style=2)
|
||||
|
||||
union = self.get_purged_inner_holes_poly(union_geom=union, min_area=self.get_converted_tolerance(tolerance=3))
|
||||
|
||||
for i, linear_ring in enumerate(union.interiors):
|
||||
poly = Polygon(linear_ring)
|
||||
poly = poly.buffer(converted_tolerance, single_sided=True, cap_style=2, join_style=2)
|
||||
|
||||
bm = self.get_bmesh_from_polygon(poly, mat, h)
|
||||
|
||||
name = "Space" + str(i)
|
||||
mesh = bpy.data.meshes.new(name=name)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
obj.matrix_world = mat
|
||||
|
||||
self.set_obj_origin_to_bboxcenter(obj)
|
||||
|
||||
if z != 0:
|
||||
obj.location = obj.location + Vector((0, 0, z))
|
||||
|
||||
context.view_layer.active_layer_collection.collection.objects.link(obj)
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
blenderbim.core.spatial.assign_container(
|
||||
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=container_obj, element_obj=obj
|
||||
)
|
||||
|
||||
def get_boundary_elements(self, selected_objects):
|
||||
boundary_elements = []
|
||||
for obj in selected_objects:
|
||||
subelement = tool.Ifc.get_entity(obj)
|
||||
if subelement.is_a("IfcWall") or subelement.is_a("IfcColumn"):
|
||||
boundary_elements.append(subelement)
|
||||
return boundary_elements
|
||||
|
||||
def get_polygons(self, boundary_elements):
|
||||
polys = []
|
||||
for boundary_element in boundary_elements:
|
||||
obj = tool.Ifc.get_object(boundary_element)
|
||||
if not obj:
|
||||
continue
|
||||
points = []
|
||||
base = self.get_obj_base_points(obj)
|
||||
for index in ["low_left", "low_right", "high_right", "high_left"]:
|
||||
point = base[index]
|
||||
points.append(point)
|
||||
|
||||
polys.append(Polygon(points))
|
||||
return polys
|
||||
|
||||
def get_obj_base_points(self, obj):
|
||||
x_values = [(obj.matrix_world @ Vector(v)).x for v in obj.bound_box]
|
||||
y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box]
|
||||
return {
|
||||
"low_left": (x_values[0], y_values[0]),
|
||||
"high_left": (x_values[3], y_values[3]),
|
||||
"low_right": (x_values[4], y_values[4]),
|
||||
"high_right": (x_values[7], y_values[7]),
|
||||
}
|
||||
|
||||
def get_converted_tolerance(self, tolerance):
|
||||
model = tool.Ifc.get()
|
||||
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT")
|
||||
prefix = getattr(project_unit, "Prefix", None)
|
||||
|
||||
converted_tolerance = ifcopenshell.util.unit.convert(
|
||||
value=tolerance,
|
||||
from_prefix=None,
|
||||
from_unit="METRE",
|
||||
to_prefix=prefix,
|
||||
to_unit=project_unit.Name,
|
||||
)
|
||||
return tolerance
|
||||
|
||||
def get_purged_inner_holes_poly(self, union_geom, min_area):
|
||||
interiors_list = []
|
||||
|
||||
if union_geom.geom_type == "MultiPolygon":
|
||||
for poly in union_geom.geoms:
|
||||
interiors_list = self.get_poly_valid_interior_list(
|
||||
poly=poly, min_area=min_area, interiors_list=interiors_list
|
||||
)
|
||||
|
||||
new_poly = Polygon(poly.exterior.coords, holes=interiors_list)
|
||||
|
||||
if union_geom.geom_type == "Polygon":
|
||||
interiors_list = self.get_poly_valid_interior_list(
|
||||
poly=union_geom, min_area=min_area, interiors_list=interiors_list
|
||||
)
|
||||
new_poly = Polygon(union_geom.exterior.coords, holes=interiors_list)
|
||||
|
||||
return new_poly
|
||||
|
||||
def get_poly_valid_interior_list(self, poly, min_area, interiors_list):
|
||||
for interior in poly.interiors:
|
||||
p = Polygon(interior)
|
||||
if p.area >= min_area:
|
||||
interiors_list.append(interior)
|
||||
return interiors_list
|
||||
|
||||
def get_bmesh_from_polygon(self, poly, mat, h):
|
||||
bm = bmesh.new()
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
mat_invert = mat.inverted()
|
||||
|
||||
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], 0])) for v in poly.exterior.coords[0:-1]]
|
||||
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
|
||||
bmesh.ops.triangle_fill(bm, edges=bm.edges)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
|
||||
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
|
||||
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
|
||||
return bm
|
||||
|
||||
def set_obj_origin_to_bboxcenter(self, obj):
|
||||
mat = obj.matrix_world
|
||||
inverted = mat.inverted()
|
||||
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
|
||||
global_bbox_center = mat @ local_bbox_center
|
||||
|
||||
oldLoc = obj.location
|
||||
newLoc = global_bbox_center
|
||||
diff = newLoc - oldLoc
|
||||
for vert in obj.data.vertices:
|
||||
aux_vector = mat @ vert.co
|
||||
aux_vector = aux_vector - diff
|
||||
vert.co = inverted @ aux_vector
|
||||
obj.location = newLoc
|
||||
|
||||
# In order to run, the active object must be a wall and
|
||||
# there must be selected walls
|
||||
core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector)
|
||||
|
||||
class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.toggle_space_visibility"
|
||||
@@ -414,26 +242,6 @@ class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Change the space visibility"
|
||||
|
||||
def execute(cls, context):
|
||||
model = tool.Ifc.get()
|
||||
core.toggle_space_visibility(tool.Ifc, tool.Spatial)
|
||||
return {"FINISHED"}
|
||||
|
||||
spaces = model.by_type("IfcSpace")
|
||||
|
||||
if not spaces:
|
||||
print(spaces)
|
||||
return {"FINISHED"}
|
||||
|
||||
first_obj = tool.Ifc.get_object(spaces[0])
|
||||
|
||||
if bpy.data.objects[first_obj.name].display_type == "TEXTURED":
|
||||
for space in spaces:
|
||||
obj = tool.Ifc.get_object(space)
|
||||
bpy.data.objects[obj.name].show_wire = True
|
||||
bpy.data.objects[obj.name].display_type = "WIRE"
|
||||
return {"FINISHED"}
|
||||
|
||||
elif bpy.data.objects[first_obj.name].display_type == "WIRE":
|
||||
for space in spaces:
|
||||
obj = tool.Ifc.get_object(space)
|
||||
bpy.data.objects[obj.name].show_wire = False
|
||||
bpy.data.objects[obj.name].display_type = "TEXTURED"
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -188,10 +188,8 @@ class BIM_PT_array(bpy.types.Panel):
|
||||
if ArrayData.data["parameters"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=ArrayData.data["parameters"]["parent_name"], icon="CON_CHILDOF")
|
||||
op = row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="")
|
||||
op.parent = ArrayData.data["parameters"]["Parent"]
|
||||
op = row.operator("bim.select_all_array_objects", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.parent = ArrayData.data["parameters"]["Parent"]
|
||||
row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="")
|
||||
row.operator("bim.select_all_array_objects", icon="RESTRICT_SELECT_OFF", text="")
|
||||
|
||||
if ArrayData.data["parameters"]["data_dict"]:
|
||||
row.operator("bim.add_array", icon="ADD", text="")
|
||||
|
||||
@@ -637,6 +637,7 @@ class DumbWallGenerator:
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Blender.remove_data_block(mesh)
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbLayer2"})
|
||||
obj.select_set(True)
|
||||
@@ -827,10 +828,10 @@ class DumbWallJoiner:
|
||||
return
|
||||
|
||||
for rel in element1.ConnectedTo:
|
||||
if rel.RelatingConnectionType in ["ATSTART", "ATEND"]:
|
||||
if rel.is_a("IfcRelConnectsPathElements") and rel.RelatingConnectionType in ["ATSTART", "ATEND"]:
|
||||
rel.RelatingConnectionType = "ATSTART" if rel.RelatingConnectionType == "ATEND" else "ATEND"
|
||||
for rel in element1.ConnectedFrom:
|
||||
if rel.RelatedConnectionType in ["ATSTART", "ATEND"]:
|
||||
if rel.is_a("IfcRelConnectsPathElements") and rel.RelatedConnectionType in ["ATSTART", "ATEND"]:
|
||||
rel.RelatedConnectionType = "ATSTART" if rel.RelatedConnectionType == "ATEND" else "ATEND"
|
||||
|
||||
layers1 = tool.Model.get_material_layer_parameters(element1)
|
||||
@@ -1119,8 +1120,13 @@ class DumbWallJoiner:
|
||||
other = tool.Ifc.get_object(rel.RelatedElement)
|
||||
if connection not in ["ATPATH", "NOTDEFINED"]:
|
||||
self.join(
|
||||
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
|
||||
)
|
||||
obj,
|
||||
other,
|
||||
connection,
|
||||
rel.RelatedConnectionType,
|
||||
is_relating=True,
|
||||
description=rel.Description
|
||||
)
|
||||
for rel in element.ConnectedFrom:
|
||||
if rel.is_a("IfcRelConnectsPathElements"):
|
||||
connection = rel.RelatedConnectionType
|
||||
@@ -1299,7 +1305,7 @@ class DumbWallJoiner:
|
||||
# The user has moved the wall into an invalid position that cannot connect at the desired end
|
||||
return False
|
||||
|
||||
self.axis[1 if connection1 == "ATEND" else 0] = intersect
|
||||
self.axis = proposed_axis
|
||||
|
||||
# Work out body
|
||||
|
||||
@@ -1309,55 +1315,79 @@ class DumbWallJoiner:
|
||||
tp1 = wall1.matrix_world @ Vector(wall1.bound_box[1])
|
||||
|
||||
# Axis lines on bottom, for reference, base, and side axes
|
||||
bra1 = (Vector((*axis1["reference"][0], bp1[2])), Vector((*axis1["reference"][1], bp1[2])))
|
||||
bba1 = (Vector((*axis1["base"][0], bp1[2])), Vector((*axis1["base"][1], bp1[2])))
|
||||
bsa1 = (Vector((*axis1["side"][0], bp1[2])), Vector((*axis1["side"][1], bp1[2])))
|
||||
bba2 = (Vector((*axis2["base"][0], bp2[2])), Vector((*axis2["base"][1], bp2[2])))
|
||||
bsa2 = (Vector((*axis2["side"][0], bp2[2])), Vector((*axis2["side"][1], bp2[2])))
|
||||
def to_3d_axis(axis, z):
|
||||
return (Vector((*axis[0], z)), Vector((*axis[1], z)))
|
||||
|
||||
bra1 = to_3d_axis(axis1["reference"], bp1.z)
|
||||
bba1 = to_3d_axis(axis1["base"], bp1.z)
|
||||
tba1 = to_3d_axis(axis1["base"], tp1.z)
|
||||
bsa1 = to_3d_axis(axis1["side"], bp1.z)
|
||||
bba2 = to_3d_axis(axis2["base"], bp2.z)
|
||||
bsa2 = to_3d_axis(axis2["side"], bp2.z)
|
||||
|
||||
# Intersecting the walls sides defined by planes gives 4 lines of intersection
|
||||
# Line point, and line direction
|
||||
lp1, ld1 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bba2[0], normal2)
|
||||
lp2, ld2 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bsa2[0], normal2)
|
||||
lp3, ld3 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bba2[0], normal2)
|
||||
lp4, ld4 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bsa2[0], normal2)
|
||||
lpb1, ldb1 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bba2[0], normal2)
|
||||
lpb2, ldb2 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bsa2[0], normal2)
|
||||
lps1, lds1 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bba2[0], normal2)
|
||||
lps2, lds2 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bsa2[0], normal2)
|
||||
|
||||
# Intersecting the 4 lines gives the 8 possible verts of intersection
|
||||
# 4 on bottom, and 4 on top. 4 on our base line, 4 on our side line.
|
||||
bb1 = mathutils.geometry.intersect_line_plane(lp1, lp1 + ld1, bp1, Vector((0, 0, 1)))
|
||||
bb2 = mathutils.geometry.intersect_line_plane(lp2, lp2 + ld2, bp1, Vector((0, 0, 1)))
|
||||
bs1 = mathutils.geometry.intersect_line_plane(lp3, lp3 + ld3, bp1, Vector((0, 0, 1)))
|
||||
bs2 = mathutils.geometry.intersect_line_plane(lp4, lp4 + ld4, bp1, Vector((0, 0, 1)))
|
||||
tb1 = mathutils.geometry.intersect_line_plane(lp1, lp1 + ld1, tp1, Vector((0, 0, 1)))
|
||||
tb2 = mathutils.geometry.intersect_line_plane(lp2, lp2 + ld2, tp1, Vector((0, 0, 1)))
|
||||
ts1 = mathutils.geometry.intersect_line_plane(lp3, lp3 + ld3, tp1, Vector((0, 0, 1)))
|
||||
ts2 = mathutils.geometry.intersect_line_plane(lp4, lp4 + ld4, tp1, Vector((0, 0, 1)))
|
||||
# Diagram: https://i.imgur.com/jwWx2Ox.png
|
||||
# NOTE: bb/bs always equal lpb/lps?
|
||||
bb1 = mathutils.geometry.intersect_line_plane(lpb1, lpb1 + ldb1, bp1, Vector((0, 0, 1)))
|
||||
bb2 = mathutils.geometry.intersect_line_plane(lpb2, lpb2 + ldb2, bp1, Vector((0, 0, 1)))
|
||||
bs1 = mathutils.geometry.intersect_line_plane(lps1, lps1 + lds1, bp1, Vector((0, 0, 1)))
|
||||
bs2 = mathutils.geometry.intersect_line_plane(lps2, lps2 + lds2, bp1, Vector((0, 0, 1)))
|
||||
|
||||
# similar to bb/bs but also have local z offset
|
||||
tb1 = mathutils.geometry.intersect_line_plane(lpb1, lpb1 + ldb1, tp1, Vector((0, 0, 1)))
|
||||
tb2 = mathutils.geometry.intersect_line_plane(lpb2, lpb2 + ldb2, tp1, Vector((0, 0, 1)))
|
||||
ts1 = mathutils.geometry.intersect_line_plane(lps1, lps1 + lds1, tp1, Vector((0, 0, 1)))
|
||||
ts2 = mathutils.geometry.intersect_line_plane(lps2, lps2 + lds2, tp1, Vector((0, 0, 1)))
|
||||
|
||||
# Let's distinguish the 8 points by whether they are nearer or further away from the other end
|
||||
# These 8 points will be used to find the final body position and clippings.
|
||||
i = 0 if connection1 == "ATEND" else 1
|
||||
j = 1 if connection1 == "ATEND" else 0
|
||||
bbn = tool.Cad.closest_vector(axis1["base"][i].to_3d(), (bb1, bb2))
|
||||
bbf = bb2 if bbn == bb1 else bb1
|
||||
bsn = tool.Cad.closest_vector(axis1["side"][i].to_3d(), (bs1, bs2))
|
||||
bsf = bs2 if bsn == bs1 else bs1
|
||||
tbn = tool.Cad.closest_vector(axis1["base"][i].to_3d(), (tb1, tb2))
|
||||
tbf = tb2 if tbn == tb1 else tb1
|
||||
tsn = tool.Cad.closest_vector(axis1["side"][i].to_3d(), (ts1, ts2))
|
||||
tsf = ts2 if tsn == ts1 else ts1
|
||||
connected_at_end = connection1 == "ATEND"
|
||||
i = 0 if connected_at_end else 1
|
||||
|
||||
def get_closest_and_furthest_vectors(ref_point_2d, vectors, clamp_axis=None):
|
||||
def clamp_point_by_direction(point, edge):
|
||||
percent = tool.Cad.edge_percent(point, edge)
|
||||
if percent < 0:
|
||||
return edge[0]
|
||||
return point
|
||||
|
||||
# When there is a small angle between walls, intersection points can occur outside the wall's axis.
|
||||
# Which can lead to inaccuracies - therefore we bottom clamp them to stay within the axis
|
||||
if clamp_axis:
|
||||
# if wall connected at the start then reference point will be at the end
|
||||
# therefore we reverse the axis
|
||||
if not connected_at_end:
|
||||
clamp_axis = clamp_axis[::-1]
|
||||
vectors = tuple([clamp_point_by_direction(v, clamp_axis) for v in vectors])
|
||||
|
||||
return tool.Cad.closest_and_furthest_vectors(ref_point_2d.to_3d(), vectors)
|
||||
|
||||
bbn, bbf = get_closest_and_furthest_vectors(axis1["base"][i], (bb1, bb2), bba1)
|
||||
bsn, bsf = get_closest_and_furthest_vectors(axis1["side"][i], (bs1, bs2))
|
||||
tbn, tbf = get_closest_and_furthest_vectors(axis1["base"][i], (tb1, tb2), tba1)
|
||||
tsn, tsf = get_closest_and_furthest_vectors(axis1["side"][i], (ts1, ts2))
|
||||
|
||||
j = 1 if connected_at_end else 0
|
||||
if description == "MITRE":
|
||||
# Mitre joints are an unofficial convention
|
||||
bsf_ = tool.Cad.point_on_edge(bsf, bba1)
|
||||
tbf_ = tool.Cad.point_on_edge(tbf, bba1)
|
||||
tsf_ = tool.Cad.point_on_edge(tsf, bba1)
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_)).copy()
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_)).copy()
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_))
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_))
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tsf_)).copy()
|
||||
self.body[j] = tool.Cad.point_on_edge(new_body, bra1).to_2d()
|
||||
|
||||
if connection1 == connection2:
|
||||
if (connection1 == "ATEND" and angle > 0) or (connection1 != "ATEND" and angle < 0):
|
||||
if (connected_at_end and angle > 0) or (not connected_at_end and angle < 0):
|
||||
pt = bbf.to_2d().to_3d()
|
||||
x_axis = bsn - bbf
|
||||
y_axis = tbf - bbf
|
||||
@@ -1366,7 +1396,7 @@ class DumbWallJoiner:
|
||||
x_axis = bsf - bbn
|
||||
y_axis = tbn - bbn
|
||||
else:
|
||||
if (connection1 == "ATEND" and angle < 0) or (connection1 != "ATEND" and angle > 0):
|
||||
if (connected_at_end and angle < 0) or (not connected_at_end and angle > 0):
|
||||
pt = bbf.to_2d().to_3d()
|
||||
x_axis = bsn - bbf
|
||||
y_axis = tbf - bbf
|
||||
@@ -1377,6 +1407,9 @@ class DumbWallJoiner:
|
||||
|
||||
if connection1 != "ATEND":
|
||||
y_axis *= -1
|
||||
|
||||
x_axis.normalize()
|
||||
y_axis.normalize()
|
||||
z_axis = x_axis.cross(y_axis)
|
||||
y_axis = z_axis.cross(x_axis)
|
||||
|
||||
|
||||
@@ -68,9 +68,7 @@ def update_simple_openings(element, opening_width, opening_height):
|
||||
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height]).xz),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
|
||||
position_x_axis=V(1, 0, 0),
|
||||
position_z_axis=V(0, -1, 0),
|
||||
extrusion_vector=V(0, 0, -1),
|
||||
**shape_builder.extrude_kwargs("Y")
|
||||
)
|
||||
|
||||
new_representation = shape_builder.get_representation(context, extrusion)
|
||||
@@ -83,6 +81,8 @@ def update_simple_openings(element, opening_width, opening_height):
|
||||
has_replaced_opening_representation = True
|
||||
|
||||
tool.Model.reload_body_representation(voided_objs)
|
||||
with bpy.context.temp_override(selected_objects=[tool.Ifc.get_object(f) for f in fillings]):
|
||||
bpy.ops.bim.recalculate_fill()
|
||||
|
||||
|
||||
def update_window_modifier_representation(context, obj):
|
||||
@@ -118,6 +118,14 @@ def update_window_modifier_representation(context, obj):
|
||||
}
|
||||
representation_data["panel_properties"].append(panel_data)
|
||||
|
||||
def get_active_representation_context(obj):
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
if active_representation:
|
||||
return active_representation.ContextOfItems
|
||||
return ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
previously_active_context = get_active_representation_context(obj)
|
||||
|
||||
# ELEVATION_VIEW representation
|
||||
profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW")
|
||||
if profile:
|
||||
@@ -128,6 +136,7 @@ def update_window_modifier_representation(context, obj):
|
||||
tool.Model.replace_object_ifc_representation(profile, obj, elevation_representation)
|
||||
|
||||
# MODEL_VIEW representation
|
||||
# (Model/Body defined only BEFORE Plan/Body to prevent #2744)
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
representation_data["context"] = body
|
||||
model_representation = ifcopenshell.api.run("geometry.add_window_representation", ifc_file, **representation_data)
|
||||
@@ -142,13 +151,20 @@ def update_window_modifier_representation(context, obj):
|
||||
)
|
||||
tool.Model.replace_object_ifc_representation(plan, obj, plan_representation)
|
||||
|
||||
# adding switch representation at the end instead of changing order of representations
|
||||
# to prevent #2744
|
||||
# adding switch representation at the end instead of changing order of representations
|
||||
# to prevent #2744
|
||||
if get_active_representation_context(obj) != previously_active_context:
|
||||
previously_active_representation = ifcopenshell.util.representation.get_representation(
|
||||
element,
|
||||
previously_active_context.ContextType,
|
||||
previously_active_context.ContextIdentifier,
|
||||
previously_active_context.TargetView,
|
||||
)
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=model_representation,
|
||||
representation=previously_active_representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=True,
|
||||
|
||||
@@ -24,7 +24,7 @@ import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.type.prop as type_prop
|
||||
from blenderbim.bim.helper import prop_with_search, close_operator_panel
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from blenderbim.bim.module.drawing.data import DecoratorData
|
||||
from blenderbim.bim.module.model.prop import get_ifc_class
|
||||
|
||||
@@ -300,7 +300,12 @@ class BimToolUI:
|
||||
|
||||
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout,
|
||||
"Mitre",
|
||||
"S_Y",
|
||||
"Join two intersecting walls using a mitre joint.\nOther selected wall is connected to the active",
|
||||
)
|
||||
|
||||
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
|
||||
@@ -342,20 +347,25 @@ class BimToolUI:
|
||||
"IfcDuctSegment",
|
||||
"IfcPipeSegment",
|
||||
):
|
||||
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "")
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__
|
||||
)
|
||||
if context.region.type != "TOOL_HEADER":
|
||||
cls.layout.operator("bim.mep_add_bend")
|
||||
cls.layout.operator("bim.mep_add_transition")
|
||||
cls.layout.operator("bim.mep_add_obstruction")
|
||||
cls.layout.operator("bim.mep_connect_elements")
|
||||
else:
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
|
||||
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
|
||||
|
||||
elif (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["pset_data"]
|
||||
and not context.active_object.BIMRailingProperties.is_editing_path
|
||||
tool.Model.is_parametric_railing_active() and not context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
# NOTE: should be above "active_representation_type" = "SweptSolid" check
|
||||
# because it could be a SweptSolid too
|
||||
@@ -364,7 +374,8 @@ class BimToolUI:
|
||||
row.operator("bim.enable_editing_railing_path", text="Edit Railing Path")
|
||||
|
||||
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
|
||||
if not tool.Model.is_parametric_window_active() and not tool.Model.is_parametric_door_active():
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
|
||||
|
||||
elif AuthoringData.data["active_class"] in (
|
||||
"IfcWindow",
|
||||
@@ -385,11 +396,7 @@ class BimToolUI:
|
||||
elif AuthoringData.data["active_class"] in ("IfcSpace",):
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
|
||||
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
and not context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
elif tool.Model.is_parametric_roof_active() and not context.active_object.BIMRoofProperties.is_editing_path:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon=f"EVENT_TAB")
|
||||
row.operator("bim.enable_editing_roof_path", text="Edit Roof Path")
|
||||
@@ -427,6 +434,11 @@ class BimToolUI:
|
||||
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings")
|
||||
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition")
|
||||
|
||||
cls.layout.separator()
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Calculate All Quantities", "S_Q", bpy.ops.bim.calculate_all_quantities.__doc__
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def draw_header_interface(cls):
|
||||
cls.draw_type_selection_interface()
|
||||
@@ -580,22 +592,20 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# NOTE: placing it before the other operations because railing can also be SweptSolid
|
||||
# and it might conflict with one of the conditions below
|
||||
if (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["pset_data"]
|
||||
tool.Model.is_parametric_railing_active()
|
||||
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.enable_editing_railing_path()
|
||||
return
|
||||
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
and not bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
elif tool.Model.is_parametric_roof_active() and not bpy.context.active_object.BIMRoofProperties.is_editing_path:
|
||||
# undo the unselection done above because roof has no usage type
|
||||
bpy.ops.bim.enable_editing_roof_path()
|
||||
return
|
||||
|
||||
elif tool.Model.is_parametric_window_active() or tool.Model.is_parametric_door_active():
|
||||
return
|
||||
|
||||
selected_usages = {}
|
||||
for obj in bpy.context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -669,7 +679,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if self.active_material_usage == "LAYER2":
|
||||
bpy.ops.bim.recalculate_wall()
|
||||
elif self.active_material_usage == "PROFILE":
|
||||
bpy.ops.bim.recalculate_profile()
|
||||
if self.active_class in (
|
||||
"IfcCableCarrierSegment",
|
||||
"IfcCableSegment",
|
||||
"IfcDuctSegment",
|
||||
"IfcPipeSegment",
|
||||
):
|
||||
bpy.ops.bim.regenerate_distribution_element()
|
||||
else:
|
||||
bpy.ops.bim.recalculate_profile()
|
||||
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
|
||||
bpy.ops.bim.recalculate_fill()
|
||||
elif self.active_class in ("IfcSpace"):
|
||||
|
||||
@@ -223,7 +223,9 @@ class PurgeUnusedProfiles(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
core.purge_unused_profiles(tool.Ifc, tool.Profile)
|
||||
purged_profiles = core.purge_unused_profiles(tool.Ifc, tool.Profile)
|
||||
self.report({"INFO"}, f"{purged_profiles} profiles were purged.")
|
||||
|
||||
if props.is_editing:
|
||||
refresh()
|
||||
bpy.ops.bim.load_profiles()
|
||||
|
||||
@@ -70,6 +70,7 @@ addon_keymaps = []
|
||||
def register():
|
||||
bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties)
|
||||
bpy.types.TOPBAR_MT_file.prepend(ui.file_menu)
|
||||
bpy.types.TOPBAR_MT_file_context_menu.prepend(ui.file_menu)
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.get("Window")
|
||||
@@ -87,6 +88,7 @@ def register():
|
||||
|
||||
def unregister():
|
||||
bpy.types.TOPBAR_MT_file.remove(ui.file_menu)
|
||||
bpy.types.TOPBAR_MT_file_context_menu.remove(ui.file_menu)
|
||||
del bpy.types.Scene.BIMProjectProperties
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
|
||||
@@ -355,8 +355,8 @@ class AppendEntireLibrary(bpy.types.Operator):
|
||||
self.file = IfcStore.get_file()
|
||||
self.library = IfcStore.library_file
|
||||
|
||||
lib_elements = ifcopenshell.util.selector.Selector().parse(
|
||||
self.library, ".IfcTypeProduct | .IfcMaterial | .IfcCostSchedule| .IfcProfileDef"
|
||||
lib_elements = ifcopenshell.util.selector.filter_elements(
|
||||
self.library, "IfcTypeProduct, IfcMaterial, IfcCostSchedule, IfcProfileDef"
|
||||
)
|
||||
for element in lib_elements:
|
||||
bpy.ops.bim.append_library_element(definition=element.id())
|
||||
@@ -429,7 +429,7 @@ class AppendLibraryElement(bpy.types.Operator):
|
||||
except:
|
||||
# TODO Remove this terrible code when I refactor this into the core
|
||||
pass
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
def import_material_from_ifc(self, element, context):
|
||||
@@ -768,12 +768,12 @@ class LoadProjectElements(bpy.types.Operator):
|
||||
return elements
|
||||
|
||||
def get_whitelist_elements(self):
|
||||
selector = ifcopenshell.util.selector.Selector()
|
||||
return set(selector.parse(self.file, self.props.filter_query))
|
||||
return set(ifcopenshell.util.selector.filter_elements(self.file, self.props.filter_query))
|
||||
|
||||
def get_blacklist_elements(self):
|
||||
selector = ifcopenshell.util.selector.Selector()
|
||||
return set(self.file.by_type("IfcElement")) - set(selector.parse(self.file, self.props.filter_query))
|
||||
return set(self.file.by_type("IfcElement")) - set(
|
||||
ifcopenshell.util.selector.filter_elements(self.file, self.props.filter_query)
|
||||
)
|
||||
|
||||
|
||||
class ToggleFilterCategories(bpy.types.Operator):
|
||||
@@ -895,6 +895,7 @@ class ReloadLink(bpy.types.Operator):
|
||||
for c in bpy.data.collections
|
||||
if "IfcProject" in c.name and c.library and os.path.basename(c.library.filepath) == selected_filename
|
||||
]
|
||||
|
||||
for library in get_linked_ifcs() or []:
|
||||
library.reload()
|
||||
return {"FINISHED"}
|
||||
@@ -920,7 +921,9 @@ class ToggleLinkSelectability(bpy.types.Operator):
|
||||
|
||||
def get_linked_collections(self):
|
||||
return [
|
||||
c for c in bpy.data.collections if "IfcProject" in c.name and c.library and c.library.filepath == self.filepath
|
||||
c
|
||||
for c in bpy.data.collections
|
||||
if "IfcProject" in c.name and c.library and c.library.filepath == self.filepath
|
||||
]
|
||||
|
||||
|
||||
@@ -974,7 +977,9 @@ class ToggleLinkVisibility(bpy.types.Operator):
|
||||
|
||||
def get_linked_collections(self):
|
||||
return [
|
||||
c for c in bpy.data.collections if "IfcProject" in c.name and c.library and c.library.filepath == self.filepath
|
||||
c
|
||||
for c in bpy.data.collections
|
||||
if "IfcProject" in c.name and c.library and c.library.filepath == self.filepath
|
||||
]
|
||||
|
||||
|
||||
@@ -1079,7 +1084,7 @@ class ExportIFC(bpy.types.Operator):
|
||||
save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath)
|
||||
if save_blend_file:
|
||||
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath)
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved',
|
||||
|
||||
@@ -398,3 +398,10 @@ class BIM_PT_purge(Panel):
|
||||
layout.operator("bim.purge_unused_profiles")
|
||||
layout.operator("bim.purge_unused_types")
|
||||
layout.operator("bim.purge_unused_representations")
|
||||
layout.separator()
|
||||
|
||||
layout.label(text="Purge unused elements by class:")
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.scene.BIMDebugProperties, "ifc_class_purge", text="")
|
||||
row.operator("bim.purge_unused_elements_by_class", text="", icon="TRASH")
|
||||
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
|
||||
|
||||
@@ -71,10 +71,10 @@ mapper = {
|
||||
'Area' : "get_net_side_area",
|
||||
},
|
||||
'Qto_DuctSegmentBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Length' : "get_length",
|
||||
'GrossCrossSectionArea' : None,
|
||||
'NetCrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'OuterSurfaceArea' : "get_outer_surface_area",
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_TransformerBaseQuantities' : {
|
||||
@@ -203,10 +203,10 @@ mapper = {
|
||||
'Weight' : None,
|
||||
},
|
||||
'Qto_DuctFittingBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Length' : "get_length",
|
||||
'GrossCrossSectionArea' : None,
|
||||
'NetCrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'OuterSurfaceArea' : "get_outer_surface_area",
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_UnitaryControlElementBaseQuantities' : {
|
||||
|
||||
@@ -91,7 +91,7 @@ class ObjectPsetsData(Data):
|
||||
return []
|
||||
psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(element.is_a(), pset_only=True)
|
||||
psetnames = cls.format_pset_enum(psets)
|
||||
assigned_names = ifcopenshell.util.element.get_psets(element, psets_only=True).keys()
|
||||
assigned_names = ifcopenshell.util.element.get_psets(element, psets_only=True, should_inherit=False).keys()
|
||||
return [p for p in psetnames if p[0] not in assigned_names]
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -31,6 +31,9 @@ NetFloorArea: it doesn't count the following entities contained in the spatial e
|
||||
NetVolume: like NetFloorArea, it doesn't count the following entities contained in the spatial entity: IfcColumn and IfcWall
|
||||
Also, the entire IfcColumn (or IfcWall) object volume is substracted, so it should be better to substract only the shared volume between IfcSpace and IfcColumn. Look at todo list
|
||||
|
||||
DUCT SEGMENTS AND DUCT FITTINGS
|
||||
The length is calculated like a beam. Also outer surface area. Gross cross section area, net cross section area and weight are not calculated right now because the parametrically cross section area seems filled (without hole).
|
||||
|
||||
WEIGHT
|
||||
The object weight is calculated by multiplying the object mass density with the object volume (net or gross).
|
||||
It's only calculated if the object material has a MassDensity property in the Pset_MaterialCommon.
|
||||
|
||||
@@ -223,7 +223,7 @@ class EnablePsetEditing(bpy.types.Operator):
|
||||
new.is_selected = enum in selected_enum_items
|
||||
else:
|
||||
if prop.is_a("IfcPropertySingleValue"):
|
||||
value = prop.NominalValue.wrappedValue
|
||||
value = prop.NominalValue.wrappedValue if prop.NominalValue else None
|
||||
elif prop.is_a("IfcPhysicalSimpleQuantity"):
|
||||
value = prop[3]
|
||||
new_prop = self.props.properties.add()
|
||||
@@ -356,24 +356,7 @@ class AddPset(bpy.types.Operator, Operator):
|
||||
obj_type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
pset_name = get_pset_props(context, self.obj, self.obj_type).pset_name
|
||||
if self.obj_type == "Object":
|
||||
if context.selected_objects:
|
||||
objects = [o.name for o in tool.Blender.get_selected_objects()]
|
||||
else:
|
||||
objects = [context.active_object.name]
|
||||
else:
|
||||
objects = [self.obj]
|
||||
for obj in objects:
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
|
||||
if not ifc_definition_id:
|
||||
continue
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
if pset_name in blenderbim.bim.schema.ifc.psetqto.get_applicable_names(element.is_a(), pset_only=True):
|
||||
bpy.ops.bim.enable_pset_editing(
|
||||
pset_id=0, pset_name=pset_name, pset_type="PSET", obj=obj, obj_type=self.obj_type
|
||||
)
|
||||
core.add_pset(tool.Ifc, tool.Pset, tool.Blender, obj_name=self.obj, obj_type=self.obj_type)
|
||||
|
||||
|
||||
class AddQto(bpy.types.Operator, Operator):
|
||||
|
||||
@@ -22,7 +22,7 @@ from . import ui, prop, operator
|
||||
classes = (
|
||||
operator.AddPropEnum,
|
||||
operator.AddPropTemplate,
|
||||
operator.AddPsetFile,
|
||||
operator.AddPsetTemplateFile,
|
||||
operator.AddPsetTemplate,
|
||||
operator.DeletePropEnum,
|
||||
operator.DisableEditingPropTemplate,
|
||||
@@ -33,7 +33,7 @@ classes = (
|
||||
operator.EnableEditingPsetTemplate,
|
||||
operator.RemovePropTemplate,
|
||||
operator.RemovePsetTemplate,
|
||||
operator.SavePsetTemplateFile,
|
||||
operator.RemovePsetTemplateFile,
|
||||
prop.PsetTemplate,
|
||||
prop.EnumerationValues,
|
||||
prop.PropTemplate,
|
||||
|
||||
@@ -56,12 +56,12 @@ class PsetTemplatesData:
|
||||
pset_dir = os.path.join(bpy.context.scene.BIMProperties.data_dir, "pset")
|
||||
files = os.listdir(pset_dir)
|
||||
for f in files:
|
||||
results.append((os.path.join(pset_dir, f), f.strip(".ifc"), "Global Pset Template"))
|
||||
results.append((os.path.join(pset_dir, f), os.path.splitext(os.path.basename(f))[0], "Global Pset Template"))
|
||||
|
||||
pset_dir = tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir)
|
||||
if os.path.isdir(pset_dir):
|
||||
for path in pathlib.Path(pset_dir).glob("*.ifc"):
|
||||
results.append((str(path), os.path.basename(str(path)).strip(".ifc"), "Project Pset Template"))
|
||||
results.append((str(path), os.path.splitext(os.path.basename(str(path)))[0], "Project Pset Template"))
|
||||
|
||||
return sorted(results, key=lambda x: x[1])
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@ class Operator:
|
||||
IfcStore.pset_template_file.redo()
|
||||
|
||||
|
||||
class AddPsetFile(bpy.types.Operator):
|
||||
bl_idname = "bim.add_pset_file"
|
||||
bl_label = "Add Pset File"
|
||||
class AddPsetTemplateFile(bpy.types.Operator):
|
||||
bl_idname = "bim.add_pset_template_file"
|
||||
bl_label = "Add Pset Template File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -58,24 +58,15 @@ class AddPsetFile(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
template = ifcopenshell.file()
|
||||
filepath = os.path.join(
|
||||
context.scene.BIMProperties.data_dir,
|
||||
"pset",
|
||||
self.props.new_template_filename + ".ifc",
|
||||
)
|
||||
filepath = os.path.join(context.scene.BIMProperties.data_dir, "pset", self.props.new_template_filename + ".ifc")
|
||||
|
||||
template.create_entity(
|
||||
"IFCPROPERTYSETTEMPLATE",
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
"Name": "Name",
|
||||
"Description": "Description",
|
||||
"TemplateType": "PSET_TYPEDRIVENONLY",
|
||||
"ApplicableEntity": "IfcTypeObject",
|
||||
}
|
||||
)
|
||||
pset_template = ifcopenshell.api.run("pset_template.add_pset_template", template)
|
||||
ifcopenshell.api.run("pset_template.add_prop_template", template, pset_template=pset_template)
|
||||
template.write(filepath)
|
||||
self.props.new_template_filename = ""
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
|
||||
context.scene.BIMPsetTemplateProperties.pset_template_files = filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -86,7 +77,10 @@ class AddPsetTemplate(bpy.types.Operator, Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
template = ifcopenshell.api.run("pset_template.add_pset_template", IfcStore.pset_template_file)
|
||||
ifcopenshell.api.run("pset_template.add_prop_template", IfcStore.pset_template_file, pset_template=template)
|
||||
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
|
||||
context.scene.BIMPsetTemplateProperties.pset_templates = str(template.id())
|
||||
|
||||
|
||||
@@ -104,6 +98,9 @@ class RemovePsetTemplate(bpy.types.Operator, Operator):
|
||||
IfcStore.pset_template_file,
|
||||
**{"pset_template": IfcStore.pset_template_file.by_id(int(props.pset_templates))}
|
||||
)
|
||||
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
|
||||
|
||||
|
||||
class EnableEditingPsetTemplate(bpy.types.Operator):
|
||||
@@ -215,6 +212,9 @@ class EditPsetTemplate(bpy.types.Operator, Operator):
|
||||
}
|
||||
)
|
||||
bpy.ops.bim.disable_editing_pset_template()
|
||||
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
|
||||
|
||||
|
||||
class SavePsetTemplateFile(bpy.types.Operator):
|
||||
@@ -223,7 +223,21 @@ class SavePsetTemplateFile(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemovePsetTemplateFile(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_pset_template_file"
|
||||
bl_label = "Remove Pset Template File"
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
os.remove(IfcStore.pset_template_path)
|
||||
except:
|
||||
pass
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -239,9 +253,12 @@ class AddPropTemplate(bpy.types.Operator, Operator):
|
||||
ifcopenshell.api.run(
|
||||
"pset_template.add_prop_template",
|
||||
IfcStore.pset_template_file,
|
||||
**{"pset_template": IfcStore.pset_template_file.by_id(pset_template_id)}
|
||||
pset_template=IfcStore.pset_template_file.by_id(pset_template_id),
|
||||
)
|
||||
bpy.ops.bim.disable_editing_prop_template()
|
||||
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
|
||||
|
||||
|
||||
class RemovePropTemplate(bpy.types.Operator, Operator):
|
||||
@@ -257,6 +274,9 @@ class RemovePropTemplate(bpy.types.Operator, Operator):
|
||||
IfcStore.pset_template_file,
|
||||
**{"prop_template": IfcStore.pset_template_file.by_id(self.prop_template)}
|
||||
)
|
||||
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
|
||||
|
||||
|
||||
class EditPropTemplate(bpy.types.Operator, Operator):
|
||||
@@ -285,6 +305,9 @@ class EditPropTemplate(bpy.types.Operator, Operator):
|
||||
}
|
||||
)
|
||||
bpy.ops.bim.disable_editing_prop_template()
|
||||
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
|
||||
|
||||
# TODO -This will need to go into the
|
||||
# api code at some point - vulevukusej
|
||||
|
||||
@@ -38,6 +38,7 @@ from bpy.props import (
|
||||
|
||||
def updatePsetTemplateFiles(self, context):
|
||||
IfcStore.pset_template_file = None
|
||||
PsetTemplatesData.is_loaded = False
|
||||
PsetTemplatesData.data["pset_template_files"] = PsetTemplatesData.pset_template_files()
|
||||
PsetTemplatesData.data["pset_templates"] = PsetTemplatesData.pset_templates()
|
||||
PsetTemplatesData.data["prop_templates"] = PsetTemplatesData.prop_templates()
|
||||
@@ -179,7 +180,7 @@ class BIMPsetTemplateProperties(PropertyGroup):
|
||||
pset_template_files: EnumProperty(
|
||||
items=getPsetTemplateFiles, name="Pset Template Files", update=updatePsetTemplateFiles
|
||||
)
|
||||
pset_templates: EnumProperty(items=getPsetTemplates, name="Pset Template Files", update=updatePsetTemplates)
|
||||
pset_templates: EnumProperty(items=getPsetTemplates, name="Pset Templates", update=updatePsetTemplates)
|
||||
active_pset_template_id: IntProperty(name="Active Pset Template Id")
|
||||
active_prop_template_id: IntProperty(name="Active Prop Template Id")
|
||||
active_pset_template: PointerProperty(type=PsetTemplate)
|
||||
|
||||
@@ -38,9 +38,9 @@ class BIM_PT_pset_template(Panel):
|
||||
self.props = context.scene.BIMPsetTemplateProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, self.props, "pset_template_files", text="")
|
||||
row.operator("bim.save_pset_template_file", text="", icon="EXPORT")
|
||||
row.operator("bim.add_pset_file", icon="ADD", text="")
|
||||
prop_with_search(row, self.props, "pset_template_files", text="", icon="FILE")
|
||||
row.operator("bim.add_pset_template_file", icon="ADD", text="")
|
||||
row.operator("bim.remove_pset_template_file", icon="X", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ classes = (
|
||||
operator.ExpandResource,
|
||||
operator.GoToResource,
|
||||
operator.ImportResources,
|
||||
operator.LoadResourceProperties,
|
||||
operator.LoadResources,
|
||||
operator.RemoveResource,
|
||||
operator.RemoveResourceQuantity,
|
||||
|
||||
@@ -33,19 +33,6 @@ class LoadResources(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadResourceProperties(bpy.types.Operator):
|
||||
bl_idname = "bim.load_resource_properties"
|
||||
bl_label = "Load Resource Properties"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
resource: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
core.load_resource_properties(
|
||||
tool.Resource, resource=tool.Ifc.get().by_id(self.resource) if self.resource else None
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddResource(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_resource"
|
||||
bl_label = "Add Resource"
|
||||
@@ -437,4 +424,4 @@ class CalculateResourceUsage(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return False
|
||||
|
||||
def _execute(self, context):
|
||||
core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(tool.Resource.get_highlighted_resource()))
|
||||
core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Resource.get_highlighted_resource())
|
||||
|
||||
@@ -81,8 +81,7 @@ def update_active_resource_index(self, context):
|
||||
|
||||
|
||||
def updateResourceUsage(self, context):
|
||||
props = context.scene.BIMResourceProperties
|
||||
if not props.is_resource_update_enabled:
|
||||
if not context.scene.BIMResourceProperties.is_resource_update_enabled:
|
||||
return
|
||||
if not self.schedule_usage:
|
||||
return
|
||||
|
||||
@@ -212,4 +212,4 @@ class CopyClass(bpy.types.Operator, Operator):
|
||||
objects = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
for obj in objects:
|
||||
core.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
@@ -33,10 +33,6 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
def purge():
|
||||
pass
|
||||
|
||||
|
||||
def get_ifc_predefined_types(self, context):
|
||||
if not IfcClassData.is_loaded:
|
||||
IfcClassData.load()
|
||||
|
||||
@@ -20,7 +20,7 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.ActivateIfcBuildingStoreyFilter,
|
||||
operator.ActivateContainerFilter,
|
||||
operator.ActivateIfcClassFilter,
|
||||
operator.AddFilter,
|
||||
operator.AddFilterGroup,
|
||||
@@ -58,6 +58,7 @@ classes = (
|
||||
prop.SearchQueryGroup,
|
||||
prop.IfcSelectorProperties,
|
||||
ui.BIM_PT_search,
|
||||
ui.BIM_PT_filter,
|
||||
ui.BIM_PT_colour_by_property,
|
||||
ui.BIM_PT_select_similar,
|
||||
ui.BIM_UL_colourscheme,
|
||||
|
||||
@@ -466,8 +466,8 @@ class ToggleFilterSelection(Operator):
|
||||
if props.filter_type == "CLASSES":
|
||||
for ifc_class in props.filter_classes:
|
||||
ifc_class.is_selected = self.selecting_actionbool
|
||||
elif props.filter_type == "BUILDINGSTOREYS":
|
||||
for building_storey in props.filter_building_storeys:
|
||||
elif props.filter_type == "CONTAINER":
|
||||
for building_storey in props.filter_container:
|
||||
building_storey.is_selected = self.selecting_actionbool
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -525,11 +525,11 @@ class ActivateIfcClassFilter(Operator):
|
||||
row.operator("bim.toggle_filter_selection", text="Deselect All").action = "DESELECT"
|
||||
|
||||
|
||||
class ActivateIfcBuildingStoreyFilter(Operator):
|
||||
class ActivateContainerFilter(Operator):
|
||||
"""Filter the current selection by Building Storey"""
|
||||
|
||||
bl_idname = "bim.activate_ifc_building_storey_filter"
|
||||
bl_label = "Filter by Building Storey"
|
||||
bl_idname = "bim.activate_ifc_container_filter"
|
||||
bl_label = "Filter by Container"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -540,27 +540,29 @@ class ActivateIfcBuildingStoreyFilter(Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
props = bpy.context.scene.BIMSearchProperties
|
||||
props.filter_building_storeys.clear()
|
||||
props.filter_container.clear()
|
||||
|
||||
ifc_building_storeys = {}
|
||||
containers = {}
|
||||
containers.setdefault("None", 0)
|
||||
for obj in context.selected_objects:
|
||||
storey = tool.Misc.get_object_storey(obj)
|
||||
if not storey:
|
||||
container = tool.Spatial.get_container(tool.Ifc.get_entity(obj))
|
||||
if not container:
|
||||
containers["None"] += 1
|
||||
continue
|
||||
ifc_building_storeys.setdefault(storey.Name, 0)
|
||||
ifc_building_storeys[storey.Name] += 1
|
||||
containers.setdefault(container.Name, 0)
|
||||
containers[container.Name] += 1
|
||||
|
||||
for name, total in dict(sorted(ifc_building_storeys.items())).items():
|
||||
new = props.filter_building_storeys.add()
|
||||
for name, total in dict(sorted(containers.items())).items():
|
||||
new = props.filter_container.add()
|
||||
new.name = name
|
||||
new.total = total
|
||||
|
||||
props.filter_type = "BUILDINGSTOREYS"
|
||||
props.filter_type = "CONTAINER"
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||
|
||||
def execute(self, context):
|
||||
bpy.context.scene.BIMSearchProperties.filter_building_storeys.clear()
|
||||
bpy.context.scene.BIMSearchProperties.filter_container.clear()
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
@@ -568,12 +570,12 @@ class ActivateIfcBuildingStoreyFilter(Operator):
|
||||
"BIM_UL_ifc_building_storey_filter",
|
||||
"",
|
||||
context.scene.BIMSearchProperties,
|
||||
"filter_building_storeys",
|
||||
"filter_container",
|
||||
context.scene.BIMSearchProperties,
|
||||
"filter_building_storeys_index",
|
||||
"filter_container_index",
|
||||
rows=20
|
||||
if len(bpy.context.scene.BIMSearchProperties.filter_building_storeys) > 20
|
||||
else len(bpy.context.scene.BIMSearchProperties.filter_building_storeys),
|
||||
if len(bpy.context.scene.BIMSearchProperties.filter_container) > 20
|
||||
else len(bpy.context.scene.BIMSearchProperties.filter_container),
|
||||
)
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT"
|
||||
|
||||
@@ -70,15 +70,15 @@ def update_is_class_selected(self, context):
|
||||
new.obj = obj
|
||||
|
||||
|
||||
def update_is_level_selected(self, context):
|
||||
def update_is_container_selected(self, context):
|
||||
if self.is_selected:
|
||||
for obj in self.unselected_objects:
|
||||
obj.obj.select_set(True)
|
||||
self.unselected_objects.clear()
|
||||
else:
|
||||
for obj in context.selected_objects:
|
||||
level = tool.Misc.get_object_storey(obj)
|
||||
if level and level.Name == self.name:
|
||||
container = tool.Spatial.get_container(tool.Ifc.get_entity(obj))
|
||||
if (container and container.Name == self.name) or (not container and self.name== "None"):
|
||||
obj.select_set(False)
|
||||
new = self.unselected_objects.add()
|
||||
new.obj = obj
|
||||
@@ -93,7 +93,7 @@ class BIMFilterClasses(PropertyGroup):
|
||||
|
||||
class BIMFilterBuildingStoreys(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
is_selected: BoolProperty(name="Is Level Selected", default=True, update=update_is_level_selected)
|
||||
is_selected: BoolProperty(name="Is Level Selected", default=True, update=update_is_container_selected)
|
||||
total: IntProperty(name="Total")
|
||||
unselected_objects: CollectionProperty(type=ObjProperty, name="Unfiltered Objects")
|
||||
|
||||
@@ -140,8 +140,8 @@ class BIMSearchProperties(PropertyGroup):
|
||||
filter_type: StringProperty(name="Filter Type")
|
||||
filter_classes: CollectionProperty(type=BIMFilterClasses, name="Filter Classes")
|
||||
filter_classes_index: IntProperty(name="Filter Classes Index")
|
||||
filter_building_storeys: CollectionProperty(type=BIMFilterBuildingStoreys, name="Filter Level")
|
||||
filter_building_storeys_index: IntProperty(name="Filter Level Index")
|
||||
filter_container: CollectionProperty(type=BIMFilterBuildingStoreys, name="Filter Level")
|
||||
filter_container_index: IntProperty(name="Filter Level Index")
|
||||
|
||||
|
||||
def get_classes(self, ifc_product):
|
||||
|
||||
@@ -42,11 +42,21 @@ class BIM_PT_search(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.search", text="Search", icon="VIEWZOOM")
|
||||
|
||||
return # Temporary for now whilst searching is being upgraded.
|
||||
return
|
||||
|
||||
|
||||
class BIM_PT_filter(Panel):
|
||||
bl_label = "Filter Selection"
|
||||
bl_idname = "BIM_PT_filter"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_grouping_and_filtering"
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.activate_ifc_class_filter", icon="FILTER")
|
||||
row.operator("bim.activate_ifc_building_storey_filter", icon="FILTER")
|
||||
row.operator("bim.activate_ifc_container_filter", icon="FILTER")
|
||||
|
||||
|
||||
class BIM_PT_colour_by_property(Panel):
|
||||
|
||||
@@ -23,7 +23,6 @@ import time
|
||||
import calendar
|
||||
import isodate
|
||||
import pystache
|
||||
import webbrowser
|
||||
import blenderbim.core.sequence as core
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.sequence.helper as helper
|
||||
@@ -51,7 +50,7 @@ class EnableStatusFilters(bpy.types.Operator):
|
||||
pset = element.PartOfPset[0]
|
||||
if pset.Name.startswith("Pset_") and pset.Name.endswith("Common"):
|
||||
statuses.update(element.EnumerationValues)
|
||||
elif pset.Name == "EPset_Status": # Our secret sauce
|
||||
elif pset.Name == "EPset_Status": # Our secret sauce
|
||||
statuses.update(element.EnumerationValues)
|
||||
elif element.Name == "UserDefinedStatus":
|
||||
statuses.add(element.NominalValue)
|
||||
@@ -67,6 +66,7 @@ class EnableStatusFilters(bpy.types.Operator):
|
||||
class DisableStatusFilters(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_status_filters"
|
||||
bl_label = "Disable Status Filters"
|
||||
bl_description = "Deactivate status filters panel.\nCan be used to refresh the displayed statuses"
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMStatusProperties
|
||||
@@ -77,6 +77,7 @@ class DisableStatusFilters(bpy.types.Operator):
|
||||
class ActivateStatusFilters(bpy.types.Operator):
|
||||
bl_idname = "bim.activate_status_filters"
|
||||
bl_label = "Activate Status Filters"
|
||||
bl_description = "Filter and display objects based on currently selected IFC statuses"
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMStatusProperties
|
||||
@@ -92,6 +93,7 @@ class ActivateStatusFilters(bpy.types.Operator):
|
||||
query = " + ".join(query)
|
||||
|
||||
if not query:
|
||||
self.report({"INFO"}, "No statuses selected.")
|
||||
return {"FINISHED"}
|
||||
|
||||
visible_elements = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
|
||||
@@ -107,6 +109,7 @@ class ActivateStatusFilters(bpy.types.Operator):
|
||||
class SelectStatusFilter(bpy.types.Operator):
|
||||
bl_idname = "bim.select_status_filter"
|
||||
bl_label = "Select Status Filter"
|
||||
bl_description = "Select elements with currently selected status"
|
||||
name: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
@@ -368,6 +371,7 @@ class EditTaskTime(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.edit_task_time(
|
||||
tool.Ifc,
|
||||
tool.Sequence,
|
||||
tool.Resource,
|
||||
task_time=tool.Ifc.get().by_id(context.scene.BIMWorkScheduleProperties.active_task_time_id),
|
||||
)
|
||||
|
||||
@@ -504,7 +508,7 @@ class AssignProcess(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
if self.related_object_type == "RESOURCE":
|
||||
core.assign_resource(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
|
||||
core.assign_resource(tool.Ifc, tool.Sequence, tool.Resource, task=tool.Ifc.get().by_id(self.task))
|
||||
elif self.related_object_type == "PRODUCT":
|
||||
if self.related_object:
|
||||
core.assign_input_products(
|
||||
@@ -537,6 +541,7 @@ class UnassignProcess(bpy.types.Operator):
|
||||
core.unassign_resource(
|
||||
tool.Ifc,
|
||||
tool.Sequence,
|
||||
tool.Resource,
|
||||
task=tool.Ifc.get().by_id(self.task),
|
||||
resource=tool.Ifc.get().by_id(self.resource),
|
||||
)
|
||||
|
||||
@@ -224,10 +224,8 @@ def updateTaskDuration(self, context):
|
||||
else:
|
||||
task_time = tool.Ifc.run("sequence.add_task_time", task=task)
|
||||
tool.Ifc.run("sequence.edit_task_time", task_time=task_time, attributes={"ScheduleDuration": duration})
|
||||
SequenceData.load()
|
||||
blenderbim.core.sequence.load_task_properties(tool.Sequence)
|
||||
bpy.ops.bim.load_task_properties()
|
||||
tool.Sequence.load_resources()
|
||||
tool.Sequence.refresh_task_resources()
|
||||
|
||||
|
||||
def get_schedule_predefined_types(self, context):
|
||||
@@ -331,22 +329,32 @@ def get_saved_color_schemes(self, context):
|
||||
def updateAssignedResourceName(self, context):
|
||||
pass
|
||||
|
||||
|
||||
def updateAssignedResourceUsage(self, context):
|
||||
if not context.scene.BIMResourceProperties.is_resource_update_enabled:
|
||||
return
|
||||
if not self.schedule_usage:
|
||||
return
|
||||
resource = tool.Ifc.get().by_id(self.ifc_definition_id)
|
||||
if resource.Usage and resource.Usage.ScheduleUsage == self.schedule_usage:
|
||||
return
|
||||
tool.Resource.run_edit_resource_time(resource, attributes={
|
||||
"ScheduleUsage": self.schedule_usage
|
||||
})
|
||||
tool.Resource.run_edit_resource_time(resource, attributes={"ScheduleUsage": self.schedule_usage})
|
||||
tool.Sequence.load_task_properties()
|
||||
tool.Resource.load_resource_properties()
|
||||
tool.Sequence.refresh_task_resources()
|
||||
blenderbim.bim.module.resource.data.refresh()
|
||||
blenderbim.bim.module.sequence.data.refresh()
|
||||
refresh_sequence_data()
|
||||
blenderbim.bim.module.pset.data.refresh()
|
||||
|
||||
|
||||
def update_task_bar_list(self, context):
|
||||
if not context.scene.BIMWorkScheduleProperties.is_task_update_enabled:
|
||||
return
|
||||
if self.has_bar_visual:
|
||||
tool.Sequence.add_task_bar(self.ifc_definition_id)
|
||||
else:
|
||||
tool.Sequence.remove_task_bar(self.ifc_definition_id)
|
||||
|
||||
class Task(PropertyGroup):
|
||||
name: StringProperty(name="Name", update=updateTaskName)
|
||||
identification: StringProperty(name="Identification", update=updateTaskIdentification)
|
||||
@@ -354,7 +362,7 @@ class Task(PropertyGroup):
|
||||
has_children: BoolProperty(name="Has Children")
|
||||
is_selected: BoolProperty(name="Is Selected")
|
||||
is_expanded: BoolProperty(name="Is Expanded")
|
||||
has_bar_visual: BoolProperty(name="Show Task Bar Animation", default=False)
|
||||
has_bar_visual: BoolProperty(name="Show Task Bar Animation", default=False, update=update_task_bar_list)
|
||||
level_index: IntProperty(name="Level Index")
|
||||
duration: StringProperty(name="Duration", update=updateTaskDuration)
|
||||
start: StringProperty(name="Start", update=updateTaskTimeStart)
|
||||
@@ -405,7 +413,7 @@ class ISODuration(PropertyGroup):
|
||||
|
||||
class IFCStatus(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
is_visible: BoolProperty(name="Is Visible", default=True)
|
||||
is_visible: BoolProperty(name="Is Visible", default=True, update=lambda x, y: bpy.ops.bim.activate_status_filters())
|
||||
|
||||
|
||||
class BIMStatusProperties(PropertyGroup):
|
||||
@@ -456,6 +464,7 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
||||
active_task_time_id: IntProperty(name="Active Task Time Id")
|
||||
task_time_attributes: CollectionProperty(name="Task Time Attributes", type=Attribute)
|
||||
contracted_tasks: StringProperty(name="Contracted Task Items", default="[]")
|
||||
task_bars: StringProperty(name="Checked Task Items", default="[]")
|
||||
is_task_update_enabled: BoolProperty(name="Is Task Update Enabled", default=True)
|
||||
editing_sequence_type: StringProperty(name="Editing Sequence Type")
|
||||
active_sequence_id: IntProperty(name="Active Sequence Id")
|
||||
|
||||
@@ -52,7 +52,7 @@ class BIM_PT_status(Panel):
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.activate_status_filters", icon="TIME")
|
||||
row.label(text="Statuses found in the project:")
|
||||
row.operator("bim.disable_status_filters", icon="CANCEL", text="")
|
||||
|
||||
for status in self.props.statuses:
|
||||
@@ -556,7 +556,7 @@ class BIM_PT_animation_tools(Panel):
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
if self.animation_props.saved_color_schemes:
|
||||
if AnimationColorSchemeData.data["saved_color_schemes"]:
|
||||
row.prop(self.animation_props, "saved_color_schemes", text="Color Scheme", icon="SEQUENCE_COLOR_04")
|
||||
else:
|
||||
row.label(text="No Color Scheme Saved", icon="INFO")
|
||||
@@ -727,7 +727,11 @@ class BIM_PT_task_icom(Panel):
|
||||
if total_task_outputs:
|
||||
op = row2.operator("bim.unassign_product", icon="REMOVE", text="")
|
||||
op.task = task.ifc_definition_id
|
||||
if not context.selected_objects and self.props.active_task_output_index < total_task_outputs:
|
||||
if (
|
||||
total_task_outputs
|
||||
and not context.selected_objects
|
||||
and self.props.active_task_output_index < total_task_outputs
|
||||
):
|
||||
output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id
|
||||
op.relating_product = output_id
|
||||
|
||||
@@ -770,6 +774,7 @@ class BIM_UL_task_resources(UIList):
|
||||
row.prop(item, "name", emboss=False, text="")
|
||||
row.prop(item, "schedule_usage", emboss=False, text="")
|
||||
|
||||
|
||||
class BIM_UL_animation_colors(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
|
||||
@@ -143,7 +143,7 @@ class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = result_objs
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
|
||||
class SelectContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@@ -44,10 +44,9 @@ def update_elevation(self, context):
|
||||
|
||||
|
||||
def update_active_container_index(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.active_container_id = self.containers[self.active_container_index].ifc_definition_id
|
||||
self.container_name = self.containers[self.active_container_index].name
|
||||
self.elevation = self.containers[self.active_container_index].elevation * si_conversion
|
||||
self.elevation = self.containers[self.active_container_index].elevation
|
||||
|
||||
|
||||
def updateContainerName(self, context):
|
||||
@@ -78,7 +77,7 @@ class BIMObjectSpatialProperties(PropertyGroup):
|
||||
|
||||
class BIMContainer(PropertyGroup):
|
||||
name: StringProperty(name="Name", update=updateContainerName)
|
||||
elevation: FloatProperty(name="Elevation")
|
||||
elevation: FloatProperty(name="Elevation", subtype="DISTANCE")
|
||||
level_index: IntProperty(name="Level Index")
|
||||
has_children: BoolProperty(name="Has Children")
|
||||
is_expanded: BoolProperty(name="Is Expanded")
|
||||
|
||||
@@ -147,7 +147,7 @@ class BIM_UL_containers_manager(UIList):
|
||||
split1 = row.split(factor=0.7)
|
||||
split1.prop(item, "name", emboss=False, text="")
|
||||
split2 = row.split(factor=1)
|
||||
split2.label(text=str(item.elevation), icon="BLANK1")
|
||||
split2.label(icon="BLANK1", text=tool.Unit.blender_format_unit(item.elevation))
|
||||
|
||||
def draw_hierarchy(self, row, item):
|
||||
for i in range(0, item.level_index):
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
from . import ui, prop, operator, decorator
|
||||
|
||||
classes = (
|
||||
operator.AddPort,
|
||||
@@ -51,7 +51,9 @@ classes = (
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMSystemProperties = bpy.props.PointerProperty(type=prop.BIMSystemProperties)
|
||||
bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMSystemProperties
|
||||
bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load)
|
||||
|
||||
@@ -27,6 +27,7 @@ def refresh():
|
||||
SystemData.is_loaded = False
|
||||
ObjectSystemData.is_loaded = False
|
||||
PortData.is_loaded = False
|
||||
SystemDecorationData.is_loaded = False
|
||||
|
||||
|
||||
class SystemData:
|
||||
@@ -67,16 +68,18 @@ class ObjectSystemData:
|
||||
cls.data = {
|
||||
"systems": cls.systems(),
|
||||
"total_systems": cls.total_systems(),
|
||||
# AFTER SYSTEMS
|
||||
"connected_elements": cls.connected_elements(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def systems(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if not element:
|
||||
cls.element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if not cls.element:
|
||||
return results
|
||||
for system in ifcopenshell.util.system.get_element_systems(element):
|
||||
for system in ifcopenshell.util.system.get_element_systems(cls.element):
|
||||
results.append({"id": system.id(), "name": system.Name or "Unnamed", "ifc_class": system.is_a()})
|
||||
return results
|
||||
|
||||
@@ -84,6 +87,12 @@ class ObjectSystemData:
|
||||
def total_systems(cls):
|
||||
return len(tool.Ifc.get().by_type("IfcSystem"))
|
||||
|
||||
@classmethod
|
||||
def connected_elements(cls):
|
||||
if not cls.element:
|
||||
return set()
|
||||
return tool.System.get_connected_elements(cls.element)
|
||||
|
||||
|
||||
class PortData:
|
||||
data = {}
|
||||
@@ -101,12 +110,13 @@ class PortData:
|
||||
"port_connected_object": cls.port_connected_object() if is_port else None,
|
||||
"port_relating_object": cls.port_relating_object() if is_port else None,
|
||||
}
|
||||
# AFTER located_ports_data
|
||||
cls.data["selected_objects_flow_direction"] = cls.selected_objects_flow_direction() if not is_port else None
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def total_ports(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
return len(ifcopenshell.util.system.get_ports(element))
|
||||
return len(ifcopenshell.util.system.get_ports(cls.element))
|
||||
|
||||
@classmethod
|
||||
def is_port(cls):
|
||||
@@ -126,17 +136,57 @@ class PortData:
|
||||
|
||||
@classmethod
|
||||
def located_ports_data(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
ports = ifcopenshell.util.system.get_ports(element)
|
||||
ports = ifcopenshell.util.system.get_ports(cls.element)
|
||||
|
||||
data = []
|
||||
for port in ports:
|
||||
port_obj = tool.Ifc.get_object(port)
|
||||
connected_port = tool.System.get_connected_port(port)
|
||||
if connected_port:
|
||||
connected_element = tool.Ifc.get_object(tool.System.get_port_relating_element(connected_port))
|
||||
connected_obj = tool.Ifc.get_object(tool.System.get_port_relating_element(connected_port))
|
||||
else:
|
||||
connected_element = None
|
||||
connected_obj = None
|
||||
|
||||
data.append((port, port_obj, connected_element))
|
||||
data.append((port, port_obj, connected_obj))
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def selected_objects_flow_direction(cls):
|
||||
for port, port_obj, connected_obj in cls.data["located_ports_data"]:
|
||||
if connected_obj in bpy.context.selected_objects:
|
||||
return port.FlowDirection
|
||||
|
||||
|
||||
class SystemDecorationData:
|
||||
data = {}
|
||||
elements_ports_positions = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {}
|
||||
cls.is_loaded = True
|
||||
cls.elements_ports_positions = {}
|
||||
|
||||
@classmethod
|
||||
def get_element_ports_data(cls, element):
|
||||
"""returns element's port data, caches the data until UI update
|
||||
|
||||
Port data includes:
|
||||
- local port position in SI units
|
||||
- port flow direction
|
||||
|
||||
"""
|
||||
if element not in cls.elements_ports_positions:
|
||||
ports = tool.System.get_ports(element)
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
ports_data = []
|
||||
for port in ports:
|
||||
position = tool.Model.get_element_matrix(port, keep_local=True).translation * si_conversion
|
||||
port_data = {
|
||||
"position": position,
|
||||
"flow_direction": port.FlowDirection,
|
||||
}
|
||||
ports_data.append(port_data)
|
||||
cls.elements_ports_positions[element] = ports_data
|
||||
return cls.elements_ports_positions[element]
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>, @Andrej730
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
import bmesh
|
||||
import blenderbim.tool as tool
|
||||
from math import sin, cos, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector, Matrix
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
import ifcopenshell
|
||||
from blenderbim.bim.module.system.data import SystemDecorationData
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
|
||||
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
|
||||
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
@persistent
|
||||
def toggle_decorations_on_load(*args):
|
||||
if bpy.context.scene.BIMSystemProperties.should_draw_decorations:
|
||||
SystemDecorator.install(bpy.context)
|
||||
else:
|
||||
SystemDecorator.uninstall()
|
||||
|
||||
|
||||
class SystemDecorator:
|
||||
installed = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
|
||||
"""Note that operators that change mesh in `exit_edit_mode_callback` can freeze blender.
|
||||
The workaround is to move their code to function and use it for callback.
|
||||
|
||||
Example: https://devtalk.blender.org/t/calling-operator-that-saves-bmesh-freezes-blender-forever/28595"""
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.installed = SpaceView3D.draw_handler_add(
|
||||
handler, (context, get_custom_bmesh, draw_faces, exit_edit_mode_callback), "WINDOW", "POST_VIEW"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_faces(self, bm, vertices_coords):
|
||||
"""mutates original bm (triangulates it)
|
||||
so the triangulation edges will be shown too
|
||||
"""
|
||||
traingulated_bm = bm
|
||||
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
|
||||
|
||||
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
|
||||
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
|
||||
self.draw_batch("TRIS", vertices_coords, faces_color, face_indices)
|
||||
|
||||
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
### Actually drawing
|
||||
all_vertices = []
|
||||
error_vertices = []
|
||||
selected_vertices = []
|
||||
unselected_vertices = []
|
||||
# special = associated with arcs/circles
|
||||
special_vertices = []
|
||||
special_vertex_indices = {}
|
||||
selected_edges = []
|
||||
unselected_edges = []
|
||||
arc_edges = []
|
||||
roof_angle_edges = []
|
||||
preview_edges = []
|
||||
|
||||
# NOTE: using live update because Data wouldn't allow
|
||||
# live time update of objects positions
|
||||
decoration_data = tool.System.get_decoration_data()
|
||||
|
||||
all_vertices = decoration_data["all_vertices"]
|
||||
preview_edges = decoration_data["preview_edges"]
|
||||
special_vertices = decoration_data["special_vertices"]
|
||||
selected_edges = decoration_data["selected_edges"]
|
||||
selected_vertices = decoration_data["selected_vertices"]
|
||||
|
||||
### Actually drawing
|
||||
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
self.shader.bind()
|
||||
|
||||
self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges)
|
||||
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("LINES", all_vertices, UNSPECIAL_ELEMENT_COLOR, arc_edges)
|
||||
self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges)
|
||||
self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges)
|
||||
|
||||
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
|
||||
self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR)
|
||||
self.draw_batch("POINTS", special_vertices, special_elements_color)
|
||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||
@@ -219,12 +219,15 @@ class DisconnectPort(bpy.types.Operator, Operator):
|
||||
class MEPConnectElements(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.mep_connect_elements"
|
||||
bl_label = "Connect MEP Elements"
|
||||
bl_description = "Connects two selected elements if they have ports with matching location"
|
||||
bl_description = "Connects two selected elements by their closest located ports and adjusts them"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_objects) == 2
|
||||
if not len(context.selected_objects) == 2:
|
||||
cls.poll_message_set("Need to select 2 objects.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
obj1 = context.active_object
|
||||
@@ -233,23 +236,32 @@ class MEPConnectElements(bpy.types.Operator, Operator):
|
||||
el1 = tool.Ifc.get_entity(obj1)
|
||||
el2 = tool.Ifc.get_entity(obj2)
|
||||
|
||||
connected_elements = ifcopenshell.util.system.get_connected_to(el1)
|
||||
connected_elements += ifcopenshell.util.system.get_connected_to(el2)
|
||||
|
||||
if el2 in connected_elements:
|
||||
self.report({"ERROR"}, "MEP elements are already connected to each other.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
obj1_ports = [p for p in tool.System.get_ports(el1) if not tool.System.get_connected_port(p)]
|
||||
obj2_ports = [p for p in tool.System.get_ports(el2) if not tool.System.get_connected_port(p)]
|
||||
|
||||
if not obj1_ports or not obj2_ports:
|
||||
self.report({"ERROR"}, "Couldn't find free ports to connect.")
|
||||
return
|
||||
return {"CANCELLED"}
|
||||
|
||||
ports_distance = dict()
|
||||
for port1 in obj1_ports:
|
||||
port1_location = tool.Model.get_element_matrix(port1).translation
|
||||
for port2 in obj2_ports:
|
||||
port2_location = tool.Model.get_element_matrix(port2).translation
|
||||
if tool.Cad.are_vectors_equal(port1_location, port2_location):
|
||||
core.connect_port(tool.Ifc, port1, port2)
|
||||
return {"FINISHED"}
|
||||
distance = (port1_location - port2_location).length
|
||||
ports_distance[(port1, port2)] = distance
|
||||
|
||||
self.report({"ERROR"}, "Couldn't find any matching ports to connect.")
|
||||
return {"CANCELLED"}
|
||||
closest_ports = min(ports_distance, key=lambda x: ports_distance[x])
|
||||
core.connect_port(tool.Ifc, *closest_ports)
|
||||
bpy.ops.bim.regenerate_distribution_element()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetFlowDirection(bpy.types.Operator, Operator):
|
||||
@@ -258,12 +270,59 @@ class SetFlowDirection(bpy.types.Operator, Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
direction: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, operator):
|
||||
if not PortData.is_loaded:
|
||||
PortData.load()
|
||||
|
||||
port = PortData.data["is_port"]
|
||||
if port:
|
||||
return f"Set port flow direction to {operator.direction}"
|
||||
else:
|
||||
return f"Set flow direction to {operator.direction} for active element relatively to the selected"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not PortData.is_loaded:
|
||||
PortData.load()
|
||||
|
||||
port = PortData.data["is_port"]
|
||||
if not port and not len(context.selected_objects) == 2:
|
||||
cls.poll_message_set("Need to select port or 2 connected objects.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
port = tool.Ifc.get_entity(context.active_object)
|
||||
second_port = tool.System.get_connected_port(port)
|
||||
if not second_port:
|
||||
self.report({"ERROR"}, "To set flow direction port has to be connected to another one.")
|
||||
return
|
||||
core.set_flow_direction(
|
||||
tool.Ifc, tool.System, port=tool.Ifc.get_entity(context.active_object), direction=self.direction
|
||||
)
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
|
||||
if element.is_a("IfcDistributionPort"):
|
||||
second_port = tool.System.get_connected_port(element)
|
||||
if not second_port:
|
||||
self.report({"ERROR"}, "To set flow direction port has to be connected to another one.")
|
||||
return
|
||||
core.set_flow_direction(tool.Ifc, tool.System, port=element, direction=self.direction)
|
||||
return {"FINISHED"}
|
||||
|
||||
selected_elements = [
|
||||
entity
|
||||
for entity in (tool.Ifc.get_entity(o) for o in context.selected_objects)
|
||||
if entity and tool.System.is_mep_element(element)
|
||||
]
|
||||
|
||||
if len(selected_elements) != 2:
|
||||
self.report({"ERROR"}, "To set flow direction selected two connected MEP elements or just 1 port.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
other_element = selected_elements[selected_elements[0] == element]
|
||||
active_element_ports = tool.System.get_ports(element)
|
||||
other_element_ports = tool.System.get_ports(other_element)
|
||||
|
||||
for port in active_element_ports:
|
||||
connected_port = tool.System.get_connected_port(port)
|
||||
if connected_port in other_element_ports:
|
||||
core.set_flow_direction(tool.Ifc, tool.System, port=port, direction=self.direction)
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
self.report({"ERROR"}, "Selected elements are not connected to set the flow direction")
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.module.system.data import SystemData
|
||||
import blenderbim.bim.module.system.decorator as decorator
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
@@ -44,6 +45,14 @@ class System(PropertyGroup):
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
|
||||
|
||||
def toggle_decorations(self, context):
|
||||
toggle = self.should_draw_decorations
|
||||
if toggle:
|
||||
decorator.SystemDecorator.install(context)
|
||||
else:
|
||||
decorator.SystemDecorator.uninstall()
|
||||
|
||||
|
||||
class BIMSystemProperties(PropertyGroup):
|
||||
system_attributes: CollectionProperty(name="System Attributes", type=Attribute)
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
@@ -52,3 +61,6 @@ class BIMSystemProperties(PropertyGroup):
|
||||
active_system_index: IntProperty(name="Active System Index")
|
||||
active_system_id: IntProperty(name="Active System Id")
|
||||
system_class: EnumProperty(items=get_system_class, name="Class")
|
||||
should_draw_decorations: BoolProperty(
|
||||
name="Should Draw Decorations", description="Toggle system decorations", update=toggle_decorations
|
||||
)
|
||||
|
||||
@@ -24,10 +24,10 @@ from blenderbim.bim.module.system.data import SystemData, ObjectSystemData, Port
|
||||
|
||||
|
||||
FLOW_DIRECTION_TO_ICON = {
|
||||
"SOURCE": "FORWARD",
|
||||
"SINK": "BACK",
|
||||
"SOURCE": "REMOVE",
|
||||
"SINK": "ADD",
|
||||
"SOURCEANDSINK": "ARROW_LEFTRIGHT",
|
||||
"NOTDEFINED": "RESTRICT_INSTANCED_ON",
|
||||
"NOTDEFINED": "CHECKBOX_DEHLT",
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,10 @@ class BIM_PT_object_systems(Panel):
|
||||
if not ObjectSystemData.is_loaded:
|
||||
ObjectSystemData.load()
|
||||
self.props = context.scene.BIMSystemProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "should_draw_decorations")
|
||||
|
||||
if self.props.is_editing:
|
||||
row = self.layout.row()
|
||||
row.alignment = "RIGHT"
|
||||
@@ -175,6 +179,19 @@ class BIM_PT_ports(Panel):
|
||||
if total_ports == 0:
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Change Flow Direction:")
|
||||
|
||||
current_flow_direction = PortData.data["selected_objects_flow_direction"]
|
||||
for flow_direction in FLOW_DIRECTION_TO_ICON.keys():
|
||||
row.operator(
|
||||
"bim.set_flow_direction",
|
||||
icon=FLOW_DIRECTION_TO_ICON[flow_direction],
|
||||
depress=flow_direction == current_flow_direction,
|
||||
text="",
|
||||
).direction = flow_direction
|
||||
row.enabled = len(context.selected_objects) == 2
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Ports located on object and connected objects:")
|
||||
row = self.layout.row(align=True)
|
||||
@@ -266,16 +283,15 @@ class BIM_PT_port(Panel):
|
||||
else:
|
||||
row.label(text="Port is not connected to any element")
|
||||
|
||||
# TODO: replace with enum property?
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Change Flow Direction:")
|
||||
for flow_direction in FLOW_DIRECTION_TO_ICON.keys():
|
||||
row = layout.row()
|
||||
row.operator(
|
||||
"bim.set_flow_direction", icon=FLOW_DIRECTION_TO_ICON[flow_direction], text=flow_direction
|
||||
"bim.set_flow_direction",
|
||||
icon=FLOW_DIRECTION_TO_ICON[flow_direction],
|
||||
depress=flow_direction == current_flow_direction,
|
||||
text="",
|
||||
).direction = flow_direction
|
||||
if flow_direction == current_flow_direction:
|
||||
row.enabled = False
|
||||
|
||||
|
||||
class BIM_UL_systems(UIList):
|
||||
|
||||
@@ -131,7 +131,8 @@ class SelectRequirement(bpy.types.Operator):
|
||||
props.failed_entities.clear()
|
||||
for e in failed_entities:
|
||||
new_entity = props.failed_entities.add()
|
||||
new_entity.element = e["element"]
|
||||
new_entity.ifc_id = e["id"]
|
||||
new_entity.element = f'{e["class"]}/{e["name"]}'
|
||||
new_entity.reason = e["reason"]
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -31,10 +31,6 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
def purge():
|
||||
pass
|
||||
|
||||
|
||||
def update_active_specification_index(self, context):
|
||||
TesterData.load()
|
||||
|
||||
@@ -45,8 +41,9 @@ class Specification(PropertyGroup):
|
||||
|
||||
|
||||
class FailedEntities(PropertyGroup):
|
||||
reason: StringProperty(name="Reason")
|
||||
ifc_id: IntProperty(name="IFC ID")
|
||||
element: StringProperty(name="Element")
|
||||
reason: StringProperty(name="Reason")
|
||||
|
||||
|
||||
class IfcTesterProperties(PropertyGroup):
|
||||
|
||||
@@ -115,17 +115,9 @@ class BIM_UL_tester_failed_entities(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
props = context.scene.IfcTesterProperties
|
||||
if item:
|
||||
if props.should_load_from_memory:
|
||||
ifc_file = tool.Ifc.get()
|
||||
ifc_id = int(item.element[1 : item.element.find("=")])
|
||||
entity = ifc_file.by_id(ifc_id)
|
||||
report_entity = f"[#{ifc_id}][{entity.is_a()}] {entity.Name}"
|
||||
else:
|
||||
report_entity = item.element
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text=report_entity)
|
||||
row.label(text=item.element)
|
||||
row.label(text=item.reason)
|
||||
if props.should_load_from_memory:
|
||||
op = row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.ifc_id = entity.id()
|
||||
op.ifc_id = item.ifc_id
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user