Merge branch 'v0.7.0' into interface-to-interact-with-Brickschema

This commit is contained in:
Riley Wong
2023-06-30 00:05:20 -07:00
committed by GitHub
230 changed files with 9556 additions and 3363 deletions
+26
View File
@@ -0,0 +1,26 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: BlenderBIM Add-on
message: >-
If you use this software, please cite it using the
metadata from this file.
type: software
authors:
- name: "IfcOpenShell contributors"
repository-code: >-
https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/blenderbim
url: 'https://blenderbim.org/'
repository-artifact: 'https://blenderbim.org/download.html'
abstract: >-
Add-on to Blender providing a graphical native IFC
authoring platform
keywords:
- IFC
- Blender
- NativeIFC
license: GPL-3.0-or-later
commit: 1fb6dac
version: v0.0.230506
date-released: '2023-05-23'
+46 -3
View File
@@ -56,6 +56,7 @@ ifeq ($(PYVERSION), py37)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp-fcl-1.7.5-py37h5f1835d_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py37h95e2c48_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py37h0379df6_3.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/linux-64/qhull-2020.2-h4bd325d_2.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/30/c0/d0526314971fc661b083ab135747dc68446a3022686da8c16d25fcf6ef07/lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/1d/a4/931d0780f31f3ea8c4f9ef6464a2825137c5241e6707a5fb03bef760a7eb/shapely-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/ed/d5/c2e84e1e36ab8ebea033921d5886a056c77e18bab5ab1051fcc22de2e8a2/Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl
@@ -64,6 +65,7 @@ ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp-fcl-1.7.5-py39hbcdfc36_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py39h5aed9d1_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py39h5472131_3.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/linux-64/qhull-2020.2-h4bd325d_2.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/19/d9/a69c6aff5673554df48120565a14a50eaa41d29ae03b02faa0b023666318/lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/2d/f2/8ec281d357e8bb7d08dc8d727f0e4c8ef3dae7d3fa75c69c8e452bb82d50/shapely-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/01/61/3ff85fb4bb596ce3d223c8fcf93c8df5c12bc8899dfb4fb3cb1c5b20dd5f/Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl
@@ -72,6 +74,7 @@ ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/linux-64/hpp-fcl-1.8.0-py310hdaf7e41_1.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/linux-64/eigenpy-2.6.11-py310hf3e5c9c_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py310h7c3ba0c_5.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/linux-64/qhull-2020.2-h4bd325d_2.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/25/1e/19b46d8e8881fe0df2e20945d51919eeb1817836d62a90efa8506530e45c/lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/a8/a5/403728b5614b28083f6424dfdefec5fcf58068495fb03bb08532671c642f/shapely-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/f6/51/320986ebd6d46a0e95c2240468ced73153b691ce07617078bcdf30c609ec/Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl
@@ -86,6 +89,7 @@ ifeq ($(PYVERSION), py37)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py37h2d7f23a_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-64/qhull-2020.2-h940c156_2.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/1e/3e/f0abc15d5dac50939bccc589aae336d5ead4c72e7ad1039a2e0f3630ea92/lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/e6/7d/4923f27c340339e1c896c77cafc8ed672c8d381a025bbab6c6ddcba27e8f/shapely-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/88/49/c26fc3b5b0e82bdc9d8751d6b939da29327b0d98f7c3b95a575cbfed2743/Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl
@@ -94,6 +98,7 @@ ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py39h1e32b98_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py39h5405915_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py39ha641261_3.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-64/qhull-2020.2-h940c156_2.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/b8/74/a71f7ad72e8db54ce899efab84507b801660750cbbfa6a39e6717557d36a/lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/36/a4/7e542a209f862f967d7cb8e939eff155f4294a27d17e16441fb8bdd51a2c/shapely-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/88/7a/ddfe28b485b623361457d4783007c1f9ba83a87f93e7fec32f64793efb6c/Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl
@@ -102,6 +107,7 @@ ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-64/hpp-fcl-1.8.0-py310h651ac30_1.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/osx-64/eigenpy-2.6.11-py310hc03097c_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py310h509978a_5.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-64/qhull-2020.2-h940c156_2.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/a1/44/17b7dac7a18807d30e2fe10c3328c152808f5464565e230bfd0e77f178c6/lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/1f/2a/dc3353c2431cf53e8d04bb8fba27e584410ca3435c9c85f76d71bf0c0e80/shapely-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/d8/60/b13c00d403f34110e96c1b5c0afa73ce461efe3fe960c3a7e3e7fe190d82/Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl
@@ -117,6 +123,7 @@ ifeq ($(PYVERSION), py37)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py37h2d7f23a_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-arm64/qhull-2020.2-hc021e02_2.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-64/lxml-4.9.1-py37h994c40b_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/e6/7d/4923f27c340339e1c896c77cafc8ed672c8d381a025bbab6c6ddcba27e8f/shapely-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/aa/bc/21097cd891dd2fa02f2b3d767e02e883e026482e59d29975d1bc30024aa3/Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl
@@ -125,6 +132,7 @@ ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-arm64/hpp-fcl-1.7.5-py39ha69f3c1_2.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.7/download/osx-arm64/eigenpy-2.6.7-py39ha69f3c1_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py39h2550fe3_0.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-arm64/qhull-2020.2-hc021e02_2.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py39h9eb174b_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/ea/aa/45fbd031edf3149cb767d8b9f9db45d5faf0324d743c6b8fb0298cc022d0/shapely-2.0.1-cp39-cp39-macosx_11_0_arm64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/aa/bc/21097cd891dd2fa02f2b3d767e02e883e026482e59d29975d1bc30024aa3/Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl
@@ -133,6 +141,7 @@ ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-arm64/hpp-fcl-1.8.0-py310h5699539_2.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.9/download/osx-arm64/eigenpy-2.6.9-py310habfc766_1.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py310h4566fd1_0.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-arm64/qhull-2020.2-hc021e02_2.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py310h02f21da_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/ec/41/d59208743e737184e1b403e95a937aebb022b8459e99efbcd5208fc8be46/shapely-2.0.1-cp310-cp310-macosx_11_0_arm64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/0c/5f/117b653cad585f3aedfe0de996c292e67d4b020ed77f652e5a6c8c24f908/Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl
@@ -206,9 +215,7 @@ endif
# IfcOpenBot sometimes lags behind, so we hotfix the Python utilities
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/util/* dist/blenderbim/libs/site/packages/ifcopenshell/util/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api/* dist/blenderbim/libs/site/packages/ifcopenshell/api/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/entity_instance.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/file.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/validate.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/*.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py dist/blenderbim/libs/site/packages/ifcopenshell/express/
# Provides bcf functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/bcf/src/bcf dist/blenderbim/libs/site/packages/
@@ -425,6 +432,10 @@ ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Lib/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
@@ -441,11 +452,31 @@ endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl except on Windows
ifneq ($(PLATFORM), win)
mkdir dist/working
cd dist/working && wget $(QHULL_URL)
cd dist/working && tar -xf qhull*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
rm -rf dist/working
endif
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(EIGENPY_URL)
@@ -456,6 +487,9 @@ endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
@@ -471,6 +505,9 @@ endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
# Uh, do nothing, apparently? No DLLs are shipped.
endif
@@ -486,6 +523,9 @@ endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
@@ -501,6 +541,9 @@ endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
+26 -2
View File
@@ -98,8 +98,8 @@ def update_bim_tool_props():
if not obj:
return
mode = bpy.context.mode
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode).idname
if current_tool != "bim.bim_tool":
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode)
if not current_tool or current_tool.idname != "bim.bim_tool":
return
element = tool.Ifc.get_entity(obj)
if not element:
@@ -168,6 +168,9 @@ 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
@@ -259,6 +262,12 @@ def get_application_version():
)
def viewport_shading_changed_callback(area):
shading = area.spaces.active.shading.type
if shading == "RENDERED":
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
@persistent
def setDefaultProperties(scene):
global global_subscription_owner
@@ -266,6 +275,21 @@ def setDefaultProperties(scene):
bpy.msgbus.subscribe_rna(
key=active_object_key, owner=global_subscription_owner, args=(), notify=active_object_callback
)
# subscribe to changes in viewport shading mode
# NOTE: couldn't find a way to make it work for new areas too
# it starts working for them after blender restart though
for screen in bpy.data.screens:
for area in screen.areas:
if area.type != "VIEW_3D":
continue
shading = area.spaces.active.shading
key = shading.path_resolve("type", False)
bpy.msgbus.subscribe_rna(
key=key, owner=global_subscription_owner, args=(area,), notify=viewport_shading_changed_callback
)
ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner)
ifcopenshell.api.owner.settings.get_application = get_application
AuthoringData.type_thumbnails = {}
+5
View File
@@ -273,6 +273,9 @@ def convert_property_group_from_si(property_group, skip_props=()):
setattr(property_group, prop_name, prop_value)
# TODO this should move into ifcopenshell.util
class IfcHeaderExtractor:
def __init__(self, filepath: str):
self.filepath = filepath
@@ -284,6 +287,8 @@ class IfcHeaderExtractor:
return self.extract_ifc_spf(ifc_file)
elif extension.lower() == "ifczip":
return self.extract_ifc_zip()
elif extension.lower() == "ifcsqlite":
return {} # TODO
def extract_ifc_spf(self, ifc_file):
# https://www.steptools.com/stds/step/IS_final_p21e3.html#clause-8
+5 -1
View File
@@ -120,6 +120,8 @@ class IfcStore:
@staticmethod
def load_file(path):
if not os.path.isfile(path):
return
extension = path.split(".")[-1]
if extension.lower() == "ifczip":
with tempfile.TemporaryDirectory() as unzipped_path:
@@ -130,7 +132,9 @@ class IfcStore:
return
elif extension.lower() == "ifcxml":
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
elif extension.lower() == "ifc":
elif bpy.context.scene.BIMProjectProperties.should_stream:
IfcStore.file = ifcopenshell.open(path, should_stream=True)
else:
IfcStore.file = ifcopenshell.open(path)
@staticmethod
+115 -214
View File
@@ -187,7 +187,7 @@ class IfcImporter:
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
self.settings_2d = ifcopenshell.geom.settings()
self.settings_2d.set(self.settings_2d.INCLUDE_CURVES, True)
self.settings_2d.set(self.settings.STRICT_TOLERANCE, True)
self.settings_2d.set(self.settings_2d.STRICT_TOLERANCE, True)
self.project = None
self.has_existing_project = False
self.collections = {}
@@ -347,7 +347,8 @@ class IfcImporter:
self.element_types = set(self.file.by_type("IfcTypeProduct"))
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
filtered_elements = self.elements | set(self.file.by_type("IfcGrid"))
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(filtered_elements)
else:
if self.file.schema == "IFC2X3":
self.spatial_elements = set(self.file.by_type("IfcSpatialStructureElement"))
@@ -361,11 +362,13 @@ class IfcImporter:
while True:
results.add(spatial_element)
spatial_element = ifcopenshell.util.element.get_aggregate(spatial_element)
if not spatial_element or spatial_element.is_a("IfcContext"):
if not spatial_element or spatial_element.is_a() in ("IfcProject", "IfcProjectLibrary"):
break
return results
def parse_native_elements(self):
if not self.ifc_import_settings.should_load_geometry:
return
for element in self.elements:
if self.is_native(element):
self.native_elements.add(element)
@@ -410,19 +413,19 @@ class IfcImporter:
return True
def is_native_swept_disk_solid(self, element, representations):
# detect BBIM Railings to represent them with meshes and not curves
if tool.Pset.get_element_pset(element, "BBIM_Railing"):
return False
for representation in representations:
items = representation["raw"].Items or [] # Be forgiving of invalid IFCs because Revit :(
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
if tool.Pset.get_element_pset(element, "BBIM_Railing"):
return False
return True
elif len(items) and ( # See #2508 why we accommodate for invalid IFCs here
items[0].is_a("IfcSweptDiskSolid")
and len({i.is_a() for i in items}) == 1
and len({i.Radius for i in items}) == 1
):
if tool.Pset.get_element_pset(element, "BBIM_Railing"):
return False
return True
return False
@@ -577,6 +580,8 @@ class IfcImporter:
return result
def create_grids(self):
if not self.ifc_import_settings.should_load_geometry:
return
grids = self.file.by_type("IfcGrid")
for grid in grids:
shape = None
@@ -622,34 +627,37 @@ class IfcImporter:
def create_element_type(self, element):
self.ifc_import_settings.logger.info("Creating object %s", element)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
mesh = None
if representation:
mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id())
mesh = self.meshes.get(mesh_name)
if mesh is None:
shape = None
try:
shape = ifcopenshell.geom.create_shape(self.settings, representation)
except:
if self.ifc_import_settings.should_load_geometry:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
if representation:
mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id())
mesh = self.meshes.get(mesh_name)
if mesh is None:
shape = None
try:
shape = ifcopenshell.geom.create_shape(self.settings_2d, representation)
shape = ifcopenshell.geom.create_shape(self.settings, representation)
except:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
if shape:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
try:
shape = ifcopenshell.geom.create_shape(self.settings_2d, representation)
except:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
if shape:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
self.link_element(element, obj)
self.material_creator.create(element, obj, mesh)
self.type_products[element.GlobalId] = obj
def create_native_elements(self):
if not self.ifc_import_settings.should_load_geometry:
return
progress = 0
checkpoint = time.time()
total = len(self.native_elements)
@@ -691,21 +699,76 @@ class IfcImporter:
self.create_generic_elements(self.elements)
def create_generic_elements(self, elements):
if isinstance(self.file, ifcopenshell.sqlite):
return self.create_generic_sqlite_elements(elements)
# Based on my experience in viewing BIM models, representations are prioritised as follows:
# 1. 3D Body, 2. 2D Body, 3. 2D Plans / annotations, 4. Point clouds, 5. No representation
# If an element has a representation that doesn't follow 1, 2, 3, or 4, it will not show by default.
# The user can load them later if they want to view them.
products = self.create_products(elements)
elements -= products
products = self.create_products(elements, settings=self.settings_curve)
elements -= products
products = self.create_products(elements, settings=self.settings_2d)
elements -= products
products = self.create_pointclouds(elements)
elements -= products
for element in elements:
if self.ifc_import_settings.should_load_geometry:
products = self.create_products(elements)
elements -= products
products = self.create_products(elements, settings=self.settings_curve)
elements -= products
products = self.create_products(elements, settings=self.settings_2d)
elements -= products
products = self.create_pointclouds(elements)
elements -= products
total = len(elements)
for i, element in enumerate(elements):
if i % 250 == 0:
print("{} / {} elements processed ...".format(i, total))
self.create_product(element)
def create_generic_sqlite_elements(self, elements):
self.geometry_cache = self.file.get_geometry([e.id() for e in elements])
for geometry_id, geometry in self.geometry_cache["geometry"].items():
mesh_name = tool.Loader.get_mesh_name(type("Geometry", (), {"id": geometry_id}))
mesh = bpy.data.meshes.new(mesh_name)
verts = geometry["verts"]
mesh["has_cartesian_point_offset"] = False
if geometry["faces"]:
num_vertices = len(verts) // 3
total_faces = len(geometry["faces"])
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
num_vertex_indices = len(geometry["faces"])
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", geometry["faces"])
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.update()
else:
e = geometry["edges"]
v = verts
vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
mesh.from_pydata(vertices, edges, [])
mesh["ios_materials"] = geometry["materials"]
mesh["ios_material_ids"] = geometry["material_ids"]
self.meshes[mesh_name] = mesh
total = len(elements)
for i, element in enumerate(elements):
if i % 250 == 0:
print("{} / {} elements processed ...".format(i, total))
mesh = None
geometry_id = self.geometry_cache["shapes"][element.id()]["geometry"]
if geometry_id:
mesh_name = tool.Loader.get_mesh_name(type("Geometry", (), {"id": geometry_id}))
mesh = self.meshes.get(mesh_name)
self.create_product(element, mesh=mesh)
def create_products(self, products, settings=None):
if settings is None:
settings = self.settings
@@ -1342,14 +1405,11 @@ class IfcImporter:
obj.BIMObjectProperties.collection = self.project["blender"]
def create_collections(self):
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
self.create_decomposition_collections()
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
self.create_spatial_decomposition_collections()
def create_decomposition_collections(self):
self.create_spatial_decomposition_collections()
self.create_aggregate_collections()
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
self.create_aggregate_collections()
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
pass
def create_spatial_decomposition_collections(self):
for rel_aggregate in self.project["ifc"].IsDecomposedBy or []:
@@ -1478,183 +1538,18 @@ class IfcImporter:
if not blender_material:
name = style.Name or str(style.id())
blender_material = bpy.data.materials.new(name)
blender_material.use_fake_user = True
self.link_element(style, blender_material)
blender_material.BIMMaterialProperties.ifc_style_id = style.id()
self.material_creator.styles[style.id()] = blender_material
rendering_style = None
texture_style = None
for surface_style in style.Styles:
if surface_style.is_a() == "IfcSurfaceStyleShading":
self.create_surface_style_shading(blender_material, surface_style)
elif surface_style.is_a("IfcSurfaceStyleRendering"):
rendering_style = surface_style
self.create_surface_style_rendering(blender_material, surface_style)
elif surface_style.is_a("IfcSurfaceStyleWithTextures"):
texture_style = surface_style
if rendering_style and texture_style:
self.create_surface_style_with_textures(blender_material, rendering_style, texture_style)
tool.Style.record_shading(blender_material)
def create_surface_style_shading(self, blender_material, surface_style):
alpha = 1.0
# Transparency was added in IFC4
if hasattr(surface_style, "Transparency") and surface_style.Transparency:
alpha = 1 - surface_style.Transparency
blender_material.diffuse_color = (
surface_style.SurfaceColour.Red,
surface_style.SurfaceColour.Green,
surface_style.SurfaceColour.Blue,
alpha,
)
def create_surface_style_rendering(self, blender_material, surface_style):
self.create_surface_style_shading(blender_material, surface_style)
if surface_style.ReflectanceMethod in ["PHYSICAL", "NOTDEFINED"]:
blender_material.use_nodes = True
bsdf = blender_material.node_tree.nodes["Principled BSDF"]
if surface_style.DiffuseColour:
if surface_style.DiffuseColour.is_a("IfcColourRgb"):
bsdf.inputs["Base Color"].default_value = (
surface_style.DiffuseColour.Red,
surface_style.DiffuseColour.Green,
surface_style.DiffuseColour.Blue,
1,
)
elif surface_style.DiffuseColour.is_a("IfcNormalisedRatioMeasure"):
bsdf.inputs["Base Color"].default_value = (
surface_style.SurfaceColour.Red * surface_style.DiffuseColour.wrappedValue,
surface_style.SurfaceColour.Green * surface_style.DiffuseColour.wrappedValue,
surface_style.SurfaceColour.Blue * surface_style.DiffuseColour.wrappedValue,
1,
)
if surface_style.SpecularColour and surface_style.SpecularColour.is_a("IfcNormalisedRatioMeasure"):
bsdf.inputs["Metallic"].default_value = surface_style.SpecularColour.wrappedValue
if surface_style.SpecularHighlight and surface_style.SpecularHighlight.is_a("IfcSpecularRoughness"):
bsdf.inputs["Roughness"].default_value = surface_style.SpecularHighlight.wrappedValue
if hasattr(surface_style, "Transparency") and surface_style.Transparency:
bsdf.inputs["Alpha"].default_value = 1 - surface_style.Transparency
blender_material.blend_method = "BLEND"
elif surface_style.ReflectanceMethod == "FLAT":
blender_material.use_nodes = True
output = {n.type: n for n in blender_material.node_tree.nodes}.get("OUTPUT_MATERIAL", None)
bsdf = blender_material.node_tree.nodes["Principled BSDF"]
mix = blender_material.node_tree.nodes.new(type="ShaderNodeMixShader")
mix.location = bsdf.location
blender_material.node_tree.links.new(mix.outputs[0], output.inputs["Surface"])
blender_material.node_tree.nodes.remove(bsdf)
lightpath = blender_material.node_tree.nodes.new(type="ShaderNodeLightPath")
lightpath.location = mix.location - mathutils.Vector((200, -200))
blender_material.node_tree.links.new(lightpath.outputs[0], mix.inputs[0])
bsdf = blender_material.node_tree.nodes.new(type="ShaderNodeBsdfTransparent")
bsdf.location = mix.location - mathutils.Vector((200, 0))
blender_material.node_tree.links.new(bsdf.outputs[0], mix.inputs[1])
rgb = blender_material.node_tree.nodes.new(type="ShaderNodeRGB")
rgb.location = mix.location - mathutils.Vector((200, 200))
blender_material.node_tree.links.new(rgb.outputs[0], mix.inputs[2])
if surface_style.DiffuseColour and surface_style.DiffuseColour.is_a("IfcColourRgb"):
rgb.outputs[0].default_value = (
surface_style.DiffuseColour.Red,
surface_style.DiffuseColour.Green,
surface_style.DiffuseColour.Blue,
1,
)
def create_surface_style_with_textures(self, blender_material, rendering_style, texture_style):
for texture in texture_style.Textures:
mode = getattr(texture, "Mode", None)
node = None
if texture.is_a("IfcImageTexture"):
image_url = texture.URLReference
if not os.path.abspath(texture.URLReference) and tool.Ifc.get_path():
image_url = os.path.join(os.path.dirname(tool.Ifc.get_path()), texture.URLReference)
if rendering_style.ReflectanceMethod in ["PHYSICAL", "NOTDEFINED"]:
bsdf = blender_material.node_tree.nodes["Principled BSDF"]
if mode == "NORMAL":
normalmap = blender_material.node_tree.nodes.new(type="ShaderNodeNormalMap")
normalmap.location = bsdf.location - mathutils.Vector((200, 0))
blender_material.node_tree.links.new(normalmap.outputs[0], bsdf.inputs["Normal"])
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
node.location = normalmap.location - mathutils.Vector((200, 0))
image = bpy.data.images.load(image_url)
image.colorspace_settings.name = "Non-Color"
node.image = image
blender_material.node_tree.links.new(node.outputs[0], normalmap.inputs["Color"])
elif mode == "EMISSIVE":
output = {n.type: n for n in blender_material.node_tree.nodes}.get("OUTPUT_MATERIAL", None)
add = blender_material.node_tree.nodes.new(type="ShaderNodeAddShader")
add.location = bsdf.location + mathutils.Vector((200, 0))
blender_material.node_tree.links.new(bsdf.outputs[0], add.inputs[1])
blender_material.node_tree.links.new(add.outputs[0], output.inputs[0])
emission = blender_material.node_tree.nodes.new(type="ShaderNodeEmission")
emission.location = add.location - mathutils.Vector((200, 0))
blender_material.node_tree.links.new(emission.outputs[0], add.inputs[0])
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
node.location = emission.location - mathutils.Vector((200, 0))
image = bpy.data.images.load(image_url)
node.image = image
blender_material.node_tree.links.new(node.outputs[0], emission.inputs[0])
elif mode == "METALLICROUGHNESS":
separate = blender_material.node_tree.nodes.new(type="ShaderNodeSeparateRGB")
separate.location = bsdf.location - mathutils.Vector((200, 0))
blender_material.node_tree.links.new(separate.outputs[1], bsdf.inputs["Roughness"])
blender_material.node_tree.links.new(separate.outputs[2], bsdf.inputs["Metallic"])
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
node.location = separate.location - mathutils.Vector((200, 0))
image = bpy.data.images.load(image_url)
image.colorspace_settings.name = "Non-Color"
node.image = image
blender_material.node_tree.links.new(node.outputs[0], separate.inputs[0])
elif mode == "OCCLUSION":
# TODO work out how to implement glTF settings here
# https://docs.blender.org/manual/en/dev/addons/import_export/scene_gltf2.html
pass
elif mode == "DIFFUSE":
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
node.location = bsdf.location - mathutils.Vector((400, 0))
image = bpy.data.images.load(image_url)
node.image = image
blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs["Base Color"])
blender_material.node_tree.links.new(node.outputs[1], bsdf.inputs["Alpha"])
blender_material.blend_method = "BLEND"
elif rendering_style.ReflectanceMethod == "FLAT":
bsdf = blender_material.node_tree.nodes["Mix Shader"]
if mode == "EMISSIVE":
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
node.location = bsdf.location - mathutils.Vector((200, 0))
image = bpy.data.images.load(image_url)
node.image = image
blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs[2])
if node and getattr(texture, "IsMappedBy", None):
coordinates = texture.IsMappedBy[0]
coord = blender_material.node_tree.nodes.new(type="ShaderNodeTexCoord")
coord.location = node.location - mathutils.Vector((200, 0))
if coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD":
blender_material.node_tree.links.new(coord.outputs["Generated"], node.inputs["Vector"])
elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE":
blender_material.node_tree.links.new(coord.outputs["Camera"], node.inputs["Vector"])
else:
blender_material.node_tree.links.new(coord.outputs["UV"], node.inputs["Vector"])
style_elements = tool.Style.get_style_elements(blender_material)
if tool.Style.has_blender_external_style(style_elements):
blender_material.BIMStyleProperties.active_style_type = "External"
else:
blender_material.BIMStyleProperties.active_style_type = "Shading"
def place_objects_in_collections(self):
for ifc_definition_id, obj in self.added_data.items():
@@ -1729,7 +1624,10 @@ class IfcImporter:
return rel.RelatingGroup
def get_element_matrix(self, element):
result = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
if isinstance(element, ifcopenshell.sqlite_entity):
result = self.geometry_cache["shapes"][element.id()]["matrix"]
else:
result = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
result[0][3] *= self.unit_scale
result[1][3] *= self.unit_scale
result[2][3] *= self.unit_scale
@@ -1886,6 +1784,7 @@ class IfcImporter:
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
mesh.update()
else:
e = geometry.edges
@@ -1962,6 +1861,7 @@ class IfcImportSettings:
self.should_use_cpu_multiprocessing = True
self.merge_mode = None
self.should_merge_materials_by_colour = False
self.should_load_geometry = True
self.should_use_native_meshes = False
self.should_clean_mesh = True
self.should_cache = True
@@ -1991,6 +1891,7 @@ class IfcImportSettings:
settings.should_use_cpu_multiprocessing = props.should_use_cpu_multiprocessing
settings.merge_mode = props.merge_mode
settings.should_merge_materials_by_colour = props.should_merge_materials_by_colour
settings.should_load_geometry = props.should_load_geometry
settings.should_use_native_meshes = props.should_use_native_meshes
settings.should_clean_mesh = props.should_clean_mesh
settings.should_cache = props.should_cache
@@ -35,6 +35,7 @@ from mathutils import Vector, Matrix
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.decorator import ProfileDecorator
from blenderbim.bim.module.boundary.decorator import BoundaryDecorator
import blenderbim.core
def get_boundaries_collection(blender_space):
@@ -163,7 +163,7 @@ class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator):
cost_item: bpy.props.IntProperty()
def _execute(self, context):
core.remove_cost_item(tool.Ifc, tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item))
core.remove_cost_item(tool.Ifc, tool.Cost, cost_item_id=self.cost_item)
class EnableEditingCostItem(bpy.types.Operator, tool.Ifc.Operator):
@@ -631,17 +631,19 @@ class ExportCostSchedules(bpy.types.Operator):
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
cost_schedule: bpy.props.IntProperty()
format: bpy.props.EnumProperty("Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
r = core.export_cost_schedules(tool.Cost, format=self.format, cost_schedule=cost_schedule)
r = core.export_cost_schedules(tool.Cost, filepath=self.filepath, format=self.format, cost_schedule=cost_schedule)
if isinstance(r, str):
self.report({"ERROR"}, r)
return {"FINISHED"}
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
wm.fileselect_add(self)
return {"RUNNING_MODAL"}
def draw(self, context):
self.layout.label(text="Choose a format")
@@ -150,14 +150,9 @@ class ExportIfcCsv(bpy.types.Operator):
selector = ifcopenshell.util.selector.Selector()
results = selector.parse(ifc_file, props.ifc_selector)
ifc_csv = ifccsv.IfcCsv()
ifc_csv.output = self.filepath
ifc_csv.attributes = [a.name for a in props.csv_attributes]
ifc_csv.selector = selector
if props.csv_delimiter == "CUSTOM":
ifc_csv.delimiter = props.csv_custom_delimiter
else:
ifc_csv.delimiter = props.csv_delimiter
ifc_csv.export(ifc_file, results)
attributes = [a.name for a in props.csv_attributes]
sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter
ifc_csv.export(ifc_file, results, attributes, output=self.filepath, format=args.format, delimiter=sep)
return {"FINISHED"}
@@ -182,12 +177,8 @@ class ImportIfcCsv(bpy.types.Operator):
else:
ifc_file = ifcopenshell.open(props.csv_ifc_file)
ifc_csv = ifccsv.IfcCsv()
ifc_csv.output = self.filepath
if props.csv_delimiter == "CUSTOM":
ifc_csv.delimiter = props.csv_custom_delimiter
else:
ifc_csv.delimiter = props.csv_delimiter
ifc_csv.Import(ifc_file)
sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter
ifc_csv.Import(ifc_file, self.filepath, delimiter=sep)
if not props.should_load_from_memory:
ifc_file.write(props.csv_ifc_file)
purge_module_data()
@@ -45,7 +45,7 @@ class Annotator:
font.name = "OpenGost Type B TT"
obj.data.font = font
obj.data.BIMTextProperties.font_size = "2.5"
collection = bpy.context.scene.camera.users_collection[0]
collection = bpy.context.scene.camera.BIMObjectProperties.collection
collection.objects.link(obj)
Annotator.resize_text(obj)
return obj
@@ -53,9 +53,10 @@ class Annotator:
@staticmethod
def resize_text(text_obj):
camera = None
for obj in text_obj.users_collection[0].objects:
if isinstance(obj.data, bpy.types.Camera):
camera = obj
group = tool.Drawing.get_drawing_group(tool.Ifc.get_entity(text_obj))
for element in tool.Drawing.get_drawing_elements(group):
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
camera = tool.Ifc.get_object(element)
break
if not camera:
return
@@ -112,7 +113,7 @@ class Annotator:
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = camera.matrix_world.copy()
matrix_world.translation = co1
collection = camera.users_collection[0]
collection = camera.BIMObjectProperties.collection
if object_type == "TEXT":
obj = bpy.data.objects.new(object_type, None)
@@ -269,7 +269,7 @@ def get_active_drawing(scene):
props = scene.DocProperties
try:
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
return camera.users_collection[0], camera
return camera.BIMObjectProperties.collection, camera
except:
return None, None
@@ -150,7 +150,7 @@ class CreateDrawing(bpy.types.Operator):
"Creates/refreshes a .svg drawing based on currently active camera.\n\n"
+ "SHIFT+CLICK to print all selected drawings"
)
print_all: bpy.props.BoolProperty(name="Print All", default=False)
print_all: bpy.props.BoolProperty(name="Print All", default=False, options={"SKIP_SAVE"})
@classmethod
def poll(cls, context):
@@ -158,12 +158,9 @@ class CreateDrawing(bpy.types.Operator):
def invoke(self, context, event):
# printing all drawings on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.print_all = True
else:
# can't rely on default value since the line above
# will set the value to `True` for all future operator calls
self.print_all = False
return self.execute(context)
def execute(self, context):
@@ -177,7 +174,7 @@ class CreateDrawing(bpy.types.Operator):
for drawing_id in drawings_to_print:
if self.print_all:
bpy.ops.bim.activate_drawing(drawing=drawing_id)
bpy.ops.bim.activate_drawing(drawing=drawing_id, camera_view_point=False)
self.camera = context.scene.camera
self.camera_element = tool.Ifc.get_entity(self.camera)
@@ -220,7 +217,7 @@ class CreateDrawing(bpy.types.Operator):
open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg_path)
if self.print_all:
bpy.ops.bim.activate_drawing(drawing=original_drawing_id)
bpy.ops.bim.activate_drawing(drawing=original_drawing_id, camera_view_point=False)
return {"FINISHED"}
def get_camera_dimensions(self):
@@ -288,7 +285,7 @@ class CreateDrawing(bpy.types.Operator):
bpy.ops.render.render(write_still=True)
else:
previous_visibility = {}
for obj in self.camera.users_collection[0].objects:
for obj in self.camera.BIMObjectProperties.collection.objects:
if bpy.context.view_layer.objects.get(obj.name):
previous_visibility[obj.name] = obj.hide_get()
obj.hide_set(True)
@@ -1191,16 +1188,13 @@ class SelectAllDrawings(bpy.types.Operator):
bl_label = "Select All Drawings"
view: bpy.props.StringProperty()
bl_description = "Select all drawings in the drawing list.\n\n" + "SHIFT+CLICK to deselect all drawings"
select_all: bpy.props.BoolProperty(name="Open All", default=False)
select_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# deselect all drawings on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.select_all = False
else:
# can't rely on default value since the line above
# will set the value to `True` for all future operator calls
self.select_all = True
return self.execute(context)
def execute(self, context):
@@ -1219,16 +1213,13 @@ class OpenDrawing(bpy.types.Operator):
+ 'or using "svg_command" from the BlenderBIM preferences (if provided).\n\n'
+ "SHIFT+CLICK to open all selected drawings"
)
open_all: bpy.props.BoolProperty(name="Open All", default=False)
open_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# opening all drawings on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.open_all = True
else:
# can't rely on default value since the line above
# will set the value to `True` for all future operator calls
self.open_all = False
return self.execute(context)
def execute(self, context):
@@ -1306,13 +1297,30 @@ class ActivateDrawing(bpy.types.Operator):
bl_idname = "bim.activate_drawing"
bl_label = "Activate Drawing"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Activates the selected drawing view"
bl_description = "Activates the selected drawing view.\n\n" + "ALT+CLICK to keep the viewport position"
drawing: bpy.props.IntProperty()
camera_view_point: bpy.props.BoolProperty(name="Camera View Point", default=True, options={"SKIP_SAVE"})
def invoke(self, context, event):
# keep the viewport position on alt+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.alt:
self.camera_view_point = False
return self.execute(context)
def execute(self, context):
drawing = tool.Ifc.get().by_id(self.drawing)
dprops = bpy.context.scene.DocProperties
if not self.camera_view_point:
viewport_position = tool.Blender.get_viewport_position()
core.activate_drawing_view(tool.Ifc, tool.Drawing, drawing=drawing)
if not self.camera_view_point:
tool.Blender.set_viewport_position(viewport_position)
dprops.active_drawing_id = self.drawing
# reset DrawingsData to reload_drawing_styles work correctly
DrawingsData.is_loaded = False
@@ -1350,7 +1358,7 @@ class ResizeText(bpy.types.Operator):
# TODO: check undo redo
def execute(self, context):
for obj in context.scene.camera.users_collection[0].objects:
for obj in context.scene.camera.BIMObjectProperties.collection.objects:
if isinstance(obj.data, bpy.types.TextCurve):
annotation.Annotator.resize_text(obj)
return {"FINISHED"}
@@ -1363,16 +1371,13 @@ class RemoveDrawing(bpy.types.Operator, Operator):
bl_description = "Remove currently selected drawing.\n\n" + "SHIFT+CLICK to remove all selected drawings"
drawing: bpy.props.IntProperty()
remove_all: bpy.props.BoolProperty(name="Remove All", default=False)
remove_all: bpy.props.BoolProperty(name="Remove All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# removing all selected drawings on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.remove_all = True
else:
# can't rely on default value since the line above
# will set the value to `True` for all future operator calls
self.remove_all = False
return self.execute(context)
def _execute(self, context):
@@ -231,7 +231,7 @@ def toggleDecorations(self, context):
toggle = self.should_draw_decorations
if toggle:
# TODO: design a proper text variable templating renderer
collection = context.scene.camera.users_collection[0]
collection = context.scene.camera.BIMObjectProperties.collection
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
@@ -28,9 +28,12 @@ import blenderbim.tool as tool
import ifcopenshell.util.geolocation
from xml.dom import minidom
from mathutils import Vector
import re
VIEW_TITLE_OFFSET_Y = 5
DEFAULT_POSITION = Vector((30, 30))
SVG = "{http://www.w3.org/2000/svg}"
XLINK = "{http://www.w3.org/1999/xlink}"
class SheetBuilder:
@@ -130,7 +133,6 @@ class SheetBuilder:
def update_sheet_drawing_sizes(self, sheet):
ET.register_namespace("", "http://www.w3.org/2000/svg")
SVG = "{http://www.w3.org/2000/svg}"
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_tree = ET.parse(layout_path)
@@ -280,8 +282,61 @@ class SheetBuilder:
titleblock.append(g)
titleblock.remove(image)
def ensure_drawing_unique_styles(self, svg, drawing_id):
"""ensures all drawing's classes and ids will be unique for the whole sheet
by adding `drawing_id` based prefix
"""
prefix = f"d{drawing_id}" # just number doesn't work
# add .prefix class to all css selectors
style = svg.find(f"{SVG}defs/{SVG}style")
style_data = style.text
text = ""
brackets_level = 0
for l in style_data:
if l == "{":
if brackets_level == 0:
cur_line = text.splitlines()[-1]
text = text[: -len(cur_line)]
css_selectors = []
# making sure cases like "text, tspan" will be
# converted to "text.prefix, tspan.prefix"
for css_selector in cur_line.split(","):
css_selector = f"{css_selector.strip()}.{prefix}"
css_selectors.append(css_selector)
text += ", ".join(css_selectors) + " "
brackets_level += 1
elif l == "}":
brackets_level -= 1
text += l
# replace urls url(#marker) with url(#prefix-marker)
# since url(#marker.prefix) doesn't seem to work
text = re.sub(r"url\(#([^\)]+)\)", rf"url(#{prefix}-\1)", text)
style.text = text
for svg_element in svg.findall(f".//*"):
if svg_element.tag in (f"{SVG}style", f"{SVG}svg"):
continue
attrib = svg_element.attrib
# add "prefix-" to all ids
if "id" in attrib:
attrib["id"] = f"{prefix}-{attrib['id']}"
# add class "prefix" to all classes
if "class" in attrib:
attrib["class"] += f" {prefix}"
if svg_element.tag == f"{SVG}use":
href_attrib = f"{XLINK}href"
if href_attrib in attrib:
href = attrib[href_attrib]
if href.startswith("#"):
attrib[href_attrib] = f"#{prefix}-{href[1:]}"
return svg
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"])
@@ -300,7 +355,9 @@ class SheetBuilder:
view_title = image
if foreground is not None:
view.append(self.parse_embedded_svg(foreground, {}))
svg = self.parse_embedded_svg(foreground, {})
svg = self.ensure_drawing_unique_styles(svg, drawing_id)
view.append(svg)
if background is not None:
background_path = os.path.join(self.layout_dir, self.get_href(background))
@@ -100,6 +100,9 @@ class SvgWriter:
if not os.path.exists(resource_path):
resource_basename = os.path.basename(resource_path)
ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename)
print(
f"WARNING. Couldn't find {resource} for the drawing by the path: {resource_path}. Default BBIM resource will be copied from {ootb_resource}"
)
if os.path.exists(ootb_resource):
shutil.copy(ootb_resource, resource_path)
self.resource_paths[resource] = resource_path
@@ -121,23 +124,35 @@ class SvgWriter:
self.height = self.raw_height * self.svg_scale
def add_stylesheet(self):
if not self.resource_paths["Stylesheet"] or not os.path.exists(self.resource_paths["Stylesheet"]):
path = self.resource_paths["Stylesheet"]
if not path:
return
with open(self.resource_paths["Stylesheet"], "r") as stylesheet:
if not os.path.exists(path):
print(f"WARNING. Couldn't find stylesheet for the drawing by the path: {path}")
return
with open(path, "r") as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
def add_markers(self):
if not self.resource_paths["Markers"] or not os.path.exists(self.resource_paths["Markers"]):
path = self.resource_paths["Markers"]
if not path:
return
tree = ET.parse(self.resource_paths["Markers"])
if not os.path.exists(path):
print(f"WARNING. Couldn't find markers for the drawing by the path: {path}")
return
tree = ET.parse(path)
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_symbols(self):
if not self.resource_paths["Symbols"] or not os.path.exists(self.resource_paths["Symbols"]):
path = self.resource_paths["Symbols"]
if not path:
return
tree = ET.parse(self.resource_paths["Symbols"])
if not os.path.exists(path):
print(f"WARNING. Couldn't find symbols for the drawing by the path: {path}")
return
tree = ET.parse(path)
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
@@ -148,9 +163,15 @@ class SvgWriter:
return External(xml_symbol) if xml_symbol else None
def add_patterns(self):
if not self.resource_paths["Patterns"] or not os.path.exists(self.resource_paths["Patterns"]):
path = self.resource_paths["Patterns"]
if not path:
return
tree = ET.parse(self.resource_paths["Patterns"])
if not os.path.exists(path):
print(f"WARNING. Couldn't find patterns for the drawing by the path: {path}")
return
if not path or not os.path.exists(path):
return
tree = ET.parse(path)
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
@@ -839,6 +860,10 @@ class SvgWriter:
text_tag.add(tspan)
line_number += 1
if add_fill_bg:
# return line_number back to the original value
line_number -= len(text_lines)
if "fill-bg" in classes:
add_text_tag(True)
add_text_tag(False)
@@ -63,6 +63,8 @@ def register():
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
bpy.types.OBJECT_PT_transform.append(ui.BIM_PT_transform)
bpy.types.VIEW3D_MT_object.append(ui.object_menu)
bpy.types.OUTLINER_MT_object.append(ui.outliner_menu)
bpy.types.VIEW3D_MT_object_context_menu.append(ui.object_menu)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
@@ -90,6 +92,8 @@ def register():
def unregister():
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
bpy.types.OBJECT_PT_transform.remove(ui.BIM_PT_transform)
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
del bpy.types.Scene.BIMGeometryProperties
del bpy.types.Object.BIMGeometryProperties
wm = bpy.context.window_manager
@@ -104,24 +104,44 @@ class ConnectionsData:
def connections(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object)
for rel in getattr(element, "ConnectedTo", []):
connected_to = getattr(element, "ConnectedTo", [])
connected_from = getattr(element, "ConnectedFrom", [])
for rel in connected_to:
if element.is_a("IfcDistributionPort"):
related_element = rel.RelatedPort
related_element_connection_type = ""
else:
related_element = rel.RelatedElement
related_element_connection_type = rel.RelatedConnectionType
results.append(
{
"id": rel.id(),
"is_relating": True,
"Name": rel.RelatedElement.Name or "Unnamed",
"ConnectionType": rel.RelatingConnectionType,
"Name": related_element.Name or "Unnamed",
"ConnectionType": related_element_connection_type,
}
)
for rel in getattr(element, "ConnectedFrom", []):
for rel in connected_from:
if element.is_a("IfcDistributionPort"):
relating_element = rel.RelatingPort
relating_element_connection_type = ""
else:
relating_element = rel.RelatingElement
relating_element_connection_type = rel.RelatingConnectionType
results.append(
{
"id": rel.id(),
"is_relating": False,
"Name": rel.RelatingElement.Name or "Unnamed",
"ConnectionType": rel.RelatedConnectionType,
"Name": relating_element.Name or "Unnamed",
"ConnectionType": relating_element_connection_type,
}
)
return results
@@ -373,6 +373,7 @@ class OverrideDelete(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
use_global: bpy.props.BoolProperty(default=False)
confirm: bpy.props.BoolProperty(default=True)
is_batch: bpy.props.BoolProperty(name="Is Batch", default=False)
@classmethod
def poll(cls, context):
@@ -380,7 +381,7 @@ class OverrideDelete(bpy.types.Operator):
def execute(self, context):
# Deep magick from the dawn of time
if IfcStore.get_file():
if tool.Ifc.get():
return IfcStore.execute_ifc_operator(self, context)
for obj in context.selected_objects:
bpy.data.objects.remove(obj)
@@ -389,21 +390,55 @@ class OverrideDelete(bpy.types.Operator):
return {"FINISHED"}
def invoke(self, context, event):
if self.confirm:
if tool.Ifc.get():
total_elements = len(tool.Ifc.get().wrapped_data.entity_names())
total_polygons = sum([len(o.data.polygons) for o in context.selected_objects if o.type == "MESH"])
# These numbers are a bit arbitrary, but basically batching is only
# really necessary on large models and large geometry removals.
self.is_batch = total_elements > 500000 and total_polygons > 2000
if self.is_batch:
return context.window_manager.invoke_props_dialog(self)
elif self.confirm:
return context.window_manager.invoke_confirm(self, event)
elif self.confirm:
return context.window_manager.invoke_confirm(self, event)
self.confirm = True
return self.execute(context)
def draw(self, context):
row = self.layout.row()
row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR")
row = self.layout.row()
row.prop(self, "is_batch", text="Enable Faster Deletion")
def _execute(self, context):
if self.is_batch:
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
for obj in context.selected_objects:
if tool.Ifc.get_entity(obj):
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()
new_file = ifcopenshell.util.element.unbatch_remove_deep2(tool.Ifc.get())
new_file.begin_transaction()
tool.Ifc.set(new_file)
self.transaction_data = {"old_file": old_file, "new_file": new_file}
IfcStore.add_transaction_operation(self)
# Required otherwise gizmos are still visible
context.view_layer.objects.active = None
return {"FINISHED"}
def rollback(self, data):
tool.Ifc.set(data["old_file"])
data["old_file"].undo()
def commit(self, data):
data["old_file"].redo()
tool.Ifc.set(data["new_file"])
class OverrideOutlinerDelete(bpy.types.Operator):
bl_idname = "bim.override_outliner_delete"
@@ -526,6 +561,8 @@ class OverrideDuplicateMove(bpy.types.Operator):
collection.objects.link(new_obj)
obj.select_set(False)
new_obj.select_set(True)
# clear object's collection so it will be able to have it's own
new_obj.BIMObjectProperties.collection = None
# Copy the actual class
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
if new:
@@ -738,13 +775,17 @@ class OverrideModeSetEdit(bpy.types.Operator):
if not obj:
continue
if not obj.data:
obj.select_set(False)
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
# We are switching from OBJECT to EDIT mode.
usage_type = tool.Model.get_usage_type(element)
if usage_type:
if usage_type is not None and usage_type not in ("PROFILE", "LAYER3"):
# Parametric objects shall not be edited as meshes as they
# can be modified to be incompatible with the parametric
# constraints.
@@ -755,6 +796,36 @@ class OverrideModeSetEdit(bpy.types.Operator):
if not representation:
continue
if (
tool.Pset.get_element_pset(element, "BBIM_Door")
or tool.Pset.get_element_pset(element, "BBIM_Window")
or tool.Pset.get_element_pset(element, "BBIM_Stair")
):
obj.select_set(False)
continue
if usage_type == "PROFILE":
if len(context.selected_objects) == 1:
bpy.ops.bim.hotkey(hotkey="A_E", description="")
return {"FINISHED"}
else:
self.report({"INFO"}, "Only a single profile-based representation can be edited at a time.")
obj.select_set(False)
continue
if (
tool.Geometry.is_profile_based(obj.data)
or usage_type == "LAYER3"
or tool.Geometry.is_swept_profile(representation)
or tool.Pset.get_element_pset(element, "BBIM_Roof")
or tool.Pset.get_element_pset(element, "BBIM_Railing")
):
if len(context.selected_objects) == 1:
bpy.ops.bim.hotkey(hotkey="S_E", description="")
return {"FINISHED"}
else:
self.report({"INFO"}, "Only a single profile-based representation can be edited at a time.")
obj.select_set(False)
continue
if tool.Geometry.is_meshlike(representation):
if getattr(element, "HasOpenings", None):
# Mesh elements with openings must disable openings
@@ -866,7 +937,22 @@ class OverrideModeSetObject(bpy.types.Operator):
if not element:
continue
if obj.data.BIMMeshProperties.ifc_definition_id:
if tool.Profile.is_editing_profile():
profile_id = context.scene.BIMProfileProperties.active_profile_id
if profile_id:
profile = tool.Ifc.get().by_id(profile_id)
if tool.Ifc.get_object(profile): # We are editing an arbitrary profile
bpy.ops.bim.edit_arbitrary_profile()
elif tool.Pset.get_element_pset(element, "BBIM_Railing"):
bpy.ops.bim.cad_hotkey(hotkey="S_Q")
elif tool.Pset.get_element_pset(element, "BBIM_Roof"):
bpy.ops.bim.cad_hotkey(hotkey="S_Q")
elif tool.Model.get_usage_type(element) == "PROFILE":
bpy.ops.bim.edit_extrusion_axis()
else:
bpy.ops.bim.edit_extrusion_profile()
return self.execute(context)
elif obj.data.BIMMeshProperties.ifc_definition_id:
if not tool.Geometry.has_geometric_data(obj):
self.is_valid = False
self.should_save = False
@@ -24,10 +24,14 @@ from blenderbim.bim.module.geometry.data import RepresentationsData, Connections
def object_menu(self, context):
self.layout.separator()
self.layout.operator("bim.override_object_duplicate_move", icon="PLUGIN")
self.layout.operator("bim.override_object_delete", icon="PLUGIN")
self.layout.operator("bim.override_paste_buffer", icon="PLUGIN")
def outliner_menu(self, context):
self.layout.separator()
self.layout.operator("bim.override_outliner_delete", icon='X')
class BIM_PT_representations(Panel):
bl_label = "IFC Representations"
@@ -33,6 +33,7 @@ classes = (
operator.ToggleGroup,
operator.UnassignGroup,
operator.UpdateGroup,
operator.SelectGroupElements,
prop.ExpandedGroups,
prop.Group,
prop.BIMGroupProperties,
@@ -275,3 +275,19 @@ class UpdateGroup(bpy.types.Operator, tool.Ifc.Operator):
)
bpy.ops.bim.load_groups()
return {"FINISHED"}
class SelectGroupElements(bpy.types.Operator):
bl_idname = "bim.select_group_elements"
bl_label = "Select Group elements"
bl_options = {"REGISTER", "UNDO"}
group: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
return bool(tool.Ifc.get() and context.active_object)
def execute(self, context):
elements = tool.Drawing.get_group_elements(tool.Ifc.get().by_id(self.group))
tool.Spatial.select_products(elements)
return {"FINISHED"}
@@ -107,6 +107,7 @@ class BIM_PT_object_groups(Panel):
for group in ObjectGroupsData.data["groups"]:
row = self.layout.row(align=True)
row.label(text=group["name"])
row.operator("bim.select_group_elements", text="", icon="RESTRICT_SELECT_OFF").group = group["id"]
op = row.operator("bim.unassign_group", text="", icon="X")
op.group = group["id"]
@@ -21,10 +21,12 @@ from . import ui, prop, operator
classes = (
operator.AddFileToRepo,
operator.AddRemote,
operator.AddTag,
operator.CloneRepo,
operator.CommitChanges,
operator.CreateRepo,
operator.DeleteRemote,
operator.DeleteTag,
operator.DiscardUncommitted,
operator.DisplayRevision,
@@ -15,6 +15,11 @@ class IfcGitData:
data = {}
is_loaded = False
@classmethod
def make_sure_is_loaded(cls):
if not cls.is_loaded:
cls.load()
@classmethod
def load(cls):
cls.data = {
@@ -29,6 +34,10 @@ class IfcGitData:
"name_ifc": cls.name_ifc(),
"dir_name": cls.dir_name(),
"base_name": cls.base_name(),
"working_dir": cls.working_dir(),
"untracked_files": cls.untracked_files(),
"is_detached": cls.is_detached(),
"active_branch_name": cls.active_branch_name(),
"is_dirty": cls.is_dirty(),
"commit": cls.commit(),
"current_revision": cls.current_revision(),
@@ -113,6 +122,27 @@ class IfcGitData:
return os.path.basename(path_ifc)
return None
@classmethod
def working_dir(cls):
if cls.repo():
return cls.repo().working_dir
@classmethod
def untracked_files(cls):
if cls.repo():
return cls.repo().untracked_files
return []
@classmethod
def is_detached(cls):
if cls.repo():
return cls.repo().head.is_detached
@classmethod
def active_branch_name(cls):
if cls.repo() and not cls.is_detached():
return cls.repo().active_branch.name
@classmethod
def is_dirty(cls):
if cls.repo() and cls.git_exe():
@@ -15,6 +15,7 @@ class CreateRepo(bpy.types.Operator):
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
path_ifc = IfcGitData.data["path_ifc"]
if not os.path.isfile(path_ifc):
return False
@@ -42,6 +43,7 @@ class AddFileToRepo(bpy.types.Operator):
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
path_ifc = IfcGitData.data["path_ifc"]
if not os.path.isfile(path_ifc):
return False
@@ -80,6 +82,7 @@ class CloneRepo(bpy.types.Operator):
props = context.scene.IfcGitProperties
core.clone_repo(tool.IfcGit, props.remote_url, props.local_folder, self)
props.remote_url = ""
refresh()
return {"FINISHED"}
@@ -107,6 +110,7 @@ class CommitChanges(bpy.types.Operator):
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
props = context.scene.IfcGitProperties
repo = IfcGitData.data["repo"]
if props.commit_message == "":
@@ -128,7 +132,7 @@ class CommitChanges(bpy.types.Operator):
def execute(self, context):
repo = IfcGitData.data["repo"]
core.commit_changes(tool.IfcGit, tool.Ifc, repo, context)
core.commit_changes(tool.IfcGit, tool.Ifc, repo)
bpy.ops.ifcgit.refresh()
refresh()
return {"FINISHED"}
@@ -143,7 +147,10 @@ class AddTag(bpy.types.Operator):
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
props = context.scene.IfcGitProperties
if props.new_tag_name == "":
return False
repo = IfcGitData.data["repo"]
if repo and (
not tool.IfcGit.is_valid_ref_format(props.new_tag_name)
@@ -187,8 +194,9 @@ class RefreshGit(bpy.types.Operator):
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
repo = IfcGitData.data["repo"]
if repo != None and repo.heads:
if repo:
return True
return False
@@ -207,9 +215,15 @@ class DisplayRevision(bpy.types.Operator):
bl_idname = "ifcgit.display_revision"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
props = context.scene.IfcGitProperties
if props.ifcgit_commits:
return True
def execute(self, context):
core.colourise_revision(tool.IfcGit, context)
core.colourise_revision(tool.IfcGit)
refresh()
return {"FINISHED"}
@@ -236,6 +250,12 @@ class SwitchRevision(bpy.types.Operator):
bl_idname = "ifcgit.switch_revision"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
props = context.scene.IfcGitProperties
if props.ifcgit_commits:
return True
def execute(self, context):
core.switch_revision(tool.IfcGit, tool.Ifc)
@@ -252,7 +272,9 @@ class Merge(bpy.types.Operator):
@classmethod
def poll(cls, context):
if IfcGitData.data["ifcmerge_exe"]:
IfcGitData.make_sure_is_loaded()
props = context.scene.IfcGitProperties
if IfcGitData.data["ifcmerge_exe"] and props.ifcgit_commits and not IfcGitData.data["is_detached"]:
return True
return False
@@ -276,8 +298,7 @@ class Push(bpy.types.Operator):
props = context.scene.IfcGitProperties
repo = IfcGitData.data["repo"]
remote = repo.remotes[props.select_remote]
remote.push(refspec=IfcGitData.data["repo"].active_branch.name)
core.push(tool.IfcGit, repo, props.select_remote, self)
return {"FINISHED"}
@@ -297,6 +318,52 @@ class Fetch(bpy.types.Operator):
return {"FINISHED"}
class AddRemote(bpy.types.Operator):
"""Add a remote repository"""
bl_label = "Add Remote"
bl_idname = "ifcgit.add_remote"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
props = context.scene.IfcGitProperties
repo = IfcGitData.data["repo"]
if (
not repo
or not tool.IfcGit.is_valid_ref_format(props.remote_name)
or not props.remote_url
or props.remote_name in [remote.name for remote in repo.remotes]
):
return False
return True
def execute(self, context):
repo = IfcGitData.data["repo"]
core.add_remote(tool.IfcGit, repo)
bpy.ops.ifcgit.refresh()
refresh()
return {"FINISHED"}
class DeleteRemote(bpy.types.Operator):
"""Delete the selected remote"""
bl_label = "Delete Remote"
bl_idname = "ifcgit.delete_remote"
bl_options = {"REGISTER"}
def execute(self, context):
repo = IfcGitData.data["repo"]
core.delete_remote(tool.IfcGit, repo)
bpy.ops.ifcgit.refresh()
refresh()
return {"FINISHED"}
class ObjectLog(bpy.types.Operator):
"""Displays Git log of selected object"""
@@ -7,7 +7,7 @@ from bpy.props import (
IntProperty,
EnumProperty,
)
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
from blenderbim.bim.module.ifcgit.data import IfcGitData
def git_branches(self, context):
@@ -22,8 +22,9 @@ def git_branches(self, context):
IfcGitData.data["branch_names"] = ["main"] + IfcGitData.data["branch_names"]
if IfcGitData.data["remotes"]:
props = context.scene.IfcGitProperties
IfcGitData.data["branch_names"] += [r.name for r in IfcGitData.data["remotes"][props.select_remote].refs]
for remote in IfcGitData.data["remotes"]:
for remote_branch in remote.refs:
IfcGitData.data["branch_names"].append(remote_branch.name)
return [(myname, myname, myname) for myname in IfcGitData.data["branch_names"]]
@@ -113,6 +114,11 @@ class IfcGitProperties(PropertyGroup):
description="An optional human readable description of this tag",
default="",
)
remote_name: StringProperty(
name="New remote name",
description="A local name for a remote Git repository",
default="",
)
remote_url: StringProperty(
name="Git URL",
description="A URL pointing to a Git repository",
@@ -1,6 +1,6 @@
import bpy
import time
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
from blenderbim.bim.module.ifcgit.data import IfcGitData
class IFCGIT_PT_panel(bpy.types.Panel):
@@ -34,8 +34,8 @@ class IFCGIT_PT_panel(bpy.types.Panel):
if path_ifc:
if IfcGitData.data["repo"]:
name_ifc = IfcGitData.data["name_ifc"]
row.label(text=IfcGitData.data["repo"].working_dir, icon="SYSTEM")
if name_ifc in IfcGitData.data["repo"].untracked_files:
row.label(text=IfcGitData.data["working_dir"], icon="SYSTEM")
if name_ifc in IfcGitData.data["untracked_files"]:
row.operator(
"ifcgit.addfile",
text="Add '" + name_ifc + "' to repository",
@@ -79,7 +79,7 @@ class IFCGIT_PT_panel(bpy.types.Panel):
row = layout.row()
row.prop(props, "commit_message")
if IfcGitData.data["repo"].head.is_detached:
if IfcGitData.data["is_detached"]:
row = layout.row()
row.label(text="HEAD is detached, commit will create a branch", icon="ERROR")
row.prop(props, "new_branch_name")
@@ -88,10 +88,10 @@ class IFCGIT_PT_panel(bpy.types.Panel):
row.operator("ifcgit.commit_changes", icon="GREASEPENCIL")
row = layout.row()
if IfcGitData.data["repo"].head.is_detached:
if IfcGitData.data["is_detached"]:
row.label(text="Working branch: Detached HEAD")
else:
row.label(text="Working branch: " + IfcGitData.data["repo"].active_branch.name)
row.label(text="Working branch: " + IfcGitData.data["active_branch_name"])
grouped = layout.row()
column = grouped.column()
@@ -120,8 +120,6 @@ class IFCGIT_PT_panel(bpy.types.Panel):
row = column.row()
row.operator("ifcgit.switch_revision", icon="CURRENT_FILE")
# TODO operator to tag selected
row = column.row()
row.operator("ifcgit.merge", icon="EXPERIMENTAL", text="")
@@ -168,10 +166,19 @@ class IFCGIT_PT_panel(bpy.types.Panel):
row.prop(props, "select_remote", text="Select remote")
urls = IfcGitData.data["remote_urls"]
row.label(text=urls[props.select_remote])
row.operator("ifcgit.delete_remote", text="", icon="PANEL_CLOSE")
row = layout.row()
row.operator("ifcgit.push", icon="EXPERIMENTAL")
row.operator("ifcgit.push", icon="EXPORT")
row.operator("ifcgit.fetch", icon="IMPORT")
box = layout.box()
row = box.row()
row.prop(props, "remote_name")
row = box.row()
row.prop(props, "remote_url")
row = box.row()
row.operator("ifcgit.add_remote", icon="ADD")
class COMMIT_UL_List(bpy.types.UIList):
"""List of Git commits"""
@@ -199,9 +206,9 @@ class COMMIT_UL_List(bpy.types.UIList):
refs += "{" + tag.name + "} "
if commit == current_revision:
layout.label(text="[HEAD] " + refs + commit.message, icon="DECORATE_KEYFRAME")
layout.label(text="[HEAD] " + refs + commit.message.split("\n")[0], icon="DECORATE_KEYFRAME")
else:
layout.label(text=refs + commit.message, icon="DECORATE_ANIMATE")
layout.label(text=refs + commit.message.split("\n")[0], icon="DECORATE_ANIMATE")
layout.label(text=time.strftime("%c", time.localtime(commit.committed_date)))
@@ -56,7 +56,7 @@ class SelectByMaterial(bpy.types.Operator, tool.Ifc.Operator):
material: bpy.props.IntProperty()
def _execute(self, context):
core.select_by_material(tool.Material, material=tool.Ifc.get().by_id(self.material))
core.select_by_material(tool.Material, tool.Spatial, material=tool.Ifc.get().by_id(self.material))
class EnableEditingMaterial(bpy.types.Operator, tool.Ifc.Operator):
@@ -174,67 +174,7 @@ class AssignMaterial(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
objects = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
active_obj = context.active_object
active_object_material_type = self.material_type or active_obj.BIMObjectMaterialProperties.material_type
material = tool.Ifc.get().by_id(int(active_obj.BIMObjectMaterialProperties.material))
for obj in objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
ifcopenshell.api.run(
"material.assign_material",
tool.Ifc.get(),
product=element,
type=active_object_material_type,
material=material,
)
assigned_material = ifcopenshell.util.element.get_material(element)
if assigned_material.is_a("IfcMaterialConstituentSet"):
if not assigned_material.MaterialConstituents:
ifcopenshell.api.run(
"material.add_constituent",
tool.Ifc.get(),
constituent_set=assigned_material,
material=material,
)
elif assigned_material.is_a() == "IfcMaterialLayerSet":
if not assigned_material.MaterialLayers:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layer = ifcopenshell.api.run(
"material.add_layer",
tool.Ifc.get(),
layer_set=assigned_material,
material=material,
)
thickness = 0.1 # Arbitrary metric thickness for now
layer.LayerThickness = thickness / unit_scale
elif assigned_material.is_a("IfcMaterialProfileSet"):
if not assigned_material.MaterialProfiles:
named_profiles = [p for p in tool.Ifc.get().by_type("IfcProfileDef") if p.ProfileName]
if named_profiles:
profile = named_profiles[0]
else:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
size = 0.5 / unit_scale
profile = tool.Ifc.get().create_entity(
"IfcRectangleProfileDef",
ProfileName="New Profile",
ProfileType="AREA",
XDim=size,
YDim=size,
)
material_profile = ifcopenshell.api.run(
"material.add_profile",
tool.Ifc.get(),
profile_set=assigned_material,
material=tool.Ifc.get().by_type("IfcMaterial")[0],
)
ifcopenshell.api.run(
"material.assign_profile",
tool.Ifc.get(),
material_profile=material_profile,
profile=profile,
)
core.assign_material(tool.Ifc, tool.Material, material_type= self.material_type , objects=objects )
class UnassignMaterial(bpy.types.Operator, tool.Ifc.Operator):
@@ -245,12 +185,7 @@ class UnassignMaterial(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
objects = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
for obj in objects:
element = tool.Ifc.get_entity(obj)
if element:
material = ifcopenshell.util.element.get_material(element, should_inherit=False)
if "Usage" not in material.is_a():
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=element)
core.unassign_material(tool.Ifc, tool.Material, objects=objects )
class AddConstituent(bpy.types.Operator, tool.Ifc.Operator):
@@ -30,7 +30,6 @@ class BIM_PT_misc_utilities(bpy.types.Panel):
def draw(self, context):
layout = self.layout
props = context.scene.BIMMiscProperties
row = layout.split(factor=0.2, align=True)
row.prop(props, "override_colour", text="")
row.operator("bim.set_override_colour")
@@ -49,6 +48,8 @@ class BIM_PT_misc_utilities(bpy.types.Panel):
row.operator("bim.draw_system_arrows")
row = layout.row()
row.operator("bim.clean_wireframes")
row = layout.row()
row.operator("bim.patch_non_parametric_mep_segment")
row = layout.row(align=True)
row.operator("bim.enable_editing_sketch_extrusion_profile", text="Start Sketching")
@@ -86,6 +86,7 @@ classes = (
profile.ExtendProfile,
profile.RecalculateProfile,
profile.Rotate90,
profile.PatchNonParametricMepSegment,
roof.GenerateHippedRoof,
slab.DisableEditingExtrusionProfile,
slab.DisableEditingSketchExtrusionProfile,
@@ -97,6 +98,7 @@ classes = (
slab.SetArcIndex,
space.GenerateSpace,
space.GenerateSpacesFromWalls,
space.ToggleSpaceVisibility,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -105,7 +107,6 @@ classes = (
prop.BIMDoorProperties,
prop.BIMRailingProperties,
prop.BIMRoofProperties,
ui.BIM_PT_authoring,
ui.BIM_PT_array,
ui.BIM_PT_stair,
ui.BIM_PT_sverchok,
@@ -115,7 +116,6 @@ classes = (
ui.BIM_PT_roof,
ui.LaunchTypeManager,
ui.BIM_MT_model,
ui.BIM_PT_GridsSpatialManager,
ui.BIM_PT_Grids,
grid.BIM_OT_add_object,
stair.BIM_OT_add_object,
@@ -128,15 +128,16 @@ def update_door_modifier_representation(context):
)
# type attributes
element.OperationType = props.door_type
if tool.Ifc.get_schema() != "IFC2X3":
element.OperationType = props.door_type
# occurences attributes
occurences = tool.Ifc.get_all_element_occurences(element)
for occurence in occurences:
occurence.OverallWidth = props.overall_width
occurence.OverallHeight = props.overall_height
occurence.OverallWidth = props.overall_width / si_conversion
occurence.OverallHeight = props.overall_height / si_conversion
update_simple_openings(element, props.overall_width, props.overall_height)
update_simple_openings(element, props.overall_width / si_conversion, props.overall_height / si_conversion)
# TODO: move it out to tools
@@ -486,7 +487,8 @@ class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
element = blenderbim.core.root.assign_class(
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcDoor", should_add_representation=False
)
element.PredefinedType = "DOOR"
if tool.Ifc.get_schema() != "IFC2X3":
element.PredefinedType = "DOOR"
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None
@@ -507,8 +509,8 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMDoorProperties
if element.is_a() not in ("IfcDoor", "IfcDoorType"):
self.report({"ERROR"}, "Object has to be IfcDoor/IfcDoorType type to add a door.")
if element.is_a() not in ("IfcDoor", "IfcDoorType", "IfcDoorStyle"):
self.report({"ERROR"}, "Object has to be IfcDoor/IfcDoorType/IfcDoorStyle type to add a door.")
return {"CANCELLED"}
door_data = props.get_general_kwargs(convert_to_project_units=True)
@@ -232,9 +232,16 @@ class FilledOpeningGenerator:
curve_3d = ifcopenshell.util.representation.resolve_representation(profile).Items[0]
def get_curve_2d_from_3d(curve_3d):
ifc_segments = [shape_builder.deep_copy(s) for s in curve_3d.Segments]
ifc_points = ifc_file.createIfcCartesianPointList2D([Vector(p).xz for p in curve_3d.Points.CoordList])
ifc_curve = ifc_file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
if tool.Ifc.get_schema() == "IFC2X3":
coords = [Vector(p).xz for p in shape_builder.get_polyline_coords(curve_3d)]
ifc_curve = shape_builder.polyline(coords, closed=True)
else:
# using different algorithm to keep arc segments possible in the future
ifc_segments = [shape_builder.deep_copy(s) for s in curve_3d.Segments]
ifc_points = ifc_file.createIfcCartesianPointList2D(
[Vector(p).xz for p in curve_3d.Points.CoordList]
)
ifc_curve = ifc_file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return ifc_curve
extrusion = shape_builder.extrude(
@@ -68,7 +68,7 @@ class PieUpdateContainer(bpy.types.Operator):
if not obj.BIMObjectProperties.ifc_definition_id:
continue
for collection in obj.users_collection:
spatial_obj = bpy.data.objects.get(collection.name)
spatial_obj = collection.BIMCollectionProperties.obj
if spatial_obj and spatial_obj.BIMObjectProperties.ifc_definition_id:
blenderbim.core.spatial.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=spatial_obj, element_obj=obj
@@ -128,20 +128,9 @@ class AddConstrTypeInstance(bpy.types.Operator):
obj.location = context.scene.cursor.location
collection = None
if (
building_obj
and building_element
and building_element.is_a() in ["IfcWall", "IfcWallStandardCase", "IfcCovering"]
and instance_class in ["IfcWindow", "IfcDoor"]
):
# Fills should be a sibling to the building element
collection = building_obj.users_collection[0]
if not collection:
collection = context.view_layer.active_layer_collection.collection
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
collection_obj = bpy.data.objects.get(collection.name)
collection_obj = collection.BIMCollectionProperties.obj
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
element = tool.Ifc.get_entity(obj)
@@ -150,6 +139,26 @@ class AddConstrTypeInstance(bpy.types.Operator):
# Update required as core.type.assign_type may change obj.data
context.view_layer.update()
if (
building_obj
and building_element
and building_element.is_a() in ["IfcWall", "IfcWallStandardCase", "IfcCovering"]
and instance_class in ["IfcWindow", "IfcDoor"]
):
# Fills should be a sibling to the building element
parent = ifcopenshell.util.element.get_aggregate(building_element)
if parent:
parent_obj = tool.Ifc.get_object(parent)
blenderbim.core.aggregate.assign_object(
tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=parent_obj, related_obj=obj
)
else:
parent = ifcopenshell.util.element.get_container(building_element)
parent_obj = tool.Ifc.get_object(parent)
blenderbim.core.spatial.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=parent_obj, element_obj=obj
)
# set occurences properties for the types defined with modifiers
if instance_class in ["IfcWindow", "IfcDoor"]:
pset_name = f"BBIM_{instance_class[3:]}"
@@ -462,11 +471,6 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
obj.matrix_world = newmat
def copy_obj(self, obj):
new = obj.copy()
new.data = wall2.data.copy()
wall1.users_collection[0].objects.link(wall2)
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
def generate_box(usecase_path, ifc_file, settings):
@@ -28,6 +28,7 @@ import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.type
import blenderbim.core.geometry
import blenderbim.core.material
from math import pi, degrees, inf
from mathutils import Vector, Matrix, Quaternion
from blenderbim.bim.module.geometry.helper import Helper
@@ -53,7 +54,7 @@ class DumbProfileGenerator:
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
props = bpy.context.scene.BIMModelProperties
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.collection_obj = self.collection.BIMCollectionProperties.obj
self.depth = props.extrusion_depth
self.rotation = 0
self.location = Vector((0, 0, 0))
@@ -857,6 +858,25 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class PatchNonParametricMepSegment(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.patch_non_parametric_mep_segment"
bl_label = "Set MEP segment Material Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.active_object
def _execute(self, context):
styles = tool.Geometry.get_styles(context.active_object)
blenderbim.core.material.patch_non_parametric_mep_segment(
tool.Ifc, tool.Material, tool.Profile, obj=context.active_object
)
bpy.ops.bim.enable_editing_extrusion_axis()
bpy.ops.bim.edit_extrusion_axis()
styles = tool.Geometry.get_styles(context.active_object)
class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_extrusion_axis"
bl_label = "Enable Editing Extrusion Axis"
@@ -927,7 +947,7 @@ class DisableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
return context.selected_objects
def _execute(self, context):
return disable_editing_extrusion_axis()
return disable_editing_extrusion_axis(context)
class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
@@ -948,6 +968,10 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
depth = (end - start).length
z_axis = (end - start).normalized()
y_axis = Vector((0, 0, 1))
# making sure z_axis != y_axis
if z_axis == y_axis:
y_axis = Vector((0,1,0))
x_axis = y_axis.cross(z_axis).normalized()
y_axis = z_axis.cross(x_axis).normalized()
@@ -133,7 +133,7 @@ class DumbSlabGenerator:
props = bpy.context.scene.BIMModelProperties
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.collection_obj = self.collection.BIMCollectionProperties.obj
self.depth = sum(thicknesses) * unit_scale
self.width = 3
self.length = 3
@@ -26,7 +26,7 @@ import blenderbim.tool as tool
import blenderbim.core.type
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon
from shapely import Polygon, MultiPolygon
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
@@ -38,7 +38,7 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = bpy.data.objects.get(collection.name)
collection_obj = collection.BIMCollectionProperties.obj
return tool.Ifc.get_entity(collection_obj)
def _execute(self, context):
@@ -60,7 +60,7 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
relating_type = None
collection = context.view_layer.active_layer_collection.collection
collection_obj = bpy.data.objects.get(collection.name)
collection_obj = collection.BIMCollectionProperties.obj
if not collection_obj:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
@@ -185,7 +185,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_spaces_from_walls"
bl_label = "Generate Spaces From Walls"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Generate spaces from selected walls. The active object must be a wall."
bl_description = "Generate spaces from selected walls. The active object must be a wall"
@classmethod
def poll(cls, context):
@@ -203,29 +203,19 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
active_obj = bpy.context.active_object
if not active_obj:
self.report({'ERROR'}, "No active object. Please select a wall")
self.report({"ERROR"}, "No active object. Please select a wall")
return
element = None
element = tool.Ifc.get_entity(active_obj)
if element:
if not element.is_a("IfcWall"):
self.report({'ERROR'}, "The active object is not a wall. Please select a wall.")
return
if element and not element.is_a("IfcWall"):
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
collection = active_obj.users_collection[0]
collection_obj = bpy.data.objects.get(collection.name)
if not collection_obj:
self.report({'ERROR'}, "No collection found. Please insert one.")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
self.report({'ERROR'}, "The collection hasn't an ifc space entity. Please provide one.")
return
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.")
self.report({"ERROR"}, "No selected objects found. Please select walls.")
return
x, y, z = active_obj.matrix_world.translation.xyz
@@ -237,19 +227,20 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
polys = self.get_polygons(boundary_elements)
converted_tolerance = self.get_converted_tolerance(tolerance = 0.03)
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 = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style=2, join_style=2)
i=0
for linear_ring in union.interiors:
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)
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)
mesh = bpy.data.meshes.new(name=name)
bm.to_mesh(mesh)
bm.free()
@@ -258,19 +249,21 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
self.set_obj_origin_to_bboxcenter(obj)
collection.objects.link(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")
i+=1
return {"FINISHED"}
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"):
if subelement.is_a("IfcWall") or subelement.is_a("IfcColumn"):
boundary_elements.append(subelement)
return boundary_elements
@@ -302,17 +295,40 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
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)
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,
)
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()
@@ -335,7 +351,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
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)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
@@ -353,3 +369,35 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
aux_vector = aux_vector - diff
vert.co = inverted @ aux_vector
obj.location = newLoc
class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.toggle_space_visibility"
bl_label = "Toggle Space Visibility"
bl_options = {"REGISTER"}
bl_description = "Change the space visibility"
def execute(cls, context):
model = tool.Ifc.get()
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"}
@@ -250,7 +250,8 @@ def update_ifc_stair_props(obj):
props = obj.BIMStairProperties
ifc_file = tool.Ifc.get()
element.PredefinedType = "STRAIGHT"
if tool.Ifc.get_schema() != "IFC2X3":
element.PredefinedType = "STRAIGHT"
number_of_risers = props.number_of_treads + 1
# update IfcStairFlight properties (seems already deprecated but keep it for now)
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcStairFlight.htm
@@ -260,7 +261,11 @@ def update_ifc_stair_props(obj):
tread_length = props.tread_depth / si_conversion
if element.is_a("IfcStairFlight"):
element.NumberOfRisers = number_of_risers
if tool.Ifc.get_schema() == "IFC2X3":
element.NumberOfRiser = number_of_risers
else:
element.NumberOfRisers = number_of_risers
element.NumberOfTreads = props.number_of_treads
element.RiserHeight = riser_height
element.TreadLength = tread_length
@@ -330,7 +335,8 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
should_add_representation=True,
context=body_context,
)
element.PredefinedType = "STRAIGHT"
if tool.Ifc.get_schema() != "IFC2X3":
element.PredefinedType = "STRAIGHT"
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None
@@ -135,32 +135,24 @@ class BIM_PT_authoring(Panel):
row.operator("bim.generate_space")
row = self.layout.row(align=True)
row.operator("bim.generate_spaces_from_walls")
row = self.layout.row(align=True)
row.operator("bim.toggle_space_visibility")
class BIM_PT_GridsSpatialManager(Panel):
bl_label = "Grids and Containers"
bl_idname = "BIM_PT_GridsSpatialManager"
class BIM_PT_Grids(Panel):
bl_label = "Grids"
bl_idname = "BIM_PT_Grids"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_options = {"DEFAULT_CLOSED"}
bl_category = "BlenderBIM"
def draw(self, context):
pass
class BIM_PT_Grids(Panel):
bl_label = "Grid Creator"
bl_idname = "BIM_PT_Grids"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_GridsSpatialManager"
def draw(self, context):
self.animation_props = context.scene.BIMAnimationProperties
row = self.layout.row()
row.operator("mesh.add_grid", icon="ADD", text="Add Grids")
class BIM_PT_array(bpy.types.Panel):
bl_label = "IFC Array"
bl_idname = "BIM_PT_array"
@@ -434,7 +434,7 @@ class DumbWallGenerator:
props = bpy.context.scene.BIMModelProperties
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.collection_obj = self.collection.BIMCollectionProperties.obj
self.width = self.layers["thickness"]
self.height = props.extrusion_depth
self.length = props.length
@@ -934,7 +934,8 @@ class DumbWallJoiner:
def duplicate_wall(self, wall1):
wall2 = wall1.copy()
wall2.data = wall2.data.copy()
wall1.users_collection[0].objects.link(wall2)
for collection in wall1.users_collection:
collection.objects.link(wall2)
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
return wall2
@@ -167,7 +167,8 @@ def update_window_modifier_representation(context):
)
# type attributes
element.PartitioningType = props.window_type
if tool.Ifc.get_schema() != "IFC2X3":
element.PartitioningType = props.window_type
# occurences attributes
occurences = tool.Ifc.get_all_element_occurences(element)
@@ -262,14 +263,19 @@ def create_bm_window(
frame_thickness,
glass_thickness,
position: Vector,
x_offsets: list = None,
):
"""`lining_thickness` expected to be defined as a list,
"""`lining_thickness` and `x_offsets` are expected to be defined as a list,
similarly to `create_bm_window_frame` `thickness` argument"""
if x_offsets is None:
x_offsets = [lining_to_panel_offset_x] * 4
# window lining
window_lining_verts = create_bm_window_frame(bm, lining_size, lining_thickness)
# window frame
frame_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y_full, lining_to_panel_offset_x)
frame_position = V(x_offsets[0], lining_to_panel_offset_y_full, x_offsets[3])
frame_verts = create_bm_window_frame(bm, frame_size, frame_thickness, frame_position)
# window glass
@@ -319,21 +325,35 @@ def update_window_modifier_bmesh(context):
unique_cols = len(set(panel_row))
for column_i, panel_i in enumerate(panel_row):
# detect mullion
has_mullion = unique_cols > 1
first_column = column_i == 0
last_column = column_i == unique_cols - 1
left_to_mullion = has_mullion and not last_column
right_to_mullion = has_mullion and not first_column
# detect transom
has_transom = unique_rows_in_col[column_i] > 1
first_row = row_i == 0
last_row = row_i == unique_rows_in_col[column_i] - 1
top_to_transom = has_transom and not first_row
bottom_to_transom = has_transom and not last_row
# calculate current panel dimensions
if unique_cols > 1:
if column_i == 0:
if has_mullion:
if first_column:
panel_width = first_mullion_offset
elif column_i == unique_cols - 1:
elif last_column:
panel_width = overall_width - accumulated_width
else:
panel_width = second_mullion_offset - accumulated_width
else:
panel_width = overall_width
if unique_rows_in_col[column_i] > 1:
if row_i == 0:
if has_transom:
if first_row:
panel_height = first_transom_offset
elif row_i == unique_rows_in_col[column_i] - 1:
elif last_row:
panel_height = overall_height - accumulated_height[column_i]
else:
panel_height = second_transom_offset - accumulated_height[column_i]
@@ -347,7 +367,7 @@ def update_window_modifier_bmesh(context):
frame_depth = props.frame_depth[panel_i]
frame_thickness = props.frame_thickness[panel_i]
lining_to_panel_offset_y_full = (lining_depth - frame_depth) + lining_to_panel_offset_y
# add window
window_lining_size = V(
panel_width,
@@ -355,25 +375,33 @@ def update_window_modifier_bmesh(context):
panel_height,
)
# calculate lining thickness
# calculate lining thickness and frame size / offset
# taking into account mullions and transoms
window_lining_thickness = [lining_thickness] * 4
# mullion thickness
if unique_cols > 1:
if column_i != 0:
window_lining_thickness[0] = mullion_thickness # left column
if column_i != unique_cols - 1:
window_lining_thickness[2] = mullion_thickness # right column
# transom thickness
if unique_rows_in_col[column_i] > 1:
if row_i != 0:
window_lining_thickness[3] = transom_thickness # bottom row
if row_i != unique_rows_in_col[column_i] - 1:
window_lining_thickness[1] = transom_thickness # top row
# fmt: off
window_lining_thickness = [
mullion_thickness if right_to_mullion else lining_thickness,
transom_thickness if bottom_to_transom else lining_thickness,
mullion_thickness if left_to_mullion else lining_thickness,
transom_thickness if top_to_transom else lining_thickness,
]
# x offsets can differ if there are mullions or transoms because we're trying to maintain symmetry
base_frame_clear = lining_to_panel_offset_x + frame_thickness - lining_thickness
current_offset_x = base_frame_clear - frame_thickness + mullion_thickness
current_offset_z = base_frame_clear - frame_thickness + transom_thickness
# fmt: off
x_offsets = [
current_offset_x if right_to_mullion else lining_to_panel_offset_x, # LEFT
current_offset_z if bottom_to_transom else lining_to_panel_offset_x, # TOP
current_offset_x if left_to_mullion else lining_to_panel_offset_x, # RIGHT
current_offset_z if top_to_transom else lining_to_panel_offset_x, # BOTTOM
]
# fmt: on
frame_size = window_lining_size.copy()
frame_size.y = frame_depth
frame_size = frame_size - V(lining_to_panel_offset_x * 2, 0, lining_to_panel_offset_x * 2)
frame_size.x -= x_offsets[0] + x_offsets[2]
frame_size.z -= x_offsets[1] + x_offsets[3]
window_position = V(accumulated_width, 0, accumulated_height[column_i])
lining_verts, panel_verts, glass_verts = create_bm_window(
@@ -381,11 +409,12 @@ def update_window_modifier_bmesh(context):
window_lining_size,
window_lining_thickness,
lining_to_panel_offset_x,
(lining_depth - frame_depth) + lining_to_panel_offset_y,
lining_to_panel_offset_y_full,
frame_size,
frame_thickness,
glass_thickness,
window_position,
x_offsets,
)
built_panels.append(panel_i)
@@ -427,7 +456,8 @@ class BIM_OT_add_window(bpy.types.Operator, tool.Ifc.Operator):
element = blenderbim.core.root.assign_class(
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcWindow", should_add_representation=False
)
element.PredefinedType = "WINDOW"
if tool.Ifc.get_schema() != "IFC2X3":
element.PredefinedType = "WINDOW"
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None
@@ -448,8 +478,8 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMWindowProperties
if element.is_a() not in ("IfcWindow", "IfcWindowType"):
self.report({"ERROR"}, "Object has to be IfcWindow/IfcWindowType type to add a window.")
if element.is_a() not in ("IfcWindow", "IfcWindowType", "IfcWindowStyle"):
self.report({"ERROR"}, "Object has to be IfcWindow/IfcWindowType/IfcWindowStyle type to add a window.")
return {"CANCELLED"}
window_data = props.get_general_kwargs(convert_to_project_units=True)
@@ -481,6 +511,8 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Window", "Data"))
data.update(data.pop("lining_properties"))
data.update(data.pop("panel_properties"))
props = obj.BIMWindowProperties
props.set_props_kwargs_from_ifc_data(data)
@@ -57,7 +57,6 @@ class BimTool(WorkSpaceTool):
("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
("bim.hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
("bim.hotkey", {"type": "B", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_B")]}),
("bim.hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
("bim.hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
@@ -271,12 +270,6 @@ class BimToolUI:
else:
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_B")
row.prop(cls.props, "boundary_class", text="")
row.operator("bim.add_boundary", text="Add Boundary")
cls.layout.row(align=True).label(text="Align")
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "")
add_layout_hotkey_operator(cls.layout, "Align Centerline", "S_C", "")
@@ -286,7 +279,6 @@ class BimToolUI:
cls.layout.row(align=True).label(text="Mode")
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings")
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition")
add_layout_hotkey_operator(cls.layout, "Boundaries", "A_B", "Toggle boundaries")
@classmethod
def draw_header_interface(cls):
@@ -602,14 +594,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
self.props.y = self.y
self.props.z = self.z
def hotkey_A_B(self):
if not bpy.context.selected_objects:
return
if AuthoringData.data["has_visible_boundaries"]:
bpy.ops.bim.hide_boundaries()
else:
bpy.ops.bim.show_boundaries()
def hotkey_A_D(self):
if not bpy.context.selected_objects:
return
@@ -74,19 +74,21 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector):
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
append_all: bpy.props.BoolProperty(default=False)
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
def execute(self, context):
IfcStore.begin_transaction(self)
old_filepath = IfcStore.library_path
result = self._execute(context)
self.transaction_data = {"old_filepath": old_filepath, "filepath": self.filepath}
self.transaction_data = {"old_filepath": old_filepath, "filepath": self.get_filepath()}
IfcStore.add_transaction_operation(self)
IfcStore.end_transaction(self)
return result
def _execute(self, context):
IfcStore.library_path = self.filepath
IfcStore.library_file = ifcopenshell.open(self.filepath)
filepath = self.get_filepath()
IfcStore.library_path = filepath
IfcStore.library_file = ifcopenshell.open(filepath)
bpy.ops.bim.refresh_library()
if context.area:
context.area.tag_redraw()
@@ -371,7 +373,8 @@ class AppendLibraryElement(bpy.types.Operator):
if not type_collection:
type_collection = bpy.data.collections.new("Types")
for collection in bpy.context.view_layer.layer_collection.children:
if "IfcProject/" in collection.name:
collection_obj = collection.collection.BIMCollectionProperties.obj
if collection_obj and tool.Ifc.get_entity(collection_obj).is_a("IfcProject"):
collection.collection.children.link(type_collection)
collection.children["Types"].hide_viewport = True
break
@@ -522,13 +525,14 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load an existing IFC project"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"})
is_advanced: bpy.props.BoolProperty(name="Enable Advanced Mode", default=False)
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
def execute(self, context):
if not self.is_existing_ifc_file():
return {"FINISHED"}
context.scene.BIMProperties.ifc_file = self.filepath
context.scene.BIMProperties.ifc_file = self.get_filepath()
context.scene.BIMProjectProperties.is_loading = True
context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
if not self.is_advanced:
@@ -817,10 +821,22 @@ class ExportIFC(bpy.types.Operator):
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version")
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False)
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False)
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
save_as_invoked: bpy.props.BoolProperty(name="Save As Dialog Was Invoked", default=False, options={"HIDDEN"})
def draw(self, context):
layout = self.layout
layout.prop(self, "json_version")
layout.prop(self, "json_compact")
if bpy.data.is_saved:
layout.prop(self, "use_relative_path")
else:
layout.label(text="Save the .blend file first ")
layout.label(text="to use relative paths for .ifc.")
def invoke(self, context, event):
self.save_as_invoked = False
if not IfcStore.get_file():
self.report({"ERROR"}, "No IFC project is available for export - create or import a project first.")
return {"FINISHED"}
@@ -834,18 +850,23 @@ class ExportIFC(bpy.types.Operator):
self.filepath = Path(bpy.data.filepath).with_suffix(".ifc").__str__()
else:
self.filepath = "untitled.ifc"
self.save_as_invoked = True
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
if context.scene.BIMProjectProperties.should_disable_undo_on_save:
project_props = context.scene.BIMProjectProperties
if self.save_as_invoked:
project_props.use_relative_project_path = self.use_relative_path
if project_props.should_disable_undo_on_save:
old_history_size = tool.Ifc.get().history_size
old_undo_steps = context.preferences.edit.undo_steps
tool.Ifc.get().history_size = 0
context.preferences.edit.undo_steps = 0
IfcStore.execute_ifc_operator(self, context)
if context.scene.BIMProjectProperties.should_disable_undo_on_save:
if project_props.should_disable_undo_on_save:
tool.Ifc.get().history_size = old_history_size
context.preferences.edit.undo_steps = old_undo_steps
return {"FINISHED"}
@@ -882,7 +903,7 @@ class ExportIFC(bpy.types.Operator):
if not scene.DocProperties.ifc_files:
new = scene.DocProperties.ifc_files.add()
new.name = output_file
if self.use_relative_path and bpy.data.is_saved:
if context.scene.BIMProjectProperties.use_relative_project_path and bpy.data.is_saved:
output_file = os.path.relpath(output_file, bpy.path.abspath("//"))
if scene.BIMProperties.ifc_file != output_file and extension not in ["ifczip", "ifcjson"]:
scene.BIMProperties.ifc_file = output_file
@@ -148,6 +148,8 @@ class BIMProjectProperties(PropertyGroup):
default="NONE",
)
should_merge_materials_by_colour: BoolProperty(name="Merge Materials by Colour", default=False)
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
should_load_geometry: BoolProperty(name="Load Geometry", default=True)
should_use_native_meshes: BoolProperty(name="Native Meshes", default=False)
should_clean_mesh: BoolProperty(name="Clean Meshes", default=True)
should_cache: BoolProperty(name="Cache", default=False)
@@ -165,6 +167,7 @@ class BIMProjectProperties(PropertyGroup):
active_link_index: IntProperty(name="Active Link Index")
export_schema: EnumProperty(items=get_export_schema, name="IFC Schema")
template_file: EnumProperty(items=get_template_file, name="Template File")
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False)
def get_library_element_index(self, lib_element):
return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element))
@@ -77,6 +77,8 @@ class BIM_PT_project(Panel):
row = self.layout.row()
row.prop(pprops, "should_cache")
row = self.layout.row()
row.prop(pprops, "should_load_geometry")
row = self.layout.row()
row.prop(pprops, "should_use_native_meshes")
row = self.layout.row()
row.prop(pprops, "should_merge_materials_by_colour")
@@ -18,7 +18,9 @@
import bpy
import ifcopenshell
import ifcopenshell.util.doc
import blenderbim.tool as tool
import blenderbim.bim.schema
# TODO: Should this cache belong here? Dunno. Maybe.
@@ -67,6 +69,8 @@ class ObjectPsetsData(Data):
cls.data = {
"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), psets_only=True),
"inherited_psets": cls.inherited_psets(),
"pset_name": cls.pset_name(),
"qto_name": cls.qto_name(),
}
cls.is_loaded = True
@@ -79,6 +83,35 @@ class ObjectPsetsData(Data):
if element_type:
return cls.psetqtos(element_type)
@classmethod
def pset_name(cls):
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
if not element:
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()
return [p for p in psetnames if p[0] not in assigned_names]
@classmethod
def qto_name(cls):
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
if not element:
return []
qtos = blenderbim.bim.schema.ifc.psetqto.get_applicable(element.is_a(), qto_only=True)
return cls.format_pset_enum(qtos)
@classmethod
def format_pset_enum(cls, psets):
enum_items = []
version = tool.Ifc.get_schema()
for pset in psets:
doc = ifcopenshell.util.doc.get_property_set_doc(version, pset.Name) or {}
enum_items.append((pset.Name, pset.Name, doc.get("description", "")))
return enum_items
class ObjectQtosData(Data):
data = {}
@@ -120,6 +120,13 @@ class EnablePsetEditing(bpy.types.Operator):
self.load_single_value(pset_template, prop_template, data)
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
self.load_enumerated_value(prop_template, data)
else:
# NOTE: currently unsupported types:
# - P_BOUNDEDVALUE
# - P_LISTVALUE
# - P_REFERENCEVALUE
# - P_TABLEVALUE
pass
def load_single_value(self, pset_template, prop_template, data):
prop = self.props.properties.add()
@@ -137,8 +144,12 @@ class EnablePsetEditing(bpy.types.Operator):
if prop_template.PrimaryMeasureType in (
"IfcPositiveLengthMeasure",
"IfcLengthMeasure",
) or prop_template.TemplateType in ("Q_LENGTH",):
) or prop_template.TemplateType == "Q_LENGTH":
special_type = "LENGTH"
elif prop_template.PrimaryMeasureType == "IfcAreaMeasure" or prop_template.TemplateType == "Q_AREA":
special_type = "AREA"
elif prop_template.PrimaryMeasureType == "IfcVolumeMeasure" or prop_template.TemplateType == "Q_VOLUME":
special_type = "VOLUME"
metadata.special_type = special_type
if metadata.data_type == "string":
@@ -22,7 +22,7 @@ import ifcopenshell
import ifcopenshell.util.element
import blenderbim.tool as tool
from blenderbim.bim.prop import Attribute, StrProperty
from blenderbim.bim.module.pset.data import AddEditCustomPropertiesData
from blenderbim.bim.module.pset.data import AddEditCustomPropertiesData, ObjectPsetsData
from blenderbim.bim.ifc import IfcStore
from bpy.types import PropertyGroup
from bpy.props import (
@@ -57,18 +57,10 @@ def blender_formatted_enum_from_psets(psets):
return enum_items
def get_pset_names(self, context):
global psetnames
obj = context.active_object
if not obj.BIMObjectProperties.ifc_definition_id:
return []
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
ifc_class = element.is_a()
if ifc_class not in psetnames:
psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True)
psetnames[ifc_class] = blender_formatted_enum_from_psets(psets)
assigned_names = ifcopenshell.util.element.get_psets(element, psets_only=True).keys()
return [p for p in psetnames[ifc_class] if p[0] not in assigned_names]
def get_pset_name(self, context):
if not ObjectPsetsData.is_loaded:
ObjectPsetsData.load()
return ObjectPsetsData.data["pset_name"]
def get_material_pset_names(self, context):
@@ -157,15 +149,10 @@ def get_work_schedule_pset_names(self, context):
return psetnames[ifc_class]
def get_qto_names(self, context):
global qtonames
if "/" in context.active_object.name:
ifc_class = context.active_object.name.split("/")[0]
if ifc_class not in qtonames:
psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True)
qtonames[ifc_class] = blender_formatted_enum_from_psets(psets)
return qtonames[ifc_class]
return []
def get_qto_name(self, context):
if not ObjectPsetsData.is_loaded:
ObjectPsetsData.load()
return ObjectPsetsData.data["qto_name"]
def get_template_type(self, context):
@@ -197,8 +184,8 @@ class PsetProperties(PropertyGroup):
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_pset_names, name="Pset Name")
qto_name: EnumProperty(items=get_qto_names, name="Qto Name")
pset_name: EnumProperty(items=get_pset_name, name="Pset Name")
qto_name: EnumProperty(items=get_qto_name, name="Qto Name")
class MaterialPsetProperties(PropertyGroup):
@@ -49,7 +49,7 @@ def draw_single_property(prop, layout, copy_operator=None):
layout.prop(
prop.metadata,
value_name,
text=prop.metadata.name,
text=prop.metadata.display_name,
)
if prop.metadata.is_uri:
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
@@ -168,6 +168,7 @@ class UnlinkObject(bpy.types.Operator):
else:
objects = context.selected_objects
for obj in objects:
object_name = obj.name
element = tool.Ifc.get_entity(obj)
if element:
if self.should_delete:
@@ -186,8 +187,8 @@ class UnlinkObject(bpy.types.Operator):
material_slot.material = material_slot.material.copy()
blenderbim.core.style.unlink_style(tool.Ifc, tool.Style, obj=material_slot.material)
blenderbim.core.material.unlink_material(tool.Ifc, obj=material_slot.material)
if "Ifc" in obj.name and "/" in obj.name:
obj.name = "/".join(obj.name.split("/")[1:])
if "Ifc" in object_name and "/" in object_name:
obj.name = object_name.split("/", 1)[1]
return {"FINISHED"}
def draw(self, context):
@@ -261,7 +261,7 @@ def update_color_full(self, context):
material = bpy.data.materials.get("color_full")
if material:
color_full = bpy.context.scene.BIMAnimationProperties.color_full
inputs = material.node_tree.nodes["Principled BSDF"].inputs
inputs = tool.Blender.get_material_node(material, "BSDF_PRINCIPLED").inputs
color = inputs["Base Color"].default_value
color[0] = color_full.r
color[1] = color_full.g
@@ -272,7 +272,7 @@ def update_color_progress(self, context):
material = bpy.data.materials.get("color_progress")
if material:
color_progress = bpy.context.scene.BIMAnimationProperties.color_progress
inputs = material.node_tree.nodes["Principled BSDF"].inputs
inputs = tool.Blender.get_material_node(material, "BSDF_PRINCIPLED").inputs
color = inputs["Base Color"].default_value
color[0] = color_progress.r
color[1] = color_progress.g
@@ -37,6 +37,7 @@ classes = (
operator.ContractContainer,
operator.ExpandContainer,
operator.DeleteContainer,
operator.SelectDecomposedElements,
prop.SpatialElement,
prop.BIMSpatialProperties,
prop.BIMObjectSpatialProperties,
@@ -45,7 +46,7 @@ classes = (
ui.BIM_PT_spatial,
ui.BIM_UL_containers,
ui.BIM_UL_containers_manager,
ui.BIM_PT_Storeys,
ui.BIM_PT_SpatialManager,
workspace.Hotkey,
)
@@ -59,6 +60,8 @@ def register():
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.SpatialTool)
del bpy.types.Scene.BIMSpatialProperties
del bpy.types.Object.BIMObjectSpatialProperties
del bpy.types.Scene.BIMSpatialManagerProperties
@@ -246,3 +246,13 @@ class AddBuildingStorey(bpy.types.Operator, tool.Ifc.Operator):
part_name="Unnamed",
)
core.load_container_manager(tool.Spatial)
class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_decomposed_elements"
bl_label = "Select Children"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.select_decomposed_elements(tool.Spatial)
return {"FINISHED"}
@@ -96,17 +96,18 @@ class BIM_UL_containers(UIList):
)
class BIM_PT_Storeys(Panel):
bl_label = "Spatial Manager"
bl_idname = "BIM_PT_Storeys"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
class BIM_PT_SpatialManager(Panel):
bl_label = "IFC Spatial Manager"
bl_idname = "BIM_PT_SpatialManager"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_GridsSpatialManager"
bl_parent_id = "BIM_PT_project_setup"
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return tool.Ifc.get() and tool.Ifc.schema().name() != "IFC2X3"
def draw(self, context):
if not SpatialData.is_loaded:
@@ -118,6 +119,7 @@ class BIM_PT_Storeys(Panel):
ifc_definition_id = self.props.containers[self.props.active_container_index].ifc_definition_id
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="Select Children")
if SpatialData.data["containers"][ifc_definition_id]["type"] in ["IfcBuildingStorey", "IfcBuilding"]:
row.operator("bim.add_building_storey", icon="ADD", text="Add storey").part_class = "IfcBuildingStorey"
row.operator("bim.delete_container", icon="X", text="Delete").container = ifc_definition_id
@@ -21,6 +21,7 @@ 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
@@ -41,10 +42,14 @@ class SpatialTool(WorkSpaceTool):
bl_label = "Spatial Tool"
bl_description = "Gives you Spatial related superpowers"
# TODO: replace with spatial icon
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.annotation")
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.spatial")
bl_widget = None
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
("bim.spatial_hotkey", {"type": "B", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_B")]}),
("bim.spatial_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.spatial_hotkey", {"type": "B", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_B")]}),
("bim.spatial_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.spatial_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
)
def draw_settings(context, layout, ws_tool):
@@ -67,6 +72,7 @@ class SpatialToolUI:
def draw(cls, context, layout):
cls.layout = layout
# cls.props = context.scene.BIMSpatialProperties
cls.model_props = context.scene.BIMModelProperties
row = cls.layout.row(align=True)
if not tool.Ifc.get():
@@ -76,19 +82,46 @@ class SpatialToolUI:
# if not SpatialData.is_loaded:
# SpatialData.load()
if not AuthoringData.is_loaded:
AuthoringData.load()
cls.draw_type_selection_interface(context)
cls.draw_default_interface(context)
if context.active_object and context.selected_objects:
cls.draw_selected_object_interface(context)
cls.draw_default_interface()
@classmethod
def draw_default_interface(cls):
add_layout_hotkey(cls.layout, "Placeholder", "S_A", "Placeholder Operator")
def draw_default_interface(cls, context):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
if context.selected_objects:
row.operator("bim.generate_spaces_from_walls")
else:
row.operator("bim.generate_space")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.toggle_space_visibility")
@classmethod
def draw_selected_object_interface(cls, context):
pass
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if element and bpy.context.selected_objects and element.is_a("IfcSpace"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
row.operator("bim.generate_space", text="Regen")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_B")
row.prop(cls.model_props, "boundary_class", text="")
row.operator("bim.add_boundary", text="Add Boundary")
add_layout_hotkey(cls.layout, "Boundaries", "A_B", "Toggle boundaries")
@classmethod
def draw_type_selection_interface(cls, context):
@@ -123,4 +156,26 @@ class Hotkey(bpy.types.Operator, Operator):
pass
def hotkey_S_A(self):
pass
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.generate_spaces_from_walls()
else:
bpy.ops.bim.generate_space()
def hotkey_S_B(self):
bpy.ops.bim.add_boundary()
def hotkey_A_B(self):
if not bpy.context.selected_objects:
return
if AuthoringData.data["has_visible_boundaries"]:
bpy.ops.bim.hide_boundaries()
else:
bpy.ops.bim.show_boundaries()
def hotkey_S_T(self):
bpy.ops.bim.toggle_space_visibility()
def hotkey_S_G(self):
bpy.ops.bim.generate_space()
@@ -100,5 +100,7 @@ def register():
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.StructuralTool)
del bpy.types.Scene.BIMStructuralProperties
del bpy.types.Object.BIMStructuralProperties
@@ -41,7 +41,7 @@ class StructuralTool(WorkSpaceTool):
bl_label = "Structural Tool"
bl_description = "Gives you Structure related superpowers"
# TODO: replace with structural icon
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.annotation")
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.structural")
bl_widget = None
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
("bim.structural_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
@@ -21,23 +21,32 @@ from . import ui, prop, operator
classes = (
operator.AddStyle,
operator.DisableEditingStyle,
operator.DisableEditingStyles,
operator.EditStyle,
operator.EnableEditingStyle,
operator.DisableEditingStyle,
operator.EditStyle,
operator.UpdateCurrentStyle,
operator.EnableEditingExternalStyle,
operator.DisableEditingExternalStyle,
operator.EditExternalStyle,
operator.DisableEditingStyles,
operator.BrowseExternalStyle,
operator.ActivateExternalStyle,
operator.LoadStyles,
operator.RemoveStyle,
operator.SelectByStyle,
operator.UnlinkStyle,
operator.UpdateStyleColours,
operator.UpdateStyleTextures,
operator.ClearTextureMapPath,
prop.Style,
prop.BIMStylesProperties,
prop.BIMStyleProperties,
ui.BIM_PT_styles,
ui.BIM_PT_style,
ui.BIM_PT_style_attributes,
ui.BIM_PT_external_style_attributes,
ui.BIM_UL_styles,
ui.BIM_PT_STYLE_GRAPH,
)
@@ -56,7 +56,12 @@ class StyleAttributesData:
@classmethod
def load(cls):
cls.data = {"ifc_style_id": cls.ifc_style_id(), "attributes": cls.get_attributes()}
cls.data = {
"ifc_style_id": cls.ifc_style_id(),
"attributes": cls.get_attributes(),
"style_elements": cls.get_style_elements(),
}
cls.data["external_style_attributes"] = cls.get_external_style_attributes()
cls.is_loaded = True
@classmethod
@@ -72,3 +77,19 @@ class StyleAttributesData:
continue
results.append({"name": name, "value": str(value)})
return results
@classmethod
def get_style_elements(cls):
return tool.Style.get_style_elements(bpy.context.active_object.active_material)
@classmethod
def get_external_style_attributes(cls):
external_style = cls.data["style_elements"].get("IfcExternallyDefinedSurfaceStyle", None)
if not external_style:
return None
results = []
for name, value in external_style.get_info().items():
if name in ["id", "type"]:
continue
results.append({"name": name, "value": str(value)})
return results
@@ -22,15 +22,34 @@ import blenderbim.tool as tool
import blenderbim.core.style as core
import ifcopenshell.util.representation
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.style.data import StylesData, StyleAttributesData
from pathlib import Path
import os
class UpdateStyleColours(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_style_colours"
bl_label = "Update Style Colours"
bl_label = "Save Current Shading Style"
bl_description = (
"Save current style values to IfcSurfaceStyleShading.\n\n" + "ALT+CLICK to see saved values details"
)
bl_options = {"REGISTER", "UNDO"}
verbose: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# verobse print to console on alt+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.alt:
self.verbose = True
return self.execute(context)
def _execute(self, context):
core.update_style_colours(tool.Ifc, tool.Style, obj=context.active_object.active_material)
mat = context.active_object.active_material
core.update_style_colours(tool.Ifc, tool.Style, obj=mat, verbose=self.verbose)
if self.verbose:
self.report({"INFO"}, "Check the system console to see saved style properties")
tool.Style.set_surface_style_props(mat)
class UpdateStyleTextures(bpy.types.Operator, tool.Ifc.Operator):
@@ -103,6 +122,249 @@ class EditStyle(bpy.types.Operator, tool.Ifc.Operator):
core.edit_style(tool.Ifc, tool.Style, obj=context.active_object.active_material)
class UpdateCurrentStyle(bpy.types.Operator):
bl_idname = "bim.update_current_style"
bl_label = "Update Current Style"
bl_description = (
"Update style for all selected objects according to current style type\n(Shading/External).\n\n"
+ "SHIFT+CLICK to update ALL styles in the .ifc file to current style type"
)
bl_options = {"REGISTER", "UNDO"}
update_all: bpy.props.BoolProperty(name="Update All", default=False, options={"SKIP_SAVE"})
@classmethod
def poll(cls, context):
poll = (
context.active_object is not None
and context.active_object.active_material is not None
and context.active_object.active_material.BIMMaterialProperties.ifc_style_id != 0
)
if not poll:
cls.poll_message_set(
"Object is not selected or material is not assigned or material doesn't have IFC Style"
)
return poll
def invoke(self, context, event):
# updating all styles on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.update_all = True
return self.execute(context)
def execute(self, context):
current_style_type = context.active_object.active_material.BIMStyleProperties.active_style_type
if self.update_all:
context.scene.BIMStylesProperties.active_style_type = current_style_type
return {"FINISHED"}
materials = []
for obj in context.selected_objects:
mat = obj.active_material
if mat and mat.BIMMaterialProperties.ifc_style_id != 0:
mat.BIMStyleProperties.active_style_type = current_style_type
return {"FINISHED"}
class BrowseExternalStyle(bpy.types.Operator):
bl_idname = "bim.browse_external_style"
bl_label = "Browse External Style"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(
name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH"
)
filter_glob: bpy.props.StringProperty(
default="*.blend",
options={"HIDDEN"},
)
def get_data_block_types(self, context):
return [("materials", "materials", "materials")]
# NOTE: the code below can be used later when we'll be adding other data-blocks besides materials
l = [("0", "", "")]
SUPPORTED_DATA_BLOCKS = ("materials", "textures", "brushes")
if os.path.exists(self.filepath) and self.filepath.endswith(".blend"):
with bpy.data.libraries.load(self.filepath) as (data_from, data_to):
for data_block_type in dir(data_from):
if data_block_type not in SUPPORTED_DATA_BLOCKS:
continue
data = getattr(data_from, data_block_type)
if data:
item = (data_block_type,) * 3
l.append(item)
return l
def get_data_blocks(self, context):
l = [("", "", "")]
if self.data_block_type != "0" and os.path.exists(self.filepath) and self.filepath.endswith(".blend"):
with bpy.data.libraries.load(self.filepath) as (data_from, data_to):
objects = getattr(data_from, self.data_block_type)
for o in objects:
l.append((o, o, o))
return l
data_block_type: bpy.props.EnumProperty(
name="Data Block Type",
description="List of data blocks in the .blend file",
items=get_data_block_types,
)
data_block: bpy.props.EnumProperty(
name="List of objects in the .blend file",
description="List of objects in the .blend file",
items=get_data_blocks,
)
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
directory: bpy.props.StringProperty(
name="Directory",
description="Start file browsing directory",
default="",
)
def invoke(self, context, event):
mat = context.active_object.active_material
external_style = tool.Style.get_style_elements(mat).get("IfcExternallyDefinedSurfaceStyle", None)
# automatically select previously selected external style in file browser
if external_style and self.filepath == "":
style_path = Path(tool.Ifc.resolve_uri(external_style.Location))
self.directory = str(style_path.parent)
self.filepath = str(style_path)
self.data_block_type, self.data_block = external_style.Identification.split("/")
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def draw(self, context):
layout = self.layout
layout.label(text="Data Block Type")
layout.prop(self, "data_block_type", text="", icon="GROUP")
layout.label(text="Data Block")
layout.prop(self, "data_block", text="")
if bpy.data.is_saved:
layout.prop(self, "use_relative_path")
else:
self.use_relative_path = False
layout.label(text="Save the .blend file first ")
layout.label(text="to use relative paths for .ifc.")
def execute(self, context):
if self.data_block_type == "0":
self.report({"ERROR"}, "Select a data block type")
return {"CANCELLED"}
if self.data_block == "":
self.report({"ERROR"}, "Select a data block")
return {"CANCELLED"}
if not os.path.exists(self.filepath):
self.report({"ERROR"}, f"File not found:\n'{self.filepath}'")
return {"CANCELLED"}
db = tool.Blender.append_data_block(self.filepath, self.data_block_type, self.data_block)
if not db["data_block"]:
self.report({"ERROR"}, db["msg"])
return {"CANCELLED"}
bpy.data.materials.remove(db["data_block"])
if not StyleAttributesData.is_loaded:
StyleAttributesData.load()
external_style = StyleAttributesData.data["style_elements"].get("IfcExternallyDefinedSurfaceStyle", None)
if self.use_relative_path:
filepath = os.path.relpath(self.filepath, bpy.path.abspath("//"))
else:
filepath = self.filepath
attributes = {
"Location": filepath,
"Identification": f"{self.data_block_type}/{self.data_block}",
"Name": self.data_block,
}
if not external_style:
core.add_external_style(
tool.Ifc, tool.Style, obj=context.active_object.active_material, attributes=attributes
)
else:
core.update_external_style(tool.Ifc, tool.Style, external_style=external_style, attributes=attributes)
StyleAttributesData.is_loaded = False
return {"FINISHED"}
class EnableEditingExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_external_style"
bl_label = "Enable Editing External Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.enable_editing_external_style(tool.Style, obj=context.active_object.active_material)
class DisableEditingExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_external_style"
bl_options = {"REGISTER", "UNDO"}
bl_label = "Disable Editing External Style"
def _execute(self, context):
core.disable_editing_external_style(tool.Style, obj=context.active_object.active_material)
class EditExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_external_style"
bl_label = "Edit External Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.edit_external_style(tool.Ifc, tool.Style, obj=context.active_object.active_material)
class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.activate_external_style"
bl_label = "Activate External Style"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
material_name: bpy.props.StringProperty(name="Material Name", default="")
def _execute(self, context):
if not self.material_name:
material = context.active_object.active_material
else:
material = bpy.data.materials[self.material_name]
external_style = tool.Style.get_style_elements(material)["IfcExternallyDefinedSurfaceStyle"]
data_block_type, data_block = external_style.Identification.split("/")
style_path = Path(tool.Ifc.resolve_uri(external_style.Location))
if style_path.suffix != ".blend":
self.report({"ERROR"}, f"Only Blender external styles are supported")
return {"CANCELLED"}
if not style_path.exists():
self.report({"ERROR"}, f"File not found:\n'{style_path}'")
return {"CANCELLED"}
db = tool.Blender.append_data_block(str(style_path), data_block_type, data_block)
if not db["data_block"]:
self.report({"ERROR"}, db["msg"])
return {"CANCELLED"}
props_to_copy = [
"diffuse_color",
"metallic",
"roughness",
"specular_intensity",
"use_nodes",
]
for prop_name in props_to_copy:
setattr(material, prop_name, getattr(db["data_block"], prop_name))
if material.use_nodes:
tool.Blender.copy_node_graph(material, db["data_block"])
bpy.data.materials.remove(db["data_block"])
class DisableEditingStyles(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_styles"
bl_options = {"REGISTER", "UNDO"}
@@ -129,4 +391,26 @@ class SelectByStyle(bpy.types.Operator, tool.Ifc.Operator):
style: bpy.props.IntProperty()
def _execute(self, context):
core.select_by_style(tool.Style, style=tool.Ifc.get().by_id(self.style))
core.select_by_style(tool.Style, tool.Spatial, style=tool.Ifc.get().by_id(self.style))
class ClearTextureMapPath(bpy.types.Operator):
bl_idname = "bim.clear_texture_map_path"
bl_label = "Clear Texture Map Path"
bl_options = {"REGISTER", "UNDO"}
texture_map_prop: bpy.props.StringProperty(default="")
@classmethod
def poll(cls, context):
poll = getattr(context, "material", None)
if not poll:
cls.poll_message_set("Select a material")
return poll
def execute(self, context):
if not self.texture_map_prop:
self.report({"ERROR"}, "Provide a texture map")
return {"CANCELLED"}
props = context.material.BIMStyleProperties
setattr(props, self.texture_map_prop, "")
return {"FINISHED"}
@@ -31,6 +31,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
import blenderbim.tool as tool
def get_style_types(self, context):
@@ -45,13 +46,172 @@ class Style(PropertyGroup):
total_elements: IntProperty(name="Total Elements")
STYLE_TYPES = [
("Shading", "Shading", ""),
("External", "External", ""),
]
def update_shading_styles(self, context):
for mat in bpy.data.materials:
if mat.BIMMaterialProperties.ifc_style_id == 0:
continue
tool.Style.change_current_style_type(mat, self.active_style_type)
class BIMStylesProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
style_type: EnumProperty(items=get_style_types, name="Style Type")
styles: CollectionProperty(name="Styles", type=Style)
active_style_index: IntProperty(name="Active Style Index")
active_style_type: EnumProperty(
name="Active Style Type",
description="Update current blender material to match style type for all objects in the scene",
items=STYLE_TYPES,
default="Shading",
update=update_shading_styles,
)
def update_shading_style(self, context):
blender_material = self.id_data
style_elements = tool.Style.get_style_elements(blender_material)
if self.active_style_type == "External":
if tool.Style.has_blender_external_style(style_elements):
bpy.ops.bim.activate_external_style(material_name=blender_material.name)
elif self.active_style_type == "Shading":
style_elements = tool.Style.get_style_elements(blender_material)
rendering_style = None
texture_style = None
for surface_style in style_elements.values():
if surface_style.is_a() == "IfcSurfaceStyleShading":
tool.Loader.create_surface_style_shading(blender_material, surface_style)
elif surface_style.is_a("IfcSurfaceStyleRendering"):
rendering_style = surface_style
tool.Loader.create_surface_style_rendering(blender_material, surface_style)
elif surface_style.is_a("IfcSurfaceStyleWithTextures"):
texture_style = surface_style
if rendering_style and texture_style:
tool.Loader.create_surface_style_with_textures(blender_material, rendering_style, texture_style)
tool.Style.set_surface_style_props(blender_material)
tool.Style.record_shading(blender_material)
# TODO: support more more methods
REFLECTANCE_METHODS = [
("PHYSICAL", "PHYSICAL", ""),
("FLAT", "FLAT", ""),
# ("METAL", "METAL", ""),
# ("MATT", "MATT", ""),
# ("GLASS", "GLASS", ""),
# ("NOTDEFINED", "NOTDEFINED", ""),
]
def update_shader_graph(self, context):
if not self.update_graph:
return
material = self.id_data
style_data = tool.Style.get_surface_style_from_props(material)
textures_data = tool.Style.get_texture_style_from_props(material)
tool.Loader.create_surface_style_rendering(material, style_data)
tool.Loader.create_surface_style_with_textures(material, style_data, textures_data)
def update_graph_get(self):
return self.get("update_graph", True)
def update_graph_set(self, value):
self["update_graph"] = value
if value:
material = self.id_data
tool.Style.set_surface_style_props(material)
class BIMStyleProperties(PropertyGroup):
attributes: CollectionProperty(name="Attributes", type=Attribute)
is_editing: BoolProperty(name="Is Editing")
external_style_attributes: CollectionProperty(name="External Style Attributes", type=Attribute)
is_editing_external_style: BoolProperty(name="Is Editing External Style")
active_style_type: EnumProperty(
name="Active Style Type",
description="Update current blender material to match style type",
items=STYLE_TYPES,
default="Shading",
update=update_shading_style,
)
# GLTF style properties
update_graph: BoolProperty(
name="Update Shade Graph on Prop Change",
description="Update shader graph in real time\nas you update style properties",
default=True,
get=update_graph_get,
set=update_graph_set,
)
reflectance_method: EnumProperty(
name="Reflectance Method",
description="Reflectance method to use for the material",
items=REFLECTANCE_METHODS,
default="PHYSICAL",
update=update_shader_graph,
)
surface_color: bpy.props.FloatVectorProperty(
name="Surface Color",
subtype="COLOR",
default=(1, 1, 1, 1),
min=0.0,
max=1.0,
size=4,
update=update_shader_graph,
)
diffuse_color: bpy.props.FloatVectorProperty(
name="Diffuse Color",
subtype="COLOR",
default=(1, 1, 1, 1),
min=0.0,
max=1.0,
size=4,
update=update_shader_graph,
)
transparency: bpy.props.FloatProperty(
name="Transparency", default=0.0, min=0.0, max=1.0, update=update_shader_graph
)
roughness: bpy.props.FloatProperty(name="Roughness", default=0.0, min=0.0, max=1.0, update=update_shader_graph)
metallic: bpy.props.FloatProperty(name="Metallic", default=0.0, min=0.0, max=1.0, update=update_shader_graph)
normal_path: bpy.props.StringProperty(
name="NormalMap",
maxlen=1024,
default="",
subtype="FILE_PATH",
update=update_shader_graph,
)
emissive_path: bpy.props.StringProperty(
name="Emissive",
maxlen=1024,
default="",
subtype="FILE_PATH",
update=update_shader_graph,
)
metallic_roughness_path: bpy.props.StringProperty(
name="Metallic/Roughness",
maxlen=1024,
default="",
subtype="FILE_PATH",
update=update_shader_graph,
description="Green Channel = Roughness,\nBlue Channel = Metallic",
)
diffuse_path: bpy.props.StringProperty(
name="Diffuse",
maxlen=1024,
default="",
subtype="FILE_PATH",
update=update_shader_graph,
)
+142 -11
View File
@@ -65,6 +65,24 @@ class BIM_PT_styles(Panel):
self.layout.template_list("BIM_UL_styles", "", self.props, "styles", self.props, "active_style_index")
def draw_style_ui(self, context):
mat = context.material
props = mat.BIMMaterialProperties
style_props = mat.BIMStyleProperties
row = self.layout.row(align=True)
if not props.ifc_style_id:
row.operator("bim.add_style", icon="ADD")
return
row.prop(style_props, "active_style_type", icon="SHADING_RENDERED", text="")
row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
row = self.layout.row(align=True)
row.operator("bim.update_style_colours", icon="GREASEPENCIL")
row.operator("bim.update_style_textures", icon="TEXTURE", text="")
row.operator("bim.unlink_style", icon="UNLINKED", text="")
row.operator("bim.remove_style", icon="X", text="").style = props.ifc_style_id
class BIM_PT_style(MaterialButtonsPanel, Panel):
bl_label = "IFC Style"
bl_idname = "BIM_PT_style"
@@ -81,19 +99,13 @@ class BIM_PT_style(MaterialButtonsPanel, Panel):
)
def draw(self, context):
props = context.active_object.active_material.BIMMaterialProperties
mat = context.material
props = mat.BIMMaterialProperties
draw_style_ui(self, context)
if not props.ifc_style_id:
return
row = self.layout.row(align=True)
if props.ifc_style_id:
row.operator("bim.update_style_colours", icon="GREASEPENCIL")
row.operator("bim.update_style_textures", icon="TEXTURE", text="")
row.operator("bim.unlink_style", icon="UNLINKED", text="")
row.operator("bim.remove_style", icon="X", text="").style = props.ifc_style_id
row = self.layout.row(align=True)
row.prop(mat, "diffuse_color", text="Color")
else:
row.operator("bim.add_style", icon="ADD")
row.prop(mat, "diffuse_color", text="Viewport Color" if mat.use_nodes else "Render Color")
class BIM_PT_style_attributes(Panel):
@@ -102,6 +114,7 @@ class BIM_PT_style_attributes(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
bl_parent_id = "BIM_PT_style"
@classmethod
def poll(cls, context):
@@ -143,6 +156,61 @@ class BIM_PT_style_attributes(Panel):
row.label(text=attribute["value"])
class BIM_PT_external_style_attributes(Panel):
bl_label = "IFC External Surface Style"
bl_idname = "BIM_PT_external_style_attributes"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
bl_parent_id = "BIM_PT_style"
@classmethod
def poll(cls, context):
if not IfcStore.get_file():
return False
try:
return bool(context.active_object.active_material.BIMMaterialProperties.ifc_style_id)
except:
return False
def draw(self, context):
if not StyleAttributesData.is_loaded or (
context.active_object.active_material.BIMMaterialProperties.ifc_style_id
!= StyleAttributesData.data["ifc_style_id"]
):
StyleAttributesData.load()
mat = context.active_object.active_material
mprops = mat.BIMMaterialProperties
props = mat.BIMStyleProperties
external_style = StyleAttributesData.data["style_elements"].get("IfcExternallyDefinedSurfaceStyle", None)
if not external_style:
row = self.layout.row(align=True)
row.operator("bim.browse_external_style", text="Add External Style", icon="ADD")
return
row = self.layout.row(align=True)
row.label(text="Parameters:")
if props.is_editing_external_style:
row.operator("bim.edit_external_style", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_external_style", icon="CANCEL", text="")
blenderbim.bim.helper.draw_attributes(props.external_style_attributes, self.layout)
else:
row.operator("bim.browse_external_style", icon="APPEND_BLEND", text="")
row.operator("bim.enable_editing_external_style", icon="GREASEPENCIL", text="")
row = self.layout.row(align=True)
row.label(text="STEP ID")
row.label(text=str(external_style.id()))
for attribute in StyleAttributesData.data["external_style_attributes"]:
row = self.layout.row(align=True)
row.label(text=attribute["name"])
row.label(text=attribute["value"])
class BIM_UL_styles(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
@@ -151,3 +219,66 @@ class BIM_UL_styles(UIList):
row2 = row.row()
row2.alignment = "RIGHT"
row2.label(text=str(item.total_elements))
class BIM_PT_STYLE_GRAPH(Panel):
bl_idname = "BIM_PT_style_graph"
bl_space_type = "NODE_EDITOR"
bl_label = "IFC Style Graph Settings"
bl_region_type = "UI"
bl_category = "BBIM"
@classmethod
def poll(cls, context):
return getattr(context, "material", None)
def draw(self, context):
layout = self.layout
props = context.active_object.active_material.BIMStyleProperties
draw_style_ui(self, context)
layout.separator()
box = layout.box()
box.label(text="Creating shader from this panel")
box.label(text="ensures that shader is")
box.label(text="GLTF compatible")
box.label(text="and therefore will be ")
box.label(text="stored in IFC safely.")
layout.separator()
layout.prop(props, "update_graph", text="Graph Auto Update")
layout.label(text="Reflectance Method:")
layout.prop(props, "reflectance_method", text="")
layout.prop(props, "surface_color")
layout.prop(props, "transparency")
if not (props.reflectance_method == "PHYSICAL" and props.diffuse_path) and not (
props.reflectance_method == "FLAT" and props.emissive_path
):
prop_name = "Emissive Color" if props.reflectance_method == "FLAT" else "Diffuse Color"
layout.prop(props, "diffuse_color", text=prop_name)
if props.reflectance_method == "PHYSICAL" and not props.metallic_roughness_path:
layout.prop(props, "metallic")
if props.reflectance_method not in ("PHYSICAL", "FLAT", "NOTDEFINED") or (
props.reflectance_method == "PHYSICAL" and not props.metallic_roughness_path
):
layout.prop(props, "roughness")
layout.label(text="Texture Maps:")
def add_texture_path(path_name):
row = layout.row(align=True)
row.prop(props, path_name)
op = row.operator("bim.clear_texture_map_path", text="", icon="X")
op.texture_map_prop = path_name
if props.reflectance_method == "PHYSICAL":
add_texture_path("diffuse_path")
add_texture_path("emissive_path")
add_texture_path("normal_path")
add_texture_path("metallic_roughness_path")
if props.reflectance_method == "FLAT":
add_texture_path("emissive_path")
@@ -140,7 +140,7 @@ class ShowPorts(bpy.types.Operator, Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.show_ports(tool.Ifc, tool.System, element=tool.Ifc.get_entity(context.active_object))
core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=tool.Ifc.get_entity(context.active_object))
class HidePorts(bpy.types.Operator, Operator):
@@ -141,14 +141,10 @@ class SelectType(bpy.types.Operator):
def execute(self, context):
element = tool.Ifc.get().by_id(self.relating_type)
tool.Spatial.select_products([element])
obj = tool.Ifc.get_object(element)
if obj:
if obj in context.selectable_objects:
tool.Blender.select_and_activate_single_object(context, obj)
else:
self.report({"INFO"}, "Type object can't be selected : It may be hidden or in an excluded collection.")
context.scene.BIMModelProperties.ifc_class = element.is_a()
context.scene.BIMModelProperties.relating_type_id = str(self.relating_type)
context.view_layer.objects.active = obj
return {"FINISHED"}
@@ -333,8 +329,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
tool.Collector,
tool.Root,
obj=obj,
predefined_type=predefined_type,
ifc_class="IfcWindowType",
predefined_type=predefined_type if tool.Ifc.get_schema() != "IFC2X3" else None,
ifc_class="IfcWindowType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcWindowStyle",
should_add_representation=False,
)
bpy.ops.object.select_all(action="DESELECT")
@@ -351,8 +347,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
tool.Collector,
tool.Root,
obj=obj,
predefined_type=predefined_type,
ifc_class="IfcDoorType",
predefined_type=predefined_type if tool.Ifc.get_schema() != "IFC2X3" else None,
ifc_class="IfcDoorType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcDoorStyle",
should_add_representation=False,
)
bpy.ops.object.select_all(action="DESELECT")
@@ -29,6 +29,14 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_opening"
bl_label = "Add Opening"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Adds an opening to an element.\n" \
"Need to select two elements, order of selection is not important"
@classmethod
def poll(cls, context):
if len(context.selected_objects) != 2:
cls.poll_message_set("Select two elements to add an opening")
return True
def _execute(self, context):
props = context.scene.BIMModelProperties
+12 -11
View File
@@ -94,10 +94,11 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
bl_description = "Select a different IFC file"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
def execute(self, context):
if self.is_existing_ifc_file():
context.scene.BIMProperties.ifc_file = self.filepath
context.scene.BIMProperties.ifc_file = self.get_filepath()
return {"FINISHED"}
def invoke(self, context, event):
@@ -105,7 +106,7 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
return {"RUNNING_MODAL"}
class ReloadSelectedIfcFile(bpy.types.Operator, IFCFileSelector):
class ReloadSelectedIfcFile(bpy.types.Operator):
bl_idname = "bim.reload_selected_ifc_file"
bl_label = "Reload selected IFC File"
bl_options = {"REGISTER", "UNDO"}
@@ -113,9 +114,12 @@ class ReloadSelectedIfcFile(bpy.types.Operator, IFCFileSelector):
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
self.filepath = context.scene.BIMProperties.ifc_file
if self.is_existing_ifc_file():
context.scene.BIMProperties.ifc_file = context.scene.BIMProperties.ifc_file
filepath = context.scene.BIMProperties.ifc_file
valid_file = os.path.exists(filepath) and "ifc" in os.path.splitext(filepath)[1].lower()
if not valid_file:
self.report({"ERROR"}, f"Couldn't find .ifc file by the path '{filepath}'")
return {"ERROR"}
context.scene.BIMProperties.ifc_file = context.scene.BIMProperties.ifc_file
return {"FINISHED"}
@@ -161,6 +165,7 @@ class FileAssociate(bpy.types.Operator):
def poll(cls, context):
if platform.system() == "Linux":
return True
cls.poll_message_set("Option available only on Linux.")
# TODO Windows and Darwin
# https://stackoverflow.com/questions/1082889/how-to-change-filetype-association-in-the-registry
return False
@@ -235,6 +240,7 @@ class FileUnassociate(bpy.types.Operator):
def poll(cls, context):
if platform.system() == "Linux":
return True
cls.poll_message_set("Option available only on Linux.")
return False
def execute(self, context):
@@ -487,7 +493,7 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
continue
material.blend_method = "HASHED"
material.shadow_method = "HASHED"
material_output = self.get_node(material.node_tree.nodes, "OUTPUT_MATERIAL")
material_output = tool.Blender.get_material_node(material, "OUTPUT_MATERIAL", {"is_active_output": True})
if not material_output:
continue
from_socket = material_output.inputs[0].links[0].from_socket
@@ -497,11 +503,6 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
material.node_tree.links.new(from_socket, section_override.inputs[0])
material.node_tree.links.new(section_override.outputs[0], material_output.inputs[0])
def get_node(self, nodes, node_type):
for node in nodes:
if node.type == node_type:
return node
class BIM_OT_remove_section_plane(bpy.types.Operator):
"""Remove selected section plane. No effect if executed on a regular object"""
+12
View File
@@ -233,9 +233,21 @@ def set_lenght_value(self, value):
self.float_value = value / si_conversion
def get_display_name(self):
name = self.name
if not self.special_type or self.special_type == "LENGTH":
return name
unit_type = f"{self.special_type}UNIT"
project_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), unit_type)
unit_symbol = ifcopenshell.util.unit.get_unit_symbol(project_unit)
return f"{name}, {unit_symbol}"
class Attribute(PropertyGroup):
tooltip = "`Right Click > IFC Description` to read the attribute description and online documentation"
name: StringProperty(name="Name")
display_name: StringProperty(name="Display Name", get=get_display_name)
description: StringProperty(name="Description")
ifc_class: StringProperty(name="Ifc Class")
data_type: StringProperty(name="Data Type")
+19 -1
View File
@@ -39,14 +39,23 @@ class IFCFileSelector:
filepath = self.filepath
return os.path.exists(filepath) and "ifc" in os.path.splitext(filepath)[1].lower()
def get_filepath(self):
"""get filepath taking into account relative paths"""
if self.use_relative_path:
filepath = os.path.relpath(self.filepath, bpy.path.abspath("//"))
else:
filepath = self.filepath
return filepath
def draw(self, context):
# Access filepath & Directory https://blender.stackexchange.com/a/207665
params = context.space_data.params
# Decode byte string https://stackoverflow.com/a/47737082/
directory = Path(params.directory.decode("utf-8"))
filepath = os.path.join(directory, params.filename)
layout = self.layout
if self.is_existing_ifc_file(filepath):
box = self.layout.box()
box = layout.box()
box.label(text="IFC Header Specifications", icon="INFO")
header_data = IfcHeaderExtractor(filepath).extract()
for key, value in header_data.items():
@@ -65,6 +74,13 @@ class IFCFileSelector:
op.outfile = filepath[0:-4] + "-IFC4.ifc"
op.schema = "IFC4"
if bpy.data.is_saved:
layout.prop(self, "use_relative_path")
else:
self.use_relative_path = False
layout.label(text="Save the .blend file first ")
layout.label(text="to use relative paths for .ifc.")
class BIM_PT_section_plane(Panel):
bl_label = "Temporary Section Cutaways"
@@ -198,6 +214,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row = layout.row()
row.prop(context.scene.BIMProjectProperties, "should_disable_undo_on_save")
row = layout.row()
row.prop(context.scene.BIMProjectProperties, "should_stream")
row = layout.row()
row.prop(context.scene.BIMModelProperties, "occurrence_name_style")
+5 -4
View File
@@ -59,9 +59,10 @@ def contract_cost_items(cost):
cost.load_cost_schedule_tree()
def remove_cost_item(ifc, cost, cost_item):
def remove_cost_item(ifc, cost, cost_item_id):
cost_item = ifc.get().by_id(cost_item_id)
ifc.run("cost.remove_cost_item", cost_item=cost_item)
cost.clean_up_cost_item_tree(cost_item)
cost.clean_up_cost_item_tree(cost_item_id)
cost.load_cost_schedule_tree()
@@ -236,9 +237,9 @@ def calculate_cost_item_resource_value(ifc, cost_item):
ifc.run("cost.calculate_cost_item_resource_value", cost_item=cost_item)
def export_cost_schedules(cost, format, cost_schedule=None):
def export_cost_schedules(cost, filepath, format, cost_schedule=None):
cost.play_sound()
return cost.export_cost_schedules(format, cost_schedule)
return cost.export_cost_schedules(filepath, format, cost_schedule)
def clear_cost_item_assignments(ifc, cost, cost_item, related_object_type):
+1 -1
View File
@@ -318,7 +318,7 @@ def update_drawing_name(ifc, drawing_tool, drawing=None, name=None):
ifc.run("attribute.edit_attributes", product=group, attributes={"Name": name})
collection = drawing_tool.get_drawing_collection(drawing)
if collection:
drawing_tool.set_drawing_collection_name(group, collection)
drawing_tool.set_drawing_collection_name(drawing, collection)
reference = drawing_tool.get_drawing_document(drawing)
information = drawing_tool.get_reference_document(reference)
+18 -3
View File
@@ -26,7 +26,7 @@ def discard_uncomitted(ifcgit, ifc):
ifcgit.load_project(path_ifc)
def commit_changes(ifcgit, ifc, repo, context):
def commit_changes(ifcgit, ifc, repo):
path_ifc = ifc.get_path()
ifcgit.git_commit(path_ifc)
@@ -42,11 +42,26 @@ def delete_tag(ifcgit, repo, tag_name):
ifcgit.delete_tag(repo, tag_name)
def add_remote(ifcgit, repo):
ifcgit.add_remote(repo)
def delete_remote(ifcgit, repo):
ifcgit.delete_remote(repo)
def push(ifcgit, repo, remote_name, operator):
error_message = ifcgit.push(repo, remote_name, repo.active_branch.name)
if error_message:
operator.report({"ERROR"}, error_message)
def refresh_revision_list(ifcgit, repo, ifc):
ifcgit.refresh_revision_list(ifc.get_path())
if repo.heads:
ifcgit.refresh_revision_list(ifc.get_path())
def colourise_revision(ifcgit, context):
def colourise_revision(ifcgit):
step_ids = ifcgit.get_revisions_step_ids()
if not step_ids:
+50 -2
View File
@@ -70,13 +70,15 @@ def disable_editing_materials(material):
material.disable_editing_materials()
def select_by_material(material_tool, material=None):
material_tool.select_elements(material_tool.get_elements_by_material(material))
def select_by_material(material_tool, spatial, material=None):
spatial.select_products(material_tool.get_elements_by_material(material))
def enable_editing_material(material_tool, material):
material_tool.load_material_attributes(material)
material_tool.enable_editing_material(material)
def edit_material(ifc, material_tool, material):
attributes = material_tool.get_material_attributes()
ifc.run("material.edit_material", material=material, attributes=attributes)
@@ -85,5 +87,51 @@ def edit_material(ifc, material_tool, material):
material_tool.import_material_definitions(material_type)
material_tool.enable_editing_materials()
def disable_editing_material(material_tool):
material_tool.disable_editing_material()
def assign_material(ifc, material_tool, material_type, objects):
material_type = material_type or material_tool.get_active_object_material()
material = material_tool.get_active_material()
for obj in objects:
element = ifc.get_entity(obj)
if not element:
continue
ifc.run("material.assign_material", product=element, type=material_type, material=material)
assigned_material = material_tool.get_material(element)
if material_tool.is_a_material_set(assigned_material):
material_tool.add_material_to_set(material_set=assigned_material, material=material)
def unassign_material(ifc, material_tool, objects):
for obj in objects:
element = ifc.get_entity(obj)
if element:
material = material_tool.get_material(element, should_inherit=False)
inherited_material = material_tool.get_material(element, should_inherit=True)
if material and "Usage" in material.is_a():
element_type = material_tool.get_type(element)
ifc.run("material.unassign_material", product=element_type)
elif not material and inherited_material:
element_type = material_tool.get_type(element)
ifc.run("material.unassign_material", product=element_type)
elif material:
ifc.run("material.unassign_material", product=element)
def patch_non_parametric_mep_segment(ifc, material_tool, profile_tool, obj):
element = ifc.get_entity(obj)
if not element:
return
if not material_tool.is_a_flow_segment(element):
return
has_material_profile = material_tool.has_material_profile(element)
if has_material_profile:
return
representation_profile = profile_tool.get_profile(element)
if not representation_profile:
return
material_profile = material_tool.replace_material_with_material_profile(element=element)
ifc.run("material.assign_profile", material_profile=material_profile, profile=representation_profile)
@@ -115,3 +115,8 @@ def expand_container(spatial, container=None):
def delete_container(ifc, spatial, geometry, container=None):
geometry.delete_ifc_object(ifc.get_object(container))
spatial.load_container_manager()
def select_decomposed_elements(spatial):
container = spatial.get_active_container()
if container:
spatial.select_products(spatial.get_decomposed_elements(container))
+46 -4
View File
@@ -33,6 +33,17 @@ def add_style(ifc, style, obj=None):
return element
def add_external_style(ifc, style, obj, attributes):
element = style.get_style(obj)
ifc.run(
"style.add_surface_style", style=element, ifc_class="IfcExternallyDefinedSurfaceStyle", attributes=attributes
)
def update_external_style(ifc, style, external_style, attributes):
ifc.run("style.edit_surface_style", style=external_style, attributes=attributes)
def remove_style(ifc, material, style_tool, style=None):
obj = ifc.get_object(style)
ifc.unlink(obj=obj, element=style)
@@ -43,18 +54,32 @@ def remove_style(ifc, material, style_tool, style=None):
style_tool.import_presentation_styles(style_tool.get_active_style_type())
def update_style_colours(ifc, style, obj=None):
def update_style_colours(ifc, style, obj=None, verbose=False):
element = style.get_style(obj)
if style.can_support_rendering_style(obj):
rendering_style = style.get_surface_rendering_style(obj)
attributes = style.get_surface_rendering_attributes(obj)
texture_style = style.get_texture_style(obj)
attributes = style.get_surface_rendering_attributes(obj, verbose)
if rendering_style:
ifc.run("style.edit_surface_style", style=rendering_style, attributes=attributes)
else:
ifc.run(
"style.add_surface_style", style=element, ifc_class="IfcSurfaceStyleRendering", attributes=attributes
)
# TODO: uvs?
textures = ifc.run("style.add_surface_textures", material=obj)
if not texture_style and textures:
ifc.run(
"style.add_surface_style",
style=element,
ifc_class="IfcSurfaceStyleWithTextures",
attributes={"Textures": textures},
)
elif texture_style:
# TODO: should we remove blender images and IFCIMAGETEXTURE here if they're not used by other objects?
ifc.run("style.edit_surface_style", style=texture_style, attributes={"Textures": textures})
else:
shading_style = style.get_surface_shading_style(obj)
attributes = style.get_surface_shading_attributes(obj)
@@ -95,16 +120,33 @@ def enable_editing_style(style, obj=None):
style.import_surface_attributes(style.get_style(obj), obj)
def enable_editing_external_style(style, obj=None):
external_style = style.get_external_style(obj)
style.enable_editing_external_style(obj)
style.import_external_style_attributes(external_style, obj)
def disable_editing_style(style, obj=None):
style.disable_editing(obj)
def disable_editing_external_style(style, obj=None):
style.disable_editing_external_style(obj)
def edit_style(ifc, style, obj=None):
attributes = style.export_surface_attributes(obj)
ifc.run("style.edit_presentation_style", style=style.get_style(obj), attributes=attributes)
style.disable_editing(obj)
def edit_external_style(ifc, style, obj=None):
attributes = style.export_external_style_attributes(obj)
external_style = style.get_style_elements(obj)["IfcExternallyDefinedSurfaceStyle"]
update_external_style(ifc, style, external_style, attributes)
style.disable_editing_external_style(obj)
def load_styles(style, style_type=None):
style.import_presentation_styles(style_type)
style.enable_editing_styles()
@@ -114,5 +156,5 @@ def disable_editing_styles(style):
style.disable_editing_styles()
def select_by_style(style_tool, style=None):
style_tool.select_elements(style_tool.get_elements_by_style(style))
def select_by_style(style_tool, spatial, style=None):
spatial.select_products(style_tool.get_elements_by_style(style))
+2 -2
View File
@@ -66,14 +66,14 @@ def select_system_products(system_tool, system=None):
system_tool.select_system_products(system)
def show_ports(ifc, system, element=None):
def show_ports(ifc, system, spatial, element=None):
obj = ifc.get_object(element)
if obj and ifc.is_moved(obj):
system.run_geometry_edit_object_placement(obj=obj)
ports = system.get_ports(element)
system.load_ports(element, ports)
system.select_elements(ports)
spatial.select_products(ports)
def hide_ports(ifc, system, element=None):
+17 -7
View File
@@ -165,7 +165,7 @@ class Cost:
def expand_cost_item_rate(cls, cost_item): pass
def expand_cost_item(cls, cost_item): pass
def expand_cost_items(cls): pass
def export_cost_schedules(cls, format, cost_schedule): pass
def export_cost_schedules(cls, filepath, format, cost_schedule): pass
def format_unit(cls, unit): pass
def get_active_cost_item(cls): pass
def get_active_cost_schedule(cls): pass
@@ -311,7 +311,7 @@ class Drawing:
def run_drawing_activate_model(cls): pass
def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass
def select_assigned_product(cls, drawing): pass
def set_drawing_collection_name(cls, group, collection): pass
def set_drawing_collection_name(cls, drawing, collection): pass
def set_name(cls, element, name): pass
def setup_annotation_object(cls, obj, object_type): pass
def setup_shading_styles_path(cls, resource_path): pass
@@ -433,21 +433,28 @@ class Loader:
@interface
class Material:
def add_default_material_object(cls): pass
def add_material_to_set(cls, material_set, material): pass
def delete_object(cls, obj): pass
def disable_editing_material(cls): pass
def disable_editing_materials(cls): pass
def enable_editing_material(cls, material): pass
def enable_editing_materials(cls): pass
def get_active_material_type(cls): pass
def get_active_material_type(cls): pass
def get_active_material(cls): pass
def get_active_object_material(cls, obj): pass
def get_elements_by_material(cls, material): pass
def get_material_attributes(cls): pass
def get_material(cls, element, should_inherit): pass
def get_name(cls, obj): pass
def get_type(cls, element): pass
def has_material_profile(cls, element): pass
def import_material_definitions(cls, material_type): pass
def is_a_flow_segment(cls, element): pass
def is_a_material_set(cls, material): pass
def is_editing_materials(cls): pass
def is_material_used_in_sets(cls, material): pass
def load_material_attributes(cls, material): pass
def select_elements(cls, elements): pass
def replace_material_with_material_profile(cls, element): pass
@interface
@@ -547,6 +554,8 @@ class Project:
@interface
class Profile:
def draw_image_for_ifc_profile(cls, draw, profile, size): pass
def is_editing_profile(cls): pass
def get_profile(cls, element): pass
@interface
@@ -806,8 +815,11 @@ class Style:
def get_elements_by_style(cls, style): pass
def get_name(cls, obj): pass
def get_style(cls, obj): pass
def get_surface_rendering_attributes(cls, obj): pass
def get_style_elements(cls, blender_material): pass
def get_surface_rendering_attributes(cls, obj, verbose=True): pass
def get_surface_rendering_style(cls, obj): pass
def get_texture_style(cls, obj): pass
def get_external_style(cls, obj): pass
def get_surface_shading_attributes(cls, obj): pass
def get_surface_shading_style(cls, obj): pass
def get_surface_texture_style(cls, obj): pass
@@ -816,7 +828,6 @@ class Style:
def import_surface_attributes(cls, style, obj): pass
def is_editing_styles(cls): pass
def record_shading(cls, obj): pass
def select_elements(cls, elements): pass
@interface
@@ -839,7 +850,6 @@ class System:
def load_ports(cls, element, ports): pass
def run_geometry_edit_object_placement(cls, obj=None): pass
def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass
def select_elements(cls, elements): pass
def select_system_products(cls, system): pass
def set_active_system(cls, system): pass
+1 -1
View File
@@ -34,8 +34,8 @@ def assign_type(ifc, type_tool, element=None, type=None):
def purge_unused_types(ifc, type):
for element_type in type.get_model_types():
if not type.get_type_occurrences(element_type):
ifc.run("root.remove_product", product=element_type)
obj = ifc.get_object(element_type)
ifc.run("root.remove_product", product=element_type)
if obj:
ifc.unlink(obj=obj)
type.remove_object(obj)
+108 -4
View File
@@ -20,6 +20,19 @@ import bpy
import ifcopenshell.api
import blenderbim.tool as tool
from mathutils import Vector
from pathlib import Path
VIEWPORT_ATTRIBUTES = [
"view_matrix",
"view_distance",
"view_perspective",
"use_box_clip",
"use_clip_planes",
"is_perspective",
"show_sync_view",
"clip_planes",
]
class Blender:
@@ -94,6 +107,24 @@ class Blender:
return False
return False
@classmethod
def show_error_message(cls, text):
"""useful for showing error messages outside blender operators"""
def error(self, context):
self.layout.label(text=text)
bpy.context.window_manager.popup_menu(error, title="Error", icon="ERROR")
@classmethod
def get_blender_prop_default_value(cls, props, prop_name):
prop_bl_rna = props.bl_rna.properties[prop_name]
if getattr(prop_bl_rna, "array_length", 0) > 0:
prop_value = prop_bl_rna.default_array
else:
prop_value = prop_bl_rna.default
return prop_value
@classmethod
def get_viewport_context(cls):
"""Get viewport area context for context overriding.
@@ -108,11 +139,70 @@ class Blender:
return context_override
@classmethod
def update_viewport(cls):
# if it stops working in future Blender versions
# there is an alternative:
# bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
def get_viewport_position(cls):
region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d
copy_if_possible = lambda x: x.copy() if hasattr(x, "copy") else x
viewport_data = {attr: copy_if_possible(getattr(region_3d, attr)) for attr in VIEWPORT_ATTRIBUTES}
return viewport_data
@classmethod
def set_viewport_position(cls, data):
region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d
for attr in VIEWPORT_ATTRIBUTES:
setattr(region_3d, attr, data[attr])
@classmethod
def get_shader_editor_context(cls):
for screen in bpy.data.screens:
for area in screen.areas:
if area.type == "NODE_EDITOR":
for space in area.spaces:
if space.tree_type == "ShaderNodeTree":
context_override = {"area": area, "space": space, "screen": screen}
return context_override
@classmethod
def copy_node_graph(cls, material_to, material_from):
temp_override = cls.get_shader_editor_context()
shader_editor = temp_override["space"]
# remove all nodes from the current material
for n in material_to.node_tree.nodes[:]:
material_to.node_tree.nodes.remove(n)
previous_pin_setting = shader_editor.pin
# required to be able to change material to something else
shader_editor.pin = True
shader_editor.node_tree = material_from.node_tree
# select all nodes and copy them to clipboard
for node in material_from.node_tree.nodes:
node.select = True
bpy.ops.node.clipboard_copy(temp_override)
# back to original material
shader_editor.node_tree = material_to.node_tree
bpy.ops.node.clipboard_paste(temp_override, offset=(0, 0))
# restore shader editor settings
shader_editor.pin = previous_pin_setting
@classmethod
def get_material_node(cls, blender_material, node_type, kwargs={}):
"""returns first node from the `blender_material` shader graph with type `node_type`"""
if not blender_material.use_nodes:
return
nodes = blender_material.node_tree.nodes
for node in nodes:
if node.type == node_type and all(getattr(node, a) == kwargs[a] for a in kwargs):
return node
@classmethod
def update_screen(cls):
bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
@classmethod
def update_viewport(cls):
tool.Blender.get_viewport_context()["area"].tag_redraw()
@classmethod
@@ -215,6 +305,20 @@ class Blender:
context.view_layer.objects.active = active_object
active_object.select_set(True)
@classmethod
def append_data_block(cls, filepath, data_block_type, name, link=False, relative=False):
if Path(filepath) == Path(bpy.data.filepath):
data_block = getattr(bpy.data, data_block_type).get(name, None)
if not data_block:
return {"data_block": None, "msg": f"Data-block {data_block_type}/{name} not found in {filepath}"}
return {"data_block": data_block.copy(), "msg": ""}
with bpy.data.libraries.load(filepath, link=link, relative=relative) as (data_from, data_to):
if name not in getattr(data_from, data_block_type):
return {"data_block": None, "msg": f"Data-block {data_block_type}/{name} not found in {filepath}"}
getattr(data_to, data_block_type).append(name)
return {"data_block": getattr(data_to, data_block_type)[0], "msg": ""}
## BMESH UTILS ##
@classmethod
def apply_bmesh(cls, mesh, bm, obj=None):
+10 -7
View File
@@ -143,11 +143,8 @@ class Collector(blenderbim.core.tool.Collector):
return axes_col[0]
return bpy.data.collections.new(axes)
if element.is_a("IfcAnnotation"):
for rel in element.HasAssignments or []:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
name = "IfcGroup/" + rel.RelatingGroup.Name
return bpy.data.collections.get(name) or bpy.data.collections.new(name)
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
return cls._create_own_collection(obj)
if element.is_a("IfcStructuralMember"):
return bpy.data.collections.get("Members") or bpy.data.collections.new("Members")
@@ -178,12 +175,18 @@ class Collector(blenderbim.core.tool.Collector):
axes = "WAxes"
grid_obj = tool.Ifc.get_object(grid)
if grid_obj:
return bpy.data.collections.get(grid_obj.name)
return grid_obj.BIMObjectProperties.collection
if element.is_a("IfcAnnotation"):
if element.ObjectType == "DRAWING":
return cls._create_project_child_collection("Views")
for rel in element.HasAssignments or []:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
return cls._create_project_child_collection("Views")
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcAnnotation") and related_object.ObjectType == "DRAWING":
drawing_obj = tool.Ifc.get_object(related_object)
if drawing_obj:
return drawing_obj.BIMObjectProperties.collection
if element.is_a("IfcStructuralItem"):
return cls._create_project_child_collection("StructuralItems")
+10 -6
View File
@@ -96,11 +96,11 @@ class Cost(blenderbim.core.tool.Cost):
props.contracted_cost_items = json.dumps(cls.contracted_cost_items)
@classmethod
def contract_cost_item(cls, cost_item):
def contract_cost_item(cls, cost_item_id):
props = bpy.context.scene.BIMCostProperties
if not hasattr(cls, "contracted_cost_items"):
cls.contracted_cost_items = json.loads(props.contracted_cost_items)
cls.contracted_cost_items.append(cost_item.id())
cls.contracted_cost_items.append(cost_item_id)
props.contracted_cost_items = json.dumps(cls.contracted_cost_items)
@classmethod
@@ -114,11 +114,11 @@ class Cost(blenderbim.core.tool.Cost):
props.contracted_cost_items = json.dumps(cls.contracted_cost_items)
@classmethod
def clean_up_cost_item_tree(cls, cost_item):
def clean_up_cost_item_tree(cls, cost_item_id):
props = bpy.context.scene.BIMCostProperties
if not hasattr(cls, "contracted_cost_items"):
cls.contracted_cost_items = json.loads(props.contracted_cost_items)
if props.active_cost_item_id == cost_item.id():
if props.active_cost_item_id == cost_item_id:
props.active_cost_item_id = 0
if props.active_cost_item_index in cls.contracted_cost_items:
cls.contracted_cost_items.remove(props.active_cost_item_index)
@@ -505,11 +505,15 @@ class Cost(blenderbim.core.tool.Cost):
props.is_cost_update_enabled = True
@classmethod
def export_cost_schedules(cls, format=None, cost_schedule=None):
def export_cost_schedules(cls, filepath, format=None, cost_schedule=None):
import subprocess
import os
import sys
path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cost_schedules")
if filepath:
path=filepath
else:
path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "build", "cost_schedules")
if not os.path.exists(path):
os.makedirs(path)
if format == "CSV":
+10 -11
View File
@@ -364,7 +364,7 @@ class Drawing(blenderbim.core.tool.Drawing):
def get_drawing_collection(cls, drawing):
obj = tool.Ifc.get_object(drawing)
if obj:
return obj.users_collection[0]
return obj.BIMObjectProperties.collection
@classmethod
def get_drawing_group(cls, drawing):
@@ -759,8 +759,8 @@ class Drawing(blenderbim.core.tool.Drawing):
)
@classmethod
def set_drawing_collection_name(cls, group, collection):
collection.name = f"IfcGroup/{group.Name}"
def set_drawing_collection_name(cls, drawing, collection):
collection.name = tool.Loader.get_name(drawing)
@classmethod
def set_name(cls, element, name):
@@ -1486,8 +1486,8 @@ class Drawing(blenderbim.core.tool.Drawing):
project_collection.children["Views"].children[collection.name].hide_viewport = True
bpy.data.collections.get(collection.name).hide_render = True
project_collection.children["Views"].children[camera.users_collection[0].name].hide_viewport = False
bpy.data.collections.get(camera.users_collection[0].name).hide_render = False
project_collection.children["Views"].children[camera.BIMObjectProperties.collection.name].hide_viewport = False
camera.BIMObjectProperties.collection.hide_render = False
tool.Spatial.set_active_object(camera)
# Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
@@ -1498,12 +1498,11 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.ops.object.hide_view_clear()
filtered_elements = cls.get_drawing_elements(drawing) | cls.get_drawing_spaces(drawing)
hidden_objs = [o for o in bpy.context.visible_objects if tool.Ifc.get_entity(o) not in filtered_elements]
for hidden_obj in hidden_objs:
if bpy.context.view_layer.objects.get(hidden_obj.name):
hidden_obj.hide_set(True)
hidden_obj.hide_render = True
for visible_obj in bpy.context.visible_objects:
hide = tool.Ifc.get_entity(visible_obj) not in filtered_elements
if bpy.context.view_layer.objects.get(visible_obj.name):
visible_obj.hide_set(hide)
visible_obj.hide_render = hide
subcontexts = []
target_view = cls.get_drawing_target_view(drawing)
@@ -356,6 +356,16 @@ class Geometry(blenderbim.core.tool.Geometry):
return True
return False
@classmethod
def is_profile_based(cls, data):
return data.BIMMeshProperties.subshape_type == "PROFILE"
@classmethod
def is_swept_profile(cls, representation):
return ifcopenshell.util.representation.resolve_representation(representation).RepresentationType in (
"SweptSolid",
)
@classmethod
def is_type_product(cls, element):
return element.is_a("IfcTypeProduct")
+41 -15
View File
@@ -1,6 +1,5 @@
import os
import re
import time
# allows git import even if git executable isn't found
os.environ["GIT_PYTHON_REFRESH"] = "quiet"
@@ -103,6 +102,30 @@ class IfcGit:
if tag_name in repo.tags:
repo.delete_tag(tag_name)
@classmethod
def add_remote(cls, repo):
props = bpy.context.scene.IfcGitProperties
repo.create_remote(name=props.remote_name, url=props.remote_url)
props.remote_name = ""
props.remote_url = ""
@classmethod
def delete_remote(cls, repo):
props = bpy.context.scene.IfcGitProperties
remote_name = props.select_remote
if remote_name in repo.remotes:
repo.delete_remote(remote_name)
if repo.remotes:
props.select_remote = repo.remotes[0].name
@classmethod
def push(cls, repo, remote_name, branch_name):
remote = repo.remotes[remote_name]
try:
remote.push(tags=True, refspec=branch_name).raise_if_error()
except git.exc.GitCommandError as exc:
return exc.stderr
@classmethod
def create_new_branch(cls):
props = bpy.context.scene.IfcGitProperties
@@ -369,11 +392,15 @@ class IfcGit:
@classmethod
def config_ifcmerge(cls):
config_reader = IfcGitRepo.repo.config_reader()
config_writer = IfcGitRepo.repo.config_writer()
section = 'mergetool "ifcmerge"'
if not config_reader.has_section(section):
config_writer = IfcGitRepo.repo.config_writer()
config_writer.set_value(section, "cmd", "ifcmerge $BASE $LOCAL $REMOTE $MERGED")
config_writer.set_value(section, "trustExitCode", True)
section = 'mergetool "ifcmerge-forward"'
if not config_reader.has_section(section):
config_writer.set_value(section, "cmd", "ifcmerge $BASE $REMOTE $LOCAL $MERGED")
config_writer.set_value(section, "trustExitCode", True)
@classmethod
def config_info_attributes(cls, repo):
@@ -402,13 +429,19 @@ class IfcGit:
for branch in lookup[item.hexsha]:
if branch.name == props.display_branch:
# this is a branch!
if re.match("^(origin/)?(HEAD|main|master)$", branch.name):
# preserve remote IDs in origin/main or main
mergetool = "ifcmerge"
else:
# rewrite remote IDs
mergetool = "ifcmerge-forward"
try:
# NOTE this is calling the git binary in a subprocess
repo.git.merge(branch)
except git.exc.GitCommandError:
# merge is expected to fail, run ifcmerge
try:
repo.git.mergetool(tool="ifcmerge")
repo.git.mergetool(tool=mergetool)
except git.exc.GitCommandError as exc:
message = re.sub("( stderr: '|')", "", exc.stderr)
# ifcmerge failed, rollback
@@ -416,23 +449,16 @@ class IfcGit:
operator.report({"ERROR"}, "IFC Merge failed:" + message)
return False
else:
if os.name == "nt":
cls.dos2unix(path_ifc)
repo.index.add(path_ifc)
repo.git.commit("--no-edit")
except:
operator.report({"ERROR"}, "Unknown IFC Merge failure")
return False
repo.index.add(path_ifc)
message_summary = ""
branch_commits = repo.iter_commits(rev=repo.active_branch.name + ".." + props.display_branch)
for commit in branch_commits:
message_summary += "\n\n" + commit.author.name + " <" + commit.author.email + ">\n"
message_summary += time.strftime("%c", time.localtime(commit.committed_date)) + "\n"
message_summary += commit.message
props.commit_message = (
"Merge branch '" + props.display_branch + "' into " + repo.active_branch.name + message_summary
)
props.display_branch = repo.active_branch.name
cls.load_project(path_ifc)
+234
View File
@@ -21,6 +21,9 @@ import bpy
import ifcopenshell.util.element
import blenderbim.core.tool
import blenderbim.tool as tool
import os
from mathutils import Vector
from pathlib import Path
# Progressively we'll refactor loading elements into Blender objects into this
@@ -28,6 +31,7 @@ import blenderbim.tool as tool
# partially load and unload objects for huge models, partial model editing, and
# supplementary objects (e.g. drawings, structural analysis models, etc).
class Loader(blenderbim.core.tool.Loader):
@classmethod
def get_mesh_name(cls, geometry):
@@ -52,3 +56,233 @@ class Loader(blenderbim.core.tool.Loader):
else:
# TODO: See #2002
mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.replace(",", ""))
@classmethod
def create_surface_style_shading(cls, blender_material, surface_style):
surface_style = cls.surface_style_to_dict(surface_style)
alpha = 1.0
# Transparency was added in IFC4
if transparency := surface_style.get("Transparency", None):
alpha = 1 - transparency
blender_material.diffuse_color = surface_style["SurfaceColour"][:3] + (alpha,)
@classmethod
def restart_material_node_tree(cls, blender_material):
nodes = blender_material.node_tree.nodes
links = blender_material.node_tree.links
for n in nodes[:]:
nodes.remove(n)
output = nodes.new("ShaderNodeOutputMaterial")
output.location = Vector((300, 300))
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = Vector((10, 300))
links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
@classmethod
def surface_style_to_dict(cls, surface_style):
if isinstance(surface_style, dict):
return surface_style
surface_style = surface_style.get_info()
color_to_tuple = lambda x: (x.Red, x.Green, x.Blue, 1)
if surface_style["SurfaceColour"]:
surface_style["SurfaceColour"] = color_to_tuple(surface_style["SurfaceColour"])
if surface_style.get("DiffuseColour", None) and surface_style["DiffuseColour"].is_a("IfcColourRgb"):
surface_style["DiffuseColour"] = ("IfcColourRgb", color_to_tuple(surface_style["DiffuseColour"]))
elif surface_style.get("DiffuseColour", None) and surface_style["DiffuseColour"].is_a("IfcNormalisedRatioMeasure"):
diffuse_color_value = surface_style["DiffuseColour"].wrappedValue
diffuse_color = [v * diffuse_color_value for v in surface_style["SurfaceColor"][:3]] + [1]
surface_style["DiffuseColour"] = ("IfcNormalisedRatioMeasure", diffuse_color)
else:
surface_style["DiffuseColour"] = None
if surface_style.get("SpecularColour", None) and surface_style["SpecularColour"].is_a("IfcNormalisedRatioMeasure"):
surface_style["SpecularColour"] = surface_style["SpecularColour"].wrappedValue
else:
surface_style["SpecularColour"] = None
if surface_style.get("SpecularHighlight", None) and surface_style["SpecularHighlight"].is_a("IfcSpecularRoughness"):
surface_style["SpecularHighlight"] = surface_style["SpecularHighlight"].wrappedValue
else:
surface_style["SpecularHighlight"] = None
return surface_style
@classmethod
def create_surface_style_rendering(cls, blender_material, surface_style):
surface_style = cls.surface_style_to_dict(surface_style)
cls.create_surface_style_shading(blender_material, surface_style)
reflectance_method = surface_style["ReflectanceMethod"]
if reflectance_method not in ("PHYSICAL", "NOTDEFINED", "FLAT"):
print(f'WARNING. Unsupported reflectance method "{reflectance_method}" on style {surface_style}')
return
if reflectance_method in ["PHYSICAL", "NOTDEFINED"]:
blender_material.use_nodes = True
cls.restart_material_node_tree(blender_material)
bsdf = tool.Blender.get_material_node(blender_material, "BSDF_PRINCIPLED")
if surface_style["DiffuseColour"]:
color_type, color_value = surface_style["DiffuseColour"]
if color_type == "IfcColourRgb":
bsdf.inputs["Base Color"].default_value = color_value
elif color_type == "IfcNormalisedRatioMeasure":
bsdf.inputs["Base Color"].default_value = color_value
if surface_style["SpecularColour"]:
bsdf.inputs["Metallic"].default_value = surface_style["SpecularColour"]
if surface_style["SpecularHighlight"]:
bsdf.inputs["Roughness"].default_value = surface_style["SpecularHighlight"]
if transparency := surface_style.get("Transparency", None):
bsdf.inputs["Alpha"].default_value = 1 - transparency
blender_material.blend_method = "BLEND"
elif reflectance_method == "FLAT":
blender_material.use_nodes = True
cls.restart_material_node_tree(blender_material)
output = tool.Blender.get_material_node(blender_material, "OUTPUT_MATERIAL")
bsdf = tool.Blender.get_material_node(blender_material, "BSDF_PRINCIPLED")
mix = blender_material.node_tree.nodes.new(type="ShaderNodeMixShader")
mix.location = bsdf.location
blender_material.node_tree.links.new(mix.outputs[0], output.inputs["Surface"])
blender_material.node_tree.nodes.remove(bsdf)
lightpath = blender_material.node_tree.nodes.new(type="ShaderNodeLightPath")
lightpath.location = mix.location - Vector((200, -200))
blender_material.node_tree.links.new(lightpath.outputs[0], mix.inputs[0])
bsdf = blender_material.node_tree.nodes.new(type="ShaderNodeBsdfTransparent")
bsdf.location = mix.location - Vector((200, 150))
blender_material.node_tree.links.new(bsdf.outputs[0], mix.inputs[1])
rgb = blender_material.node_tree.nodes.new(type="ShaderNodeRGB")
rgb.location = mix.location - Vector((200, 250))
blender_material.node_tree.links.new(rgb.outputs[0], mix.inputs[2])
if surface_style["DiffuseColour"]:
color_type, color_value = surface_style["DiffuseColour"]
if color_type == "IfcColourRgb":
rgb.outputs[0].default_value = color_value
@classmethod
def create_surface_style_with_textures(cls, blender_material, rendering_style, texture_style):
"""supposed to be called after `create_surface_style_rendering`"""
if not isinstance(texture_style, list):
textures = [t.get_info() for t in texture_style.Textures]
else:
textures = texture_style
rendering_style = cls.surface_style_to_dict(rendering_style)
reflectance_method = rendering_style["ReflectanceMethod"]
if reflectance_method not in ("PHYSICAL", "NOTDEFINED", "FLAT"):
print(f'WARNING. Unsupported reflectance method "{reflectance_method}" on style {rendering_style}')
return
for texture in textures:
mode = texture.get("Mode", None)
node = None
if texture["type"] == "IfcImageTexture":
image_url = texture["URLReference"]
ifc_path = tool.Ifc.get_path()
if ifc_path:
image_url = bpy.path.abspath(image_url, start=Path(ifc_path).parent)
if not os.path.exists(image_url):
print(f"WARNING. Couldn't find texture by path {image_url}, it will be skipped.")
continue
if reflectance_method in ["PHYSICAL", "NOTDEFINED"]:
bsdf = tool.Blender.get_material_node(blender_material, "BSDF_PRINCIPLED")
if mode == "NORMAL":
# add normal map node
normalmap = blender_material.node_tree.nodes.new(type="ShaderNodeNormalMap")
normalmap.location = bsdf.location - Vector((200, 600))
blender_material.node_tree.links.new(normalmap.outputs[0], bsdf.inputs["Normal"])
# add normal map sampler
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
node.location = normalmap.location - Vector((300, 0))
image = bpy.data.images.load(image_url)
image.colorspace_settings.name = "Non-Color"
node.image = image
blender_material.node_tree.links.new(node.outputs[0], normalmap.inputs["Color"])
elif mode == "EMISSIVE":
output = tool.Blender.get_material_node(blender_material, "OUTPUT_MATERIAL")
# add "Add Shader" node
add = blender_material.node_tree.nodes.new(type="ShaderNodeAddShader")
add.location = bsdf.location + Vector((200, 350))
blender_material.node_tree.links.new(bsdf.outputs[0], add.inputs[1])
blender_material.node_tree.links.new(add.outputs[0], output.inputs[0])
# add emssion shader node
emission = blender_material.node_tree.nodes.new(type="ShaderNodeEmission")
emission.location = add.location - Vector((200, 0))
blender_material.node_tree.links.new(emission.outputs[0], add.inputs[0])
# add emission texture sampler
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
node.location = emission.location - Vector((350, 0))
image = bpy.data.images.load(image_url)
node.image = image
blender_material.node_tree.links.new(node.outputs[0], emission.inputs[0])
elif mode == "METALLICROUGHNESS":
separate = blender_material.node_tree.nodes.new(type="ShaderNodeSeparateRGB")
separate.location = bsdf.location - Vector((200, 300))
blender_material.node_tree.links.new(separate.outputs[1], bsdf.inputs["Roughness"])
blender_material.node_tree.links.new(separate.outputs[2], bsdf.inputs["Metallic"])
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
node.location = separate.location - Vector((300, 0))
image = bpy.data.images.load(image_url)
image.colorspace_settings.name = "Non-Color"
node.image = image
blender_material.node_tree.links.new(node.outputs[0], separate.inputs[0])
elif mode == "OCCLUSION":
# TODO work out how to implement glTF settings here
# https://docs.blender.org/manual/en/dev/addons/import_export/scene_gltf2.html
pass
elif mode == "DIFFUSE":
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
node.location = bsdf.location - Vector((400, 0))
image = bpy.data.images.load(image_url)
node.image = image
blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs["Base Color"])
blender_material.node_tree.links.new(node.outputs[1], bsdf.inputs["Alpha"])
blender_material.blend_method = "BLEND"
elif reflectance_method == "FLAT":
bsdf = tool.Blender.get_material_node(blender_material, "MIX_SHADER")
if mode != "EMISSIVE":
continue
# remove RGB node from `create_surface_style_rendering`
prev_node = bsdf.inputs[2].links[0].from_node
blender_material.node_tree.nodes.remove(prev_node)
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
node.location = bsdf.location - Vector((200, 250))
image = bpy.data.images.load(image_url)
# TODO: orphaned textures after shader recreated?
node.image = image
blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs[2])
# TODO: add support for texture data not ifc elements
if node and getattr(texture, "IsMappedBy", None):
coordinates = texture.IsMappedBy[0]
coord = blender_material.node_tree.nodes.new(type="ShaderNodeTexCoord")
coord.location = node.location - Vector((200, 0))
if coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD":
blender_material.node_tree.links.new(coord.outputs["Generated"], node.inputs["Vector"])
elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE":
blender_material.node_tree.links.new(coord.outputs["Camera"], node.inputs["Vector"])
else:
blender_material.node_tree.links.new(coord.outputs["UV"], node.inputs["Vector"])
+101 -12
View File
@@ -21,6 +21,8 @@ import ifcopenshell
import blenderbim.core.tool
import blenderbim.tool as tool
import blenderbim.bim.helper
import ifcopenshell.util.unit
import ifcopenshell.util.element
class Material(blenderbim.core.tool.Material):
@@ -101,17 +103,6 @@ class Material(blenderbim.core.tool.Material):
return True
return False
@classmethod
def select_elements(cls, elements):
for element in elements:
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
@classmethod
def get_active_material_type(cls):
return bpy.context.scene.BIMMaterialProperties.material_type
@classmethod
def load_material_attributes(cls, material):
props = bpy.context.scene.BIMMaterialProperties
@@ -122,7 +113,7 @@ class Material(blenderbim.core.tool.Material):
def enable_editing_material(cls, material):
props = bpy.context.scene.BIMMaterialProperties
props.active_material_id = material.id()
props.editing_material_type = "ATTRIBUTES"
props.editing_material_type = "ATTRIBUTES"
@classmethod
def get_material_attributes(cls):
@@ -133,3 +124,101 @@ class Material(blenderbim.core.tool.Material):
props = bpy.context.scene.BIMMaterialProperties
props.active_material_id = 0
props.editing_material_type = ""
@classmethod
def get_type(cls, element):
return ifcopenshell.util.element.get_type(element)
@classmethod
def get_active_object_material(cls):
active_obj = bpy.context.active_object
if not active_obj:
return
return active_obj.BIMObjectMaterialProperties.material_type
@classmethod
def get_active_material(cls):
return tool.Ifc.get().by_id(int(bpy.context.active_object.BIMObjectMaterialProperties.material))
@classmethod
def get_material(cls, element, should_inherit=False):
return ifcopenshell.util.element.get_material(element, should_inherit=should_inherit)
@classmethod
def is_a_material_set(cls, material):
return material.is_a() in [
"IfcMaterialConstituentSet",
"IfcMaterialLayerSet",
"IfcMaterialProfileSet",
]
@classmethod
def add_material_to_set(cls, material_set, material):
if material_set.is_a("IfcMaterialConstituentSet"):
if not material_set.MaterialConstituents:
tool.Ifc.run(
"material.add_constituent",
constituent_set=material_set,
material=material,
)
elif material_set.is_a() == "IfcMaterialLayerSet":
if not material_set.MaterialLayers:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layer = tool.Ifc.run(
"material.add_layer",
layer_set=material_set,
material=material,
)
thickness = 0.1 # Arbitrary metric thickness for now
layer.LayerThickness = thickness / unit_scale
elif material_set.is_a("IfcMaterialProfileSet"):
if not material_set.MaterialProfiles:
named_profiles = [p for p in tool.Ifc.get().by_type("IfcProfileDef") if p.ProfileName]
if named_profiles:
profile = named_profiles[0]
else:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
size = 0.5 / unit_scale
profile = tool.Ifc.get().create_entity(
"IfcRectangleProfileDef",
ProfileName="New Profile",
ProfileType="AREA",
XDim=size,
YDim=size,
)
material_profile = tool.Ifc.run(
"material.add_profile",
profile_set=material_set,
material=material,
profile=profile,
)
@classmethod
def has_material_profile(cls, element):
material = cls.get_material(element, should_inherit=False)
inherited_material = cls.get_material(element, should_inherit=True)
if material and "Profile" in material.is_a():
return True
if inherited_material and "Profile" in inherited_material.is_a():
return True
return False
@classmethod
def is_a_flow_segment(cls, element):
return element.is_a("IfcFlowSegment")
@classmethod
def replace_material_with_material_profile(cls, element):
old_material = cls.get_material(element, should_inherit=False)
old_inherited_material = cls.get_material(element, should_inherit=True)
material = old_material if old_material and old_material.is_a("IfcMaterial") else None
if not material and old_inherited_material:
material = old_inherited_material if old_inherited_material.is_a("IfcMaterial") else None
if not material:
material = tool.Ifc.get().by_type("IfcMaterial")[0]
else:
blenderbim.core.material.unassign_material(tool.Ifc, tool.Material, objects=[tool.Ifc.get_object(element)])
tool.Ifc.run("material.assign_material", product=element, type="IfcMaterialProfileSet", material=material)
assinged_material = cls.get_material(element)
material_profile = tool.Ifc.run("material.add_profile", profile_set=assinged_material, material=material)
return material_profile
+18
View File
@@ -21,6 +21,7 @@ import ifcopenshell.util.unit
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.core.tool
from blenderbim.bim.module.model.decorator import ProfileDecorator
class Profile(blenderbim.core.tool.Profile):
@@ -52,3 +53,20 @@ class Profile(blenderbim.core.tool.Profile):
for e in grouped_edges:
draw.line((tuple(grouped_verts[e[0]]), tuple(grouped_verts[e[1]])), fill="white", width=2)
@classmethod
def is_editing_profile(cls):
return ProfileDecorator.installed
@classmethod
def get_profile(cls, element):
representations = element.Representation
for representation in representations.Representations:
if not representation.is_a("IfcShapeRepresentation"):
continue
for representation_item in representation.Items:
if representation_item.is_a("IfcExtrudedAreaSolid"):
profile = representation_item.SweptArea
if profile:
return profile
return None
+1 -1
View File
@@ -947,7 +947,7 @@ class Sequence(blenderbim.core.tool.Sequence):
def set_material(name, r, g, b):
material = bpy.data.materials.new(name)
material.use_nodes = True
material.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (r, g, b, 1.0)
tool.Blender.get_material_node(material, "BSDF_PRINCIPLED").inputs[0].default_value = (r, g, b, 1.0)
return material
def get_animation_materials():
+2 -2
View File
@@ -203,7 +203,7 @@ class Spatial(blenderbim.core.tool.Spatial):
cls.props.is_container_update_enabled = False
parent = tool.Ifc.get().by_type("IfcProject")[0]
for object in ifcopenshell.util.element.get_parts(parent):
for object in ifcopenshell.util.element.get_parts(parent) or []:
if object.is_a("IfcSpatialElement"):
cls.create_new_storey_li(object, 0)
cls.props.is_container_update_enabled = True
@@ -222,7 +222,7 @@ class Spatial(blenderbim.core.tool.Spatial):
if new.has_decomposition:
new.has_children = True
if new.is_expanded:
for related_object in ifcopenshell.util.element.get_parts(element):
for related_object in ifcopenshell.util.element.get_parts(element) or []:
if related_object.is_a("IfcSpatialElement"):
cls.create_new_storey_li(related_object, level_index + 1)
+284 -30
View File
@@ -22,6 +22,30 @@ import ifcopenshell
import blenderbim.core.tool
import blenderbim.tool as tool
import blenderbim.bim.helper
import os.path
# fmt: off
TEXTURE_MAPS_BY_METHODS = {
"PHYSICAL": ("NORMAL", "EMISSIVE", "METALLICROUGHNESS", "DIFFUSE"),
"FLAT": ("EMISSIVE",)
}
# fmt: on
STYLE_PROPS_MAP = {
"reflectance_method": "ReflectanceMethod",
"diffuse_color": "DiffuseColour",
"surface_color": "SurfaceColour",
"transparency": "Transparency",
"roughness": "SpecularHighlight",
"metallic": "SpecularColour",
}
STYLE_TEXTURE_PROPS_MAP = {
"EMISSIVE": "emissive_path",
"NORMAL": "normal_path",
"METALLICROUGHNESS": "metallic_roughness_path",
"DIFFUSE": "diffuse_path",
}
class Style(blenderbim.core.tool.Style):
@@ -33,6 +57,10 @@ class Style(blenderbim.core.tool.Style):
def disable_editing(cls, obj):
obj.BIMStyleProperties.is_editing = False
@classmethod
def disable_editing_external_style(cls, obj):
obj.BIMStyleProperties.is_editing_external_style = False
@classmethod
def disable_editing_styles(cls):
bpy.context.scene.BIMStylesProperties.is_editing = False
@@ -41,6 +69,10 @@ class Style(blenderbim.core.tool.Style):
def enable_editing(cls, obj):
obj.BIMStyleProperties.is_editing = True
@classmethod
def enable_editing_external_style(cls, obj):
obj.BIMStyleProperties.is_editing_external_style = True
@classmethod
def enable_editing_styles(cls):
bpy.context.scene.BIMStylesProperties.is_editing = True
@@ -49,6 +81,10 @@ class Style(blenderbim.core.tool.Style):
def export_surface_attributes(cls, obj):
return blenderbim.bim.helper.export_attributes(obj.BIMStyleProperties.attributes)
@classmethod
def export_external_style_attributes(cls, obj):
return blenderbim.bim.helper.export_attributes(obj.BIMStyleProperties.external_style_attributes)
@classmethod
def get_active_style_type(cls):
return bpy.context.scene.BIMStylesProperties.style_type
@@ -74,70 +110,272 @@ class Style(blenderbim.core.tool.Style):
return
@classmethod
def get_surface_rendering_attributes(cls, obj):
def get_style_elements(cls, blender_material):
if not blender_material.BIMMaterialProperties.ifc_style_id:
return {}
style = tool.Ifc.get().by_id(blender_material.BIMMaterialProperties.ifc_style_id)
style_elements = {}
for style in style.Styles:
style_elements[style.is_a()] = style
return style_elements
@classmethod
def get_surface_style_from_props(cls, blender_material):
"""convert blender style props to ifc props"""
surface_style_data = dict()
props = blender_material.BIMStyleProperties
for prop_blender, prop_ifc in STYLE_PROPS_MAP.items():
surface_style_data[prop_ifc] = getattr(props, prop_blender)
if surface_style_data["ReflectanceMethod"] == "PHYSICAL" and tool.Ifc.get_schema() != "IFC4X3":
surface_style_data["ReflectanceMethod"] = "NOTDEFINED"
surface_style_data["DiffuseColour"] = ("IfcColourRgb", surface_style_data["DiffuseColour"])
return surface_style_data
@classmethod
def get_texture_style_from_props(cls, blender_material):
props = blender_material.BIMStyleProperties
textures = []
texture_maps = TEXTURE_MAPS_BY_METHODS[props.reflectance_method]
for prop_mode in texture_maps:
prop_name = STYLE_TEXTURE_PROPS_MAP[prop_mode]
path = getattr(props, prop_name)
if not path:
continue
if not os.path.abspath(path) and tool.Ifc.get_path():
path = os.path.join(os.path.dirname(tool.Ifc.get_path()), path)
texture_data = {
"Mode": prop_mode,
"type": "IfcImageTexture",
"URLReference": path,
}
textures.append(texture_data)
return textures
@classmethod
def set_surface_style_props(cls, blender_material):
"""set blender style props based on current surface material"""
props = blender_material.BIMStyleProperties
# make sure won't be updating while we changing it
prev_update_graph_value = props.update_graph
props["update_graph"] = False
style_elements = tool.Style.get_style_elements(blender_material)
surface_style = style_elements.get("IfcSurfaceStyleRendering", None)
texture_style = style_elements.get("IfcSurfaceStyleWithTextures", None)
# in case we have just IfcSurfaceStyleShading
if not surface_style:
return
style_data = tool.Loader.surface_style_to_dict(surface_style)
if style_data["ReflectanceMethod"] == "NOTDEFINED":
style_data["ReflectanceMethod"] = "PHYSICAL"
style_data["DiffuseColour"] = style_data["DiffuseColour"][1]
for prop_blender, prop_ifc in STYLE_PROPS_MAP.items():
prop_value = style_data[prop_ifc]
if prop_value is None:
prop_value = tool.Blender.get_blender_prop_default_value(props, prop_blender)
setattr(props, prop_blender, prop_value)
texture_maps = TEXTURE_MAPS_BY_METHODS[style_data["ReflectanceMethod"]]
unused_texture_maps = list(STYLE_TEXTURE_PROPS_MAP.keys())
if texture_style:
for texture in texture_style.Textures:
if texture.Mode not in texture_maps:
print(f"WARNING. Unsupported texture mode: {texture.Mode}. Supported maps: {texture_maps}")
continue
prop_blender = STYLE_TEXTURE_PROPS_MAP.get(texture.Mode, None)
setattr(props, prop_blender, texture.URLReference)
unused_texture_maps.remove(texture.Mode)
# clear empty texture fields
for texture_mode in unused_texture_maps:
prop_blender = STYLE_TEXTURE_PROPS_MAP[texture_mode]
setattr(props, prop_blender, "")
props["update_graph"] = prev_update_graph_value
@classmethod
def get_surface_rendering_attributes(cls, obj, verbose=True):
report = (lambda *x: print(*x)) if verbose else (lambda *x: None)
def color_to_ifc_format(color):
return {
"Name": None,
"Red": color[0],
"Green": color[1],
"Blue": color[2],
}
def get_input_node(node, input_name=None, of_type=None, input_index=None):
input_pin = node.inputs[input_name] if input_index is None else node.inputs[input_index]
if of_type:
return next((l.from_node for l in input_pin.links if l.from_node.type == of_type), None)
return next((l.from_node for l in input_pin.links), None)
props = obj.BIMStyleProperties
transparency = 1 - obj.diffuse_color[3]
diffuse_color = obj.diffuse_color
viewport_color = color_to_ifc_format(obj.diffuse_color)
attributes = {
"SurfaceColour": {
"Name": None,
"Red": obj.diffuse_color[0],
"Green": obj.diffuse_color[1],
"Blue": obj.diffuse_color[2],
},
"SurfaceColour": viewport_color,
"Transparency": transparency,
}
GREEN = "\033[32m"
BLUE = "\033[1;34m"
R = "\033[0m" # RESET symbol
bsdfs = {n.type: n for n in obj.node_tree.nodes}
if "BSDF_GLOSSY" in bsdfs:
attributes["ReflectanceMethod"] = "METAL"
bsdf = bsdfs["BSDF_GLOSSY"]
report("--------------------")
report("Verbose method of getting surface rendering attributes enabled.")
report("If some attribute is not mentioned below, then it won't be saved to IFC.")
report("--------------------")
report(f"{GREEN}Viewport color{R} saved as {GREEN}SurfaceColour{R}")
# TODO: make sure bsdf is connected to the output?
bsdfs = {n.type: n for n in obj.node_tree.nodes if n.outputs and n.outputs[0].is_linked}
if "BSDF_PRINCIPLED" not in bsdfs:
report(f"{GREEN}Viewport color alpha{R} saved as {GREEN}Transparency{R}")
# TODO: should escape referring to pins by their name to support different languages
material_output = tool.Blender.get_material_node(obj, "OUTPUT_MATERIAL", {"is_active_output": True})
surface_output = get_input_node(material_output, "Surface")
if surface_output and surface_output.type == "MIX_SHADER":
mix_shader = surface_output
if (
get_input_node(mix_shader, "Fac", "LIGHT_PATH")
and get_input_node(mix_shader, input_index=1, of_type="BSDF_TRANSPARENT")
and (second_input_node := get_input_node(mix_shader, input_index=2))
and second_input_node.type in ("RGB", "TEX_IMAGE")
):
report(
f"Because of {BLUE}MIX_SHADER + LIGHT_PATH + BSDF_TRANSPARENT + RGB/TEX{R} node setup reflectance method identified as {BLUE}FLAT{R}"
)
attributes["ReflectanceMethod"] = "FLAT"
attributes["SpecularHighlight"] = None
if second_input_node.type == "RGB":
report(f"RGB {GREEN}Color{R} saved as {GREEN}DiffuseColour{R}")
diffuse_color = second_input_node.outputs[0].default_value
elif second_input_node.type == "TEX_IMAGE":
report(f"{GREEN}BBIM Panel Diffuse Color{R} saved as {GREEN}DiffuseColour{R}")
diffuse_color = props.diffuse_color
elif surface_output and (
(surface_output.type == "BSDF_PRINCIPLED" and (bsdf := surface_output))
or (
surface_output.type == "ADD_SHADER"
and (bsdf := get_input_node(surface_output, input_index=1, of_type="BSDF_PRINCIPLED"))
)
):
report(f"Because of {BLUE}BSDF_PRINCIPLED{R} node reflectance method identified as {BLUE}PHYSICAL{R}")
attributes["ReflectanceMethod"] = "NOTDEFINED" if tool.Ifc.get_schema() != "IFC4X3" else "PHYSICAL"
report(f"BSDF {GREEN}Base Color{R} saved as {GREEN}DiffuseColour{R}")
diffuse_color = bsdf.inputs["Base Color"].default_value
report(f"BSDF {GREEN}Metallic{R} saved as {GREEN}SpecularColour{R}")
attributes["SpecularColour"] = round(bsdf.inputs["Metallic"].default_value, 3)
report(f"BSDF {GREEN}Roughness{R} saved as {GREEN}IfcSpecularRoughness{R}")
attributes["SpecularHighlight"] = {"IfcSpecularRoughness": round(bsdf.inputs["Roughness"].default_value, 3)}
report(f"BSDF {GREEN}Alpha{R} saved as {GREEN}Transparency{R}")
attributes["Transparency"] = 1 - bsdf.inputs["Alpha"].default_value
elif "BSDF_GLOSSY" in bsdfs:
attributes["ReflectanceMethod"] = "METAL"
report(f"Because of {BLUE}BSDF_GLOSSY{R} node reflectance method identified as {BLUE}METAL{R}")
bsdf = bsdfs["BSDF_GLOSSY"]
report(f"BSDF {GREEN}Roughness{R} saved as {GREEN}IfcSpecularRoughness{R}")
attributes["SpecularHighlight"] = {"IfcSpecularRoughness": round(bsdf.inputs["Roughness"].default_value, 3)}
report(f"BSDF {GREEN}Color{R} saved as {GREEN}DiffuseColour{R}")
diffuse_color = bsdf.inputs["Color"].default_value
elif "BSDF_DIFFUSE" in bsdfs:
report(f"Because of {BLUE}BSDF_DIFFUSE{R} node reflectance method identified as {BLUE}MATT{R}")
attributes["ReflectanceMethod"] = "MATT"
bsdf = bsdfs["BSDF_DIFFUSE"]
report(f"BSDF {GREEN}Roughness{R} saved as {GREEN}IfcSpecularRoughness{R}")
attributes["SpecularHighlight"] = {"IfcSpecularRoughness": round(bsdf.inputs["Roughness"].default_value, 3)}
report(f"BSDF {GREEN}Color{R} saved as {GREEN}DiffuseColour{R}")
diffuse_color = bsdf.inputs["Color"].default_value
elif "BSDF_GLASS" in bsdfs:
report(f"Because of {BLUE}BSDF_GLASS{R} node reflectance method identified as {BLUE}GLASS{R}")
attributes["ReflectanceMethod"] = "GLASS"
bsdf = bsdfs["BSDF_GLASS"]
report(f"BSDF {GREEN}Roughness{R} saved as {GREEN}IfcSpecularRoughness{R}")
attributes["SpecularHighlight"] = {"IfcSpecularRoughness": round(bsdf.inputs["Roughness"].default_value, 3)}
report(f"BSDF {GREEN}Color{R} saved as {GREEN}DiffuseColour{R}")
diffuse_color = bsdf.inputs["Color"].default_value
# TODO: remove?
elif "EMISSION" in bsdfs:
report(f"Because of {BLUE}EMISSION{R} node reflectance method identified as {BLUE}FLAT{R}")
attributes["ReflectanceMethod"] = "FLAT"
bsdf = bsdfs["EMISSION"]
attributes["SpecularHighlight"] = None
report(f"BSDF {GREEN}Color{R} saved as {GREEN}DiffuseColour{R}")
diffuse_color = bsdf.inputs["Color"].default_value
# TODO: remove?
elif "BSDF_PRINCIPLED" in bsdfs:
attributes["ReflectanceMethod"] = "NOTDEFINED"
report(f"Because of {BLUE}BSDF_PRINCIPLED{R} node reflectance method identified as {BLUE}PHYSICAL{R}")
attributes["ReflectanceMethod"] = "NOTDEFINED" if tool.Ifc.get_schema() != "IFC4X3" else "PHYSICAL"
bsdf = bsdfs["BSDF_PRINCIPLED"]
attributes["SpecularColour"] = round(bsdf.inputs["Metallic"].default_value, 3)
attributes["SpecularHighlight"] = {"IfcSpecularRoughness": round(bsdf.inputs["Roughness"].default_value, 3)}
report(f"BSDF {GREEN}Base Color{R} saved as {GREEN}DiffuseColour{R}")
diffuse_color = bsdf.inputs["Base Color"].default_value
report(f"BSDF {GREEN}Metallic{R} saved as {GREEN}SpecularColour{R}")
attributes["SpecularColour"] = round(bsdf.inputs["Metallic"].default_value, 3)
report(f"BSDF {GREEN}Roughness{R} saved as {GREEN}IfcSpecularRoughness{R}")
attributes["SpecularHighlight"] = {"IfcSpecularRoughness": round(bsdf.inputs["Roughness"].default_value, 3)}
report(f"BSDF {GREEN}Alpha{R} saved as {GREEN}Transparency{R}")
attributes["Transparency"] = 1 - bsdf.inputs["Alpha"].default_value
else:
report(f"No supported bsdfs found - reflectance method identified as {BLUE}NOTDEFINED{R}")
attributes["ReflectanceMethod"] = "NOTDEFINED"
attributes["SpecularHighlight"] = None
attributes["DiffuseColour"] = attributes["SurfaceColour"]
report(f"{GREEN}Viewport color{R} saved as {GREEN}DiffuseColour{R}")
attributes["DiffuseColour"] = viewport_color
return attributes
attributes["DiffuseColour"] = {
"Name": None,
"Red": diffuse_color[0],
"Green": diffuse_color[1],
"Blue": diffuse_color[2],
}
attributes["DiffuseColour"] = color_to_ifc_format(diffuse_color)
return attributes
@classmethod
def get_surface_rendering_style(cls, obj):
if obj.BIMMaterialProperties.ifc_style_id:
style = tool.Ifc.get().by_id(obj.BIMMaterialProperties.ifc_style_id)
items = [s for s in style.Styles if s.is_a("IfcSurfaceStyleRendering")]
if items:
return items[0]
style_elements = cls.get_style_elements(obj)
return style_elements.get("IfcSurfaceStyleRendering", None)
@classmethod
def get_texture_style(cls, obj):
style_elements = cls.get_style_elements(obj)
return style_elements.get("IfcSurfaceStyleWithTextures", None)
@classmethod
def get_external_style(cls, obj):
style_elements = cls.get_style_elements(obj)
return style_elements.get("IfcExternallyDefinedSurfaceStyle", None)
@classmethod
def get_surface_shading_attributes(cls, obj):
@@ -197,8 +435,20 @@ class Style(blenderbim.core.tool.Style):
@classmethod
def import_surface_attributes(cls, style, obj):
obj.BIMStyleProperties.attributes.clear()
blenderbim.bim.helper.import_attributes2(style, obj.BIMStyleProperties.attributes)
attributes = obj.BIMStyleProperties.attributes
attributes.clear()
blenderbim.bim.helper.import_attributes2(style, attributes)
@classmethod
def import_external_style_attributes(cls, style, obj):
attributes = obj.BIMStyleProperties.external_style_attributes
attributes.clear()
blenderbim.bim.helper.import_attributes2(style, attributes)
@classmethod
def has_blender_external_style(cls, style_elements):
external_style = style_elements.get("IfcExternallyDefinedSurfaceStyle", None)
return bool(external_style and external_style.Location.endswith(".blend"))
@classmethod
def is_editing_styles(cls):
@@ -214,3 +464,7 @@ class Style(blenderbim.core.tool.Style):
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
@classmethod
def change_current_style_type(cls, blender_material, style_type):
blender_material.BIMStyleProperties.active_style_type = style_type
+1 -7
View File
@@ -129,16 +129,10 @@ class System(blenderbim.core.tool.System):
ifc_representation_class=ifc_representation_class,
)
@classmethod
def select_elements(cls, elements):
for element in elements:
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
@classmethod
def select_system_products(cls, system):
cls.select_elements(ifcopenshell.util.system.get_system_elements(system))
tool.Spatial.select_products(ifcopenshell.util.system.get_system_elements(system))
@classmethod
def set_active_system(cls, system):
Binary file not shown.
+18 -13
View File
@@ -72,34 +72,39 @@ restart Blender to see changes).
For Linux or Mac:
::
:: code-block:: console
$ git clone https://github.com/IfcOpenShell/IfcOpenShell.git
$ cd IfcOpenShell
# path to BlenderBIM addon
# default path on Mac: "/Users/$USER/Library/Application Support/Blender/X.X/scripts/addons/blenderbim"
# default path on Linux: "$HOME/.config/blender/X.X/"
$ BLENDER_ADDON_PATH="/path/to/blender/X.XX/scripts/addons/blenderbim"
# Remove the Blender add-on Python code
$ rm -r /path/to/blender/X.XX/scripts/addons/blenderbim/core/
$ rm -r /path/to/blender/X.XX/scripts/addons/blenderbim/tool/
$ rm -r /path/to/blender/X.XX/scripts/addons/blenderbim/bim/
$ rm -r $BLENDER_ADDON_PATH/core/
$ rm -r $BLENDER_ADDON_PATH/tool/
$ rm -r $BLENDER_ADDON_PATH/bim/
# Replace them with links to the Git repository
$ ln -s src/blenderbim/blenderbim/core /path/to/blender/X.XX/scripts/addons/blenderbim/core
$ ln -s src/blenderbim/blenderbim/tool /path/to/blender/X.XX/scripts/addons/blenderbim/tool
$ ln -s src/blenderbim/blenderbim/bim /path/to/blender/X.XX/scripts/addons/blenderbim/bim
$ ln -s $PWD/src/blenderbim/blenderbim/core $BLENDER_ADDON_PATH/core
$ ln -s $PWD/src/blenderbim/blenderbim/tool $BLENDER_ADDON_PATH/tool
$ ln -s $PWD/src/blenderbim/blenderbim/bim $BLENDER_ADDON_PATH/bim
# Remove the IfcOpenShell dependency Python code
$ rm -r /path/to/blender/X.XX/scripts/addons/blenderbim/libs/site/packages/ifcopenshell/api
$ rm -r /path/to/blender/X.XX/scripts/addons/blenderbim/libs/site/packages/ifcopenshell/util
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcopenshell/api
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcopenshell/util
# Replace them with links to the Git repository
$ ln -s src/ifcopenshell-python/ifcopenshell/api /path/to/blender/X.XX/scripts/addons/blenderbim/libs/site/packages/ifcopenshell/api
$ ln -s src/ifcopenshell-python/ifcopenshell/util /path/to/blender/X.XX/scripts/addons/blenderbim/libs/site/packages/ifcopenshell/util
$ ln -s $PWD/src/ifcopenshell-python/ifcopenshell/api $BLENDER_ADDON_PATH/libs/site/packages/ifcopenshell/api
$ ln -s $PWD/src/ifcopenshell-python/ifcopenshell/util $BLENDER_ADDON_PATH/libs/site/packages/ifcopenshell/util
# Manually download some third party dependencies
$ cd /path/to/blender/X.XX/scripts/addons/blenderbim/bim/data/gantt
$ cd $BLENDER_ADDON_PATH/bim/data/gantt
$ wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js
$ wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css
$ cd /path/to/blender/X.XX/scripts/addons/blenderbim/bim/schema
$ cd $BLENDER_ADDON_PATH/bim/schema
$ wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
Or, if you're on Windows, you can use the batch script below.
@@ -33,6 +33,12 @@ Any Blender object that you want to be part of IFC project must be converted
into a IFC object by assigning a category. This category is known as the **IFC
Class**.
.. seealso::
Use the `IFC Class search tool
<https://blenderbim.org/search-ifc-class.html>`__ to help choose an **IFC
Class**!
Select only the default Blender Cube (selected objects are highlighted in
orange, careful not to select anything else!), switch to the **Object
Properties** tab, and find the **IFC Class** panel. Let's pretend our Cube is a
+22 -2
View File
@@ -137,13 +137,21 @@ FAQ
**Unstable installation** section to check that you have installed the
correct version.
2. **I am on Ubuntu and get an error similar to "ImportError: /lib/x86_64-linux-gnu/libm.so.6: version GLIBC_2.29 not found"**
2. **I am on Ubuntu and get an error similar to "ImportError:
/lib/x86_64-linux-gnu/libm.so.6: version GLIBC_2.29 not found"**
Our latest package which uses IfcOpenShell v0.7.0 is built using Ubuntu 20 LTS.
If you have an older Ubuntu version, you can either upgrade to 19.10 or above,
or you'll need to compile IfcOpenShell yourself.
3. **Some other error prevents me from installing or doing basic functions with
3. **I get an error saying "ModuleNotFoundError: No module named 'numpy'"**"
If you have installed Blender from another source instead of from
`Blender.org <https://www.blender.org/download/>`__, such as from your
distro's package repositories, then you may be missing some modules like
``numpy``. Try installing it manually like ``apt install python-numpy``.
4. **Some other error prevents me from installing or doing basic functions with
the add-on. Is it specific to my environment?**
Sometimes it is helpful to try installing and using the BlenderBIM Add-on on
@@ -157,3 +165,15 @@ FAQ
If this fixes your issue, consider disabling other add-ons one by one until
you find a conflict as a next step to isolating the issue.
5. **I get an error similar to RuntimeError: Instance #1234 not found**
Blender saves and loads projects to a ``.blend`` file. However. the
BlenderBIM Add-on works with native IFC, and this means instead of saving
and loading ``.blend`` files, you should instead save and load the ``.ifc``
project.
If you have opened a ``.blend`` file, there is a risk that the contents of
the ``.blend`` session do not correlate to the contents of the ``.ifc``,
which can cause this error. Unless you are an advanced user, only save and
load ``.ifc`` files.
@@ -226,7 +226,7 @@ Scenario: Add classification reference - cost
And I press "bim.add_cost_schedule"
And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()"
And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})"
And I press "bim.add_summary_cost_item(cost_schedule={cost_schedule})"
And I press "bim.add_summary_cost_item()"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
When I press "bim.add_classification_reference(reference={reference}, obj='', obj_type='Cost')"
@@ -241,7 +241,7 @@ Scenario: Remove classification reference - cost
And I press "bim.add_cost_schedule"
And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()"
And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})"
And I press "bim.add_summary_cost_item(cost_schedule={cost_schedule})"
And I press "bim.add_summary_cost_item()"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.add_classification_reference(reference={reference}, obj='', obj_type='Cost')"
@@ -57,9 +57,9 @@ Scenario: Remove drawing
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And the collection "IfcGroup/PLAN_VIEW" exists
And the collection "IfcAnnotation/PLAN_VIEW" exists
When I press "bim.remove_drawing(drawing={drawing})"
Then the collection "IfcGroup/PLAN_VIEW" does not exist
Then the collection "IfcAnnotation/PLAN_VIEW" does not exist
Scenario: Remove drawing - via object deletion
Given an empty IFC project
@@ -70,9 +70,10 @@ Scenario: Remove drawing - via object deletion
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And the collection "IfcAnnotation/PLAN_VIEW" exists
And the object "IfcAnnotation/PLAN_VIEW" is selected
When I press "bim.override_object_delete"
Then the collection "IfcGroup/PLAN_VIEW" does not exist
Then the collection "IfcAnnotation/PLAN_VIEW" does not exist
Scenario: Remove drawing - deleting active drawing
Given an empty IFC project
@@ -83,9 +84,10 @@ Scenario: Remove drawing - deleting active drawing
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And the collection "IfcAnnotation/PLAN_VIEW" exists
And I set "scene.DocProperties.active_drawing_index" to "0"
And I press "bim.activate_drawing(drawing={drawing})"
And the object "IfcAnnotation/PLAN_VIEW" is selected
When I press "bim.override_object_delete"
Then the collection "IfcGroup/PLAN_VIEW" does not exist
Then the collection "IfcAnnotation/PLAN_VIEW" does not exist
@@ -156,26 +156,31 @@ Scenario: Unassign material - material layer set
When I press "bim.unassign_material"
Then nothing happens
Scenario: Unassign material - material layer set usages will not be removed
Scenario: Unassign material - removing inherited material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And the object "IfcWall/Cube" is selected
When the variable "type" is "{ifc}.by_type('IfcWallType')[0].id()"
And I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')"
Then the object "IfcWall/Cube" has a "100" thick layered material containing the material "Default"
When I press "bim.unassign_material"
Then the object "IfcWall/Cube" has a "100" thick layered material containing the material "Default"
Then the object "IfcWall/Cube" has no IFC materials
Scenario: Enable editing assigned material - material layer set
Given an empty IFC project
@@ -435,17 +440,20 @@ Scenario: Remove material set layer
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
And the variable "material_set" is "{ifc}.by_type('IfcMaterialLayerSet')[0].id()"
And I press "bim.remove_layer(layer={ifc}.by_id({material_set}).MaterialLayers[0].id())"
And I press "bim.add_layer(layer_set={material_set})"
When I press "bim.remove_layer(layer={ifc}.by_id({material_set}).MaterialLayers[0].id())"
Then nothing happens
And I press "bim.remove_layer(layer={ifc}.by_id({material_set}).MaterialLayers[0].id())"
Then I press "bim.remove_layer(layer={ifc}.by_id({material_set}).MaterialLayers[0].id())" and expect error "Error: At least one layer must exist"
@@ -393,7 +393,7 @@ Scenario: Export IFC - with basic contents and saving as a relative path
Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')"
When I press "wm.save_mainfile(filepath='{cwd}/test/files/temp/export.blend')"
And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc', use_relative_path=True)"
And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc', use_relative_path=True, save_as_invoked=True)"
Then "scene.BIMProperties.ifc_file" is "export.ifc"
Scenario: Export IFC - with deleted objects synchronised

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