Compare commits

..

1 Commits

Author SHA1 Message Date
Andrej730 9a13e4de5c expose ifc file identity to entity mapping to python api
Can be useful for debugging or identifying entities that were already added to the ifc file. E.g. if entity was added before and adding it again won't update it but with identity map it's possible to know that entity will need to be updated manually.
2024-03-21 17:49:25 +05:00
803 changed files with 19039 additions and 31962 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
- run: echo ok go
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
needs: activate
steps:
- uses: actions/checkout@v2
@@ -80,7 +80,7 @@ jobs:
path: build/assets/Ifc*
deliver:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
needs: build
name: Docker Build, Tag, Push
-48
View File
@@ -1,48 +0,0 @@
name: ci-bsdd-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/bsdd &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/bsdd/dist
-48
View File
@@ -1,48 +0,0 @@
name: ci-ifcclash-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/ifcclash &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcclash/dist
-48
View File
@@ -1,48 +0,0 @@
name: ci-ifccsv-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/ifccsv &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifccsv/dist
+2 -3
View File
@@ -1,7 +1,6 @@
name: ci-ifcopenshell-pypi
on:
workflow_dispatch:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
@@ -36,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312]
pyver: [py36, py37, py38, py39, py310, py311]
config:
- {
name: "Windows Build",
@@ -58,7 +57,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current date
-48
View File
@@ -1,48 +0,0 @@
name: ci-ifcpatch-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/ifcpatch &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcpatch/dist
@@ -1,53 +0,0 @@
name: Publish-ifcsverchok
on:
push:
paths:
- '.github/workflows/ci-ifcsverchok-build.yml'
- 'src/ifcsverchok/*'
branches:
- v0.7.0
env:
major: 0
minor: 0
name: ifcsverchok
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
name: ifcsverchok
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
- run: echo ${{ env.DATE }}
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%y%m%d')"
- name: Compile
run: |
cd src/ifcsverchok
make dist
- name: Upload Zip file to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/ifcsverchok/dist/ifcsverchok-${{steps.date.outputs.date}}.zip
asset_name: ifcsverchok-${{steps.date.outputs.date}}.zip
tag: "ifcsverchok-${{steps.date.outputs.date}}"
overwrite: true
body: "ifcsverchok build for ${{steps.date.outputs.date}}"
+10 -1
View File
@@ -9,6 +9,9 @@ on:
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
push:
paths:
- '.github/workflows/ci-ifctester-pypi.yml'
workflow_dispatch:
env:
@@ -27,6 +30,7 @@ jobs:
build:
needs: activate
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -34,7 +38,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%y%m%d')"
- name: Compile
run: |
pip install build
-3
View File
@@ -81,9 +81,6 @@ src/blenderbim/blenderbim/translations.py
# blenderbim test temp files
src/blenderbim/test/files/temp
src/blenderbim/test/files/basic.ifc.cache.blend
src/blenderbim/test/files/basic.ifc.cache.sqlite
src/blenderbim/drawings
src/blenderbim/layouts
+7 -7
View File
@@ -18,14 +18,14 @@ and many other libraries, CLI apps, and more. Support is also provided for auxil
For more information, see:
* [IfcOpenShell Website](http://ifcopenshell.org)
* [IfcOpenShell Documentation](https://docs.ifcopenshell.org)
* [IfcOpenShell C++ Installation](https://docs.ifcopenshell.org/ifcopenshell/installation.html)
* [IfcOpenShell Python Installation](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
* [IfcOpenShell Python Hello World Tutorial](https://docs.ifcopenshell.org/ifcopenshell-python/hello_world.html)
* [IfcOpenShell Documentation](http://blenderbim.org/docs-python)
* [IfcOpenShell C++ Installation](https://blenderbim.org/docs-python/ifcopenshell/installation.html)
* [IfcOpenShell Python Installation](https://blenderbim.org/docs-python/ifcopenshell-python/installation.html)
* [IfcOpenShell Python Hello World Tutorial](https://blenderbim.org/docs-python/ifcopenshell-python/hello_world.html)
* [BlenderBIM Add-on Website](https://blenderbim.org)
* [BlenderBIM Add-on Documentation](https://docs.blenderbim.org/index.html)
* [Add-on Installation](https://docs.blenderbim.org/users/installation.html)
* [Exploring an IFC model](https://docs.blenderbim.org/users/exploring_an_ifc_model.html)
* [BlenderBIM Add-on Documentation](http://blenderbim.org/docs)
* [Add-on Installation](https://blenderbim.org/docs/users/installation.html)
* [Exploring an IFC model](https://blenderbim.org/docs/users/exploring_an_ifc_model.html)
| Service | Status |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
+1 -1
View File
@@ -12,7 +12,7 @@ RUN apt-get -y update && apt-get -y install unzip curl
# Install AWS Lambda runtime interface client
RUN pip install --target ${FUNCTION_DIR} awslambdaric
# Set the IfcOpenShell build version (check available builds at: https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
# Set the IfcOpenShell build version (check available builds at: https://blenderbim.org/docs-python/ifcopenshell-python/installation.html)
ARG IFC_OPENSHELL_BUILD="39-v0.7.0-476ab50"
# Download and extract IfcOpenShell
+1 -1
View File
@@ -15,7 +15,7 @@
<licenseUrl>https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/COPYING</licenseUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/IfcOpenShell/IfcOpenShell</projectSourceUrl>
<docsUrl>https://docs.blenderbim.org/</docsUrl>
<docsUrl>https://blenderbim.org/docs/</docsUrl>
<!--<mailingListUrl></mailingListUrl>-->
<bugTrackerUrl>https://github.com/IfcOpenShell/IfcOpenShell/issues</bugTrackerUrl>
<tags>blender bim blenderbim ifc python opensource foss</tags>
+5 -9
View File
@@ -137,7 +137,7 @@ endif
cp -r blenderbim/* dist/blenderbim/
# Provides IfcOpenShell Python functionality
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -151,10 +151,8 @@ endif
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/archive/v0.7.0.zip
cd dist/working && unzip v0.7.0.zip
# IfcOpenBot sometimes lags behind, so we hotfix the Python utilities
rm -rf dist/blenderbim/libs/site/packages/ifcopenshell/util/
rm -rf dist/blenderbim/libs/site/packages/ifcopenshell/api/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/util dist/blenderbim/libs/site/packages/ifcopenshell/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api dist/blenderbim/libs/site/packages/ifcopenshell/
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/*.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
@@ -364,9 +362,7 @@ endif
# Provides express rule validation for ifcopenshell.validate
cd dist/working && . env/bin/activate && $(PIP) install pytest --target=./site-packages
# Provides Brickschema functionality
cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages
# Required for SVG to DXF conversion
cd dist/working && . env/bin/activate && $(PIP) install "ezdxf" --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install brickschema[persistence] --target=./site-packages
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
rm -rf dist/working
@@ -434,7 +430,7 @@ endif
# Required by brickschema
# This is a bit of a dodgy one, it should be provided by setuptools which Blender doesn't ship.
mkdir dist/working
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/raw/v0.7.0/src/blenderbim/scripts/pkg_resources.tar.gz
cd dist/working && wget https://blenderbim.org/builds/pkg_resources.tar.gz
cd dist/working && tar -xzvf pkg_resources*
cd dist/working/ && cp -r pkg_resources ../blenderbim/libs/site/packages/
rm -rf dist/working
+7 -147
View File
@@ -18,13 +18,7 @@
import os
import sys
import bpy
import platform
import traceback
import subprocess
import webbrowser
import addon_utils
from collections import deque
import site
bl_info = {
"name": "BlenderBIM",
@@ -33,155 +27,21 @@ bl_info = {
"blender": (3, 1, 0),
"version": (0, 0, 999999),
"location": "File Menu, Scene Properties Tab. See documentation for more.",
"doc_url": "https://docs.blenderbim.org/",
"doc_url": "https://blenderbim.org/docs",
"tracker_url": "https://github.com/IfcOpenShell/IfcOpenShell/issues",
"category": "System",
}
last_error = None
last_actions: deque = deque(maxlen=10)
def get_debug_info():
version = ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
return {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
"architecture": platform.architecture(),
"machine": platform.machine(),
"processor": platform.processor(),
"blender_version": bpy.app.version_string,
"blenderbim_version": version,
"last_actions": last_actions,
"last_error": last_error,
}
def format_debug_info(info: dict):
last_actions = ""
for action in info["last_actions"]:
last_actions += f"\n# {action['type']}: {action['name']}"
if settings := action.get("settings"):
last_actions += f"\n>>> {settings}"
info["last_actions"] = last_actions
text = "\n".join(f"{k}: {v}" for k, v in info.items())
return text.strip()
if sys.modules.get("bpy", None):
# Process *.pth in /libs/site/packages to setup globally importable modules
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
# site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
try:
import blenderbim.bim
import ifcopenshell.api
import blenderbim.bim
def log_api(usecase_path, ifc_file, settings):
last_actions.append(
{
"type": "ifcopenshell.api",
"name": usecase_path,
"settings": ifcopenshell.api.serialise_settings(settings),
}
)
def register():
blenderbim.bim.register()
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
def register():
blenderbim.bim.register()
def unregister():
blenderbim.bim.unregister()
except:
last_error = traceback.format_exc()
print(last_error)
print(format_debug_info(get_debug_info()))
print("\nFATAL ERROR: Unable to load the BlenderBIM Add-on")
class BIM_PT_fatal_error(bpy.types.Panel):
bl_label = "BlenderBIM Fatal Error"
bl_idname = "SCENE_PT_error_message"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
def draw(self, context):
layout = self.layout
layout.label(text="BlenderBIM could not load.", icon="ERROR")
layout.label(text="View the console for full logs.", icon="CONSOLE")
box = layout.box()
info = get_debug_info()
py = ".".join(info["python_version"].split(".")[0:2])
b3d = ".".join(info["blender_version"].split(".")[0:2])
box.label(text=f"Blender {b3d} {info['os']} {info['machine']}", icon="BLENDER")
box.label(text=f"Python {py} BBIM {info['blenderbim_version']}", icon="SCRIPTPLUGINS")
layout.operator("bim.copy_debug_information", text="Copy Error Message To Clipboard")
op = layout.operator("bim.open_uri", text="How Can I Fix This?")
op.uri = "https://docs.blenderbim.org/users/troubleshooting.html#installation-issues"
class OpenUri(bpy.types.Operator):
bl_idname = "bim.open_uri"
bl_label = "Open URI"
uri: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.uri)
return {"FINISHED"}
class CopyDebugInformation(bpy.types.Operator):
bl_idname = "bim.copy_debug_information"
bl_label = "Copy Debug Information"
bl_description = "Copies debugging information to your clipboard for use in bugreports"
def execute(self, context):
info = format_debug_info(get_debug_info())
if platform.system() == "Windows":
command = "echo | set /p nul=" + info
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
return {"FINISHED"}
class HiddenPanel:
@classmethod
def false_poll(cls, context):
return False
def register():
# Only show our error panel and nothing else in the scene tab
for item_name in dir(bpy.types):
item = getattr(bpy.types, item_name)
if not hasattr(item, "bl_rna") or not isinstance(item.bl_rna, bpy.types.Panel):
continue
if getattr(item, "bl_context", None) != "scene":
continue
# Reregister scene panel with a new poll to hide it
item.poll = HiddenPanel.false_poll
bpy.utils.unregister_class(item)
bpy.utils.register_class(item)
bpy.utils.register_class(BIM_PT_fatal_error)
bpy.utils.register_class(CopyDebugInformation)
bpy.utils.register_class(OpenUri)
def unregister():
bpy.utils.unregister_class(OpenUri)
bpy.utils.unregister_class(CopyDebugInformation)
bpy.utils.unregister_class(BIM_PT_fatal_error)
def unregister():
blenderbim.bim.unregister()
+7 -12
View File
@@ -17,13 +17,12 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
from pathlib import Path
import bpy
import bpy.utils.previews
import blenderbim
import importlib
from pathlib import Path
from . import handler, ui, prop, operator, helper
from typing import Callable, Union
try:
from blenderbim.translations import translations_dict
@@ -97,13 +96,10 @@ for name in modules.keys():
classes = [
operator.AddIfcFile,
operator.BIM_OT_add_section_plane,
operator.BIM_OT_delete_object,
operator.BIM_OT_open_webbrowser,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.ClippingPlaneCutWithCappings,
operator.CloseError,
operator.EditBlenderCollection,
operator.FileAssociate,
operator.FileUnassociate,
@@ -134,7 +130,6 @@ classes = [
prop.BIMMeshProperties,
prop.BIMFacet,
prop.BIMFilterGroup,
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.BIM_UL_topics,
ui.BIM_ADDON_preferences,
@@ -192,7 +187,7 @@ addon_keymaps = []
icons = None
is_registering = False
last_commit_hash = "8888888"
original_scene_panels_polls: dict[bpy.types.Panel, Union[Callable, None]] = dict()
overridden_scene_panels = dict()
def on_register(scene):
@@ -300,10 +295,10 @@ def unregister():
km.keymap_items.remove(kmi)
addon_keymaps.clear()
import blenderbim.tool as tool
# use tuple() as method will be removing keys from dict
for panel in tuple(original_scene_panels_polls.keys()):
tool.Blender.remove_scene_panel_override(panel)
for panel in tuple(overridden_scene_panels.keys()):
original_panel, override_panel = overridden_scene_panels[panel]
bpy.utils.unregister_class(override_panel)
bpy.utils.register_class(original_panel)
del overridden_scene_panels[panel]
bpy.app.translations.unregister("blenderbim")
@@ -10,7 +10,7 @@
* (at your option) any later version.
*
* BlenderBIM Add-on is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY, without even the implied warranty of
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
@@ -10,7 +10,7 @@
* (at your option) any later version.
*
* BlenderBIM Add-on is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY, without even the implied warranty of
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -5,20 +5,20 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_An
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/BATTING,IfcTypeProduct',(#6,#7));
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#6,#7));
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/SECTION,IfcTypeProduct',(#9,#10,#11,#12,#13));
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#9,#10,#11,#12,#13));
#9=IFCSIMPLEPROPERTYTEMPLATE('2a_9s8spHDc9dZHtgg71XL',$,'ShowStartArrow','Display start arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3i6SH_GbT7zhKea$wVE56A',$,'StartArrowSymbol','Custom symbol for the start of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1DtsPn5a9FG8$zXHDDMavY',$,'ShowEndArrow','Display end arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('2$6U0mLI9AiPRWeBabdY3u',$,'EndArrowSymbol','Custom symbol for the end of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('1naFqntIL7igCY7hCaE7kq',$,'HasConnectedSectionLine','Connect or disconnect section markers with line (by default = True).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22,#23));
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22,#23));
#15=IFCSIMPLEPROPERTYTEMPLATE('0ulAhgk3v9qfGlDILauR6J',$,'SheetsDir','Default sheets directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#16=IFCSIMPLEPROPERTYTEMPLATE('2yvlVKiQXASucfH40deCvu',$,'LayoutsDir','Default layouts directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#17=IFCSIMPLEPROPERTYTEMPLATE('2kXZqXicL3jRwsOnLM_0ho',$,'TitleblocksDir','Default titleblocks directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -28,7 +28,7 @@ DATA;
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcTypeProduct',(#25,#26,#27,#28));
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#25,#26,#27,#28));
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
+14 -20
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os
import bpy
import json
@@ -36,8 +35,6 @@ import blenderbim.core.spatial
import blenderbim.core.style
from blenderbim.bim.ifc import IfcStore
from mathutils import Vector
from typing import Union
from logging import Logger
class IfcExporter:
@@ -89,8 +86,8 @@ class IfcExporter:
self.get_application_name(), tool.Blender.get_blenderbim_version()
)
def sync_all_objects(self, skip_unlinking=False) -> list[ifcopenshell.entity_instance]:
results: list[ifcopenshell.entity_instance] = []
def sync_all_objects(self):
results = []
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
for ifc_definition_id in list(IfcStore.id_map.keys()):
obj = IfcStore.id_map[ifc_definition_id]
@@ -99,21 +96,19 @@ class IfcExporter:
continue
if obj.library:
continue
tool.Collector.sync(obj, skip_unlinking)
tool.Collector.sync(obj)
result = self.sync_object_placement(obj)
if result:
results.append(result)
result = self.sync_object_material(obj)
# TODO: sync_object_material always returns None
# so it's never really appended
if result:
results.append(result)
except ReferenceError:
pass # The object is likely deleted
return results
def sync_edited_objects(self) -> list[ifcopenshell.entity_instance]:
results: list[ifcopenshell.entity_instance] = []
def sync_edited_objects(self):
results = []
for obj in IfcStore.edited_objs.copy():
if not obj:
continue
@@ -131,7 +126,7 @@ class IfcExporter:
IfcStore.edited_objs.clear()
return results
def sync_object_material(self, obj: bpy.types.Object) -> None:
def sync_object_material(self, obj):
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
return
if not self.has_changed_materials(obj):
@@ -142,10 +137,11 @@ class IfcExporter:
checksum = obj.data.BIMMeshProperties.material_checksum
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
def sync_object_placement(self, obj):
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if not tool.Ifc.is_moved(obj):
return
blender_matrix = np.array(obj.matrix_world)
if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4:
bpy.ops.bim.update_representation(obj=obj.name)
return element
@@ -156,7 +152,7 @@ class IfcExporter:
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
return element
def sync_grid_axis_object_placement(self, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
def sync_grid_axis_object_placement(self, obj, element):
grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
grid_obj = tool.Ifc.get_object(grid)
if grid_obj:
@@ -165,10 +161,10 @@ class IfcExporter:
bpy.ops.bim.update_representation(obj=obj.name)
tool.Geometry.record_object_position(obj)
def get_application_name(self) -> str:
def get_application_name(self):
return "BlenderBIM"
def get_application_version(self) -> str:
def get_application_version(self):
version = ".".join(
[
str(x)
@@ -186,13 +182,11 @@ class IfcExporter:
class IfcExportSettings:
def __init__(self):
self.logger: Logger = None
self.output_file: str = None
self.json_version: str = None
self.json_compact: bool = None
self.logger = None
self.output_file = None
@staticmethod
def factory(context: bpy.types.Context, output_file: str, logger: Logger) -> IfcExportSettings:
def factory(context, output_file, logger):
settings = IfcExportSettings()
settings.output_file = output_file
settings.logger = logger
+68 -26
View File
@@ -31,7 +31,6 @@ from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP
from mathutils import Vector
from math import cos, degrees
from typing import Union
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -192,8 +191,6 @@ def refresh_ui_data():
if isinstance(tool.Ifc.get(), ifcopenshell.sqlite):
tool.Ifc.get().clear_cache()
bpy.context.scene.DocProperties.should_draw_decorations = bpy.context.scene.DocProperties.should_draw_decorations
@persistent
def loadIfcStore(scene):
@@ -223,7 +220,7 @@ def redo_post(scene):
tool.Ifc.rebuild_element_maps()
def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
def get_application(ifc):
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
version = get_application_version()
for element in ifc.by_type("IfcApplication"):
@@ -238,20 +235,7 @@ def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
)
def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
if pao := next(iter(ifc.by_type("IfcPersonAndOrganization")), None):
return pao
elif ifc.schema == "IFC2X3":
if (person := next(iter(ifc.by_type("IfcPerson")), None)) is None:
person = tool.Ifc.run("owner.add_person")
if (organization := next(iter(ifc.by_type("IfcOrganization")), None)) is None:
organization = tool.Ifc.run("owner.add_organisation")
pao = tool.Ifc.run("owner.add_person_and_organisation", person=person, organisation=organization)
return pao
def get_application_version() -> str:
def get_application_version():
return ".".join(
[
str(x)
@@ -270,6 +254,60 @@ def viewport_shading_changed_callback(area):
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
@classmethod
def poll_check_blender_tab(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
def get_override_scene_panel(panel_name):
original_panel = getattr(bpy.types, panel_name)
override_panel = type(f"Override_{panel_name}", (original_panel,), {})
override_panel.bl_idname = f"{panel_name}_override"
# some blender panel depend on other and register_class will throw an error
# if we won't override their `bl_parent_id`
bl_parent_id = getattr(override_panel, "bl_parent_id", None)
if bl_parent_id is not None:
override_panel.bl_parent_id = f"{bl_parent_id}_override"
# override poll method
poll = getattr(override_panel, "poll", None)
if poll is None:
override_panel.poll = poll_check_blender_tab
else:
@classmethod
def wrapped_poll(cls, context):
return super(override_panel, cls).poll(context) and poll_check_blender_tab.__func__(cls, context)
override_panel.poll = wrapped_poll
return override_panel
# TODO: possibly override scene panels from other addons too?
# list can be updated from
# https://projects.blender.org/blender/blender/src/branch/main/scripts/startup/bl_ui/properties_scene.py#L421
OVERRIDE_SCENE_PANELS = (
"SCENE_PT_scene",
"SCENE_PT_unit",
"SCENE_PT_physics",
"SCENE_PT_rigid_body_world",
"SCENE_PT_rigid_body_world_settings",
"SCENE_PT_rigid_body_cache",
"SCENE_PT_rigid_body_field_weights",
"SCENE_PT_audio",
"SCENE_PT_keying_sets",
"SCENE_PT_custom_props",
# after SCENE_PT_keying_sets
"SCENE_PT_keying_set_paths",
"SCENE_PT_keyframing_settings",
)
if bpy.app.version >= (4, 0):
OVERRIDE_SCENE_PANELS += ("SCENE_PT_simulation",)
@persistent
def load_post(scene):
global global_subscription_owner
@@ -292,7 +330,7 @@ def load_post(scene):
key=key, owner=global_subscription_owner, args=(area,), notify=viewport_shading_changed_callback
)
ifcopenshell.api.owner.settings.get_user = get_user
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 = {}
@@ -305,11 +343,15 @@ def load_post(scene):
else:
bpy.ops.workspace.append_activate(idname="BIM", filepath=os.path.join(cwd, "data", "workspace.blend"))
# To improve usability for new users, we hijack the scene properties
# tab. We override default scene properties panels with our own poll
# to hide them unless the user has chosen to view Blender properties.
for panel in tool.Blender.get_scene_panels_list():
if panel in blenderbim.bim.original_scene_panels_polls:
continue
tool.Blender.override_scene_panel(panel)
# To improve usability for new users, we hijack the scene properties
# tab. We override default scene properties panels with our own poll
# to hide them unless the user has chosen to view Blender properties.
for panel in OVERRIDE_SCENE_PANELS:
if panel in blenderbim.bim.overridden_scene_panels:
continue
override_panel = get_override_scene_panel(panel)
original_panel = getattr(bpy.types, panel)
bpy.utils.register_class(override_panel)
bpy.utils.unregister_class(original_panel)
blenderbim.bim.overridden_scene_panels[panel] = (original_panel, override_panel)
tool.Blender.setup_tabs()
+40 -10
View File
@@ -29,7 +29,6 @@ from mathutils import geometry
from mathutils import Vector
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from typing import Optional, Callable, Any
def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None):
@@ -76,10 +75,6 @@ def draw_attribute(attribute, layout, copy_operator=None):
op.data_path = attribute.path_from_id("string_value")
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if attribute.name == "GlobalId":
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
if copy_operator:
op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN")
op.name = attribute.name
@@ -121,7 +116,7 @@ def import_attribute(attribute, props, data, callback=None):
elif is_handled_by_callback is False:
props.remove(len(props) - 1)
elif data_type == "string":
new.string_value = "" if new.is_null else str(data[attribute.name()]).replace("\n", "\\n")
new.string_value = "" if new.is_null else str(data[attribute.name()])
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
new.is_uri = True
elif data_type == "boolean":
@@ -183,7 +178,7 @@ def add_attribute_description(attribute_blender, attribute_ifc=None):
attribute_blender.description = description
def export_attributes(props, callback: Optional[Callable] = None) -> dict[str, Any]:
def export_attributes(props, callback=None):
attributes = {}
for prop in props:
is_handled_by_callback = callback(attributes, prop) if callback else False
@@ -221,6 +216,39 @@ def get_enum_items(data, prop_name, context=None):
return items
def get_obj_ifc_definition_id(context, obj, obj_type):
if obj_type == "Object":
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "Material":
return bpy.data.materials.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "MaterialSet":
return ifcopenshell.util.element.get_material(
tool.Ifc.get_entity(bpy.data.objects.get(obj)), should_skip_usage=True
).id()
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
elif obj_type == "Task":
return context.scene.BIMTaskTreeProperties.tasks[
context.scene.BIMWorkScheduleProperties.active_task_index
].ifc_definition_id
elif obj_type == "Cost":
return context.scene.BIMCostProperties.cost_items[
context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
elif obj_type == "Resource":
return context.scene.BIMResourceTreeProperties.resources[
context.scene.BIMResourceProperties.active_resource_index
].ifc_definition_id
elif obj_type == "Profile":
return context.scene.BIMProfileProperties.profiles[
context.scene.BIMProfileProperties.active_profile_index
].ifc_definition_id
elif obj_type == "WorkSchedule":
return context.scene.BIMWorkScheduleProperties.active_work_schedule_id
elif obj_type == "Group":
prop = context.scene.BIMGroupProperties
return prop.groups[prop.active_group_index].ifc_definition_id
# hack to close popup
# https://blender.stackexchange.com/a/202576/130742
def close_operator_panel(event):
@@ -276,9 +304,6 @@ def draw_filter(layout, filter_groups, data, module):
op.type = sprops.facet
op.index = i
op.module = module
op = row.operator("bim.remove_filter_group", text="", icon="X")
op.index = i
op.module = module
for j, ifc_filter in enumerate(filter_group.filters):
if ifc_filter.type == "entity":
@@ -325,6 +350,11 @@ def draw_filter(layout, filter_groups, data, module):
op.index = j
op.module = module
row = box.row()
op = row.operator("bim.remove_filter_group", icon="X")
op.index = i
op.module = module
# TODO this should move into ifcopenshell.util
+12 -26
View File
@@ -19,15 +19,12 @@
import os
import bpy
import uuid
import shutil
import hashlib
import zipfile
import tempfile
import traceback
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper
import blenderbim
import blenderbim.bim.handler
import blenderbim.tool as tool
from pathlib import Path
@@ -40,19 +37,19 @@ IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
class IfcStore:
path: str = ""
file: Optional[ifcopenshell.file] = None
schema: Optional[ifcopenshell.ifcopenshell_wrapper.schema_definition] = None
cache: Optional[ifcopenshell.ifcopenshell_wrapper.HdfSerializer] = None
cache_path: Optional[str] = None
file: ifcopenshell.file = None
schema: ifcopenshell.ifcopenshell_wrapper.schema_definition = None
cache: ifcopenshell.ifcopenshell_wrapper.HdfSerializer = None
cache_path: str = None
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
edited_objs: Set[bpy.types.Object] = set()
pset_template_path: str = ""
pset_template_file: Optional[ifcopenshell.file] = None
pset_template_file: ifcopenshell.file = None
classification_path: str = ""
classification_file: Optional[ifcopenshell.file] = None
classification_file: ifcopenshell.file = None
library_path: str = ""
library_file: Optional[ifcopenshell.file] = None
library_file: ifcopenshell.file = None
current_transaction = ""
last_transaction = ""
history = []
@@ -122,13 +119,7 @@ class IfcStore:
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
IfcStore.cache = None
try:
shutil.move(IfcStore.cache_path, new_cache_path)
except PermissionError:
try:
shutil.copy2(IfcStore.cache_path, new_cache_path)
except PermissionError:
pass # Well we tried. No cache for you!
os.replace(IfcStore.cache_path, new_cache_path)
IfcStore.get_cache()
@staticmethod
@@ -338,7 +329,6 @@ class IfcStore:
@staticmethod
def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False):
blenderbim.last_actions.append({"type": "operator", "name": operator.bl_idname})
bpy.context.scene.BIMProperties.is_dirty = True
is_top_level_operator = not bool(IfcStore.current_transaction)
@@ -353,14 +343,10 @@ class IfcStore:
else:
operator.transaction_key = IfcStore.current_transaction
try:
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
result = getattr(operator, "_execute")(context)
except:
blenderbim.last_error = traceback.format_exc()
raise
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
result = getattr(operator, "_execute")(context)
if is_top_level_operator:
if tool.Ifc.get():
+38 -70
View File
@@ -34,7 +34,6 @@ import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
import blenderbim.tool as tool
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from itertools import chain, accumulate
@@ -200,7 +199,7 @@ class MaterialCreator:
class IfcImporter:
def __init__(self, ifc_import_settings: IfcImportSettings):
def __init__(self, ifc_import_settings):
self.ifc_import_settings = ifc_import_settings
self.diff = None
self.file: ifcopenshell.file = None
@@ -299,7 +298,6 @@ class IfcImporter:
if self.ifc_import_settings.should_setup_viewport_camera:
self.setup_viewport_camera()
self.setup_arrays()
self.profile_code("Setup arrays")
self.update_progress(100)
bpy.context.window_manager.progress_end()
@@ -432,7 +430,7 @@ class IfcImporter:
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
self.annotations -= drawing_annotations
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
if self.ifc_import_settings.is_coordinating:
self.elements = [e for e in self.elements if e.Representation]
@@ -603,9 +601,6 @@ class IfcImporter:
return products
def predict_dense_mesh(self):
if self.ifc_import_settings.should_use_native_meshes:
return
threshold = 10000 # Just from experience.
faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")]
@@ -702,17 +697,15 @@ class IfcImporter:
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
point = mat @ np.array(
point = np.array(
(
shape.geometry.verts[0],
shape.geometry.verts[1],
shape.geometry.verts[2],
shape.geometry.verts[0] / self.unit_scale,
shape.geometry.verts[1] / self.unit_scale,
shape.geometry.verts[2] / self.unit_scale,
0.0,
)
)
point = point / self.unit_scale
if self.is_point_far_away(point, is_meters=False):
return point
return mat @ point
def does_element_likely_have_geometry_far_away(self, element):
for representation in element.Representation.Representations:
@@ -732,7 +725,7 @@ class IfcImporter:
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
return True
def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix:
def apply_blender_offset_to_matrix_world(self, obj, matrix):
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
if obj.data and obj.data.get("has_cartesian_point_offset", None):
@@ -864,10 +857,7 @@ class IfcImporter:
print("Done creating geometry")
def create_spatial_elements(self) -> None:
if bpy.context.preferences.addons["blenderbim"].preferences.spatial_elements_unselectable:
self.create_generic_elements(self.spatial_elements, unselectable=True)
else:
self.create_generic_elements(self.spatial_elements, unselectable=False)
self.create_generic_elements(self.spatial_elements, unselectable=True)
def create_elements(self) -> None:
self.create_generic_elements(self.elements)
@@ -959,9 +949,7 @@ class IfcImporter:
self.create_product(element, mesh=mesh)
def create_products(
self,
products: set[ifcopenshell.entity_instance],
settings: Optional[ifcopenshell.geom.main.settings] = None,
self, products, settings: Optional[ifcopenshell.geom.main.settings] = None
) -> set[ifcopenshell.entity_instance]:
results = set()
if not products:
@@ -1176,9 +1164,6 @@ class IfcImporter:
return obj
def load_existing_meshes(self) -> None:
self.meshes.update({m.name: m for m in bpy.data.meshes})
def get_representation_item_material_name(self, item):
if not item.StyledByItem:
return
@@ -1500,10 +1485,7 @@ class IfcImporter:
# Occurs when reloading a project
pass
project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name]
types_collection = project_collection.children[self.type_collection.name]
types_collection.hide_viewport = False
for obj in types_collection.collection.objects: # turn off all objects inside Types collection.
obj.hide_set(True)
project_collection.children[self.type_collection.name].hide_viewport = True
def clean_mesh(self):
obj = None
@@ -1638,34 +1620,30 @@ class IfcImporter:
if self.ifc_import_settings.has_filter:
rel_aggregates = set()
for element in self.elements:
if decomposed_by := element.IsDecomposedBy:
rel_aggregates.add(decomposed_by[0])
elif decomposes := element.Decomposes:
rel_aggregates.add(decomposes[0])
elif nested_by := getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
if next((e for e in nested_by[0].RelatedObjects if not e.is_a("IfcPort")), None):
rel_aggregates.add(nested_by[0])
elif nests := getattr(element, "Nests", []):
rel_aggregates.add(nests[0])
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
rel_aggregates.add(element.AdheresToElement[0])
if element.IsDecomposedBy:
rel_aggregates.add(element.IsDecomposedBy[0])
elif element.Decomposes:
rel_aggregates.add(element.Decomposes[0])
elif getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
if [e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")]:
rel_aggregates.add(element.IsNestedBy[0])
elif getattr(element, "Nests", []):
rel_aggregates.add(element.Nests[0])
else:
rel_aggregates = [
r
for r in self.file.by_type("IfcRelAggregates")
if (relating_obj := r.RelatingObject).is_a("IfcElement") or relating_obj.is_a("IfcElementType")
if r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType")
] + [
r
for r in self.file.by_type("IfcRelNests")
if (
(relating_obj := r.RelatingObject).is_a("IfcElement")
or relating_obj.is_a("IfcElementType")
or (relating_obj.is_a("IfcPositioningElement") and not relating_obj.is_a("IfcGrid"))
r.RelatingObject.is_a("IfcElement")
or r.RelatingObject.is_a("IfcElementType")
or (r.RelatingObject.is_a("IfcPositioningElement") and not r.RelatingObject.is_a("IfcGrid"))
)
and [e for e in r.RelatedObjects if not e.is_a("IfcPort")]
]
if self.file.schema == "IFC4X3":
rel_aggregates += [r for r in self.file.by_type("IfcRelAdheresToElement")]
if len(rel_aggregates) > 10000:
# More than 10,000 collections makes Blender unhappy
@@ -1675,9 +1653,7 @@ class IfcImporter:
aggregates: dict[str, dict] = {}
for rel_aggregate in rel_aggregates:
element: ifcopenshell.entity_instance = getattr(rel_aggregate, "RelatingObject", None) or getattr(
rel_aggregate, "RelatingElement"
)
element: ifcopenshell.entity_instance = rel_aggregate.RelatingObject
collection = bpy.data.collections.new(tool.Loader.get_name(element))
aggregates[element.GlobalId] = {"element": element, "collection": collection}
self.collections[element.GlobalId] = collection
@@ -1778,9 +1754,6 @@ class IfcImporter:
elif getattr(element, "Nests", None) and not element.is_a("IfcPort"):
nest = ifcopenshell.util.element.get_nest(element)
return self.collections[nest.GlobalId].objects.link(obj)
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
adherend = element.AdheresToElement[0].RelatingElement
return self.collections[adherend.GlobalId].objects.link(obj)
return self.place_object_in_spatial_decomposition_collection(element, obj)
@@ -1830,7 +1803,7 @@ class IfcImporter:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
return rel.RelatingGroup
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.ndarray:
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.array:
if isinstance(element, ifcopenshell.sqlite_entity):
result = self.geometry_cache["shapes"][element.id()]["matrix"]
else:
@@ -1913,7 +1886,11 @@ class IfcImporter:
and geometry.verts
and self.is_point_far_away((geometry.verts[0], geometry.verts[1], geometry.verts[2]))
):
offset_point = np.array(
m = shape.transformation.matrix.data
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
offset_point = np.linalg.inv(mat) @ np.array(
(
float(props.blender_eastings),
float(props.blender_northings),
@@ -1921,15 +1898,8 @@ class IfcImporter:
0.0,
)
)
if geometry != shape:
m = shape.transformation.matrix.data
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
offset_point = np.linalg.inv(mat) @ offset_point
verts = [None] * len(geometry.verts)
for i in range(0, len(geometry.verts), 3):
# Note: this enh2xyz call is crazy slow.
verts[i], verts[i + 1], verts[i + 2] = ifcopenshell.util.geolocation.enh2xyz(
geometry.verts[i],
geometry.verts[i + 1],
@@ -1955,9 +1925,7 @@ class IfcImporter:
# See bug 3546
# ios_edges holds true edges that aren't triangulated.
#
# we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry)))
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
@@ -1984,14 +1952,14 @@ class IfcImporter:
print(traceback.format_exc())
def a2p(self, o: mathutils.Vector, z: mathutils.Vector, x: mathutils.Vector) -> mathutils.Matrix:
def a2p(self, o, z, x):
y = z.cross(x)
r = mathutils.Matrix((x, y, z, o))
r.resize_4x4()
r.transpose()
return r
def get_axis2placement(self, plc: ifcopenshell.entity_instance) -> mathutils.Matrix:
def get_axis2placement(self, plc):
if plc.is_a("IfcAxis2Placement3D"):
z = mathutils.Vector(plc.Axis.DirectionRatios if plc.Axis else (0, 0, 1))
x = mathutils.Vector(plc.RefDirection.DirectionRatios if plc.RefDirection else (1, 0, 0))
@@ -2011,7 +1979,7 @@ class IfcImporter:
o = plc.LocalOrigin.Coordinates
return self.a2p(o, z, x)
def get_local_placement(self, plc: Optional[ifcopenshell.entity_instance] = None) -> mathutils.Matrix:
def get_local_placement(self, plc):
if plc is None:
return mathutils.Matrix()
if plc.PlacementRelTo is None:
@@ -2026,11 +1994,11 @@ class IfcImporter:
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
break
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
def link_element(self, element, obj):
self.added_data[element.id()] = obj
tool.Ifc.link(element, obj)
def set_matrix_world(self, obj: bpy.types.Object, matrix_world: mathutils.Matrix) -> None:
def set_matrix_world(self, obj, matrix_world):
obj.matrix_world = matrix_world
tool.Geometry.record_object_position(obj)
@@ -2053,7 +2021,7 @@ class IfcImporter:
class IfcImportSettings:
def __init__(self):
self.logger: logging.Logger = None
self.logger = None
self.input_file = None
self.diff_file = None
self.should_use_cpu_multiprocessing = True
@@ -94,15 +94,14 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
relating_obj=tool.Ifc.get_object(aggregate),
related_obj=tool.Ifc.get_object(element),
)
# Removes Pset related to Linked Aggregates
if not element.is_a('IfcElementAssembly'):
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
"""Enable editing aggregation relationship"""
@@ -133,7 +132,6 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -141,15 +139,13 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
def draw(self, context):
row = self.layout
row.prop(self, "ifc_class")
row = self.layout
row.prop(self, "aggregate_name")
def _execute(self, context):
try:
ifc_class = tool.Ifc.schema().declaration_by_name(self.ifc_class).name()
except:
return
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
aggregate = self.create_aggregate(context, ifc_class)
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -177,8 +173,8 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
def create_aggregate(self, context, ifc_class, aggregate_name):
aggregate = bpy.data.objects.new(aggregate_name, None)
def create_aggregate(self, context, ifc_class):
aggregate = bpy.data.objects.new("Assembly", None)
aggregate.location = context.scene.cursor.location
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
return aggregate
@@ -194,17 +190,12 @@ class BIM_OT_select_parts(bpy.types.Operator):
def execute(self, context):
self.file = IfcStore.get_file()
# obj = bpy.data.objects.get(self.obj) or context.active_object
for obj in context.selected_objects:
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj))
if parts:
parts_objs = set(tool.Ifc.get_object(part) for part in parts)
selectable_parts_objs = set(context.selectable_objects).intersection(parts_objs)
for selectable_part_obj in selectable_parts_objs:
selectable_part_obj.select_set(True)
else:
obj.select_set(False)
obj = bpy.data.objects.get(self.obj) or context.active_object
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj))
parts_objs = set(tool.Ifc.get_object(part) for part in parts)
selectable_parts_objs = set(context.selectable_objects).intersection(parts_objs)
for selectable_part_obj in selectable_parts_objs:
selectable_part_obj.select_set(True)
return {"FINISHED"}
@@ -215,54 +206,15 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
bl_label = "Select Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
select_parts: bpy.props.BoolProperty(default=False)
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select Aggregate and Parts"
else:
return "Select Aggregate"
def execute(self, context):
self.file = IfcStore.get_file()
# obj = bpy.data.objects.get(self.obj) or context.active_object
# aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj))
# aggregate_obj = tool.Ifc.get_object(aggregate)
all_parts = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element:
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
all_parts.append(aggregate)
obj.select_set(False)
else:
pass
if not element:
obj.select_set(False)
if self.select_parts:
all_objs = []
for part in all_parts:
if part.IsDecomposedBy:
for subpart in part.IsDecomposedBy[0].RelatedObjects:
all_parts.append(subpart)
all_objs.append(part)
for element in all_objs:
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
else:
for aggregate_element in all_parts:
aggregate_obj = tool.Ifc.get_object(aggregate_element)
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
obj = bpy.data.objects.get(self.obj) or context.active_object
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj))
aggregate_obj = tool.Ifc.get_object(aggregate)
if aggregate_obj in context.selectable_objects:
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
return {"FINISHED"}
@@ -290,7 +242,6 @@ class BIM_OT_add_part_to_object(bpy.types.Operator, Operator):
part_name=self.part_name,
)
class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
bl_idname = "bim.break_link_to_other_aggregates"
bl_label = "Break link to other aggregates"
@@ -305,72 +256,48 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
return []
parts = ifcopenshell.util.element.get_parts(aggregate)
for part in parts:
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Linked_Aggregate")
pset = ifcopenshell.util.element.get_pset(part, 'BBIM_Linked_Aggregate')
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[aggregate])
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], product=aggregate)
return {"FINISHED"}
class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator):
bl_idname = "bim.select_linked_aggregates"
bl_label = "Select linked aggregates"
bl_options = {"REGISTER", "UNDO"}
select_parts: bpy.props.BoolProperty(default=False)
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select all aggregates, subaggregates and all their parts"
else:
return "Select all aggregates"
def _execute(self, context):
element = tool.Ifc.get_entity(bpy.context.active_object)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
return []
if not element:
return []
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
for obj in context.selected_objects:
obj.select_set(False)
element = tool.Ifc.get_entity(obj)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
continue
if not element:
continue
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
group_rel = linked_aggregate_group[0].IsGroupedBy or []
for group_link in group_rel:
parts = list(group_link.RelatedObjects)
if self.select_parts:
parts_objs = []
for part in parts:
if part.IsDecomposedBy:
for subpart in part.IsDecomposedBy[0].RelatedObjects:
parts.append(subpart)
parts_objs.append(part)
for element in parts_objs:
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
else:
for element in parts:
obj = tool.Ifc.get_object(element)
obj.select_set(True)
for rel in linked_aggregate_group[0].IsGroupedBy or []:
for element in rel.RelatedObjects:
obj = tool.Ifc.get_object(element)
obj.select_set(True)
return {"FINISHED"}
@@ -20,7 +20,6 @@ from bpy.types import Panel
from blenderbim.bim.module.aggregate.data import AggregateData
from blenderbim.bim.module.group.data import GroupsData, ObjectGroupsData
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
class BIM_PT_aggregate(Panel):
@@ -63,17 +62,13 @@ class BIM_PT_aggregate(Panel):
row = layout.row(align=True)
if AggregateData.data["has_relating_object"]:
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="")
op.obj = context.active_object.name
op.select_parts = False
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
op.obj = context.active_object.name
op.select_parts = True
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
op = row.operator("bim.aggregate_unassign_object", icon="X", text="")
else:
row.label(text="No Whole relation defined", icon="TRIA_UP")
row.label(text="No Aggregate", icon="TRIA_UP")
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
@@ -132,29 +127,19 @@ class BIM_PT_linked_aggregate(Panel):
if not AggregateData.is_loaded:
AggregateData.load()
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMObjectAggregateProperties
props = context.active_object.BIMObjectAggregateProperties
row = layout.row(align=True)
if element.Decomposes:
Number_Linked_Aggregates = AggregateData.data['total_linked_aggregate']
if not Number_Linked_Aggregates:
row.label(text="Not a Linked Aggregate")
else:
row.label(text=f"{Number_Linked_Aggregates} Linked Aggregates")
op = row.operator("bim.object_duplicate_move_linked_aggregate", text="", icon="DUPLICATE")
if type(Number_Linked_Aggregates) is int:
if Number_Linked_Aggregates > 0:
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD")
op.select_parts = False
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_OB_POINTCLOUD")
op.select_parts = True
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
row.label(text="Advanced Users Only", icon="ERROR")
row = layout.row(align=True)
if type(AggregateData.data['total_linked_aggregate']) is int:
if AggregateData.data['total_linked_aggregate'] > 0:
row.label(text=f"{AggregateData.data['total_linked_aggregate']} Linked Aggregates")
row.operator("bim.select_linked_aggregates", text="", icon="RESTRICT_SELECT_OFF")
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
else:
row.label(text="Not an Aggregate")
row.label(text="No Linked Aggregates")
@@ -20,7 +20,6 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
@@ -48,18 +47,10 @@ class EnableEditingAttributes(bpy.types.Operator):
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
oprops = obj.BIMObjectProperties
props = obj.BIMAttributeProperties
props.attributes.clear()
element = tool.Ifc.get_entity(obj)
has_inherited_predefined_type = False
if not element.is_a("IfcTypeObject") and (element_type := ifcopenshell.util.element.get_type(element)):
# Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
has_inherited_predefined_type = ifcopenshell.util.element.get_predefined_type(element_type) not in (
"NOTDEFINED",
None,
)
def callback(name, prop, data):
if name in ("RefLatitude", "RefLongitude"):
new = props.attributes.add()
@@ -71,11 +62,10 @@ class EnableEditingAttributes(bpy.types.Operator):
new.string_value = "" if new.is_null else json.dumps(data[name])
blenderbim.bim.helper.add_attribute_description(new)
new.description += " The degrees, minutes and seconds should follow this format : [12,34,56]"
if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type:
props.attributes.remove(len(props.attributes) - 1)
return True
blenderbim.bim.helper.import_attributes2(element, props.attributes, callback=callback)
blenderbim.bim.helper.import_attributes2(
tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes, callback=callback
)
props.is_editing_attributes = True
return {"FINISHED"}
@@ -130,46 +120,20 @@ class EditAttributes(bpy.types.Operator, Operator):
return {"FINISHED"}
class GenerateGlobalId(bpy.types.Operator, Operator):
class GenerateGlobalId(bpy.types.Operator):
bl_idname = "bim.generate_global_id"
bl_label = "Regenerate GlobalId"
bl_description = "Regenerate GlobalId\n\nSHIFT+CLICK to regenerate GlobalIds for all selected objects"
bl_options = {"REGISTER", "UNDO"}
use_selected: bpy.props.BoolProperty(name="Use All Selected Objects", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# using all selected objects on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.use_selected = True
return self.execute(context)
def _execute(self, context):
if self.use_selected:
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcRoot"):
continue
element.GlobalId = ifcopenshell.guid.new()
obj = context.active_object
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
return {"FINISHED"}
props = obj.BIMAttributeProperties
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRoot"):
return {"FINISHED"}
if self.use_selected and obj in context.selected_objects:
# guid value was already regenerated, just update the ui prop
guid_value = element.GlobalId
def execute(self, context):
index = context.active_object.BIMAttributeProperties.attributes.find("GlobalId")
if index >= 0:
global_id = context.active_object.BIMAttributeProperties.attributes[index]
else:
guid_value = ifcopenshell.guid.new()
props.attributes["GlobalId"].string_value = guid_value
global_id = context.active_object.BIMAttributeProperties.attributes.add()
global_id.name = "GlobalId"
global_id.data_type = "string"
global_id.string_value = ifcopenshell.guid.new()
return {"FINISHED"}
@@ -20,7 +20,6 @@ import bpy
import bmesh
import logging
import shapely
import shapely.ops
import mathutils
import numpy as np
import multiprocessing
@@ -24,10 +24,10 @@ from math import pi
class BIMCadProperties(PropertyGroup):
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE")
y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE")
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
x: bpy.props.FloatProperty(name="X", default=0.2)
y: bpy.props.FloatProperty(name="Y", default=0.1)
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
@@ -20,7 +20,6 @@ import os
import bpy
import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
import ifcopenshell.util.unit
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
@@ -240,9 +239,8 @@ class CadHotkey(bpy.types.Operator):
row.prop(props, "resolution")
def hotkey_S_C(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
else:
bpy.ops.bim.cad_arc_from_2_points()
@@ -250,15 +248,13 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
else:
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
def hotkey_S_O(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
bpy.ops.bim.cad_offset(distance=self.props.distance)
def hotkey_S_Q(self):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -274,8 +270,7 @@ class CadHotkey(bpy.types.Operator):
def hotkey_S_R(self):
if self.is_profile():
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
@@ -25,12 +25,15 @@ classes = (
operator.ExecuteIfcClash,
operator.ExportClashSets,
operator.ImportClashSets,
operator.LoadIfcClashes,
operator.SaveIfcClashes,
operator.LoadSmartGroupsForActiveClashSet,
operator.RemoveClashSet,
operator.RemoveClashSource,
operator.SelectClash,
operator.SelectClashResults,
operator.SelectClashSource,
operator.SelectIfcClashResults,
operator.SelectSmartGroup,
operator.SelectSmartGroupedClashesPath,
operator.SmartClashGroup,
@@ -41,6 +44,7 @@ classes = (
prop.BIMClashProperties,
ui.BIM_PT_ifcclash,
ui.BIM_PT_clash_manager,
ui.BIM_PT_dumb_clash_manager,
ui.BIM_PT_smart_clash_manager,
ui.BIM_UL_clashes,
ui.BIM_UL_clash_sets,
@@ -1,52 +0,0 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import ifcopenshell.util.element
import blenderbim.tool as tool
def refresh():
ClashData.is_loaded = False
class ClashData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {}
cls.data["saved_searches"] = cls.saved_searches()
@classmethod
def saved_searches(cls):
if not tool.Ifc.get():
return []
groups = tool.Ifc.get().by_type("IfcGroup")
results = []
for group in groups:
try:
data = json.loads(group.Description)
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
results.append(group)
except:
pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
@@ -1,102 +0,0 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import blf
import gpu
import bmesh
import blenderbim.tool as tool
from bpy.types import SpaceView3D
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d
class ClashDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_text(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
text = context.scene.BIMClashProperties.active_clash_text
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
font_id = 0
blf.size(font_id, 12)
coords_2d = location_3d_to_region_2d(context.region, context.region_data, p)
color = self.addon_prefs.decorations_colour
blf.color(font_id, *color)
if coords_2d:
w, h = blf.dimensions(font_id, text)
coords_2d -= Vector((w * .5, 0))
blf.position(font_id, coords_2d[0], coords_2d[1], 0)
blf.draw(font_id, text) # Set your text here
def draw_geometry(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
selected_edges = []
if selected_vertices[0] != selected_vertices[1]:
selected_edges = [[0, 1]]
self.draw_batch("POINTS", selected_vertices, special_elements_color)
if selected_edges:
self.draw_batch("LINES", selected_vertices, special_elements_color, selected_edges)
@@ -23,11 +23,10 @@ import bmesh
import logging
import numpy as np
import ifcopenshell
import blenderbim.tool as tool
from math import radians
from mathutils import Matrix, Vector
from math import radians
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.clash.decorator import ClashDecorator
import blenderbim.tool as tool
class ExportClashSets(bpy.types.Operator):
@@ -46,7 +45,18 @@ class ExportClashSets(bpy.types.Operator):
def execute(self, context):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets = tool.Clash.export_clash_sets()
clash_sets = []
for clash_set in context.scene.BIMClashProperties.clash_sets:
self.a = []
self.b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
if data.selector:
clash_source["selector"] = data.selector
clash_source["mode"] = data.mode
getattr(self, ab).append(clash_source)
clash_sets.append({"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b})
with open(self.filepath, "w") as destination:
destination.write(json.dumps(clash_sets, indent=4))
return {"FINISHED"}
@@ -68,34 +78,25 @@ class ImportClashSets(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
tool.Clash.load_clash_sets(self.filepath)
context.scene.BIMClashProperties.clash_sets.clear()
for clash_set in tool.Clash.get_clash_sets():
with open(self.filepath) as f:
clash_sets = json.load(f)
for clash_set in clash_sets:
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = clash_set["name"]
new.mode = clash_set["mode"]
if new.mode == "intersection":
new.tolerance = clash_set["tolerance"]
new.check_all = clash_set["check_all"]
elif new.mode == "collision":
new.allow_touching = clash_set["allow_touching"]
elif new.mode == "clearance":
new.clearance = clash_set["clearance"]
new.check_all = clash_set["check_all"]
new.tolerance = clash_set["tolerance"]
for clash_source in clash_set["a"]:
new_source = new.a.add()
new_source.name = clash_source["file"]
if "selector" in clash_source:
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
new_source.selector = clash_source["selector"]
new_source.mode = clash_source["mode"]
if "b" in clash_set and clash_set["b"]:
if clash_set["b"]:
for clash_source in clash_set["b"]:
new_source = new.b.add()
new_source.name = clash_source["file"]
if "selector" in clash_source:
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
new_source.selector = clash_source["selector"]
new_source.mode = clash_source["mode"]
tool.Clash.import_active_clashes()
return {"FINISHED"}
@@ -108,6 +109,7 @@ class AddClashSet(bpy.types.Operator):
def execute(self, context):
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = "New Clash Set"
new.tolerance = 0.01
return {"FINISHED"}
@@ -209,11 +211,8 @@ class ExecuteIfcClash(bpy.types.Operator):
def invoke(self, context, event):
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
# TODO Temporarily until BCF support comes back
# if extension != ".json":
# self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
if extension != ".json":
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -221,12 +220,7 @@ class ExecuteIfcClash(bpy.types.Operator):
def execute(self, context):
from ifcclash import ifcclash
self.props = context.scene.BIMClashProperties
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
# TODO Temporarily until BCF support comes back
if extension != ".json":
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
@@ -236,7 +230,7 @@ class ExecuteIfcClash(bpy.types.Operator):
settings.logger.setLevel(logging.DEBUG)
clasher = ifcclash.Clasher(settings)
if self.props.should_create_clash_snapshots:
if context.scene.BIMClashProperties.should_create_clash_snapshots:
def get_viewpoint_snapshot(viewpoint):
camera = bpy.data.objects.get("IFC Clash Camera")
@@ -277,13 +271,23 @@ class ExecuteIfcClash(bpy.types.Operator):
clasher.get_viewpoint_snapshot = get_viewpoint_snapshot
clasher.clash_sets = tool.Clash.export_clash_sets()
clasher.clash_sets = []
for clash_set in context.scene.BIMClashProperties.clash_sets:
self.a = []
self.b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
if data.selector:
clash_source["selector"] = data.selector
clash_source["mode"] = data.mode
getattr(self, ab).append(clash_source)
clash_set_data = {"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a}
if self.b:
clash_set_data["b"] = self.b
clasher.clash_sets.append(clash_set_data)
clasher.clash()
clasher.export()
if extension == ".json":
tool.Clash.load_clash_sets(self.filepath)
tool.Clash.import_active_clashes()
return {"FINISHED"}
@@ -302,7 +306,6 @@ class SelectIfcClashResults(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
# TODO refactor into new clash results system
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
@@ -349,6 +352,83 @@ class SelectIfcClashResults(bpy.types.Operator):
return {"FINISHED"}
class LoadIfcClashes(bpy.types.Operator):
bl_idname = "bim.load_ifc_clashes"
bl_label = "Load IFC Clashes"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load the clashing IFC geometry stored in a file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
clash_sets_json = json.load(f)
active_clash_set = context.scene.BIMClashProperties.active_clash_set
for clash_set in clash_sets_json:
if not "clashes" in clash_set.keys():
self.report({"WARNING"}, "No clashes found for the selected Clash Set.")
return {"CANCELLED"}
active_clash_set.clashes.clear()
for clash in clash_set["clashes"].values():
blender_clash = active_clash_set.clashes.add()
blender_clash.a_global_id = clash["a_global_id"]
blender_clash.b_global_id = clash["b_global_id"]
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
return {"FINISHED"}
class SaveIfcClashes(bpy.types.Operator):
bl_idname = "bim.save_ifc_clashes"
bl_label = "Save IFC Clashes"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save the clashing IFC geometry stored in a file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets_json = self.load_json()
active_clash_set = context.scene.BIMClashProperties.active_clash_set
self.update_clash_sets(clash_sets_json, active_clash_set)
self.save_json(clash_sets_json)
return {"FINISHED"}
def load_json(self):
with open(self.filepath) as f:
return json.load(f)
def update_clash_sets(self, clash_sets_json, active_clash_set):
for clash_set in clash_sets_json:
if clash_set["name"] != active_clash_set.name or "clashes" not in clash_set.keys():
continue
for clash in active_clash_set.clashes:
clash_key = clash.a_global_id + "-" + clash.b_global_id
if clash_set.get("clashes", {}).get(clash_key, None) is not None:
clash_set["clashes"][clash_key]["status"] = clash.status
def save_json(self, clash_sets_json):
with open(self.filepath, "w") as destination:
json.dump(clash_sets_json, destination, indent=4)
class SelectClash(bpy.types.Operator):
bl_idname = "bim.select_clash"
bl_label = "Select Clash"
@@ -357,29 +437,31 @@ class SelectClash(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
self.props = context.scene.BIMClashProperties
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
if not clash:
return {"FINISHED"}
clash = context.scene.BIMClashProperties.active_clash
products = []
for global_id in (clash["a_global_id"], clash["b_global_id"]):
try:
products.append(tool.Ifc.get().by_guid(global_id))
except:
pass
linked_objects = []
try:
products.append(tool.Ifc.get().by_guid(clash.a_global_id))
except:
linked_objects.append(clash.a_name)
try:
products.append(tool.Ifc.get().by_guid(clash.b_global_id))
except:
linked_objects.append(clash.b_name)
tool.Spatial.select_products(products, unhide=True)
ClashDecorator.install(bpy.context)
target = Vector(clash["p1"])
tool.Clash.look_at(target, target + Vector((5, 5, 5)))
self.props.p1 = clash["p1"]
self.props.p2 = clash["p2"]
self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm"
print(linked_objects)
for obj_name in linked_objects:
obj = bpy.data.objects.get(obj_name)
if obj:
obj.select_set(True)
else:
print("Object not found:", obj_name)
if context.scene.BIMClashProperties.sould_focus_on_clash:
context_override = tool.Blender.get_viewport_context()
with bpy.context.temp_override(**context_override):
bpy.ops.view3d.view_selected()
return {"FINISHED"}
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty, Attribute, BIMFilterGroup
from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -33,12 +33,11 @@ from bpy.props import (
class ClashSource(PropertyGroup):
name: StringProperty(name="File")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
selector: StringProperty(name="Selector")
mode: EnumProperty(
items=[
("a", "All Elements", "All elements will be used for clashing"),
("i", "Include", "Only the selected elements are included for clashing"),
("e", "Exclude", "All elements except the selected elements are included for clashing"),
("i", "Include", "Only the selected objects are included for clashing"),
("e", "Exclude", "All objects except the selected objects are included for clashing"),
],
name="Mode",
)
@@ -54,24 +53,7 @@ class Clash(PropertyGroup):
class ClashSet(PropertyGroup):
name: StringProperty(name="Name")
mode: EnumProperty(
items=[
(
"intersection",
"Intersection",
"Detect objects that protrude or pierce another object",
"PIVOT_MEDIAN",
1,
),
("collision", "Collision", "Detect touching objects with any surface collision", "PIVOT_INDIVIDUAL", 2),
("clearance", "Clearance", "Detect objects within a proximity threshold", "PIVOT_ACTIVE", 3),
],
name="Mode",
)
tolerance: FloatProperty(name="Tolerance", default=0.002)
clearance: FloatProperty(name="Clearance", default=0.01)
allow_touching: BoolProperty(name="Allow Touching", default=False)
check_all: BoolProperty(name="Check All", default=False)
tolerance: FloatProperty(name="Tolerance")
a: CollectionProperty(name="Group A", type=ClashSource)
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
@@ -96,21 +78,22 @@ class BIMClashProperties(PropertyGroup):
smart_clash_grouping_max_distance: IntProperty(
name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10
)
p1: FloatVectorProperty(name="P1", default=(0.0, 0.0, 0.0), subtype="XYZ")
p2: FloatVectorProperty(name="P2", default=(0.0, 0.0, 0.0), subtype="XYZ")
active_clash_text: StringProperty(name="Active Clash Text")
sould_focus_on_clash: BoolProperty(name="Show Focus Clash", default=False)
@property
def active_clash_set(self):
if self.active_clash_set_index < len(self.clash_sets):
return self.clash_sets[self.active_clash_set_index]
if not self.clash_sets:
return None
return self.clash_sets[self.active_clash_set_index]
@property
def active_smart_group(self):
if self.active_smart_group_index < len(self.smart_clash_groups):
return self.smart_clash_groups[self.active_smart_group_index]
if not self.smart_clash_groups:
return None
return self.smart_clash_groups[self.active_smart_group_index]
@property
def active_clash(self):
if self.active_clash_index < len(self.active_clash_set.clashes):
return self.active_clash_set.clashes[self.active_clash_index]
if not self.active_clash_set:
return None
return self.active_clash_set.clashes[self.active_clash_index]
@@ -17,9 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.bim.helper
from bpy.types import Panel
from blenderbim.bim.module.clash.data import ClashData
class BIM_PT_ifcclash(Panel):
@@ -32,9 +30,6 @@ class BIM_PT_ifcclash(Panel):
bl_parent_id = "BIM_PT_tab_clash_detection"
def draw(self, context):
if not ClashData.is_loaded:
ClashData.load()
layout = self.layout
scene = context.scene
@@ -50,83 +45,57 @@ class BIM_PT_ifcclash(Panel):
layout.template_list("BIM_UL_clash_sets", "", props, "clash_sets", props, "active_clash_set_index")
if not props.active_clash_set:
return
if props.active_clash_set_index < len(props.clash_sets):
clash_set = props.active_clash_set
clash_set = props.active_clash_set
row = layout.row(align=True)
row.prop(clash_set, "name")
row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index
row = layout.row(align=True)
row.prop(clash_set, "name")
row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index
row = layout.row()
row.prop(clash_set, "mode")
if clash_set.mode == "intersection":
row = layout.row()
row = layout.row(align=True)
row.prop(clash_set, "tolerance")
row = layout.row()
row.prop(clash_set, "check_all")
elif clash_set.mode == "collision":
row = layout.row()
row.prop(clash_set, "allow_touching")
elif clash_set.mode == "clearance":
row = layout.row()
row.prop(clash_set, "clearance")
row = layout.row()
row.prop(clash_set, "check_all")
row = layout.row(align=True)
row.label(text="Group A:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "a"
layout.label(text="Group A:")
row = layout.row()
row.operator("bim.add_clash_source").group = "a"
for index, source in enumerate(clash_set.a):
for index, source in enumerate(clash_set.a):
row = layout.row(align=True)
row.prop(source, "name", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "a"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "a"
row = layout.row(align=True)
row.prop(source, "mode", text="")
row.prop(source, "selector", text="")
layout.label(text="Group B:")
row = layout.row()
row.operator("bim.add_clash_source").group = "b"
for index, source in enumerate(clash_set.b):
row = layout.row(align=True)
row.prop(source, "name", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "b"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "b"
row = layout.row(align=True)
row.prop(source, "mode", text="")
row.prop(source, "selector", text="")
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
row = layout.row(align=True)
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "a"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "a"
if source.mode != "a":
blenderbim.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_a_{index}"
)
row = layout.row(align=True)
row.label(text="Group B:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "b"
for index, source in enumerate(clash_set.b):
row = layout.row(align=True)
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "b"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "b"
if source.mode != "a":
blenderbim.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_b_{index}"
)
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
row = layout.row()
row.operator("bim.execute_ifc_clash")
row = layout.row()
row.label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
row = layout.row()
row.operator("bim.select_clash")
row.operator("bim.execute_ifc_clash")
row.operator("bim.select_ifc_clash_results")
class BIM_PT_clash_manager(Panel):
@@ -142,6 +111,29 @@ class BIM_PT_clash_manager(Panel):
pass
class BIM_PT_dumb_clash_manager(Panel):
bl_idname = "BIM_PT_dumb_clash_manager"
bl_label = "Dumb Manager"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_clash_manager"
def draw(self, context):
layout = self.layout
props = context.scene.BIMClashProperties
row = layout.row(align=True)
row.operator("bim.load_ifc_clashes", text="LOAD IFC CLASHES", icon="IMPORT")
row.operator("bim.save_ifc_clashes", text="SAVE CLASH RESULTS", icon="EXPORT")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
# bim.select_clash
row = layout.row(align=True)
row.operator("bim.select_clash", text="SELECT CLASH", icon="IMPORT")
row.prop(props, "sould_focus_on_clash", icon="RESTRICT_VIEW_OFF")
class BIM_PT_smart_clash_manager(Panel):
bl_idname = "BIM_PT_smart_clash_manager"
bl_label = "Smart Clash Manager"
@@ -20,8 +20,6 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.classification
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.bim.helper
from blenderbim.bim.ifc import IfcStore
@@ -89,7 +87,7 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
products=[product],
product=product,
classification=classification,
identification="X",
name="Unnamed",
@@ -209,10 +207,6 @@ class DisableEditingClassification(bpy.types.Operator):
class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_classification"
bl_label = "Remove Classification"
bl_description = (
"The classification and all of its relationships, children references, "
"and relationships between objects and child references will be completely removed from a project"
)
bl_options = {"REGISTER", "UNDO"}
classification: bpy.props.IntProperty()
@@ -298,25 +292,20 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
active_reference = tool.Ifc.get().by_id(self.reference)
identification = active_reference[1]
elements_by_references: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = {}
for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
element = tool.Ifc.get().by_id(ifc_definition_id)
references = ifcopenshell.util.classification.get_references(element, should_inherit=False)
for reference in references:
if (identification and reference[1] == identification) or (
not identification and reference == active_reference
):
elements_by_references.setdefault(reference, []).append(element)
if elements_by_references:
for reference, products in elements_by_references.items():
ifcopenshell.api.run(
"classification.remove_reference",
tool.Ifc.get(),
reference=reference,
products=products,
)
ifcopenshell.api.run(
"classification.remove_reference",
tool.Ifc.get(),
reference=reference,
product=element,
)
class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
@@ -368,18 +357,15 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
classification = element
break
ifc_file = tool.Ifc.get()
products = [
ifc_file.by_id(ifc_definition_id)
for obj in objects
if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
]
if products:
for obj in objects:
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
if not ifc_definition_id:
continue
ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
reference=IfcStore.classification_file.by_id(self.reference),
products=products,
product=tool.Ifc.get().by_id(ifc_definition_id),
classification=classification,
)
@@ -420,14 +406,14 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
classification.Location = bsdd_classification.domain_namespace_uri
for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
if not ifc_definition_id:
continue
element = tool.Ifc.get().by_id(ifc_definition_id)
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
products=[element],
product=element,
classification=classification,
identification=bsdd_classification.reference_code,
name=bsdd_classification.name,
@@ -114,7 +114,7 @@ class EditObjective(bpy.types.Operator):
ifcopenshell.api.run(
"constraint.edit_objective",
self.file,
**{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes},
**{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes}
)
bpy.ops.bim.load_objectives()
return {"FINISHED"}
@@ -179,17 +179,19 @@ class AssignConstraint(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = tool.Ifc.get()
self.file = IfcStore.get_file()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
if products:
for obj in objs:
obj_id = obj.BIMObjectProperties.ifc_definition_id
if not obj_id:
continue
ifcopenshell.api.run(
"constraint.assign_constraint",
self.file,
**{
"products": products,
"product": self.file.by_id(obj_id),
"constraint": self.file.by_id(self.constraint),
},
}
)
return {"FINISHED"}
@@ -205,16 +207,18 @@ class UnassignConstraint(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = tool.Ifc.get()
self.file = IfcStore.get_file()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
if products:
for obj in objs:
obj_id = obj.BIMObjectProperties.ifc_definition_id
if not obj_id:
continue
ifcopenshell.api.run(
"constraint.unassign_constraint",
self.file,
**{
"products": products,
"product": self.file.by_id(obj_id),
"constraint": self.file.by_id(self.constraint),
},
}
)
return {"FINISHED"}
@@ -53,10 +53,6 @@ class AddContext(bpy.types.Operator, Operator):
class RemoveContext(bpy.types.Operator, Operator):
bl_idname = "bim.remove_context"
bl_label = "Remove Context"
bl_description = (
"Remove representation context. Any representation geometry that is assigned to the context is also removed. "
"If a context is removed, then any subcontexts are also removed"
)
bl_options = {"REGISTER", "UNDO"}
context: bpy.props.IntProperty()
@@ -107,88 +107,59 @@ class CoveringToolUI:
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
# if (type_material_usage == "IfcMaterialLayerSet" and
# not bpy.context.selected_objects):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# if tool.Ifc.get_entity(collection_obj):
# if AuthoringData.data["predefined_type"] == "FLOORING":
# op = row.operator("bim.add_instance_flooring_covering_from_cursor")
# elif AuthoringData.data["predefined_type"] == "CEILING":
# op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
# else:
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
#
# else:
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
#
# elif (AuthoringData.data["predefined_type"] == "FLOORING" and
# type_material_usage == "IfcMaterialLayerSet" and
# element and
# bpy.context.selected_objects and
# element.is_a("IfcWall")):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_instance_flooring_coverings_from_walls")
#
# elif (AuthoringData.data["predefined_type"] == "CEILING" and
# type_material_usage == "IfcMaterialLayerSet" and
# element and
# bpy.context.selected_objects and
# element.is_a("IfcWall")):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
#
# elif (element and
# bpy.context.selected_objects and
# element.is_a("IfcCovering") and
## AuthoringData.data["predefined_type"] == "FLOORING" and
# AuthoringData.data["active_material_usage"] == "LAYER3"):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_G")
# op = row.operator("bim.regen_selected_covering_object")
if (type_material_usage == "IfcMaterialLayerSet" and
not bpy.context.selected_objects):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
if tool.Ifc.get_entity(collection_obj):
if AuthoringData.data["predefined_type"] == "FLOORING":
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
elif AuthoringData.data["predefined_type"] == "CEILING":
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
elif (AuthoringData.data["predefined_type"] == "FLOORING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
elif (AuthoringData.data["predefined_type"] == "CEILING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
elif (element and
bpy.context.selected_objects and
element.is_a("IfcCovering") and
# AuthoringData.data["predefined_type"] == "FLOORING" and
AuthoringData.data["active_material_usage"] == "LAYER3"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
op = row.operator("bim.regen_selected_covering_object")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
op = row.operator("bim.regen_selected_covering_object")
# elif AuthoringData.data["predefined_type"] == "CEILING":
# row = cls.layout.row(align=True)
@@ -203,14 +174,14 @@ class CoveringToolUI:
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
# else:
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
else:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
@classmethod
def draw_type_selection_interface(cls):
@@ -220,11 +191,11 @@ class CoveringToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
else:
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
row = cls.layout.row()
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
@classmethod
def draw_basic_bim_tool_interface(cls):
@@ -72,7 +72,7 @@ class ReorderCsvAttribute(bpy.types.Operator):
def execute(self, context):
old = context.scene.CsvProperties.csv_attributes[self.old_index]
new = context.scene.CsvProperties.csv_attributes[self.new_index]
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
props = ["name", "header", "sort", "group", "varies_value", "summary"]
for prop in props:
value = getattr(new, prop)
setattr(new, prop, getattr(old, prop))
@@ -246,7 +246,6 @@ class ExportIfcCsv(bpy.types.Operator):
if props.format != "csv" and props.should_generate_svg:
schedule_creator = scheduler.Scheduler()
schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg"))
self.report({"INFO"}, f"Data is exported to {props.format.upper()}.")
return {"FINISHED"}
@@ -286,7 +285,6 @@ class ImportIfcCsv(bpy.types.Operator):
if not props.should_load_from_memory:
ifc_file.write(props.csv_ifc_file)
refresh_ui_data()
self.report({"INFO"}, "Data is imported to IFC.")
return {"FINISHED"}
@@ -24,7 +24,6 @@ classes = (
operator.CopyDebugInformation,
operator.CreateAllShapes,
operator.CreateShapeFromStepId,
operator.DebugActiveDrawing,
operator.InspectFromObject,
operator.InspectFromStepId,
operator.OverrideDisplayType,
@@ -35,6 +34,7 @@ classes = (
operator.PrintUnusedElementStats,
operator.ProfileImportIFC,
operator.PurgeHdf5Cache,
operator.PurgeIfcLinks,
operator.PurgeUnusedElementsByClass,
operator.RewindInspector,
operator.SelectExpressFile,
@@ -20,20 +20,18 @@ import os
import sys
import bpy
import time
import random
import logging
import platform
import subprocess
import addon_utils
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.tool as tool
import blenderbim.core.debug as core
import blenderbim.bim.handler
import blenderbim.bim.import_ifc as import_ifc
from blenderbim import get_debug_info, format_debug_info
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
@@ -43,7 +41,28 @@ class CopyDebugInformation(bpy.types.Operator):
bl_description = "Copies debugging information to your clipboard for use in bugreports"
def execute(self, context):
info = get_debug_info()
version = ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
info = {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
"architecture": platform.architecture(),
"machine": platform.machine(),
"processor": platform.processor(),
"blender_version": bpy.app.version_string,
"blenderbim_version": version,
"ifc": False,
}
if tool.Ifc.get():
info.update(
{
@@ -54,18 +73,16 @@ class CopyDebugInformation(bpy.types.Operator):
}
)
text = format_debug_info(info)
print("-" * 80)
# Format it in a readable way
text = "\n".join(f"{k}: {v}" for k, v in info.items())
print(text)
print("-" * 80)
if platform.system() == "Windows":
command = "echo | set /p nul=" + text
command = "echo | set /p nul=" + text.strip()
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
return {"FINISHED"}
@@ -84,11 +101,10 @@ class PrintIfcFile(bpy.types.Operator):
return {"FINISHED"}
class ConvertToBlender(bpy.types.Operator):
bl_idname = "bim.convert_to_blender"
bl_label = "Convert To Blender File"
bl_description = "Removes all IFC data and revert to basic Blender objects.\nWarning : Cannot be undone."
bl_options = {"REGISTER", "UNDO"}
class PurgeIfcLinks(bpy.types.Operator):
bl_idname = "bim.purge_ifc_links"
bl_label = "Purge IFC Links"
bl_description = "Purge all definitions and references from the file.\nWarning : Cannot be undone."
def execute(self, context):
for obj in bpy.data.objects:
@@ -105,6 +121,26 @@ class ConvertToBlender(bpy.types.Operator):
return {"FINISHED"}
class ConvertToBlender(bpy.types.Operator):
bl_idname = "bim.convert_to_blender"
bl_label = "Convert To Blender File"
bl_description = "Removes all IFC data, and converts the file to a simple Blender file."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for o in bpy.data.objects:
if o.type in {"MESH", "EMPTY"}:
o.BIMObjectProperties.ifc_definition_id = 0
if o.data:
o.data.BIMMeshProperties.ifc_definition_id = 0
for m in bpy.data.materials:
m.BIMMaterialProperties.ifc_style_id = False
bpy.context.scene.BIMProperties.ifc_file = ""
IfcStore.purge()
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
class ValidateIfcFile(bpy.types.Operator):
bl_idname = "bim.validate_ifc_file"
bl_label = "Validate IFC File"
@@ -624,104 +660,3 @@ class PipInstall(bpy.types.Operator):
]
)
return {"FINISHED"}
class DebugActiveDrawing(bpy.types.Operator):
bl_idname = "bim.debug_active_drawing"
bl_label = "Search Active Drawing For Failing Elements"
bl_description = (
"Will iterate over all visible drawing's objects, trying to narrow down the list of possible failing objects"
)
def execute(self, context: bpy.types.Context):
props = context.scene.DocProperties
drawing_item = props.drawings[props.active_drawing_index]
drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id)
GREEN = "\033[92m"
CYAN = "\033[96m"
END = "\033[0m"
ATTEMPS = 10
# run create drawing with sync for once
# to make sure everything is actually in sync
try:
bpy.ops.bim.create_drawing(sync=True)
except:
pass
else:
self.report({"INFO"}, "No errors creating drawing, nothing to investigate.")
return {"FINISHED"}
all_elements = [e for obj in context.visible_objects if (e := tool.Ifc.get_entity(obj))]
all_elements = set(all_elements)
original_exclude = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude")
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
def drawing_fails_to_load(chunk_to_include: set) -> bool:
current_elements = all_elements - chunk_to_include
excluded_guids = ", ".join([e.GlobalId for e in current_elements if hasattr(e, "GlobalId")])
tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": f"{original_exclude}, {excluded_guids}"})
try:
bpy.ops.bim.create_drawing(sync=False)
result = False
except Exception as e:
# print(e)
result = True
tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": original_exclude})
return result
def test_elements(elements: list[ifcopenshell.entity_instance], attempts: int = ATTEMPS) -> None:
print(f"{CYAN}processing {len(elements)} elements{END}")
if not elements:
print(f"Empty list of elements, will stop...")
return
n_elements = len(elements)
middle = int(n_elements / 2)
chunk1, chunk2 = elements[:middle], elements[middle:]
test_chunk_1 = drawing_fails_to_load(set(chunk1))
test_chunk_2 = drawing_fails_to_load(set(chunk2))
if (
# both chunks do not fail anymore
(not test_chunk_1 and not test_chunk_2)
# or we have 1 element chunk that is still failing
or (test_chunk_1 and not chunk2)
or (test_chunk_2 and not chunk1)
):
if attempts == 0 or n_elements in (1, 2):
print(f"{GREEN}Couldn't narrow it down any further.{END}")
print(f"It's some of the {n_elements} elements:")
print(elements)
print("Let's test excluding them...")
for element in elements:
test = drawing_fails_to_load(all_elements - {element})
if test:
print(f"{CYAN}Excluding element didn't fixed the drawing: {END}")
print(element)
else:
print(f"{GREEN}Excluding element fixed the drawing: {END}")
print(element)
else:
print(f"{CYAN}Will try to reshuffle elements and try again, attempt {ATTEMPS-attempts+1}/{ATTEMPS}")
attempts -= 1
random.shuffle(elements)
test_elements(elements, attempts)
return
# if chunk fails we need to investigate it further
if test_chunk_1:
test_elements(chunk1)
if test_chunk_2:
test_elements(chunk2)
test_elements(list(all_elements))
self.report({"INFO"}, "See system console for the results")
return {"FINISHED"}
@@ -60,6 +60,9 @@ class BIM_PT_debug(Panel):
row = layout.row()
row.operator("bim.purge_hdf5_cache")
row = layout.row()
row.operator("bim.purge_ifc_links")
row = layout.row()
row.operator("bim.update_representation", text="Manually Save Representation")
@@ -72,9 +75,6 @@ class BIM_PT_debug(Panel):
row = layout.row()
row.operator("bim.profile_import_ifc")
row = layout.row()
row.operator("bim.debug_active_drawing")
row = layout.split(factor=0.5, align=True)
row.operator("bim.create_shape_from_step_id").should_include_curves = False
row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True
@@ -38,7 +38,6 @@ class DiffData:
cls.data["total_deleted"] = cls.total_deleted()
cls.data["total_changed"] = cls.total_changed()
cls.data["changes"] = cls.changes()
cls.data["saved_searches"] = cls.saved_searches()
cls.is_loaded = True
@classmethod
@@ -85,18 +84,3 @@ class DiffData:
elif element.GlobalId in diff["deleted"]:
changes["Deleted"] = True
return changes
@classmethod
def saved_searches(cls):
if not tool.Ifc.get():
return []
groups = tool.Ifc.get().by_type("IfcGroup")
results = []
for group in groups:
try:
data = json.loads(group.Description)
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
results.append(group)
except:
pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
@@ -135,7 +135,6 @@ class ExecuteIfcDiff(bpy.types.Operator):
bl_label = "Execute IFC Diff"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
@@ -163,7 +162,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
new = ifcopenshell.open(self.props.new_file)
relationships = [r.relationship for r in self.props.diff_relationships]
query = tool.Search.export_filter_query(self.props.filter_groups) or None
query = self.props.diff_filter_elements
ifc_diff = ifcdiff.IfcDiff(old, new, relationships=relationships, filter_elements=query)
ifc_diff.diff()
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty, BIMFilterGroup
from blenderbim.bim.prop import StrProperty
from blenderbim.bim.module.diff.data import DiffData
from bpy.types import PropertyGroup
from bpy.props import (
@@ -48,7 +48,7 @@ class DiffProperties(PropertyGroup):
old_file: StringProperty(default="", name="Old IFC File")
new_file: StringProperty(default="", name="New IFC File")
diff_relationships: CollectionProperty(type=Relationships, name="Relationships")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
diff_filter_elements: StringProperty(default="", name="Filter")
should_load_changed_elements: BoolProperty(name="Load Changed Elements", default=True)
active_file: EnumProperty(
items=[
@@ -19,8 +19,8 @@
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.diff.data import DiffData
import blenderbim.bim.helper
import blenderbim.tool as tool
import json
class BIM_PT_diff(Panel):
@@ -93,7 +93,9 @@ class BIM_PT_diff(Panel):
remove.collection = "diff_relationships"
remove.index = index
blenderbim.bim.helper.draw_filter(self.layout, props.filter_groups, DiffData, "diff")
row = layout.row(align=True)
row.prop(props, "diff_filter_elements")
row.operator("bim.ifc_selector", icon="FILTER", text="")
row = layout.row()
row.operator("bim.execute_ifc_diff")
@@ -41,7 +41,6 @@ classes = (
operator.CleanWireframes,
operator.ContractSheet,
operator.ContractTargetView,
operator.ConvertSVGToDXF,
operator.CreateDrawing,
operator.CreateSheets,
operator.DisableAddAnnotationType,
@@ -127,7 +126,7 @@ def register():
bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties)
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.app.handlers.load_post.append(handler.load_post)
bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad)
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button)
@@ -141,6 +140,6 @@ def unregister():
del bpy.types.Object.BIMAssignedProductProperties
del bpy.types.Object.BIMTextProperties
del bpy.types.TextCurve.BIMTextProperties
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.app.handlers.load_post.remove(handler.toggleDecorationsOnLoad)
bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler)
bpy.types.VIEW3D_MT_image_add.remove(ui.add_object_button)
@@ -606,7 +606,7 @@ class BaseDecorator:
for literal_data in literals_data:
box_alignment = literal_data["BoxAlignment"]
for line in literal_data["CurrentValue"].split("\n"):
for line in literal_data["CurrentValue"].split("\\n"):
self.draw_label(
context,
line,
@@ -23,7 +23,7 @@ from bpy.app.handlers import persistent
@persistent
def load_post(*args):
def toggleDecorationsOnLoad(*args):
if bpy.context.scene.DocProperties.should_draw_decorations:
decoration.DecorationsHandler.install(bpy.context)
else:
@@ -188,6 +188,7 @@ def format_distance(
feet += 1
inches = 0
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches
tx_dist = ""
@@ -298,9 +299,7 @@ def parse_diagram_scale(camera):
return float(numerator) / float(denominator)
def ortho_view_frame(
camera: bpy.types.Camera, margin: float = 0.015
) -> tuple[float, float, float, float, float, float]:
def ortho_view_frame(camera, margin=0.015):
"""Calculates 2d bounding box of camera view area.
Similar to `bpy.types.Camera.view_frame`
@@ -29,7 +29,6 @@ import subprocess
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.geom
import ifcopenshell.util.selector
import ifcopenshell.util.representation
@@ -45,7 +44,7 @@ import blenderbim.bim.module.drawing.helper as helper
import blenderbim.bim.export_ifc
from blenderbim.bim.module.drawing.decoration import CutDecorator
from blenderbim.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List, Union, Optional
from typing import NamedTuple, List
from lxml import etree
from mathutils import Vector, Color, Matrix
from timeit import default_timer as timer
@@ -200,11 +199,6 @@ class CreateDrawing(bpy.types.Operator):
+ "SHIFT+CLICK to print all selected drawings"
)
print_all: bpy.props.BoolProperty(name="Print All", default=False, options={"SKIP_SAVE"})
sync: bpy.props.BoolProperty(
name="Sync Before Creating Drawing",
description="Could save some time if you're sure IFC and current Blender session are already in sync",
default=True,
)
@classmethod
def poll(cls, context):
@@ -220,15 +214,13 @@ class CreateDrawing(bpy.types.Operator):
def execute(self, context):
self.props = context.scene.DocProperties
active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id
if self.print_all:
original_drawing_id = active_drawing_id
original_drawing_id = self.props.active_drawing_id
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
else:
drawings_to_print = [active_drawing_id]
drawings_to_print = [self.props.active_drawing_id]
for drawing_i, drawing_id in enumerate(drawings_to_print):
self.drawing_index = drawing_i
for drawing_id in drawings_to_print:
if self.print_all:
bpy.ops.bim.activate_drawing(drawing=drawing_id, camera_view_point=False)
@@ -343,13 +335,6 @@ class CreateDrawing(bpy.types.Operator):
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"):
return
svg_path = self.get_svg_path(cache_type="underlay")
if os.path.isfile(svg_path) and self.props.should_use_underlay_cache:
return svg_path
visible_object_names = {obj.name for obj in bpy.context.visible_objects}
for obj in bpy.context.view_layer.objects:
obj.hide_render = obj.name not in visible_object_names
context.scene.render.filepath = svg_path[0:-4] + ".png"
drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
@@ -460,14 +445,8 @@ class CreateDrawing(bpy.types.Operator):
return LineworkContexts(body_contexts, annotation_contexts)
def serialize_contexts_elements(
self,
ifc: ifcopenshell.file,
tree: ifcopenshell.geom.tree,
contexts: LineworkContexts,
context_type: str,
drawing_elements: set[ifcopenshell.entity_instance],
target_view: str,
) -> None:
self, ifc, tree: ifcopenshell.geom.tree, contexts: LineworkContexts, context_type, drawing_elements, target_view
):
drawing_elements = drawing_elements.copy()
contexts = getattr(contexts, context_type)
for context in contexts:
@@ -493,24 +472,20 @@ class CreateDrawing(bpy.types.Operator):
tree.add_element(elem)
drawing_elements -= processed
def generate_linework(self, context: bpy.types.Context) -> Union[str, None]:
def generate_linework(self, context):
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
svg_path = self.get_svg_path(cache_type="linework")
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
return svg_path
# in case of printing multiple drawings we need to sync just once
if self.sync and self.drawing_index == 0:
with profile("sync"):
# All very hackish whilst prototyping
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
invalidated_elements = exporter.sync_all_objects(skip_unlinking=True)
invalidated_elements += exporter.sync_edited_objects()
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
cache = IfcStore.get_cache()
[cache.remove(guid) for guid in invalidated_guids]
with profile("sync"):
# All very hackish whilst prototyping
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
invalidated_elements = exporter.sync_all_objects()
invalidated_elements += exporter.sync_edited_objects()
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
# If we have already calculated it in the SVG in the past, don't recalculate
edited_guids = set()
@@ -531,7 +506,6 @@ class CreateDrawing(bpy.types.Operator):
for ifc_path, ifc in files.items():
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
ifc_cache_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
@@ -544,6 +518,8 @@ class CreateDrawing(bpy.types.Operator):
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element)
self.setup_serialiser(ifc, target_view)
cache = IfcStore.get_cache()
[cache.remove(guid) for guid in invalidated_guids]
tree = ifcopenshell.geom.tree()
tree.enable_face_styles(True)
@@ -1139,14 +1115,14 @@ class CreateDrawing(bpy.types.Operator):
continue
return space
def get_material_name(self, element: ifcopenshell.entity_instance) -> str:
def get_material_name(self, element):
if hasattr(element, "Name") and element.Name:
return element.Name
elif hasattr(element, "LayerSetName") and element.LayerSetName:
return element.LayerSetName
return "mat-" + str(element.id())
def get_svg_path(self, cache_type: Optional[str] = None) -> str:
def get_svg_path(self, cache_type=None):
drawing_path = tool.Drawing.get_document_uri(self.camera_document)
drawings_dir = os.path.dirname(drawing_path)
@@ -1243,14 +1219,12 @@ class AddDrawingToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
),
Location=drawing_reference.Location,
Description="DRAWING",
)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": drawing_reference.Location,
"Description": "DRAWING",
}
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
@@ -1318,10 +1292,9 @@ class CreateSheets(bpy.types.Operator, Operator):
has_sheet_reference = False
for reference in tool.Drawing.get_document_references(sheet):
reference_description = tool.Drawing.get_reference_description(reference)
if reference_description == "SHEET":
if reference.Description == "SHEET":
has_sheet_reference = True
elif reference_description == "RASTER":
elif reference.Description == "RASTER":
if reference.Location in raster_references:
raster_references.remove(reference.Location)
else:
@@ -1332,9 +1305,7 @@ class CreateSheets(bpy.types.Operator, Operator):
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(svg), Description="SHEET"
),
attributes={"Location": tool.Ifc.get_relative_uri(svg), "Description": "SHEET"},
)
for raster_reference in raster_references:
@@ -1342,9 +1313,7 @@ class CreateSheets(bpy.types.Operator, Operator):
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(raster_reference), Description="RASTER"
),
attributes={"Location": tool.Ifc.get_relative_uri(raster_reference), "Description": "RASTER"},
)
svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command
@@ -1376,7 +1345,7 @@ 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=True, options={"SKIP_SAVE"})
select_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# deselect all drawings on shift+click
@@ -1453,19 +1422,13 @@ class ActivateModel(bpy.types.Operator):
CutDecorator.uninstall()
# save current visibility statuses for Views and Types collections
visibility_status: dict[bpy.types.Object, bool] = {}
for col in bpy.data.collections["Views"].children:
for obj in col.objects:
visibility_status[obj] = obj.hide_get()
for obj in bpy.data.collections["Types"].objects:
visibility_status[obj] = obj.hide_get()
if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.object.hide_view_clear()
bpy.ops.bim.activate_status_filters()
subcontext = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
@@ -1483,16 +1446,15 @@ class ActivateModel(bpy.types.Operator):
is_global=True,
should_sync_changes_first=True,
)
# restore visibility after hide_view_clear()
for obj, hide_status in visibility_status.items():
obj.hide_set(hide_status)
tool.Blender.update_viewport()
return {"FINISHED"}
class ActivateDrawing(bpy.types.Operator):
"""
Activates the selected drawing view
"""
bl_idname = "bim.activate_drawing"
bl_label = "Activate Drawing"
bl_options = {"REGISTER", "UNDO"}
@@ -1515,14 +1477,7 @@ class ActivateDrawing(bpy.types.Operator):
if not self.camera_view_point:
viewport_position = tool.Blender.get_viewport_position()
try:
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
except core.CameraNotAvailableError:
self.report(
{"ERROR"},
"The drawing view is not available. Ensure you have not excluded it in the active view layer.",
)
return {"CANCELLED"}
core.activate_drawing_view(tool.Ifc, tool.Drawing, drawing=drawing)
if not self.camera_view_point:
tool.Blender.set_viewport_position(viewport_position)
@@ -1645,14 +1600,8 @@ class ReloadDrawingStyles(bpy.types.Operator):
json_path = Path(tool.Ifc.resolve_uri(rel_path))
if not json_path.exists():
ootb_resource = Path(context.scene.BIMProperties.data_dir) / "assets" / "shading_styles.json"
print(
f"WARNING. Couldn't find shading_styles for the drawing by the path: {json_path}. "
f"Default BBIM resource will be copied from {ootb_resource}"
)
if ootb_resource.exists():
os.makedirs(json_path.parent, exist_ok=True)
shutil.copy(ootb_resource, json_path)
self.report({"ERROR"}, "Shading styles file not found: {}".format(json_path))
return {"CANCELLED"}
with open(json_path, "r") as fi:
shading_styles_json = json.load(fi)
@@ -2015,14 +1964,12 @@ class AddScheduleToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
),
Location=schedule_location,
Description="SCHEDULE",
)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": schedule_location,
"Description": "SCHEDULE",
}
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -2071,15 +2018,12 @@ class AddReferenceToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "REFERENCE")])
+ 1
),
Location=extref_location,
Description="REFERENCE",
)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "REFERENCE")]) + 1),
"Location": extref_location,
"Description": "REFERENCE",
}
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -2405,7 +2349,6 @@ class LoadSheets(bpy.types.Operator, Operator):
if not filepath.is_file():
sheet_name = f"{sheet_prop.identification} - {sheet_prop.name}"
sheets_not_found.append(f'"{sheet_name}" - {document_uri}')
core.regenerate_sheet(tool.Drawing, sheet)
if sheets_not_found:
self.report({"ERROR"}, "Some sheets svg files are missing:\n" + "\n".join(sheets_not_found))
@@ -2424,8 +2367,8 @@ class EditSheet(bpy.types.Operator, Operator):
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
self.name = sheet.Name
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
self.identification = sheet.Identification
elif sheet.is_a("IfcDocumentReference") and sheet.Description == "TITLEBLOCK":
self.document_type = "TITLEBLOCK"
else:
self.document_type = "EMBEDDED"
@@ -2451,7 +2394,7 @@ class EditSheet(bpy.types.Operator, Operator):
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
elif self.document_type == "EMBEDDED":
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
core.rename_reference(tool.Ifc, reference=sheet, identification=self.identification)
elif self.document_type == "TITLEBLOCK":
titleblock = self.props.titleblock
reference = sheet
@@ -2603,16 +2546,8 @@ class EnableEditingElementFilter(bpy.types.Operator, Operator):
def _execute(self, context):
obj = bpy.context.scene.camera
if not obj:
return
obj.data.BIMCameraProperties.filter_mode = self.filter_mode
element = tool.Ifc.get_entity(obj)
if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()):
filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
try:
tool.Search.import_filter_query(query, filter_groups)
except:
pass
if obj:
obj.data.BIMCameraProperties.filter_mode = self.filter_mode
class EditElementFilter(bpy.types.Operator, Operator):
@@ -2754,50 +2689,3 @@ class AddReferenceImage(bpy.types.Operator, Operator):
)
tool.Style.reload_material_from_ifc(material)
tool.Geometry.record_object_materials(obj)
class ConvertSVGToDXF(bpy.types.Operator):
bl_idname = "bim.convert_svg_to_dxf"
bl_label = "Convert SVG to DXF"
bl_options = {"REGISTER", "UNDO"}
view: bpy.props.StringProperty()
bl_description = "Convert current drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all selected drawings"
convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# convert 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
return self.execute(context)
def execute(self, context):
if self.convert_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected
]
else:
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
drawing_uris: list[Path] = []
drawings_not_found: list[str] = []
for drawing in drawings:
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
if drawing_uri is None or not os.path.exists(drawing_uri):
drawings_not_found.append(drawing.Name)
else:
drawing_uris.append(Path(drawing_uri))
if drawings_not_found:
msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format(
"\n".join(drawings_not_found)
)
self.report({"ERROR"}, msg)
return {"CANCELLED"}
for drawing_uri in drawing_uris:
tool.Drawing.convert_svg_to_dxf(drawing_uri, drawing_uri.with_suffix(".dxf"))
self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.")
return {"FINISHED"}
@@ -66,11 +66,6 @@ def get_location_hint(self, context):
def update_diagram_scale(self, context):
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
if not element:
return
try:
element = (
tool.Ifc.get()
@@ -92,11 +87,6 @@ def update_diagram_scale(self, context):
def update_is_nts(self, context):
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
if not element:
return
try:
element = (
tool.Ifc.get()
@@ -204,7 +194,7 @@ def update_document_name(self, context):
def update_has_underlay(self, context):
update_layer(self, context, "HasUnderlay", self.has_underlay)
# making sure that camera is active
if self.has_underlay and (context.scene.camera and context.scene.camera.data == self.id_data):
if self.has_underlay and (context.active_object and context.active_object.data == self.id_data):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
@@ -218,12 +208,10 @@ def update_has_annotation(self, context):
def update_layer(self, context, name, value):
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
element = tool.Ifc.get_entity(context.active_object)
if not element:
return
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
@@ -241,8 +229,9 @@ def update_titleblocks(self, context):
SheetsData.data["titleblocks"] = SheetsData.titleblocks()
def update_should_draw_decorations(self, context):
if self.should_draw_decorations:
def toggleDecorations(self, context):
toggle = self.should_draw_decorations
if toggle:
# TODO: design a proper text variable templating renderer
collection = context.scene.camera.BIMObjectProperties.collection
for obj in collection.objects:
@@ -354,7 +343,7 @@ class DocProperties(PropertyGroup):
active_sheet_index: IntProperty(name="Active Sheet Index")
ifc_files: CollectionProperty(name="IFCs", type=StrProperty)
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=update_should_draw_decorations)
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations)
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
titleblocks_dir: StringProperty(
@@ -41,7 +41,7 @@ class SheetBuilder:
self.data_dir = None
self.scale = "NTS"
def create(self, layout_path: str, titleblock_name: str) -> None:
def create(self, layout_path, titleblock_name):
root = ET.Element("svg")
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
@@ -76,12 +76,7 @@ class SheetBuilder:
with open(layout_path, "w") as f:
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
def add_drawing(
self,
reference: ifcopenshell.entity_instance,
drawing: ifcopenshell.entity_instance,
sheet: ifcopenshell.entity_instance,
) -> None:
def add_drawing(self, reference, drawing, sheet):
filename = drawing.Name
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_dir = os.path.dirname(layout_path)
@@ -136,7 +131,7 @@ class SheetBuilder:
)
layout_tree.write(layout_path)
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
def update_sheet_drawing_sizes(self, sheet):
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -176,7 +171,7 @@ class SheetBuilder:
layout_tree.write(layout_path)
def remove_drawing(self, reference: ifcopenshell.entity_instance, sheet: ifcopenshell.entity_instance) -> None:
def remove_drawing(self, reference, sheet):
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -192,12 +187,7 @@ class SheetBuilder:
layout_tree.write(layout_path)
def add_document(
self,
reference: ifcopenshell.entity_instance,
document: ifcopenshell.entity_instance,
sheet: ifcopenshell.entity_instance,
) -> None:
def add_document(self, reference, document, sheet):
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(document), "svg")
if not os.path.exists(view_path):
tool.Drawing.create_svg_document(document)
@@ -234,7 +224,7 @@ class SheetBuilder:
)
layout_tree.write(layout_path)
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
def add_view_title(self, x, y, parent, layout_dir):
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
os.makedirs(os.path.dirname(title_path), exist_ok=True)
if not os.path.exists(title_path):
@@ -251,7 +241,7 @@ class SheetBuilder:
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet: ifcopenshell.entity_instance) -> dict:
def build(self, sheet):
self.references = {"SHEET": None, "RASTER": []}
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -282,7 +272,7 @@ class SheetBuilder:
return self.references
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
def build_titleblock(self, root, sheet):
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
g = self.parse_embedded_svg(image, sheet.get_info())
@@ -295,7 +285,7 @@ class SheetBuilder:
titleblock.append(g)
titleblock.remove(image)
def ensure_drawing_unique_styles(self, svg: ET.Element, drawing_id: int) -> ET.Element:
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
"""
@@ -323,7 +313,7 @@ class SheetBuilder:
brackets_level -= 1
text += l
def replace_urls(text: str) -> str:
def replace_urls(text):
"""replace urls `url(#marker)` with `url(#prefix-marker)`
since `url(#marker.prefix)` doesn't seem to work
"""
@@ -353,7 +343,7 @@ class SheetBuilder:
return svg
def build_drawings(self, root: ET.Element, sheet: ifcopenshell.entity_instance):
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"])
try:
@@ -400,7 +390,7 @@ class SheetBuilder:
for image in images:
view.remove(image)
def build_documents(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
def build_documents(self, root, sheet):
schedules = root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]')
references = root.findall('{http://www.w3.org/2000/svg}g[@data-type="reference"]')
documents = schedules + references
@@ -437,10 +427,10 @@ class SheetBuilder:
for image in images:
view.remove(image)
def get_href(self, element: ET.Element) -> str:
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace("\\", "/")
def get_href(self, element):
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace('\\','/')
def parse_embedded_svg(self, image: ET.Element, data: dict) -> ET.Element:
def parse_embedded_svg(self, image, data):
group = ET.Element("g")
group.attrib["transform"] = "translate({},{})".format(
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
@@ -476,7 +466,7 @@ class SheetBuilder:
group.append(child)
return group
def change_titleblock(self, sheet: ifcopenshell.entity_instance, titleblock_name: str) -> None:
def change_titleblock(self, sheet, titleblock_name):
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
titleblock_path = tool.Drawing.get_default_titleblock_path(titleblock_name)
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -509,7 +499,7 @@ class SheetBuilder:
sheet_tree.write(sheet_path)
def convert_to_mm(self, value: str) -> float:
def convert_to_mm(self, value):
# CSS is what defines these possibilities
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
# https://www.w3.org/TR/css-values-3/#absolute-lengths
@@ -530,5 +520,5 @@ class SheetBuilder:
return float(value[0:-2]) * (1 / 96) * 2.54 * 10
return float(value)
def mm_to_px(self, value: float) -> float:
def mm_to_px(self, value):
return (value / 25.4) * 96
@@ -93,7 +93,7 @@ class SvgWriter:
def setup_drawing_resource_paths(self, element):
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles"):
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"):
resource_path = pset.get(resource)
if not resource_path:
self.resource_paths[resource] = None
@@ -719,15 +719,14 @@ class SvgWriter:
self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), class_="ELEVATION", **text_style)
)
def get_reference_and_sheet_id_from_annotation(self, element: ifcopenshell.entity_instance) -> tuple[str, str]:
def get_reference_and_sheet_id_from_annotation(self, element):
reference_id = "-"
sheet_id = "-"
drawing = tool.Drawing.get_annotation_element(element)
reference = tool.Drawing.get_drawing_reference(drawing)
if reference:
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
reference_description = tool.Drawing.get_reference_description(sheet_reference)
if reference_description != "DRAWING" or sheet_reference.Location != reference.Location:
if sheet_reference.Description != "DRAWING" or sheet_reference.Location != reference.Location:
continue
sheet = tool.Drawing.get_reference_document(sheet_reference)
if sheet:
@@ -268,11 +268,6 @@ class BIM_PT_drawings(Panel):
col = row.column()
col.alignment = "RIGHT"
convert_to_dxf = row.row(align=True)
op = convert_to_dxf.operator("bim.convert_svg_to_dxf", text="", icon="IMAGE_DATA")
op.view = active_drawing.name
convert_to_dxf.enabled = active_drawing.ifc_definition_id > 0
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
open_drawing_button = row.row(align=True)
@@ -250,7 +250,7 @@ class AnnotationToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.launch_annotation_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
row.operator("bim.launch_annotation_type_manager", icon="LIGHTPROBE_GRID", text="")
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
@@ -17,7 +17,6 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell.util.element
import blenderbim.tool as tool
import ifcopenshell.util.placement
from mathutils import Vector
@@ -223,7 +222,7 @@ class ConnectionsData:
@classmethod
def is_connection_realization(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
connections = getattr(element, "IsConnectionRealization", None)
connections = element.IsConnectionRealization
if not connections:
return
@@ -21,14 +21,12 @@ import bmesh
import mathutils
import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.tool as tool
from math import pi, pow
from mathutils import Vector, Matrix, geometry
from typing import Union
class Helper:
def __init__(self, file: ifcopenshell.file):
def __init__(self, file):
self.file = file
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -36,7 +34,7 @@ class Helper:
# edge that shares a single vertex only with that face to find the extrusion
# edge. A face with the normal facing down is prioritised. A limited
# dissolve ensure that faces are quads and not tris.
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh: bpy.types.Mesh) -> dict:
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh):
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
@@ -117,9 +115,7 @@ class Helper:
return {"profile": profile, "extrusion": extrusion}
def auto_detect_arbitrary_profile_with_voids(
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
) -> Union[tuple, dict]:
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
@@ -142,31 +138,28 @@ class Helper:
total_groups = 0
is_circle = False
for group_type, group_indices in groups.items():
if not group_indices:
continue
is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices)
if not is_special:
continue
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
for group_index in group_indices:
if group_index in vert[deform_layer]:
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
if total_groups > 1: # A vert can only belong to one group
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
elif is_circle:
pass # Circles are allowed to be unclosed
pass # Circles are allowed to be unclosed
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
return (False, "UNCLOSED_LOOP")
for group_type, group_counts in group_verts.items():
if group_type == "IFCARCINDEX":
for group_count in group_counts.values():
if group_count != 3: # Each arc needs 3 verts
if group_count != 3: # Each arc needs 3 verts
return (False, "3POINT_ARC")
elif group_type == "IFCCIRCLE":
for group_count in group_counts.values():
if group_count != 2: # Each circle needs 2 verts
if group_count != 2: # Each circle needs 2 verts
return (False, "CIRCLE")
loop_edges = set(bm.edges)
@@ -276,7 +269,7 @@ class Helper:
inner_loops.remove(outer_loop)
# Copy vectors to prevent random data mangling after bmesh is freed.
points = [v.co.copy() for v in bm.verts]
points = [Vector(list(v.co)) for v in bm.verts]
bm.to_mesh(mesh)
mesh.update()
@@ -538,7 +531,7 @@ class Helper:
)
position = None
if self.file.schema == "IFC2X3":
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0]))
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0, 0.0]))
curve = self.file.createIfcRectangleProfileDef("AREA", None, position, xdim, ydim)
return {"curve_ucs": curve_ucs, "curve": curve}
@@ -136,23 +136,19 @@ class AddRepresentation(bpy.types.Operator, Operator):
"for Profile - 2D bounding box by local XZ axes.\n"
"For other contexts - bounding box is 3d.",
),
("OBJECT", "From Object", "Copies geometry from another object"),
("PROJECT", "Full Representation", "Reuses the current representation"),
("PROJECT", "Full Representation", ""),
],
name="Representation Conversion Method",
)
def _execute(self, context):
obj = context.active_object
props = context.scene.BIMGeometryProperties
oprops = obj.BIMGeometryProperties
ifc_context = int(oprops.contexts or "0") or None
props = obj.BIMGeometryProperties
ifc_context = int(props.contexts or "0") or None
if not ifc_context:
return
ifc_context = tool.Ifc.get().by_id(ifc_context)
original_data = obj.data
if self.representation_conversion_method == "OUTLINE":
if ifc_context.ContextType == "Plan":
data = tool.Geometry.generate_outline_mesh(obj, axis="+Z")
@@ -169,31 +165,17 @@ class AddRepresentation(bpy.types.Operator, Operator):
else:
data = tool.Geometry.generate_3d_box_mesh(obj)
tool.Geometry.change_object_data(obj, data, is_global=True)
elif (
self.representation_conversion_method == "OBJECT"
and props.representation_from_object
and props.representation_from_object.data
):
data = tool.Geometry.duplicate_object_data(props.representation_from_object)
tool.Geometry.change_object_data(obj, data, is_global=True)
try:
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=ifc_context,
ifc_representation_class=None,
profile_set_usage=None,
)
except core.IncompatibleRepresentationError:
if obj.data != original_data:
tool.Geometry.change_object_data(obj, original_data, is_global=True)
bpy.data.meshes.remove(data)
self.report({"ERROR"}, "No compatible representation for the context could be created.")
return {"CANCELLED"}
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=ifc_context,
ifc_representation_class=None,
profile_set_usage=None,
)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -201,9 +183,6 @@ class AddRepresentation(bpy.types.Operator, Operator):
def draw(self, context):
row = self.layout.row()
row.prop(self, "representation_conversion_method", text="")
if self.representation_conversion_method == "OBJECT":
row = self.layout.row()
row.prop(context.scene.BIMGeometryProperties, "representation_from_object", text="")
class SelectConnection(bpy.types.Operator, Operator):
@@ -236,13 +215,6 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
disable_opening_subtractions: bpy.props.BoolProperty()
should_switch_all_meshes: bpy.props.BoolProperty()
@classmethod
def poll(cls, context):
if context.active_object.mode == "OBJECT":
return True
cls.poll_message_set("Only available in OBJECT mode - Press TAB in the viewport")
return False
def _execute(self, context):
target_representation = tool.Ifc.get().by_id(self.ifc_definition_id)
target = target_representation.ContextOfItems
@@ -251,8 +223,6 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not obj.mode == "OBJECT":
continue
if obj == context.active_object:
representation = target_representation
else:
@@ -354,8 +324,8 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
# We are explicitly casting to a tessellation, so remove all parametric materials.
element_type = ifcopenshell.util.element.get_type(product)
if element_type: # Some invalid IFCs use material sets without a type.
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[element_type])
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[product])
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=element_type)
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=product)
else:
# These objects are parametrically based on an axis and should not be modified as a mesh
return
@@ -568,8 +538,6 @@ class OverrideDelete(bpy.types.Operator):
row.prop(self, "is_batch", text="Enable Faster Deletion")
def _execute(self, context):
start_time = time()
if self.is_batch:
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
@@ -594,11 +562,6 @@ class OverrideDelete(bpy.types.Operator):
IfcStore.add_transaction_operation(self)
# Required otherwise gizmos are still visible
context.view_layer.objects.active = None
operator_time = time() - start_time
if operator_time > 10:
self.report({"INFO"}, "IFC Delete was finished in {:.2f} seconds".format(operator_time))
return {"FINISHED"}
def rollback(self, data):
@@ -880,7 +843,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Recreate decompositions
tool.Root.recreate_decompositions(decomposition_relationships, old_to_new)
OverrideDuplicateMove.remove_linked_aggregate_data(old_to_new)
OverrideDuplicateMove.handle_linked_aggregates(old_to_new)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
@@ -933,21 +896,25 @@ class OverrideDuplicateMove(bpy.types.Operator):
if entity in old_to_new.keys():
core.remove_connection(tool.Geometry, connection=connection)
def remove_linked_aggregate_data(old_to_new):
@staticmethod
def handle_linked_aggregates(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[new[0]])
old_aggregate = ifcopenshell.util.element.get_aggregate(old)
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if old_aggregate == new_aggregate:
parts = ifcopenshell.util.element.get_parts(new_aggregate)
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index += 1
pset = tool.Ifc.get().by_id(pset['id'])
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
@@ -973,7 +940,6 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_description = "Create a new linked aggregate"
bl_idname = "bim.object_duplicate_move_linked_aggregate_macro"
bl_label = "IFC Duplicate Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
@@ -1008,33 +974,34 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
obj.select_set(True)
parts = ifcopenshell.util.element.get_parts(element)
if parts:
index = get_max_index(parts)
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
add_linked_aggregate_pset(element, index)
index += 1
index +=1
for part in parts:
if part.is_a("IfcElementAssembly"):
select_objects_and_add_data(part)
else:
index = add_linked_aggregate_pset(part, index)
# index += 1
add_linked_aggregate_pset(part, index)
index += 1
obj = tool.Ifc.get_object(part)
obj.select_set(True)
def add_linked_aggregate_pset(part, index):
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name)
pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name
)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
index += 1
else:
pass
@@ -1062,53 +1029,21 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
][0]
number = len(group_elements) - 1
number = f"{number:02d}"
new_obj = tool.Ifc.get_object(new[0])
pattern1 = r"_\d"
pattern1 = r'_\d'
if re.findall(pattern1, new_obj.name):
split_name = new_obj.name.split("_")
new_obj.name = split_name[0] + "_" + number
continue
pattern2 = r"\.\d{3}"
pattern2 = r'\.\d{3}'
if re.findall(pattern2, new_obj.name):
split_name = new_obj.name.split(".")
new_obj.name = split_name[0] + "_" + number
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
def copy_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
if pset:
new_pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=new[0], name=self.pset_name
)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=new_pset,
properties={"Index": pset["Index"]},
)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(old, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.assign_group", group=linked_aggregate_group[0], products=new)
if len(context.selected_objects) != 1:
return {"FINISHED"}
@@ -1129,16 +1064,29 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
tool.Root.recreate_aggregate(old_to_new)
copy_linked_aggregate_data(old_to_new)
custom_incremental_naming_for_element_assembly(old_to_new)
# Recreate aggregate relationship
for old in old_to_new.keys():
if old.is_a("IfcElementAssembly"):
tool.Root.recreate_aggregate(old_to_new)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
@staticmethod
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i['Index'] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
class RefreshLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.refresh_linked_aggregate"
@@ -1186,9 +1134,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
original_names[group] = {}
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"]
index = pset['Index']
original_names[group][index] = tool.Ifc.get_object(element).name
parts = ifcopenshell.util.element.get_parts(element)
if parts:
for part in parts:
@@ -1196,22 +1144,20 @@ class RefreshLinkedAggregate(bpy.types.Operator):
original_names | get_original_names(part)
else:
try:
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
except:
continue
index = pset["Index"]
continue
index = pset['Index']
original_names[group][index] = tool.Ifc.get_object(part).name
return original_names
def set_original_name(obj, original_names):
element = tool.Ifc.get_entity(obj)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if ifcopenshell.util.element.get_parts(
element
): # if element has parts it means it is the base of and aggregate or sub-aggregate
if ifcopenshell.util.element.get_parts(element): # if element has parts it means it is the base of and aggregate or sub-aggregate
aggregate = element
group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
@@ -1220,16 +1166,18 @@ class RefreshLinkedAggregate(bpy.types.Operator):
]
if not group:
return
group = group[0].id()
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"]
index = pset['Index']
try:
obj.name = original_names[group][index]
except:
return
def get_element_assembly(element):
if element.is_a("IfcElementAssembly"):
@@ -1272,22 +1220,6 @@ class RefreshLinkedAggregate(bpy.types.Operator):
return list(set(linked_aggregate_groups)), selected_parents
def get_original_matrix(element, base_instance):
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
return selected_matrix, duplicate_matrix
def set_new_matrix(selected_matrix, duplicate_matrix, old_to_new):
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
active_element = tool.Ifc.get_entity(context.active_object)
if not active_element:
self.report({"INFO"}, "Object has no Ifc metadata.")
@@ -1327,10 +1259,13 @@ class RefreshLinkedAggregate(bpy.types.Operator):
element_aggregate = ifcopenshell.util.element.get_aggregate(element)
selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance)
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
original_names = get_original_names(element)
delete_objects(element)
for obj in context.selected_objects:
@@ -1339,26 +1274,30 @@ class RefreshLinkedAggregate(bpy.types.Operator):
tool.Ifc.get_object(base_instance).select_set(True)
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
set_new_matrix(selected_matrix, duplicate_matrix, old_to_new)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
for old, new in old_to_new.items():
if element_aggregate and new[0].is_a("IfcElementAssembly"):
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if not new_aggregate:
blenderbim.core.aggregate.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(element_aggregate),
related_obj=tool.Ifc.get_object(new[0]),
)
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(element_aggregate),
related_obj=tool.Ifc.get_object(new[0]),
)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
set_original_name(new_obj, original_names)
blenderbim.bim.handler.refresh_ui_data()
operator_time = time() - refresh_start_time
@@ -1472,13 +1411,14 @@ class OverridePasteBuffer(bpy.types.Operator):
def execute(self, context):
bpy.ops.view3d.pastebuffer()
for obj in context.selected_objects:
# Pasted objects may come from another Blender session, or even
# from the same session where the original object has since
# been deleted. As the source element may not exist, paste will
# always unlink the element. If you want to duplicate an
# element, use the duplicate commands.
tool.Root.unlink_object(obj)
if IfcStore.get_file():
for obj in context.selected_objects:
# Pasted objects may come from another Blender session, or even
# from the same session where the original object has since
# been deleted. As the source element may not exist, paste will
# always unlink the element. If you want to duplicate an
# element, use the duplicate commands.
tool.Root.unlink_object(obj)
return {"FINISHED"}
@@ -1505,9 +1445,7 @@ class OverrideModeSetEdit(bpy.types.Operator):
for obj in selected_objs:
if not obj:
continue
obj_supports_edit_mode, message = tool.Blender.object_supports_edit_mode(obj)
if not obj_supports_edit_mode:
self.report({"INFO"}, message)
if not obj.data:
obj.select_set(False)
continue
element = tool.Ifc.get_entity(obj)
@@ -138,4 +138,3 @@ class BIMGeometryProperties(PropertyGroup):
name="IFC Interaction Mode",
update=update_mode,
)
representation_from_object: PointerProperty(type=bpy.types.Object)
@@ -212,7 +212,7 @@ class BIM_PT_representation_items(Panel):
if props.is_editing_item_style:
# NOTE: we currently support 1 item having just 1 style
# when IfcStyledItem can actually have multiple styles
prop_with_search(row, props, "representation_item_style", icon="SHADING_RENDERED", text="")
row.prop(props, "representation_item_style", icon="SHADING_RENDERED", text="")
row.operator("bim.edit_representation_item_style", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_representation_item_style", icon="CANCEL", text="")
else:
@@ -212,7 +212,7 @@ class UnassignGroup(bpy.types.Operator, tool.Ifc.Operator):
"group.unassign_group",
self.file,
**{
"products": [self.file.by_id(product.BIMObjectProperties.ifc_definition_id)],
"product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id),
"group": self.file.by_id(self.group),
}
)
@@ -151,10 +151,7 @@ class AssignPresentationLayer(bpy.types.Operator):
ifcopenshell.api.run(
"layer.assign_layer",
self.file,
**{
"items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
"layer": self.file.by_id(self.layer),
}
**{"item": self.file.by_id(item.BIMMeshProperties.ifc_definition_id), "layer": self.file.by_id(self.layer)}
)
return {"FINISHED"}
@@ -176,10 +173,7 @@ class UnassignPresentationLayer(bpy.types.Operator):
ifcopenshell.api.run(
"layer.unassign_layer",
self.file,
**{
"items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
"layer": self.file.by_id(self.layer),
}
**{"item": self.file.by_id(item.BIMMeshProperties.ifc_definition_id), "layer": self.file.by_id(self.layer)}
)
return {"FINISHED"}
@@ -24,7 +24,6 @@ classes = (
operator.AddLayer,
operator.AddListItem,
operator.AddMaterial,
operator.DuplicateMaterial,
operator.AddMaterialSet,
operator.AddProfile,
operator.AssignMaterial,
@@ -19,8 +19,6 @@
import os
import bpy
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.doc
import ifcopenshell.util.schema
import blenderbim.tool as tool
@@ -111,41 +109,29 @@ class MaterialsData:
def active_styles(cls):
props = bpy.context.scene.BIMMaterialProperties
results = []
if not props.materials or props.active_material_index >= len(props.materials):
return results
material = props.materials[props.active_material_index].ifc_definition_id
if not material:
return results
material = tool.Ifc.get_entity_by_id(material)
# NOTE: it's possible that data will be refreshed during material removal
# (when it will try to fetch active material type and will reach for material_types)
# and props.materials[i].ifc_definition_id will contain already removed id
if not material or not material.is_a("IfcMaterial"):
return results
for definition in material.HasRepresentation:
for representation in definition.Representations:
if not representation.is_a("IfcStyledRepresentation"):
continue
context = representation.ContextOfItems
for item in representation.Items:
if not item.is_a("IfcStyledItem"):
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index].ifc_definition_id
if not material:
return results
material = tool.Ifc.get().by_id(material)
for definition in material.HasRepresentation:
for representation in definition.Representations:
if not representation.is_a("IfcStyledRepresentation"):
continue
for style in item.Styles:
if style.is_a("IfcSurfaceStyle"):
results.append(
{
context = representation.ContextOfItems
for item in representation.Items:
if not item.is_a("IfcStyledItem"):
continue
for style in item.Styles:
if style.is_a("IfcSurfaceStyle"):
results.append({
"context_type": context.ContextType,
"context_identifier": getattr(context, "ContextIdentifier", ""),
"target_view": getattr(context, "TargetView", ""),
"name": style.Name or "Unnamed",
"id": style.id(),
"context_id": context.id(),
}
)
})
return results
@@ -167,9 +153,6 @@ class ObjectMaterialData:
cls.data["type_material"] = cls.type_material()
cls.data["material_type"] = cls.material_type()
cls.data["active_material_constituents"] = cls.active_material_constituents()
# after material_name and type_material
cls.data["is_type_material_overridden"] = cls.is_type_material_overridden()
cls.is_loaded = True
@classmethod
@@ -298,7 +281,8 @@ class ObjectMaterialData:
@classmethod
def material_name(cls):
material = cls.material
element = tool.Ifc.get_entity(bpy.context.active_object)
material = ifcopenshell.util.element.get_material(element)
if material:
return getattr(material, "Name", None) or "Unnamed"
@@ -342,18 +326,3 @@ class ObjectMaterialData:
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
return []
return [m.Name for m in material.MaterialConstituents if m.Name]
@classmethod
def is_type_material_overridden(cls) -> bool:
if not cls.data["type_material"]:
return False
# try to avoid accessing ifc
if cls.data["material_name"] != cls.data["type_material"]:
return True
# in theory material can be overridden by the same material
# so we check occurrence material explicitly
element = tool.Ifc.get_entity(bpy.context.active_object)
occurrence_material = ifcopenshell.util.element.get_material(element, should_inherit=False)
return bool(occurrence_material)
@@ -118,7 +118,7 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_material"
bl_label = "Add Material"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(name="Material Name")
obj: bpy.props.StringProperty()
name: bpy.props.StringProperty(default="Default")
def invoke(self, context, event):
@@ -134,19 +134,6 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
material_prop_purge()
class DuplicateMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_material"
bl_label = "Diplicate Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material ID")
def _execute(self, context):
ifc_file = tool.Ifc.get()
tool.Material.duplicate_material(ifc_file.by_id(self.material))
material_prop_purge()
bpy.ops.bim.load_materials()
class AddMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_material_set"
bl_label = "Add Material Set"
@@ -61,8 +61,6 @@ class BIM_PT_materials(Panel):
if self.props.materials and self.props.active_material_index < len(self.props.materials):
material = self.props.materials[self.props.active_material_index]
if material.ifc_definition_id:
op = row.operator("bim.duplicate_material", text="", icon="DUPLICATE")
op.material = material.ifc_definition_id
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
op = row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL")
@@ -177,13 +175,7 @@ class BIM_PT_object_material(Panel):
if ObjectMaterialData.data["type_material"]:
row = self.layout.row(align=True)
if ObjectMaterialData.data["is_type_material_overridden"]:
row.label(
text=f"Inherited Material Is Occurrence Overridden",
icon="CON_CHILDOF",
)
else:
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="CON_CHILDOF")
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="FILE_PARENT")
if ObjectMaterialData.data["material_class"]:
return self.draw_material_ui()
@@ -83,17 +83,8 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props = obj.BIMArrayProperties
relating_obj = props.relating_array_object
if relating_obj:
element = tool.Ifc.get_entity(relating_obj)
parent_globalid = ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Parent")
parent_element = tool.Ifc.get().by_guid(parent_globalid)
data = json.loads(ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array", "Data"))[self.item]
else:
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props.count = data["count"]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props.x = data["x"] * si_conversion
@@ -102,9 +93,7 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
props.use_local_space = data.get("use_local_space", False)
props.sync_children = data.get("sync_children", False)
props.method = data.get("method", "OFFSET")
props.is_editing = self.item
return {"FINISHED"}
@@ -148,10 +137,6 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
#clears the relating_array_object so it doesn't show again next time
props.relating_array_object = None
return {"FINISHED"}
@@ -32,9 +32,7 @@ class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operato
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
return tool.Ifc.get_entity(collection_obj) and relating_type == "FLOORING"
return tool.Ifc.get_entity(collection_obj)
def _execute(self, context):
@@ -63,9 +61,7 @@ class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
return tool.Ifc.get_entity(collection_obj) and relating_type == "CEILING"
return tool.Ifc.get_entity(collection_obj)
def _execute(self, context):
@@ -120,11 +116,9 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if element:
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects and relating_type == "FLOORING"
return context.selected_objects
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -153,7 +147,7 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_ceiling_coverings_from_walls"
bl_label = "Add Ceiling From Walls"
bl_label = "Add Ceilings From Walls"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add instance ceiling coverings from selected walls. The active object must be a wall and layered vertically"
@@ -161,11 +155,9 @@ class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if element:
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects and relating_type == "CEILING"
return context.selected_objects
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -23,7 +23,6 @@ from math import sin, cos, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from typing import Union
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
@@ -36,6 +35,15 @@ def transparent_color(color, alpha=0.1):
return color
def bm_check_vertex_in_groups(vertex, deform_layer, groups):
"""returns tuple boolean (whether vertex is in any of the groups)
and related group index"""
for group_index in vertex[deform_layer].keys():
if group_index in groups:
return True, group_index
return False, None
class ProfileDecorator:
installed = None
@@ -140,12 +148,12 @@ class ProfileDecorator:
# deform_layer is None if there are no verts assigned to vertex groups
# even if there are vertex groups in the obj.vertex_groups
if deform_layer:
is_arc, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, arc_groups)
is_arc, group_index = bm_check_vertex_in_groups(vertex, deform_layer, arc_groups)
if is_arc:
arcs.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
is_circle, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, circle_groups)
is_circle, group_index = bm_check_vertex_in_groups(vertex, deform_layer, circle_groups)
if is_circle:
circles.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
@@ -542,7 +542,7 @@ class MEPGenerator:
ifc_representation_class=None,
)
rel = ifcopenshell.api.run("material.assign_material", ifc_file, products=[element], type="IfcMaterialProfileSet")
rel = ifcopenshell.api.run("material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet")
profile_set = rel.RelatingMaterial
material_profile = ifcopenshell.api.run(
"material.add_profile", ifc_file, profile_set=profile_set, material=material
@@ -24,14 +24,10 @@ import shapely
import logging
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.geom
import ifcopenshell.util.shape
import ifcopenshell.util.element
import ifcopenshell.util.shape_builder
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.core.geometry
import blenderbim.bim.import_ifc as import_ifc
@@ -111,7 +107,7 @@ class FilledOpeningGenerator:
# Equivalent to "side Z" for a wall axis, so that stuff like skylights appear on the top.
local_position_on_voided_obj.z = layers["offset"] + layers["thickness"]
new_matrix.translation.xyz = voided_obj.matrix_world @ local_position_on_voided_obj
rotation_matrix = Matrix.Rotation(radians(-90), 4, "X")
rotation_matrix = Matrix.Rotation(radians(-90), 4, 'X')
new_matrix @= rotation_matrix
filling_obj.matrix_world = new_matrix
@@ -188,13 +184,7 @@ class FilledOpeningGenerator:
)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
relating_type = settings["relating_type"]
for related_object in settings["related_objects"]:
self._regenerate_from_type(related_object)
def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None:
filling = related_object
filling = settings["related_object"]
if not getattr(filling, "FillsVoids", None):
return
@@ -25,13 +25,9 @@ import ifcopenshell.util.system
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.type
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.core.aggregate
import blenderbim.core.type
import blenderbim.core.geometry
import blenderbim.core.spatial
from . import wall, slab, profile, mep
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.data import AuthoringData
@@ -39,7 +35,6 @@ from mathutils import Vector, Matrix
from bpy_extras.object_utils import AddObjectHelper
from . import prop
import json
from typing import Any, Union
class EnableAddType(bpy.types.Operator, tool.Ifc.Operator):
@@ -278,7 +273,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
obj.location[2] = collection_obj.location[2] + bpy.context.scene.BIMModelProperties.rl2
@staticmethod
def generate_layered_element(ifc_class: str, relating_type: ifcopenshell.entity_instance) -> bool:
def generate_layered_element(ifc_class, relating_type):
layer_set_direction = None
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
@@ -302,7 +297,6 @@ class AddConstrTypeInstance(bpy.types.Operator):
material = ifcopenshell.util.element.get_material(tool.Ifc.get_entity(obj))
material.LayerSetDirection = layer_set_direction
return True
return False
class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
@@ -512,11 +506,11 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings):
)
def ensure_material_assigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
def ensure_material_assigned(usecase_path, ifc_file, settings):
if usecase_path == "material.assign_material":
if not settings.get("material", None):
return
elements = settings["products"]
elements = [settings["product"]]
else:
elements = []
for rel in ifc_file.by_type("IfcRelAssociatesMaterial"):
@@ -525,76 +519,53 @@ def ensure_material_assigned(usecase_path: str, ifc_file: ifcopenshell.file, set
]:
elements.extend(rel.RelatedObjects)
update_blender_ifc_materials(elements)
def ensure_material_unassigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
elements = settings["products"]
if elements[0].is_a("IfcElementType"):
elements.extend(ifcopenshell.util.element.get_types(elements[0]))
update_blender_ifc_materials(elements)
def update_blender_ifc_materials(elements: list[ifcopenshell.entity_instance]) -> None:
"""update mesh blender materials that have ifc material connected to them
by replacing them with `blender_material`"""
# since different elements can share meshes (e.g. occurrecnes without openings)
# we need to make sure not to affect them accidentally
meshes_users: dict[bpy.types.Mesh, set[bpy.types.Object]] = dict()
for obj in bpy.data.objects:
if not obj.data:
continue
meshes_users.setdefault(obj.data, set()).add(obj)
objects: set[bpy.types.Object] = set()
for element in elements:
obj: bpy.types.Object = tool.Ifc.get_object(element)
obj = IfcStore.get_element(element.GlobalId)
if not obj or not obj.data:
continue
objects.add(obj)
meshes: set[bpy.types.Mesh] = {obj.data for obj in objects}
element_material = ifcopenshell.util.element.get_material(element)
material = [m for m in ifc_file.traverse(element_material) if m.is_a("IfcMaterial")]
for mesh in meshes:
mesh_users = meshes_users[mesh]
if not mesh_users.issubset(objects):
object_material_ids = [
om.BIMObjectProperties.ifc_definition_id
for om in obj.data.materials
if om is not None and om.BIMObjectProperties.ifc_definition_id
]
if material and material[0].id() in object_material_ids:
continue
# NOTE: we need `obj` as removing materials and appending them to `mesh.materials`
# will mess up mesh faces material indices
if len(obj.data.materials) == 1:
obj.data.materials.clear()
# NOTE: we make an assumption here that all mesh users
# have the same material - they either inherit it from the type
# or type doesn't have a material.
#
# If we add option to UI to add materials overriding type materials
# then this assumption won't be safe anymore
if not material:
continue
obj = next(iter(mesh_users))
element = tool.Ifc.get_entity(obj)
current_material = ifcopenshell.util.element.get_material(element)
if current_material:
current_material = tool.Ifc.get_object(current_material)
obj.data.materials.append(IfcStore.get_element(material[0].id()))
material_replaced = False
for material_slot in obj.material_slots:
material = material_slot.material
if material is None:
def ensure_material_unassigned(usecase_path, ifc_file, settings):
elements = [settings["product"]]
if elements[0].is_a("IfcElementType"):
elements.extend(ifcopenshell.util.element.get_types(elements[0]))
for element in elements:
obj = tool.Ifc.get_object(element)
if not obj or not obj.data:
continue
element_material = ifcopenshell.util.element.get_material(element)
if element_material:
continue
to_remove = []
for i, slot in enumerate(obj.material_slots):
if not slot.material:
continue
ifc_material = tool.Ifc.get_entity(material)
# it's blender material for style, so ignore it
if not ifc_material:
continue
if ifc_material == current_material:
continue
material_slot.material = current_material
material_replaced = True
if not material_replaced and current_material:
mesh.materials.append(current_material)
# clear empty slots
for i, material in reversed(list(enumerate(mesh.materials[:]))):
if material is None:
mesh.materials.pop(index=i)
material = tool.Ifc.get_entity(slot.material)
if material:
to_remove.append(i)
total_removed = 0
for i in to_remove:
obj.active_material_index = i - total_removed
with bpy.context.temp_override(object=obj):
bpy.ops.object.material_slot_remove()
total_removed += 1
@@ -21,18 +21,14 @@ import copy
import bmesh
import mathutils.geometry
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.type
import blenderbim.core.geometry
import blenderbim.core.material
import blenderbim.core.root
from math import pi, degrees, inf
from mathutils import Vector, Matrix, Quaternion
from blenderbim.bim.module.geometry.helper import Helper
@@ -96,7 +92,7 @@ class DumbProfileGenerator:
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
material = ifcopenshell.util.element.get_material(element)
material.CardinalPoint = self.cardinal_point
@@ -211,19 +207,12 @@ class DumbProfileRegenerator:
return results
def regenerate_from_type(self, usecase_path, ifc_file, settings):
relating_type = settings["relating_type"]
new_material = ifcopenshell.util.element.get_material(relating_type)
if not new_material or not new_material.is_a("IfcMaterialProfileSet"):
return
for related_object in settings["related_objects"]:
self._regenerate_from_type(related_object)
def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None:
obj = tool.Ifc.get_object(related_object)
obj = tool.Ifc.get_object(settings["related_object"])
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
if not new_material or not new_material.is_a("IfcMaterialProfileSet"):
return
DumbProfileRecalculator().recalculate([obj])
@@ -87,21 +87,6 @@ def update_type_page(self, context):
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
def update_relating_array_from_object(self, context):
bpy.ops.bim.enable_editing_array(item=self.is_editing)
return
def is_object_array_applicable(self, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return False
return ifcopenshell.util.element.get_pset(element, "BBIM_Array")
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
relating_type_id: bpy.props.EnumProperty(
@@ -218,14 +203,6 @@ class BIMArrayProperties(PropertyGroup):
description="Regenerate all children based on the parent object",
default=False,
)
relating_array_object: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Copy Array Properties",
update=update_relating_array_from_object,
poll=is_object_array_applicable,
)
class BIMStairProperties(PropertyGroup):
@@ -20,22 +20,17 @@ import bpy
import json
import bmesh
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.util.element
import blenderbim.bim.handler
import blenderbim.core.type
import blenderbim.core.geometry
import blenderbim.core.root
import blenderbim.tool as tool
from math import radians
from mathutils import Vector, Matrix
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.decorator import ProfileDecorator
from typing import Optional
def calculate_quantities(usecase_path, ifc_file, settings):
@@ -115,7 +110,7 @@ def calculate_quantities(usecase_path, ifc_file, settings):
class DumbSlabGenerator:
def __init__(self, relating_type: ifcopenshell.entity_instance):
def __init__(self, relating_type):
self.relating_type = relating_type
def generate(self):
@@ -178,7 +173,7 @@ class DumbSlabGenerator:
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
representation = ifcopenshell.api.run(
@@ -251,38 +246,27 @@ class DumbSlabPlaner:
self.change_thickness(element, total_thickness)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
relating_type = settings["relating_type"]
new_material = ifcopenshell.util.element.get_material(relating_type)
obj = tool.Ifc.get_object(settings["related_object"])
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
if not new_material or not new_material.is_a("IfcMaterialLayerSet"):
return
parametric = ifcopenshell.util.element.get_psets(settings["relating_type"]).get("EPset_Parametric")
layer_set_direction = None
if parametric:
layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction)
new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers])
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
for related_object in settings["related_objects"]:
self._regenerate_from_type(related_object, layer_set_direction, new_thickness)
def _regenerate_from_type(
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str], new_thickness: float
) -> None:
obj = tool.Ifc.get_object(related_object)
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
material = ifcopenshell.util.element.get_material(related_object)
material = ifcopenshell.util.element.get_material(settings["related_object"])
if not material or not material.is_a("IfcMaterialLayerSetUsage"):
return
if layer_set_direction:
material.LayerSetDirection = layer_set_direction
if material.LayerSetDirection == "AXIS3":
self.change_thickness(related_object, new_thickness)
self.change_thickness(settings["related_object"], new_thickness)
def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float) -> None:
def change_thickness(self, element, thickness):
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
obj = tool.Ifc.get_object(element)
if not obj:
@@ -714,7 +698,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
new_footprint = ifcopenshell.api.run(
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
)
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
old_footprint = ifcopenshell.util.representation.get_representation(
element, "Plan", "FootPrint", "SKETCH_VIEW"
)
if old_footprint:
for inverse in tool.Ifc.get().get_inverse(old_footprint):
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
@@ -27,7 +27,7 @@ import blenderbim.core.type
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_space"
bl_label = "Generate Space from Cursor"
bl_label = "Generate Space"
bl_options = {"REGISTER"}
bl_description = (
"Create a space from the cursor position. "
@@ -35,34 +35,30 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
"select the right space collection and run the operator"
)
# @classmethod
# def poll(cls, context):
# print(context)
# collection = context.view_layer.active_layer_collection.collection
# collection_obj = collection.BIMCollectionProperties.obj
# active_obj = context.active_object
# element = tool.Ifc.get_entity(active_obj)
# return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall")
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall")
def _execute(self, context):
# This works as a 2.5 extruded polygon based on a cutting plane. Note
# that rooms exclude walls (i.e. not to wall midpoint or exterior /
# exterior edge.
def msg_no_collection(self, context):
self.layout.label(text="NO ACTIVE COLLECTION. PLEASE SELECT A SPATIAL COLLECTION OBJECT OR A WALL")
def msg_no_active_storey(self, context):
self.layout.label(text="NO ACTIVE STOREY. PLEASE SELECT A SPATIAL COLLECTION OBJECT")
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if not collection_obj:
context.window_manager.popup_menu(msg_no_collection, title="Error", icon="ERROR")
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element or not spatial_element.is_a("IfcBuildingStorey"):
context.window_manager.popup_menu(msg_no_active_storey, title="Error", icon="ERROR")
if not spatial_element:
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
core.generate_space(tool.Ifc, tool.Spatial, tool.Model, tool.Type)
@@ -74,12 +70,12 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Generate spaces from selected walls. The active object must be a wall"
# @classmethod
# def poll(cls, context):
# active_obj = context.active_object
# element = tool.Ifc.get_entity(active_obj)
# if element:
# return context.selected_objects and element.is_a("IfcWall")
@classmethod
def poll(cls, context):
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
if element:
return context.selected_objects and element.is_a("IfcWall")
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -91,30 +87,19 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(active_obj)
container = tool.Spatial.get_container(element)
def msg_no_active_object(self, context):
self.layout.label(text="No active object. Please select a wall")
def msg_no_active_wall(self, context):
self.layout.label(text="The active object is not a wall. Please select a wall.")
def msg_no_container(self, context):
self.layout.label(text="The wall is not contained. Please the selected wall in a building container")
def msg_no_selected_objects(self, context):
self.layout.label(text="No selected objects found. Please select walls.")
if not active_obj:
context.window_manager.popup_menu(msg_no_active_object, title="Error", icon="ERROR")
self.report({"ERROR"}, "No active object. Please select a wall")
return
element = tool.Ifc.get_entity(active_obj)
if element and not element.is_a("IfcWall"):
context.window_manager.popup_menu(msg_no_active_wall, title="Error", icon="ERROR")
return
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
if not container:
context.window_manager.popup_menu(msg_no_container, title="Error", icon="ERROR")
return
self.report({"ERROR"}, "The wall is not contained.")
if not context.selected_objects:
context.window_manager.popup_menu(msg_no_selected_objects, title="Error", icon="ERROR")
self.report({"ERROR"}, "No selected objects found. Please select walls.")
return
core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector)
@@ -54,11 +54,6 @@ class LaunchTypeManager(bpy.types.Operator):
props.type_page = 1
if get_ifc_class(None, context):
ifc_class = props.ifc_class or AuthoringData.data["ifc_element_type"]
else:
ifc_class = AuthoringData.data["ifc_element_type"]
# will be None if project has no types
if ifc_class is not None:
props.type_class = ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
return context.window_manager.invoke_popup(self, width=550)
@@ -223,8 +218,6 @@ class BIM_PT_array(bpy.types.Panel):
row = col.row(align=True)
row.prop(props, "z")
row.operator("bim.input_cursor_z_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "relating_array_object", icon="COPYDOWN")
else:
row = box.row(align=True)
name = f"{array['count']} Items ({array.get('method', 'OFFSET').capitalize()})"
@@ -24,9 +24,7 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.type
import mathutils.geometry
import blenderbim.bim.handler
import blenderbim.core.type
@@ -37,7 +35,6 @@ from blenderbim.bim.ifc import IfcStore
from math import pi, sin, cos, degrees, radians
from mathutils import Vector, Matrix
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
from typing import Optional
class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
@@ -558,10 +555,7 @@ class DumbWallGenerator:
for face in sibling_obj.data.polygons:
normal = (sibling_obj.matrix_world.to_quaternion() @ face.normal).normalized()
face_center = sibling_obj.matrix_world @ face.center
if (
normal.z != 0
or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01
):
if normal.z != 0 or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01:
continue
rotation = math.atan2(normal[1], normal[0])
@@ -609,7 +603,7 @@ class DumbWallGenerator:
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
if self.axis_context:
representation = ifcopenshell.api.run(
"geometry.add_axis_representation",
@@ -730,29 +724,18 @@ class DumbWallPlaner:
DumbWallRecalculator().recalculate([w for w in set(walls) if w])
def regenerate_from_type(self, usecase_path, ifc_file, settings):
relating_type = settings["relating_type"]
new_material = ifcopenshell.util.element.get_material(relating_type)
obj = tool.Ifc.get_object(settings["related_object"])
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
if not new_material or not new_material.is_a("IfcMaterialLayerSet"):
return
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
parametric = ifcopenshell.util.element.get_psets(settings["relating_type"]).get("EPset_Parametric")
layer_set_direction = None
if parametric:
layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction)
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
for related_object in settings["related_objects"]:
self._regenerate_from_type(related_object, layer_set_direction)
def _regenerate_from_type(
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str]
) -> None:
obj = tool.Ifc.get_object(related_object)
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
return
material = ifcopenshell.util.element.get_material(related_object)
material = ifcopenshell.util.element.get_material(settings["related_object"])
if not material or not material.is_a("IfcMaterialLayerSetUsage"):
return
if layer_set_direction:
@@ -871,11 +854,11 @@ class DumbWallJoiner:
if (location_on_base - location).length < (location_on_side - location).length:
axis_offset = location_on_side - location_on_base
offset_from_axis = location_on_base - location
opening_matrix.translation = location_on_base - axis_offset - offset_from_axis
opening_matrix.translation = (location_on_base - axis_offset - offset_from_axis)
else:
axis_offset = location_on_side - location_on_base
offset_from_axis = location_on_side - location
opening_matrix.translation = location_on_side - axis_offset - offset_from_axis
opening_matrix.translation = (location_on_side - axis_offset - offset_from_axis)
opening_matrixes[opening] = opening_matrix
for filling in [r.RelatedBuildingElement for r in opening.HasFillings]:
@@ -888,11 +871,11 @@ class DumbWallJoiner:
if (location_on_base - location).length < (location_on_side - location).length:
axis_offset = location_on_side - location_on_base
offset_from_axis = location_on_base - location
filling_matrix.translation = location_on_base - axis_offset - offset_from_axis
filling_matrix.translation = (location_on_base - axis_offset - offset_from_axis)
else:
axis_offset = location_on_side - location_on_base
offset_from_axis = location_on_side - location
filling_matrix.translation = location_on_side - axis_offset - offset_from_axis
filling_matrix.translation = (location_on_side - axis_offset - offset_from_axis)
filling_matrixes[filling] = filling_matrix
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
@@ -1137,8 +1120,13 @@ class DumbWallJoiner:
other = tool.Ifc.get_object(rel.RelatedElement)
if connection not in ["ATPATH", "NOTDEFINED"]:
self.join(
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
)
obj,
other,
connection,
rel.RelatedConnectionType,
is_relating=True,
description=rel.Description
)
for rel in element.ConnectedFrom:
if rel.is_a("IfcRelConnectsPathElements"):
connection = rel.RelatedConnectionType
@@ -378,7 +378,6 @@ class BimToolUI:
op.depth = cls.props.extrusion_depth
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
if AuthoringData.data["active_class"] in (
"IfcCableCarrierSegment",
@@ -386,8 +385,8 @@ class BimToolUI:
"IfcDuctSegment",
"IfcPipeSegment",
):
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "")
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "")
if context.region.type != "TOOL_HEADER":
cls.layout.operator("bim.mep_add_bend")
cls.layout.operator("bim.mep_add_transition")
@@ -395,6 +394,7 @@ class BimToolUI:
else:
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
@@ -516,21 +516,20 @@ class BimToolUI:
prop_with_search(row, cls.props, "relating_type_id", text="")
else:
row.label(text="No Construction Type", icon="FILE_3D")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
else:
if AuthoringData.data["ifc_element_type"]:
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
row = cls.layout.row()
row.prop(cls.props, "type_name")
row = cls.layout.row(align=True)
row = cls.layout.row()
op = row.operator(
"bim.add_default_type", icon="ADD", text=f"Add {AuthoringData.data['ifc_element_type']}"
)
op.ifc_element_type = AuthoringData.data["ifc_element_type"]
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
else:
row.label(text="No Element Types Found", icon="ERROR")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
@classmethod
def draw_basic_bim_tool_interface(cls):
@@ -719,9 +718,10 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.flip_wall()
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
bpy.ops.bim.flip_fill()
elif self.active_material_usage == "PROFILE":
elif self.active_class in ("IfcBeam", "IfcColumn"):
bpy.ops.bim.flip_object(flip_local_axes="XZ")
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
bpy.ops.bim.fit_flow_segments()
def hotkey_S_G(self):
obj = bpy.context.active_object
@@ -807,12 +807,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.join_wall(join_type="V")
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
bpy.ops.bim.fit_flow_segments()
elif self.active_material_usage == "PROFILE":
bpy.ops.bim.extend_profile(join_type="V")
def hotkey_S_B(self):
bpy.ops.bim.add_boundary()
@@ -21,7 +21,6 @@ from . import ui, prop, operator, data
classes = (
operator.AddProfileDef,
operator.DuplicateProfileDef,
operator.DisableEditingArbitraryProfile,
operator.DisableEditingProfile,
operator.DisableProfileEditingUI,
@@ -66,15 +66,9 @@ class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator):
profile: bpy.props.IntProperty()
def _execute(self, context):
props = context.scene.BIMProfileProperties
current_index = props.active_profile_index
ifcopenshell.api.run("profile.remove_profile", tool.Ifc.get(), profile=tool.Ifc.get().by_id(self.profile))
bpy.ops.bim.load_profiles()
# preserve selected index if possible
if props.profiles:
props.active_profile_index = min(current_index, len(props.profiles) - 1)
class EnableEditingProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_profile"
@@ -132,27 +126,6 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.load_profiles()
class DuplicateProfileDef(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_profile_def"
bl_label = "Duplicate Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = context.scene.BIMProfileProperties
if len(props.profiles) > props.active_profile_index:
return True
cls.poll_message_set("No profile selected to duplicate.")
return False
def _execute(self, context):
props = context.scene.BIMProfileProperties
ifc_file = tool.Ifc.get()
profile = ifc_file.by_id(props.profiles[props.active_profile_index].ifc_definition_id)
tool.Profile.duplicate_profile(profile)
bpy.ops.bim.load_profiles()
class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_arbitrary_profile"
bl_label = "Enable Editing Arbitrary Profile"
@@ -70,7 +70,6 @@ class BIM_PT_profiles(Panel):
row = self.layout.row(align=True)
row.prop(self.props, "profile_classes", text="")
row.operator("bim.add_profile_def", text="", icon="ADD")
row.operator("bim.duplicate_profile_def", icon="DUPLICATE", text="")
self.layout.template_list(
"BIM_UL_profiles",
@@ -25,8 +25,6 @@ classes = (
operator.AppendLibraryElement,
operator.AppendLibraryElementByQuery,
operator.AssignLibraryDeclaration,
operator.BIM_OT_load_clipping_planes,
operator.BIM_OT_save_clipping_planes,
operator.ChangeLibraryElement,
operator.CreateClippingPlane,
operator.CreateProject,
@@ -21,7 +21,6 @@ import bpy
import time
import logging
import tempfile
import traceback
import subprocess
import numpy as np
import ifcopenshell
@@ -40,15 +39,12 @@ from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.ui import IFCFileSelector
from blenderbim.bim import import_ifc
from blenderbim.bim import export_ifc
from collections import defaultdict
import json
from math import radians
from pathlib import Path
from mathutils import Vector, Matrix
from bpy.app.handlers import persistent
from blenderbim.bim.module.project.data import LinksData
from blenderbim.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
from typing import Union
class NewProject(bpy.types.Operator):
@@ -117,10 +113,6 @@ class CreateProject(bpy.types.Operator):
if tool.Blender.is_default_scene():
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
for mesh in bpy.data.meshes:
bpy.data.meshes.remove(mesh)
for mat in bpy.data.materials:
bpy.data.materials.remove(mat)
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
tool.Blender.register_toolbar()
@@ -303,7 +295,7 @@ class AssignLibraryDeclaration(bpy.types.Operator):
ifcopenshell.api.run(
"project.assign_declaration",
self.file,
definitions=[self.file.by_id(self.definition)],
definition=self.file.by_id(self.definition),
relating_context=self.file.by_type("IfcProjectLibrary")[0],
)
element_name = self.props.active_library_element
@@ -339,7 +331,7 @@ class UnassignLibraryDeclaration(bpy.types.Operator):
ifcopenshell.api.run(
"project.unassign_declaration",
self.file,
definitions=[self.file.by_id(self.definition)],
definition=self.file.by_id(self.definition),
relating_context=self.file.by_type("IfcProjectLibrary")[0],
)
element_name = self.props.active_library_element
@@ -675,23 +667,19 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
return self.finish_loading_project(context)
def finish_loading_project(self, context):
try:
if not self.is_existing_ifc_file():
return {"FINISHED"}
if not self.is_existing_ifc_file():
return {"FINISHED"}
if tool.Blender.is_default_scene():
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
if tool.Blender.is_default_scene():
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
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"))
tool.Blender.register_toolbar()
if not self.is_advanced:
bpy.ops.bim.load_project_elements()
except:
blenderbim.last_error = traceback.format_exc()
raise
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"))
tool.Blender.register_toolbar()
if not self.is_advanced:
bpy.ops.bim.load_project_elements()
return {"FINISHED"}
def invoke(self, context, event):
@@ -868,21 +856,9 @@ class LinkIfc(bpy.types.Operator):
continue
new = context.scene.BIMProjectProperties.links.add()
if self.use_relative_path:
try:
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
except:
pass # Perhaps on another drive or something
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
new.name = filepath
status = bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
if status == {"CANCELLED"}:
error_msg = (
f'Error processing IFC file "{self.filepath}" '
"was critical and blend file either wasn't saved or wasn't updated. "
"See logs above in system console for details."
)
print(error_msg)
self.report({"ERROR"}, error_msg)
return {"FINISHED"}
bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
print(f"Finished linking {len(files)} IFCs", time.time() - start)
return {"FINISHED"}
@@ -923,11 +899,11 @@ class UnloadLink(bpy.types.Operator):
if filepath.suffix.lower() == ".ifc":
filepath = filepath.with_suffix(".ifc.cache.blend")
for collection in context.scene.collection.children[:]:
for collection in context.scene.collection.children:
if collection.library and Path(collection.library.filepath) == filepath:
bpy.data.collections.remove(collection)
context.scene.collection.children.unlink(collection)
for scene in bpy.data.scenes[:]:
for scene in bpy.data.scenes:
if scene.library and Path(scene.library.filepath) == filepath:
bpy.data.scenes.remove(scene)
@@ -961,12 +937,10 @@ class LoadLink(bpy.types.Operator):
if self.filepath.lower().endswith(".blend"):
self.link_blend(filepath)
elif self.filepath.lower().endswith(".ifc"):
status = self.link_ifc()
if status:
return status
self.link_ifc()
return {"FINISHED"}
def link_blend(self, filepath: str) -> None:
def link_blend(self, filepath):
with bpy.data.libraries.load(filepath, link=True) as (data_from, data_to):
data_to.scenes = data_from.scenes
for scene in bpy.data.scenes:
@@ -979,7 +953,7 @@ class LoadLink(bpy.types.Operator):
link = bpy.context.scene.BIMProjectProperties.links.get(self.filepath)
link.is_loaded = True
def link_ifc(self) -> Union[set[str], None]:
def link_ifc(self):
blend_filepath = self.filepath + ".cache.blend"
h5_filepath = self.filepath + ".cache.h5"
@@ -999,14 +973,11 @@ except Exception as e:
exit(1)
"""
t = time.time()
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
temp_file.write(code)
run = subprocess.run([bpy.app.binary_path, "-b", "--python", temp_file.name, "--python-exit-code", "1"])
if run.returncode == 1:
print("An error occurred while processing your IFC.")
if not os.path.exists(blend_filepath) or os.stat(blend_filepath).st_mtime < t:
return {"CANCELLED"}
self.link_blend(blend_filepath)
@@ -1037,24 +1008,24 @@ class ToggleLinkSelectability(bpy.types.Operator):
bl_label = "Toggle Link Selectability"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle selectability"
link: bpy.props.StringProperty(name="Linked IFC Filepath")
link: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.BIMProjectProperties
link = props.links.get(self.link)
if not os.path.isabs(self.link):
self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link))
self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend")
self.filepath = self.link
if not os.path.isabs(self.filepath):
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/")
for collection in self.get_linked_collections():
collection.hide_select = not collection.hide_select
link.is_selectable = not collection.hide_select
return {"FINISHED"}
def get_linked_collections(self) -> list[bpy.types.Collection]:
def get_linked_collections(self):
return [
c
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath
if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath
]
@@ -1063,15 +1034,15 @@ class ToggleLinkVisibility(bpy.types.Operator):
bl_label = "Toggle Link Visibility"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle visibility between SOLID and WIREFRAME"
link: bpy.props.StringProperty(name="Linked IFC Filepath")
mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")))
link: bpy.props.StringProperty()
mode: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.BIMProjectProperties
link = props.links.get(self.link)
if not os.path.isabs(self.link):
self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link))
self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend")
self.filepath = self.link
if not os.path.isabs(self.filepath):
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/")
if self.mode == "WIREFRAME":
self.toggle_wireframe(link)
elif self.mode == "VISIBLE":
@@ -1092,17 +1063,25 @@ class ToggleLinkVisibility(bpy.types.Operator):
def toggle_visibility(self, link):
linked_collections = self.get_linked_collections()
queue = [bpy.context.view_layer.layer_collection]
layer_collection = None
layer_collections = tool.Blender.get_layer_collections_mapping(linked_collections)
for layer_collection in layer_collections.values():
while queue:
layer = queue.pop()
if layer.collection in linked_collections:
layer_collection = layer
break
queue.extend(list(layer.children))
if layer_collection:
layer_collection.exclude = not layer_collection.exclude
link.is_hidden = layer_collection.exclude
def get_linked_collections(self) -> list[bpy.types.Collection]:
def get_linked_collections(self):
return [
c
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath
if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath
]
@@ -1316,8 +1295,6 @@ class LoadLinkedProject(bpy.types.Operator):
if len(shape.geometry.faces) > 1000 and self.is_local(shape): # 333 tris
self.process_occurrence(shape)
if not iterator.next():
if not chunked_verts:
break
mats = np.concatenate(chunked_materials)
midx = np.concatenate(chunked_material_ids)
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
@@ -1353,9 +1330,6 @@ class LoadLinkedProject(bpy.types.Operator):
ms = np.vstack([default_mat, np.frombuffer(shape.geometry.colors_buffer).reshape((-1, 4))])
mi = np.frombuffer(shape.geometry.material_ids_buffer, dtype=np.int32)
for geom_material_idx, geom_material in enumerate(shape.geometry.materials):
if not geom_material.has_diffuse:
ms[geom_material_idx + 1] = (0.8, 0.8, 0.8, 1)
chunked_materials.append(ms)
chunked_material_ids.append(mi + material_offset + 1)
material_offset += len(ms)
@@ -1476,10 +1450,7 @@ class LoadLinkedProject(bpy.types.Operator):
alpha = 1.0
if material.has_transparency and material.transparency > 0:
alpha = 1.0 - material.transparency
if material.has_diffuse:
diffuse = material.diffuse + (alpha,)
else:
diffuse = (0.8, 0.8, 0.8, 1) # Blender's default material
diffuse = material.diffuse + (alpha,)
material_name = f"{diffuse[0]}-{diffuse[1]}-{diffuse[2]}-{diffuse[3]}"
blender_mat = self.blender_mats.get(material_name, None)
if not blender_mat:
@@ -1942,32 +1913,32 @@ class CreateClippingPlane(bpy.types.Operator):
bl_label = "Create Clipping Plane"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.area.type == "VIEW_3D"
def execute(self, context):
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
# Clean up deleted planes
if len(context.scene.BIMProjectProperties.clipping_planes) > 5:
if len(bpy.context.scene.BIMProjectProperties.clipping_planes) > 5:
self.report({"INFO"}, "Maximum of six clipping planes allowed.")
return {"FINISHED"}
for area in context.screen.areas:
for area in bpy.context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
region = context.region
rv3d = context.region_data
if rv3d: # Called from a 3D viewport
coord = (self.mouse_x, self.mouse_y)
origin = region_2d_to_origin_3d(region, rv3d, coord)
direction = region_2d_to_vector_3d(region, rv3d, coord)
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
if not hit:
self.report({"INFO"}, "No object found.")
return {"FINISHED"}
else: # Not Called from a 3D viewport
location = (0, 0, 1)
normal = (0, 0, 1)
coord = (self.mouse_x, self.mouse_y)
origin = region_2d_to_origin_3d(region, rv3d, coord)
direction = region_2d_to_vector_3d(region, rv3d, coord)
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
if not hit:
self.report({"INFO"}, "No object found.")
return {"FINISHED"}
vertices = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (0.5, 0.5, 0), (-0.5, 0.5, 0)]
@@ -1978,20 +1949,20 @@ class CreateClippingPlane(bpy.types.Operator):
mesh.update()
plane_obj = bpy.data.objects.new("ClippingPlane", mesh)
context.collection.objects.link(plane_obj)
bpy.context.collection.objects.link(plane_obj)
z_axis = Vector((0, 0, 1))
rotation_matrix = z_axis.rotation_difference(normal).to_matrix().to_4x4()
plane_obj.matrix_world = rotation_matrix
plane_obj.matrix_world.translation = location
context.scene.cursor.location = location
bpy.context.scene.cursor.location = location
new = context.scene.BIMProjectProperties.clipping_planes.add()
new = bpy.context.scene.BIMProjectProperties.clipping_planes.add()
new.obj = plane_obj
tool.Blender.set_active_object(plane_obj)
ClippingPlaneDecorator.install(context)
ClippingPlaneDecorator.install(bpy.context)
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
return {"FINISHED"}
@@ -2013,69 +1984,11 @@ class FlipClippingPlane(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.active_object
return context.area.type == "VIEW_3D"
def execute(self, context):
obj = context.active_object
if obj in context.scene.BIMProjectProperties.clipping_planes_objs:
obj = bpy.context.active_object
if obj in [cp.obj for cp in bpy.context.scene.BIMProjectProperties.clipping_planes]:
obj.rotation_euler[0] += radians(180)
context.view_layer.update()
return {"FINISHED"}
CLIPPING_PLANES_FILE_NAME = "ClippingPlanes.json" # TODO un-hardcode :=
class BIM_OT_save_clipping_planes(bpy.types.Operator):
bl_idname = "bim.save_clipping_planes"
bl_label = "Save Clipping Planes"
bl_description = "Save Clipping Planes to Disk"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if IfcStore.path:
return context.scene.BIMProjectProperties.clipping_planes
cls.poll_message_set("Please Save The IFC File")
def execute(self, context):
clipping_planes_to_serialize = defaultdict(dict)
clipping_planes = context.scene.BIMProjectProperties.clipping_planes
for clipping_plane in clipping_planes:
obj = clipping_plane.obj
name = obj.name
clipping_planes_to_serialize[name]["location"] = obj.location[0:3]
clipping_planes_to_serialize[name]["rotation"] = obj.rotation_euler[0:3]
with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "w") as file:
json.dump(clipping_planes_to_serialize, file, indent=4)
return {"FINISHED"}
class BIM_OT_load_clipping_planes(bpy.types.Operator):
bl_idname = "bim.load_clipping_planes"
bl_label = "Load Clipping Planes"
bl_description = "Load Clipping Planes from Disk"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if filepath := IfcStore.path:
if Path(filepath).with_name(CLIPPING_PLANES_FILE_NAME).exists():
return True
else:
cls.poll_message_set(f"No Clipping Planes File in Folder {filepath}")
else:
cls.poll_message_set("Please Save The IFC File")
def execute(self, context):
bpy.data.batch_remove(context.scene.BIMProjectProperties.clipping_planes_objs)
context.scene.BIMProjectProperties.clipping_planes.clear()
with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "r") as file:
clipping_planes_dict = json.load(file)
for name, values in clipping_planes_dict.items():
bpy.ops.bim.create_clipping_plane()
obj = context.scene.BIMProjectProperties.clipping_planes_objs[-1]
obj.name = name
obj.location = values["location"]
obj.rotation_euler = values["rotation"]
bpy.context.view_layer.update()
return {"FINISHED"}
@@ -187,11 +187,6 @@ class BIMProjectProperties(PropertyGroup):
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False)
queried_obj: bpy.props.PointerProperty(type=bpy.types.Object)
clipping_planes: bpy.props.CollectionProperty(type=ObjProperty)
clipping_planes_active: bpy.props.IntProperty(min=0, default=0, max=5)
@property
def clipping_planes_objs(self):
return list({cp.obj for cp in self.clipping_planes if cp.obj})
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))
@@ -69,7 +69,6 @@ class ObjectPsetsData(Data):
@classmethod
def load(cls):
cls.data = {
"is_occurrence": cls.is_occurrence(),
"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), psets_only=True),
"inherited_psets": cls.inherited_psets(),
"pset_name": cls.pset_name(),
@@ -77,10 +76,6 @@ class ObjectPsetsData(Data):
}
cls.is_loaded = True
@classmethod
def is_occurrence(cls):
return not tool.Ifc.get_entity(bpy.context.active_object).is_a("IfcTypeObject")
@classmethod
def inherited_psets(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -131,16 +126,11 @@ class ObjectQtosData(Data):
@classmethod
def load(cls):
cls.data = {
"is_occurrence": cls.is_occurrence(),
"qtos": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), qtos_only=True),
"inherited_qsets": cls.inherited_qsets(),
}
cls.is_loaded = True
@classmethod
def is_occurrence(cls):
return not tool.Ifc.get_entity(bpy.context.active_object).is_a("IfcTypeObject")
@classmethod
def inherited_qsets(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -85,6 +85,7 @@ class EnablePsetEditing(bpy.types.Operator):
def execute(self, context):
self.props = get_pset_props(context, self.obj, self.obj_type)
self.props.properties.clear()
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
if self.pset_id:
pset = tool.Ifc.get().by_id(self.pset_id)
@@ -249,7 +250,7 @@ class DisablePsetEditing(bpy.types.Operator, Operator):
props = get_pset_props(context, self.obj, self.obj_type)
if props.active_pset_id:
pset = tool.Ifc.get().by_id(props.active_pset_id)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
if tool.Pset.is_pset_empty(pset):
ifcopenshell.api.run(
"pset.remove_pset", tool.Ifc.get(), product=tool.Ifc.get().by_id(ifc_definition_id), pset=pset
@@ -271,7 +272,7 @@ class EditPset(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
element = tool.Ifc.get().by_id(ifc_definition_id)
properties = {}
@@ -342,7 +343,7 @@ class RemovePset(bpy.types.Operator, Operator):
pset_name = tool.Ifc.get().by_id(self.pset_id).Name
for obj in objects:
props = get_pset_props(context, obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
element = tool.Ifc.get().by_id(ifc_definition_id)
pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None)
if pset:
@@ -373,7 +374,7 @@ class AddQto(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
element = tool.Ifc.get().by_id(ifc_definition_id)
bpy.ops.bim.enable_pset_editing(
pset_id=0, pset_name=props.qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type
@@ -25,16 +25,8 @@ from shapely.geometry import Polygon
from shapely.ops import unary_union
import blenderbim.tool as tool
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.element
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
import blenderbim.bim
from typing import Literal, Union, Optional
AxisType = Literal["x", "y", "z"]
VectorTuple = tuple[float, float, float]
QuanityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"]
class QtoCalculator:
@@ -53,7 +45,7 @@ class QtoCalculator:
else:
self.mapping_dict[key][item] = None
def calculate_quantity(self, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
def calculate_quantity(self, qto_name, quantity_name, obj):
"""calculates the value of the quantity in the project units"""
string = "self.mapping_dict[qto_name][quantity_name](obj"
if isinstance(mapper[qto_name][quantity_name], dict):
@@ -62,13 +54,11 @@ class QtoCalculator:
args = ""
string += args
string += ")"
value: float = eval(string)
value = eval(string)
return tool.Qto.convert_to_project_units(value, qto_name, quantity_name) or value
def guess_quantity(
self, prop_name: str, alternative_prop_names: list[str], obj: bpy.types.Object
) -> Union[float, None]:
def guess_quantity(self, prop_name, alternative_prop_names, obj):
"""guess the value of the quantity by name, returns the value in the project units"""
prop_name = prop_name.lower()
alternative_prop_names = [p.lower() for p in alternative_prop_names]
@@ -99,7 +89,7 @@ class QtoCalculator:
if value is None:
return
unit_type_keywords: dict[str, QuanityTypes] = {
unit_type_keywords = {
"length": "Q_LENGTH",
"width": "Q_LENGTH",
"height": "Q_LENGTH",
@@ -112,10 +102,10 @@ class QtoCalculator:
unit_type = next(unit_type_keywords[k] for k in unit_type_keywords if k in prop_name)
return tool.Qto.convert_to_project_units(value, quantity_type=unit_type) or value
def get_units(self, o: bpy.types.Object, vg_index: int) -> int:
def get_units(self, o, vg_index):
return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]])
def get_linear_length(self, o: bpy.types.Object) -> float:
def get_linear_length(self, o):
"""_summary_: Returns the length of the longest edge of the object bounding box
:param blender-object o: Blender Object
@@ -126,7 +116,7 @@ class QtoCalculator:
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return max(x, y, z)
def get_length(self, o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "") -> float:
def get_length(self, o, vg_index=None, main_axis: str = ""):
if vg_index is None:
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
@@ -151,23 +141,23 @@ class QtoCalculator:
length += self.get_edge_distance(o, e)
return length
def get_stair_length(self, obj: bpy.types.Object) -> float:
def get_stair_length(self, obj):
length = self.get_length(obj)
height = self.get_height(obj)
stair_length = math.sqrt(pow(length, 2) + pow(height, 2))
return stair_length
def get_net_stair_area(self, obj: bpy.types.Object) -> float:
def get_net_stair_area(self, obj):
OBB_obj = self.get_OBB_object(obj)
OBB_net_footprint_area = self.get_net_footprint_area(OBB_obj)
return OBB_net_footprint_area
def get_gross_stair_area(self, obj: bpy.types.Object) -> float:
def get_gross_stair_area(self, obj):
OBB_obj = self.get_OBB_object(obj)
OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj)
return OBB_gross_footprint_area
def get_parametric_axis(self, obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]:
def get_parametric_axis(self, obj):
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj))
if relating_type:
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
@@ -182,7 +172,7 @@ class QtoCalculator:
return None
return None
def get_covering_gross_area(self, obj: bpy.types.Object) -> float:
def get_covering_gross_area(self, obj):
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_gross_footprint_area(obj)
@@ -191,7 +181,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_gross_footprint_area(obj)
def get_covering_net_area(self, obj: bpy.types.Object) -> float:
def get_covering_net_area(self, obj):
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_net_footprint_area(obj)
@@ -200,7 +190,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_net_footprint_area(obj)
def get_covering_width(self, obj: bpy.types.Object) -> float:
def get_covering_width(self, obj):
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_height(obj)
@@ -209,7 +199,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_height(obj)
def get_width(self, o: bpy.types.Object) -> float:
def get_width(self, o):
"""_summary_: Returns the width of the object bounding box
:param blender-object o: blender object
@@ -219,7 +209,7 @@ class QtoCalculator:
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
return min(x, y)
def get_height(self, o: bpy.types.Object) -> float:
def get_height(self, o):
"""_summary_: Returns the height of the object bounding box
:param blender-object o: blender object
@@ -227,77 +217,64 @@ class QtoCalculator:
"""
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
def get_opening_height(self, obj: bpy.types.Object) -> float:
def get_opening_height(self, obj):
if self.is_opening_horizontal(obj):
return self.get_width(obj)
else:
return self.get_height(obj)
def get_opening_depth(self, obj: bpy.types.Object) -> float:
def get_opening_depth(self, obj):
if self.is_opening_horizontal(obj):
return self.get_height(obj)
else:
return self.get_width(obj)
def get_opening_mapping_area(self, obj: bpy.types.Object) -> float:
def get_opening_mapping_area(self, obj):
if self.is_opening_horizontal(obj):
return self.get_net_footprint_area(obj)
else:
return self.get_net_side_area(obj)
def get_finish_ceiling_height(self, obj: bpy.types.Object) -> float:
def get_finish_ceiling_height(self, obj):
space_height = self.get_height(obj)
floor_height = self.get_finish_floor_height(obj)
ceiling_height = self.get_ceiling_height(obj)
finish_ceiling_height = ceiling_height - floor_height
finish_ceiling_height = space_height - floor_height - ceiling_height
return finish_ceiling_height
def get_max_global_z(self, obj: bpy.types.Object) -> float:
z_values = [(obj.matrix_world @ Vector(co))[2] for co in obj.bound_box]
return max(z_values)
def get_min_global_z(self, obj: bpy.types.Object) -> float:
z_values = [(obj.matrix_world @ Vector(co))[2] for co in obj.bound_box]
return min(z_values)
def get_finish_floor_height(self, obj: bpy.types.Object) -> float:
space_min_z_value = self.get_min_global_z(obj)
space_max_z_value = self.get_max_global_z(obj)
def get_finish_floor_height(self, obj):
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
flooring_max_z_value = space_min_z_value
finish_floor_height = 0
for decomposition in decompositions:
if (
decomposition.is_a() == "IfcCovering"
and ifcopenshell.util.element.get_predefined_type(decomposition) == "FLOORING"
decomposition.get_info()["PredefinedType"] == "FLOORING"
and decomposition.get_info()["type"] == "IfcCovering"
):
flooring_obj = tool.Ifc.get_object(decomposition)
flooring_z_value = self.get_max_global_z(flooring_obj)
if flooring_z_value > space_min_z_value:
flooring_max_z_value = flooring_z_value
floor_obj = tool.Ifc.get_object(decomposition)
new_finish_floor_height = self.get_height(floor_obj)
if new_finish_floor_height > finish_floor_height:
finish_floor_height = new_finish_floor_height
return flooring_max_z_value - space_min_z_value
def get_ceiling_height(self, obj: bpy.types.Object) -> float:
space_min_z_value = self.get_min_global_z(obj)
space_max_z_value = self.get_max_global_z(obj)
return finish_floor_height
def get_ceiling_height(self, obj):
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
ceiling_min_z_value = space_max_z_value
finish_ceiling_height = 0
for decomposition in decompositions:
if (
decomposition.is_a() == "IfcCovering"
and ifcopenshell.util.element.get_predefined_type(decomposition) == "CEILING"
decomposition.get_info()["PredefinedType"] == "CEILING"
and decomposition.get_info()["type"] == "IfcCovering"
):
ceiling_obj = tool.Ifc.get_object(decomposition)
ceiling_z_value = self.get_min_global_z(ceiling_obj)
if ceiling_z_value < space_max_z_value:
ceiling_min_z_value = ceiling_z_value
new_finish_ceiling_height = self.get_height(ceiling_obj)
if new_finish_ceiling_height > finish_ceiling_height:
finish_ceiling_height = new_finish_ceiling_height
return ceiling_min_z_value - space_min_z_value
return finish_ceiling_height
def get_net_perimeter(self, o: bpy.types.Object) -> float:
def get_net_perimeter(self, o):
parsed_edges = []
shared_edges = []
perimeter = 0
@@ -312,7 +289,7 @@ class QtoCalculator:
perimeter -= self.get_edge_key_distance(o, edge_key)
return perimeter
def get_gross_perimeter(self, o: bpy.types.Object) -> float:
def get_gross_perimeter(self, o):
element = tool.Ifc.get_entity(o)
mesh = self.get_gross_element_mesh(element)
gross_obj = bpy.data.objects.new("GrossObj", mesh)
@@ -320,15 +297,15 @@ class QtoCalculator:
self.delete_obj(gross_obj)
return gross_perimeter
def get_space_net_perimeter(self, obj: bpy.types.Object) -> float:
def get_space_net_perimeter(self, obj):
pass
def get_rectangular_perimeter(self, obj: bpy.types.Object) -> float:
def get_rectangular_perimeter(self, obj):
length = self.get_length(obj, main_axis="x")
height = self.get_height(obj)
return (length + height) * 2
def get_lowest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
def get_lowest_polygons(self, o):
lowest_polygons = []
lowest_z = None
for polygon in o.data.polygons:
@@ -344,7 +321,7 @@ class QtoCalculator:
lowest_z = z
return lowest_polygons
def get_highest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
def get_highest_polygons(self, o):
highest_polygons = []
highest_z = None
for polygon in o.data.polygons:
@@ -360,13 +337,13 @@ class QtoCalculator:
highest_z = z
return highest_polygons
def get_edge_key_distance(self, obj: bpy.types.Object, edge_key: tuple[int, int]) -> float:
def get_edge_key_distance(self, obj, edge_key):
return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length
def get_edge_distance(self, obj: bpy.types.Object, edge: bpy.types.MeshEdge) -> float:
def get_edge_distance(self, obj, edge):
return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length
def get_net_floor_area(self, obj: bpy.types.Object) -> float:
def get_net_floor_area(self, obj):
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_footprint_area(obj)
@@ -382,7 +359,7 @@ class QtoCalculator:
return total_net_floor_area
def get_gross_ceiling_area(self, obj: bpy.types.Object) -> float:
def get_gross_ceiling_area(self, obj):
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_top_area(obj)
@@ -390,15 +367,15 @@ class QtoCalculator:
total_gross_ceiling_area = 0
for decomposition in decompositions:
decomposition_class = decomposition.is_a()
decomposition_predefined_type = ifcopenshell.util.element.get_predefined_type(decomposition)
if decomposition_class == "IfcCovering" and decomposition_predefined_type == "CEILING":
decomposition_type = decomposition.get_info()["type"]
decomposition_predefined_type = decomposition.get_info()["PredefinedType"]
if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING":
decomposition_obj = tool.Ifc.get_object(decomposition)
total_gross_ceiling_area += self.get_gross_footprint_area(decomposition_obj)
return total_gross_ceiling_area
def get_net_ceiling_area(self, obj: bpy.types.Object) -> float:
def get_net_ceiling_area(self, obj):
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_net_top_area(obj)
@@ -406,19 +383,19 @@ class QtoCalculator:
total_net_ceiling_area = 0
for decomposition in decompositions:
decomposition_class = decomposition.is_a()
decomposition_predefined_type = ifcopenshell.util.element.get_predefined_type(decomposition)
if decomposition_class == "IfcCovering" and decomposition_predefined_type == "CEILING":
decomposition_type = decomposition.get_info()["type"]
decomposition_predefined_type = decomposition.get_info()["PredefinedType"]
if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING":
decomposition_obj = tool.Ifc.get_object(decomposition)
total_net_ceiling_area += self.get_net_footprint_area(decomposition_obj)
if decomposition_class == "IfcWall" or decomposition_class == "IfcColumn":
if decomposition_type == "IfcWall" or decomposition_type == "IfcColumn":
decomposition_obj = tool.Ifc.get_object(decomposition)
total_net_ceiling_area -= self.get_net_roofprint_area(decomposition_obj)
return total_net_ceiling_area
def get_space_net_volume(self, obj: bpy.types.Object) -> float:
def get_space_net_volume(self, obj):
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_volume(obj)
@@ -433,7 +410,7 @@ class QtoCalculator:
return total_space_net_volume
def get_net_footprint_area(self, o: bpy.types.Object) -> float:
def get_net_footprint_area(self, o):
"""_summary_: Returns the area of the footprint of the object, excluding any holes
:param blender-object o: blender object
@@ -444,7 +421,7 @@ class QtoCalculator:
area += polygon.area
return area
def get_gross_footprint_area(self, o: bpy.types.Object) -> float:
def get_gross_footprint_area(self, o):
"""_summary_: Returns the area of the footprint of the object, without related opening and excluding any holes
:param blender-object o: blender object
@@ -460,7 +437,7 @@ class QtoCalculator:
self.delete_mesh(mesh)
return gross_footprint_area
def get_net_roofprint_area(self, o: bpy.types.Object) -> float:
def get_net_roofprint_area(self, o):
# Is roofprint the right word? Couldn't think of anything better - vulevukusej
"""_summary_: Returns the area of the net roofprint of the object, excluding any holes
@@ -472,7 +449,7 @@ class QtoCalculator:
area += polygon.area
return area
def get_side_area(self, o: bpy.types.Object) -> float:
def get_side_area(self, o):
# There are a few dumb options for this, but this seems the dumbest
# until I get more practical experience on what works best.
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
@@ -480,7 +457,8 @@ class QtoCalculator:
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return max(x * z, y * z)
def get_cross_section_area(self, obj: bpy.types.Object) -> float:
def get_cross_section_area(self, obj):
element = tool.Ifc.get_entity(obj)
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
item = representation.Items[0]
while True:
@@ -496,7 +474,7 @@ class QtoCalculator:
return area
# TODO handle other types of sections, and then fall back to mesh parsing
def get_gross_surface_area(self, o: bpy.types.Object, vg_index: Optional[int] = None) -> float:
def get_gross_surface_area(self, o, vg_index=None):
if vg_index is None:
if not self.has_openings(o):
return self.get_net_surface_area(o)
@@ -514,29 +492,29 @@ class QtoCalculator:
area += polygon.area
return area
def get_net_surface_area(self, obj: bpy.types.Object) -> float:
def get_net_surface_area(self, obj):
return self.get_mesh_area(obj.data)
def get_mesh_area(self, mesh: bpy.types.Mesh) -> float:
def get_mesh_area(self, mesh):
area = 0
for polygon in mesh.polygons:
area += polygon.area
return area
def is_polygon_in_vg(self, polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.types.MeshVertex]) -> bool:
def is_polygon_in_vg(self, polygon, vertices_in_vg):
for v in polygon.vertices:
if v not in vertices_in_vg:
return False
return True
def get_net_volume(self, o: bpy.types.Object) -> float:
def get_net_volume(self, o):
o_mesh = bmesh.new()
o_mesh.from_mesh(o.data)
volume = o_mesh.calc_volume()
o_mesh.free()
return volume
def get_gross_volume(self, o: bpy.types.Object) -> float:
def get_gross_volume(self, o):
if not self.has_openings(o):
return self.get_net_volume(o)
@@ -551,18 +529,16 @@ class QtoCalculator:
return gross_volume
def has_openings(
self, obj: bpy.types.Object
) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
def has_openings(self, obj):
element = tool.Ifc.get_entity(obj)
return element and getattr(element, "HasOpenings", [])
def get_obj_decompositions(self, obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]:
def get_obj_decompositions(self, obj):
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
return decompositions
def get_gross_weight(self, obj: bpy.types.Object) -> Union[float, None]:
def get_gross_weight(self, obj):
obj_mass_density = self.get_obj_mass_density(obj)
if not obj_mass_density:
return
@@ -570,7 +546,7 @@ class QtoCalculator:
gross_weight = obj_mass_density * gross_volume
return gross_weight
def get_net_weight(self, obj: bpy.types.Object) -> Union[float, None]:
def get_net_weight(self, obj):
obj_mass_density = self.get_obj_mass_density(obj)
if not obj_mass_density:
return
@@ -578,7 +554,7 @@ class QtoCalculator:
net_weight = obj_mass_density * net_volume
return net_weight
def get_obj_mass_density(self, obj: bpy.types.Object) -> Union[float, None]:
def get_obj_mass_density(self, obj):
entity = tool.Ifc.get_entity(obj)
material = ifcopenshell.util.element.get_material(entity)
if material is None:
@@ -653,7 +629,7 @@ class QtoCalculator:
# volume += v1.dot(v2.cross(v3)) / 6.0
# return volume
def get_opening_type(self, opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]:
def get_opening_type(self, opening, obj):
"""_summary_: Returns the opening type - OPENING / RECESS
:param blender-object opening: blender opening object
@@ -673,22 +649,14 @@ class QtoCalculator:
return "OPENING" if ray_intersections % 2 == 0 else "RECESS"
def get_opening_area(
self,
obj: bpy.types.Object,
angle_z1: int = 45,
angle_z2: int = 135,
min_area: int = 0,
ignore_recesses: bool = False,
) -> float:
self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False
):
"""_summary_: Returns the lateral area of the openings in the object.
:param obj: blender object
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area.
Openings with a normal_vector lower than this value will be ignored, defaults to 45
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area.
Openings with a normal_vector greater than this value will be ignored,defaults to 135
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored,
defaults to 0
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector lower than this value will be ignored, defaults to 45
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector greater than this value will be ignored,defaults to 135
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored, defaults to 0
:param bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False
:return float: Opening Area
"""
@@ -734,14 +702,14 @@ class QtoCalculator:
def get_lateral_area(
self,
obj: bpy.types.Object,
obj,
subtract_openings: bool = True,
exclude_end_areas: bool = False,
exclude_side_areas: bool = False,
angle_z1: int = 45,
angle_z2: int = 135,
main_axis: str = "",
) -> float:
):
"""_summary_
:param blender-object obj: blender object, bpy.types.Object
@@ -792,7 +760,7 @@ class QtoCalculator:
area += polygon.area
return area + total_opening_area
def get_gross_side_area(self, obj: bpy.types.Object) -> float:
def get_gross_side_area(self, obj):
if not self.has_openings(obj):
return self.get_net_side_area(obj)
@@ -800,15 +768,15 @@ class QtoCalculator:
return gross_side_area
def get_net_side_area(self, obj: bpy.types.Object) -> float:
def get_net_side_area(self, obj):
net_side_area = self.get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2
return net_side_area
def get_outer_surface_area(self, obj: bpy.types.Object) -> float:
def get_outer_surface_area(self, obj):
outer_surface_area = self.get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360)
return outer_surface_area
def get_end_area(self, obj: bpy.types.Object) -> float:
def get_end_area(self, obj):
element = tool.Ifc.get_entity(obj)
gross_mesh = self.get_gross_element_mesh(element)
gross_obj = bpy.data.objects.new("MyObject", gross_mesh)
@@ -822,7 +790,7 @@ class QtoCalculator:
return end_area
def get_gross_top_area(self, obj: bpy.types.Object, angle: int = 45) -> float:
def get_gross_top_area(self, obj, angle: int = 45):
"""_summary_: Returns the gross top area of the object.
:param blender-object obj: blender object
@@ -859,14 +827,12 @@ class QtoCalculator:
return area + opening_area
# curently net top area is larger then projected area, because its taking into account internal polygons, or window sills
def get_net_top_area(self, obj: bpy.types.Object, angle: int = 45, ignore_internal: bool = True) -> float:
def get_net_top_area(self, obj, angle: int = 45, ignore_internal: bool = True):
"""_summary_: Returns the net top area of the object.
:param blender-object obj: blender object
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area.
Values lower than this will be ignored, defaults to 45
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills),
defaults to True
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), defaults to True
:return float: Net Top Area
"""
z_axis = (0, 0, 1)
@@ -886,11 +852,11 @@ class QtoCalculator:
return area
def get_projected_area(self, obj, projection_axis: AxisType = "z", is_gross: bool = True) -> float:
def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True):
"""_summary_: Returns the projected area of the object.
:param blender-object obj: blender object
:param str projection_axis: Axis to project the area onto. Can be "x", "y" or "z"
:param str projection_axis: Axis to project the area onto. Can be "X", "Y" or "Z"
:param bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings
:return float: Projected Area
"""
@@ -925,7 +891,7 @@ class QtoCalculator:
return projected_polygon.area + void_area
return projected_polygon.area
def get_OBB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
def get_OBB_object(self, obj):
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
:param blender-object obj: Blender Object
@@ -966,7 +932,7 @@ class QtoCalculator:
return new_OBB_object
def get_AABB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
def get_AABB_object(self, obj):
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
:param blender-object obj: Blender Object
@@ -1022,12 +988,12 @@ class QtoCalculator:
def get_bisected_obj(
self,
obj: bpy.types.Object,
plane_co_pos: VectorTuple,
plane_no_pos: VectorTuple,
plane_co_neg: VectorTuple,
plane_no_neg: VectorTuple,
) -> bpy.types.Object:
obj,
plane_co_pos,
plane_no_pos,
plane_co_neg,
plane_no_neg,
):
"""_summary_: Returns the object bisected by two planes.
:param blender-object obj: Blender Object
@@ -1065,12 +1031,11 @@ class QtoCalculator:
return bis_obj
def get_total_contact_area(self, obj: bpy.types.Object, class_filter: list[str] = ["IfcElement"]) -> float:
def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]):
"""_summary_: Returns the total contact area of the object with other objects.
:param blender-object obj: Blender Object
:param list [] class_filter: A list of classes used to filter the objects
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:return float: Total contact area of the object with other objects.
"""
total_contact_area = 0
@@ -1081,12 +1046,11 @@ class QtoCalculator:
return total_contact_area
def get_touching_objects(self, obj: bpy.types.Object, class_filter: list[str]) -> list[bpy.types.Object]:
def get_touching_objects(self, obj, class_filter):
"""_summary_: Returns a list of objects that are touching the object.
:param blender-object obj: Blender Object
:param list [] class_filter: A list of classes used to filter the objects
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:return list: List of touching objects
"""
# rotate the object ever so slightly, otherwise bvhtree.overlap won't work properly. https://blender.stackexchange.com/a/275244/130742
@@ -1130,7 +1094,7 @@ class QtoCalculator:
return touching_objects
def get_contact_area(self, object1: bpy.types.Object, object2: bpy.types.Object) -> float:
def get_contact_area(self, object1, object2):
"""_summary_: Returns the contact area between two objects.
:param blender-object obj: Blender Object
@@ -1145,13 +1109,7 @@ class QtoCalculator:
total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2)
return total_area
def get_intersection_between_polygons(
self,
object1: bpy.types.Object,
poly1: bpy.types.MeshPolygon,
object2: bpy.types.Object,
poly2: bpy.types.MeshPolygon,
) -> float:
def get_intersection_between_polygons(self, object1, poly1, object2, poly2):
"""_summary_: Returns the intersection between two polygons.
:param blender-object object1: Blender Object
@@ -1194,9 +1152,7 @@ class QtoCalculator:
# TopologicalError - Generated Geometry might be invalid
return 0
def create_shapely_polygon(
self, obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix
) -> Polygon:
def create_shapely_polygon(self, obj, polygon, trans_matrix):
"""_summary_: Create a shapely polygon
:param blender-object obj: Blender Object
@@ -1215,14 +1171,12 @@ class QtoCalculator:
polygon_tuples.append((x, y))
return Polygon(polygon_tuples)
def get_gross_element_mesh(self, element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
def get_gross_element_mesh(self, element):
settings = ifcopenshell.geom.settings()
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
return self.create_mesh_from_shape(element, settings)
def create_mesh_from_shape(
self, element: ifcopenshell.entity_instance, settings: Optional[ifcopenshell.geom.settings] = None
) -> bpy.types.Mesh:
def create_mesh_from_shape(self, element, settings=None):
if settings is None:
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, element)
@@ -1249,12 +1203,12 @@ class QtoCalculator:
mesh.update()
return mesh
def get_bmesh_from_mesh(self, mesh: bpy.types.Mesh) -> bmesh.types.BMesh:
def get_bmesh_from_mesh(self, mesh):
bm = bmesh.new()
bm.from_mesh(mesh)
return bm
def get_object_main_axis(self, o: bpy.types.Object) -> AxisType:
def get_object_main_axis(self, o):
"""_summary_: Returns the main object axis. Useful for profile-defined objects.
:param blender-object o: Blender Object
@@ -1273,18 +1227,18 @@ class QtoCalculator:
else:
return "x"
def is_opening_horizontal(self, o: bpy.types.Object) -> bool:
def is_opening_horizontal(self, o):
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return z < x and z < y
def delete_mesh(self, mesh: bpy.types.Mesh) -> None:
def delete_mesh(self, mesh):
mesh.user_clear()
bpy.data.meshes.remove(mesh)
def delete_obj(self, obj: bpy.types.Object) -> None:
def delete_obj(self, obj):
bpy.data.objects.remove(obj, do_unlink=True)
+13 -27
View File
@@ -90,7 +90,7 @@ def get_active_pset_obj_name(context, obj_type):
return ""
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_removing=True):
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
box = layout.box()
row = box.row(align=True)
if "is_expanded" not in pset:
@@ -114,20 +114,16 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
remove_pset_row = row.row(align=True)
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
op = row.operator("bim.remove_pset", icon="X", text="")
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
remove_pset_row.enabled = allow_removing
elif props.active_pset_id != pset_id:
row.label(text=pset["Name"], icon="COPY_ID")
remove_pset_row = row.row(align=True)
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
op = row.operator("bim.remove_pset", icon="X", text="")
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
remove_pset_row.enabled = allow_removing
if pset["is_expanded"]:
if props.active_pset_id == pset_id:
for prop in props.properties:
@@ -175,7 +171,7 @@ def draw_psetqto_editable_ui(box, props, prop):
class BIM_PT_object_psets(Panel):
bl_label = "Property Sets"
bl_label = "Object Property Sets"
bl_idname = "BIM_PT_object_psets"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
@@ -207,22 +203,17 @@ class BIM_PT_object_psets(Panel):
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET":
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
if ObjectPsetsData.data["psets"]:
if ObjectPsetsData.data["is_occurrence"]:
self.layout.label(text="Occurrence Properties:")
else:
self.layout.label(text="Type Properties:")
for pset in ObjectPsetsData.data["psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
for pset in ObjectPsetsData.data["psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
if ObjectPsetsData.data["inherited_psets"]:
self.layout.label(text="Inherited Type Properties:", icon="CON_CHILDOF")
self.layout.label(text="Inherited Psets:", icon="FILE_PARENT")
for pset in ObjectPsetsData.data["inherited_psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object", allow_removing=False)
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
class BIM_PT_object_qtos(Panel):
bl_label = "Quantity Sets"
bl_label = "Object Quantity Sets"
bl_idname = "BIM_PT_object_qtos"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
@@ -254,18 +245,13 @@ class BIM_PT_object_qtos(Panel):
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "QTO":
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
if ObjectQtosData.data["qtos"]:
if ObjectQtosData.data["is_occurrence"]:
self.layout.label(text="Occurrence Quantities:")
else:
self.layout.label(text="Type Quantities:")
for qto in ObjectQtosData.data["qtos"]:
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
for qto in ObjectQtosData.data["qtos"]:
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
if ObjectQtosData.data["inherited_qsets"]:
self.layout.label(text="Inherited Type Quantities:", icon="CON_CHILDOF")
self.layout.label(text="Inherited Qsets:", icon="FILE_PARENT")
for qset in ObjectQtosData.data["inherited_qsets"]:
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object", allow_removing=False)
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object")
class BIM_PT_material_psets(Panel):
@@ -34,7 +34,6 @@ classes = (
operator.RemovePropTemplate,
operator.RemovePsetTemplate,
operator.RemovePsetTemplateFile,
operator.SavePsetTemplateFile,
prop.PsetTemplate,
prop.EnumerationValues,
prop.PropTemplate,
@@ -268,6 +268,7 @@ class RemovePropTemplate(bpy.types.Operator, Operator):
prop_template: bpy.props.IntProperty()
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
ifcopenshell.api.run(
"pset_template.remove_prop_template",
IfcStore.pset_template_file,
@@ -286,21 +287,21 @@ class EditPropTemplate(bpy.types.Operator, Operator):
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
if props.active_prop_template.template_type == "P_ENUMERATEDVALUE":
data_type = props.active_prop_template.get_value_name()
prop = props.active_prop_template
enumerators = [getattr(ev, data_type) for ev in prop.enum_values]
enumerator = self.generate_prop_enum(props)
else:
enumerators = None
enumerator = None
ifcopenshell.api.run(
"pset_template.edit_prop_template",
IfcStore.pset_template_file,
prop_template=IfcStore.pset_template_file.by_id(props.active_prop_template_id),
attributes={
"Name": props.active_prop_template.name,
"Description": props.active_prop_template.description,
"PrimaryMeasureType": props.active_prop_template.primary_measure_type,
"TemplateType": props.active_prop_template.template_type,
"Enumerators": enumerators,
**{
"prop_template": IfcStore.pset_template_file.by_id(props.active_prop_template_id),
"attributes": {
"Name": props.active_prop_template.name,
"Description": props.active_prop_template.description,
"PrimaryMeasureType": props.active_prop_template.primary_measure_type,
"TemplateType": props.active_prop_template.template_type,
"Enumerators": enumerator,
},
}
)
bpy.ops.bim.disable_editing_prop_template()
@@ -308,3 +309,18 @@ class EditPropTemplate(bpy.types.Operator, Operator):
blenderbim.bim.handler.refresh_ui_data()
if tool.Ifc.get():
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
# TODO -This will need to go into the
# api code at some point - vulevukusej
def generate_prop_enum(self, props):
self.file = IfcStore.pset_template_file
data_type = props.active_prop_template.get_value_name()
prop = props.active_prop_template
prop_enum = self.file.create_entity(
"IFCPROPERTYENUMERATION",
Name=prop.name,
EnumerationValues=tuple(
self.file.create_entity(prop.primary_measure_type, getattr(ev, data_type)) for ev in prop.enum_values
),
)
return prop_enum
@@ -42,7 +42,6 @@ class IfcClassData:
cls.data["contexts"] = cls.contexts()
cls.data["has_entity"] = cls.has_entity()
cls.data["name"] = cls.name()
cls.data["has_inherited_predefined_type"] = cls.has_inherited_predefined_type()
cls.data["ifc_class"] = cls.ifc_class()
cls.data["ifc_predefined_types"] = cls.ifc_predefined_types()
cls.data["can_reassign_class"] = cls.can_reassign_class()
@@ -163,16 +162,6 @@ class IfcClassData:
name += f"[{predefined_type}]"
return name
@classmethod
def has_inherited_predefined_type(cls):
element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active)
if not element:
return
if element_type := ifcopenshell.util.element.get_type(element):
# Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
return ifcopenshell.util.element.get_predefined_type(element_type) not in ("NOTDEFINED", None)
return False
@classmethod
def ifc_class(cls):
element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active)
@@ -185,8 +174,6 @@ class IfcClassData:
if element:
if element.is_a("IfcOpeningElement") or element.is_a("IfcOpeningStandardCase"):
return False
if element.is_a() in ("IfcWindowStyle", "IfcDoorStyle"): #see https://github.com/IfcOpenShell/IfcOpenShell/issues/4622#issuecomment-2095676368
return True
for product in cls.ifc_products():
if element.is_a(product[0]):
return True
@@ -215,7 +215,7 @@ class UnlinkObject(bpy.types.Operator):
bl_idname = "bim.unlink_object"
bl_label = "Unlink Object"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(name="Object Name")
obj: bpy.props.StringProperty()
should_delete: bpy.props.BoolProperty(name="Delete IFC Element", default=True)
def execute(self, context):
@@ -227,7 +227,6 @@ class UnlinkObject(bpy.types.Operator):
else:
objects = context.selected_objects
objects: list[bpy.types.Object]
for obj in objects:
was_active_object = obj == context.active_object
@@ -243,27 +242,6 @@ class UnlinkObject(bpy.types.Operator):
if obj.data:
obj_copy.data = obj.data.copy()
# prevent unlinking materials that might be used elsewhere
replacements: dict[bpy.types.Material, bpy.types.Material] = dict()
for material_slot in obj_copy.material_slots:
material = material_slot.material
if material is None:
continue
if material in replacements:
material_replacement = replacements[material]
# no need to copy non-ifc materials as unlinking won't do anything to them
elif tool.Ifc.get_entity(material) is None and tool.Style.get_style(material) is None:
replacements[material] = material
continue
else:
material_replacement = material.copy()
replacements[material] = material_replacement
material_slot.material = material_replacement
tool.Geometry.delete_ifc_object(obj)
obj = obj_copy
@@ -61,10 +61,7 @@ class BIM_PT_class(Panel):
self.layout.prop(context.scene.BIMRootProperties, "relating_class_object", icon="COPYDOWN")
else:
row = self.layout.row(align=True)
row.label(
text=IfcClassData.data["name"],
icon="CON_CHILDOF" if IfcClassData.data["has_inherited_predefined_type"] else "NONE",
)
row.label(text=IfcClassData.data["name"])
row.operator("bim.select_ifc_class", text="", icon="RESTRICT_SELECT_OFF")
row.operator("bim.unlink_object", icon="UNLINKED", text="")
if IfcClassData.data["can_reassign_class"]:
@@ -24,15 +24,21 @@ classes = (
operator.ActivateIfcClassFilter,
operator.AddFilter,
operator.AddFilterGroup,
operator.AddToIfcGroup,
operator.ColourByProperty,
operator.EditFilterQuery,
operator.FilterModelElements,
operator.IfcSelector,
operator.LoadColourscheme,
operator.LoadQuery,
operator.LoadSearch,
operator.OpenQueryLibrary,
operator.RemoveFilter,
operator.RemoveFilterGroup,
operator.ResetObjectColours,
operator.SaveColourscheme,
operator.SaveSearch,
operator.SaveSelectorQuery,
operator.Search,
operator.SelectByProperty,
operator.SelectFilterElements,
@@ -45,6 +51,11 @@ classes = (
prop.BIMFilterClasses,
prop.BIMFilterBuildingStoreys,
prop.BIMSearchProperties,
prop.SearchCollection,
prop.SearchQueryFilter,
prop.SearchQuery,
prop.SearchQueryGroup,
prop.IfcSelectorProperties,
ui.BIM_PT_search,
ui.BIM_PT_filter,
ui.BIM_PT_colour_by_property,
@@ -52,12 +63,15 @@ classes = (
ui.BIM_UL_colourscheme,
ui.BIM_UL_ifc_class_filter,
ui.BIM_UL_ifc_building_storey_filter,
ui.BIM_PT_IFCSelector,
)
def register():
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
bpy.types.Scene.IfcSelectorProperties = bpy.props.PointerProperty(type=prop.IfcSelectorProperties)
def unregister():
del bpy.types.Scene.BIMSearchProperties
del bpy.types.Scene.IfcSelectorProperties
@@ -20,7 +20,6 @@ import re
import bpy
import json
import ifcopenshell
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.selector
from ifcopenshell.util.selector import Selector
@@ -236,7 +235,7 @@ class LoadSearch(Operator, tool.Ifc.Operator):
def _execute(self, context):
filter_groups = tool.Search.get_filter_groups(self.module)
group = tool.Ifc.get().by_id(int(context.scene.BIMSearchProperties.saved_searches))
tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
query = tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
def draw(self, context):
props = context.scene.BIMSearchProperties
@@ -272,29 +271,25 @@ class ColourByProperty(Operator):
colourscheme = {}
if len(props.colourscheme):
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme}
colourscheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
value = str(ifcopenshell.util.selector.get_element_value(element, query))
if value in colourscheme:
colourscheme[value]["total"] += 1
else:
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
obj.color = (*colourscheme[value]["colour"], 1)
if value not in colourscheme:
colourscheme[value] = next(colours)[0:3]
obj.color = (*colourscheme[value], 1)
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
if areas:
areas[0].spaces[0].shading.color_type = "OBJECT"
props.colourscheme.clear()
for value in sorted(colourscheme.keys()):
data = colourscheme[value]
for value, colour in colourscheme.items():
new = props.colourscheme.add()
new.name = str(value)
new.total = data["total"]
new.colour = data["colour"][0:3]
new.colour = colour[0:3]
return {"FINISHED"}
def store_state(self, context):
@@ -356,16 +351,16 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
query = props.colourscheme_query
group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name]
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": cs.total} for cs in props.colourscheme}
coulour_scheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
if group:
group = group[0]
description = json.loads(group.Description)
description["colourscheme"] = colourscheme
description["colourscheme"] = coulour_scheme
description["colourscheme_query"] = query
group.Description = json.dumps(description)
else:
description = json.dumps(
{"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}
{"type": "BBIM_Search", "colourscheme": coulour_scheme, "colourscheme_query": query}
)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
@@ -385,11 +380,10 @@ class LoadColourscheme(Operator, tool.Ifc.Operator):
description = json.loads(group.Description)
props.colourscheme_query = description.get("colourscheme_query")
props.colourscheme.clear()
for name, data in description.get("colourscheme", {}).items():
for name, colour in description.get("colourscheme", {}).items():
new = props.colourscheme.add()
new.name = name
new.total = data["total"]
new.colour = data["colour"]
new.colour = colour
def draw(self, context):
props = context.scene.BIMSearchProperties
@@ -629,6 +623,223 @@ class ShowAllElements(Operator):
return {"FINISHED"}
class FilterModelElements(Operator):
"""Filter model elements based on selection"""
bl_idname = "bim.filter_model_elements"
bl_label = "Filter Model Elements"
option: StringProperty("select|isolate|hide")
def execute(self, context):
selection_props = context.scene.IfcSelectorProperties
selection = (
selection_props.selector_query_syntax
if selection_props.manual_override
else self.add_groups(selection_props)
)
selection_props.selector_query_syntax = selection
r = core.search(tool.Search, tool.Spatial, query=selection, action=self.option)
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
def add_groups(self, selector: str) -> str:
selection = ""
for group_index, group in enumerate(selector.groups):
if group_index != 0:
selection += " | "
selection += "(" if len(selector.groups) > 1 else ""
selection = self.add_queries(selection, group)
selection += ")" if len(selector.groups) > 1 else ""
return selection
def add_queries(self, selection: str, group: str) -> str:
for query_index, query in enumerate(group.queries):
if query_index != 0:
selection += " & " if query.and_or == "and" else " | "
if query.selector == "IFC Class":
active_option = query.active_option.split(": ")[1]
selection += f".{active_option}"
selection = self.add_filters(selection, query)
elif query.selector == "GlobalId":
selection += f"#{query.value}"
elif query.selector == "IfcElementType":
index = int(query.active_sub_option.split(":")[0])
selection += f"* #{query.sub_options[index].global_id}"
elif query.selector == "IfcSpatialElement":
index = int(query.active_sub_option.split(":")[0])
selection += f"@ #{query.sub_options[index].global_id}"
return selection
def add_filters(self, selection: str, query: str) -> str:
for f_index, f in enumerate(query.filters):
if f_index != 0:
selection += " & " if f.and_or == "and" else " | "
selection += f".{query.active_option}"
if f.value in ("True", "False"):
value = f.value
elif f.value.isnumeric() and str(float(f.value))[0] == f.value[0]:
value = f.value
else:
value = f'"{f.value}"'
selection += "["
if f.selector == "IfcPropertySet":
selection += f'{f.active_option.split(": ")[1]}.{f.active_sub_option.split(": ")[1]} {"!" if f.negation else ""}{f.comparison} {value}'
elif f.selector == "Attribute":
# we're using the prop_search functionality in blender which returns the index of the option. Sometimes the user can override this and enter a value that doesn't exist in the list. In this case there is no index and we need to handle it. @vulevukusej
pattern = re.compile(r"^[0-9]+:")
match = pattern.search(f.active_option)
selection += f'{f.active_option.split(": ")[1] if match else f.active_option} {"!" if f.negation else ""}{f.comparison} {value}'
selection += "]"
return selection
# This needs to be moved into ui code, I know ;) - vulevukusej
class IfcSelector(Operator):
"""Select elements in model with IFC Selector"""
bl_idname = "bim.ifc_selector"
bl_label = "Select elements with IFC Selector"
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=800)
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
from . import ui
ui.IfcSelectorUI.draw(context, self.layout)
class SaveSelectorQuery(Operator):
bl_idname = "bim.save_selector_query"
bl_label = "Save Selector Query"
save_name: StringProperty()
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=400)
def draw(self, context):
layout = self.layout
layout.prop(self, "save_name")
def execute(self, context):
ifc_selector = context.scene.IfcSelectorProperties
new = ifc_selector.query_library.add()
new.name = self.save_name
new.query = ifc_selector.selector_query_syntax
return {"FINISHED"}
class OpenQueryLibrary(Operator):
"""Open Query Library"""
bl_idname = "bim.open_query_library"
bl_label = "Open Query Library"
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=400)
def draw(self, context):
layout = self.layout
ifc_selector = context.scene.IfcSelectorProperties
for index, query in enumerate(ifc_selector.query_library):
row = layout.row(align=True)
row.prop(query, "query", text=query.name)
op = row.operator("bim.load_query", icon="SORT_ASC", text="")
op.index = index
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
op.option = "remove"
op.collection = "query_library"
op.index = index
def execute(self, context):
return {"FINISHED"}
class LoadQuery(Operator):
bl_idname = "bim.load_query"
bl_label = "Load Query"
index: IntProperty()
def invoke(self, context, event):
close_operator_panel(event)
return self.execute(context)
def execute(self, context):
ifc_selector = context.scene.IfcSelectorProperties
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
return {"FINISHED"}
class AddToIfcGroup(Operator):
bl_idname = "bim.add_to_ifc_group"
bl_label = "Add to IFC Group"
group_name: StringProperty(name="Group Name")
def invoke(self, context, event):
bpy.ops.bim.load_groups()
return context.window_manager.invoke_props_dialog(self, width=400)
def draw(self, context):
self.props = context.scene.BIMGroupProperties
row = self.layout.row()
row.operator("bim.add_group")
self.layout.template_list(
"BIM_UL_groups",
"",
self.props,
"groups",
self.props,
"active_group_index",
)
if self.props.active_group_id:
for attribute in self.props.group_attributes:
if attribute.name in ["Name", "Description"]:
row = self.layout.row(align=True)
row.prop(attribute, "string_value", text=attribute.name)
def execute(self, context):
active_group_index = self.props.active_group_index
ifc_definition_id = self.props.groups[active_group_index].ifc_definition_id
bpy.ops.bim.enable_editing_group(group=ifc_definition_id)
selector_query_syntax = context.scene.IfcSelectorProperties.selector_query_syntax
for attribute in self.props.group_attributes:
if attribute.name == "Description":
if "*selector*" not in attribute.string_value:
attribute.string_value += f" *selector*{selector_query_syntax}*selector*"
else:
new_description = attribute.string_value.split("*selector*")
new_description[1] = selector_query_syntax
attribute.string_value = "*selector*".join(new_description)
bpy.ops.bim.edit_group()
bpy.ops.bim.disable_group_editing_ui()
return {"FINISHED"}
class SelectSimilar(Operator, tool.Ifc.Operator):
bl_idname = "bim.select_similar"
bl_label = "Select Similar"

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