Merge branch 'v0.8.0' into tfk-rocksdb-storage

This commit is contained in:
Thomas Krijnen
2025-03-25 13:05:45 +01:00
735 changed files with 25354 additions and 10696 deletions
+11 -1
View File
@@ -7,14 +7,24 @@ assignees: ''
---
Replace this text describing what problem occurred and what you expected to happen instead.
**Error Description and Steps to Reproduce**
<!--
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
-->
**Attachments**
<!--
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
-->
**Debug information**
<!--
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
-->
+6 -1
View File
@@ -7,4 +7,9 @@ assignees: ''
---
Replace this text and describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
**Feature Description**
<!--
Describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
-->
+21
View File
@@ -0,0 +1,21 @@
name: Dispatch Build IfcOpenShell
on:
workflow_dispatch:
jobs:
trigger-workflows:
runs-on: ubuntu-latest
strategy:
matrix:
workflow:
- 'Build IfcOpenShell Linux'
- 'Build IfcOpenShell Linux ARM'
- 'Build IfcOpenShell OSX'
- 'Build IfcOpenShell WASM / Pyodide'
- 'Build IfcOpenShell Windows'
steps:
- name: Trigger binary build workflows
uses: benc-uk/workflow-dispatch@v1
with:
workflow: ${{ matrix.workflow }}
+1 -3
View File
@@ -128,10 +128,8 @@ jobs:
#
# To force the link and overwrite all conflicting files:
# brew link --overwrite python@3.13
brew link --overwrite python@3.12
brew link --overwrite python@3.13
# https://github.com/rust-lang/rustup/pull/3989/files
brew install --overwrite awscli
brew install --overwrite awscli | true
- name: Upload .zip archives to S3
run: |
+1 -1
View File
@@ -104,7 +104,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://ftp.nluug.nl/pub/graphics/blender/release/Blender4.3/blender-4.3.0-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
-1
View File
@@ -48,7 +48,6 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install mathutils
pip install src/bcf --no-deps
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
@@ -9,6 +9,8 @@ on:
paths:
- 'src/pyodide/**'
- '.github/workflows/publish-pyodide-demo-app.yml'
branches:
- v0.8.0
jobs:
activate:
+1 -1
View File
@@ -350,7 +350,7 @@ def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
run([git, "clone", "--recursive", clone_url, target_dir])
else:
logger.info(f"directory '{target_dir}' already cloned. Pulling latest changes.")
run([git, "-C", target_dir, "fetch", "--all", "--tags"])
run([git, "-C", target_dir, "fetch", "--all", "--tags", "--force"])
# detect whether we are on a branch and pull
if run([git, "rev-parse", "--abbrev-ref", "HEAD"], cwd=target_dir) != "HEAD":
+2
View File
@@ -46,6 +46,7 @@ VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_DATE:=$(shell date '+%y%m%d')
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
PYVERSION:=py310
PYPI_IMP:=cp
@@ -254,6 +255,7 @@ ifeq ($(IS_STABLE), TRUE)
else
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
endif
+9
View File
@@ -40,6 +40,7 @@ from typing import Union, Any, Generator
last_commit_hash = "8888888"
last_commit_date = "9999999"
def get_last_commit_hash() -> Union[str, None]:
@@ -51,6 +52,12 @@ def get_last_commit_hash() -> Union[str, None]:
return last_commit_hash[:7]
def get_last_commit_date() -> Union[str, None]:
if last_commit_date == str(9_999999):
return None
return last_commit_date
# Accessed from bonsai extension:
bbim_semver: dict[str, Any] = {}
@@ -96,6 +103,7 @@ def get_debug_info():
"blender_version": bpy.app.version_string,
"bonsai_version": bbim_version,
"bonsai_commit_hash": get_last_commit_hash(),
"bonsai_commit_date": get_last_commit_date(),
"last_actions": last_actions,
"last_error": last_error,
}
@@ -218,6 +226,7 @@ if IN_BLENDER:
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat()
except:
pass
+2 -3
View File
@@ -83,6 +83,7 @@ modules = {
"covering": None,
"web": None,
"light": None,
"alignment": None,
# Uncomment this line to enable loading of the demo module. Happy hacking!
# The name "demo" must correlate to a folder name in `bim/module/`.
# "demo": None,
@@ -115,10 +116,8 @@ classes = [
operator.ReloadIfcFile,
operator.RemoveIfcFile,
operator.RevertClippingPlaneCut,
operator.SelectDataDir,
operator.SelectCacheDir,
operator.SelectDir,
operator.SelectIfcFile,
operator.SelectSchemaDir,
operator.SelectURIAttribute,
operator.SetTab,
operator.SwitchTab,
@@ -28,11 +28,13 @@ 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_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcTypeProduct',(#25,#26,#27,#28,#30));
#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.);
#28=IFCSIMPLEPROPERTYTEMPLATE('0bnzttUb9BPuN597uNTXOE',$,'TextSuffix','Text to add after annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.);
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
ENDSEC;
END-ISO-10303-21;
+3 -2
View File
@@ -132,11 +132,12 @@ class IfcExporter:
bpy.ops.bim.update_representation(obj=obj.name)
def has_changed_materials(self, obj: bpy.types.Object) -> bool:
checksum = obj.data.BIMMeshProperties.material_checksum
mprops = tool.Geometry.get_mesh_props(obj.data)
checksum = mprops.material_checksum
return checksum != tool.Geometry.get_material_checksum(obj)
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
if tool.Geometry.is_scaled(obj):
bpy.ops.bim.update_representation(obj=obj.name)
# update_representation might not apply scale if the object has openings
+31 -28
View File
@@ -24,17 +24,15 @@ import ifcopenshell.util.unit
import ifcopenshell.api.owner.settings
import bonsai.bim
import bonsai.tool as tool
import bonsai.core.owner as core_owner
from bpy.app.handlers import persistent
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation
from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
from bonsai.bim.module.nest.decorator import NestDecorator
from mathutils import Vector
from math import cos, degrees
from math import cos
from typing import Union, Callable
@@ -55,23 +53,24 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
return
if isinstance(obj, bpy.types.Material):
props = obj.BIMStyleProperties
props = tool.Style.get_material_style_props(obj)
if ifc_definition_id := props.ifc_definition_id:
if props.is_renaming:
props.is_renmaing = False
props.is_renaming = False
return
IfcStore.get_file().by_id(ifc_definition_id).Name = obj.name
tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name
refresh_ui_data()
return
if not obj.BIMObjectProperties.ifc_definition_id:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return
if obj.BIMObjectProperties.is_renaming:
obj.BIMObjectProperties.is_renaming = False
if props.is_renaming:
props.is_renaming = False
return
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
element = tool.Ifc.get().by_id(props.ifc_definition_id)
if "/" in obj.name:
object_name = obj.name
element_name = obj.name.split("/", 1)[1]
@@ -87,8 +86,8 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
if not element.is_a("IfcRoot"):
return
element.Name = element_name
if obj.BIMObjectProperties.collection:
obj.BIMObjectProperties.collection.name = object_name
if props.collection:
props.collection.name = object_name
refresh_ui_data()
@@ -221,35 +220,38 @@ 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
props = tool.Drawing.get_document_props()
props.should_draw_decorations = props.should_draw_decorations
if bpy.context.scene.WebProperties.is_connected:
tool.Web.send_webui_data()
@persistent
def loadIfcStore(scene):
def loadIfcStore(scene: bpy.types.Scene) -> None:
IfcStore.purge()
refresh_ui_data()
if not IfcStore.get_file():
if not tool.Ifc.get():
return
IfcStore.get_schema()
tool.Ifc.schema()
IfcStore.relink_all_objects()
@persistent
def undo_post(scene):
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
IfcStore.undo(until_key=bpy.context.scene.BIMProperties.last_transaction)
def undo_post(scene: bpy.types.Scene) -> None:
props = tool.Blender.get_bim_props()
if IfcStore.last_transaction != props.last_transaction:
IfcStore.last_transaction = props.last_transaction
IfcStore.undo(until_key=props.last_transaction)
refresh_ui_data()
tool.Ifc.rebuild_element_maps()
@persistent
def redo_post(scene):
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
IfcStore.redo(until_key=bpy.context.scene.BIMProperties.last_transaction)
def redo_post(scene: bpy.types.Scene) -> None:
props = tool.Blender.get_bim_props()
if IfcStore.last_transaction != props.last_transaction:
IfcStore.last_transaction = props.last_transaction
IfcStore.redo(until_key=props.last_transaction)
refresh_ui_data()
tool.Ifc.rebuild_element_maps()
@@ -282,10 +284,10 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
return pao
def viewport_shading_changed_callback(area):
def viewport_shading_changed_callback(area: bpy.types.Area) -> None:
shading = area.spaces.active.shading.type
if shading == "RENDERED":
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
tool.Style.get_style_props().active_style_type = "External"
def subscribe_to_viewport_shading_changes():
@@ -340,10 +342,11 @@ def load_post(scene):
tool.Blender.setup_tabs()
if tool.Ifc.get() and bpy.data.is_saved:
bpy.context.scene.BIMProperties.has_blend_warning = True
props = tool.Blender.get_bim_props()
props.has_blend_warning = True
# Bonsai overlays
georeference_props = bpy.context.scene.BIMGeoreferenceProperties
georeference_props = tool.Georeference.get_georeference_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
model_props = tool.Model.get_model_props()
+15 -5
View File
@@ -27,12 +27,12 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING
if TYPE_CHECKING:
import bonsai.bim.prop
from bonsai.bim.prop import Attribute
from bonsai.bim.module.search.prop import BIMFilterGroup
# ImportCallback return values:
# - None - property should be imported by default workflow
@@ -119,7 +119,8 @@ def import_attributes(
data: dict[str, Any],
callback: Optional[ImportCallback] = None,
) -> None:
for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes():
schema = tool.Ifc.schema()
for attribute in schema.declaration_by_name(ifc_class).all_attributes():
import_attribute(attribute, props, data, callback=callback)
@@ -292,7 +293,10 @@ def prop_with_search(
should_click_ok: bool = False,
original_operator_path: Optional[str] = None,
**kwargs: Any,
):
) -> bpy.types.UILayout:
"""
:return: Added row.
"""
# kwargs are layout.prop arguments (text, icon, etc.)
row = layout.row(align=True)
row.prop(data, prop_name, **kwargs)
@@ -306,6 +310,7 @@ def prop_with_search(
op.original_operator_path = original_operator_path or ""
except TypeError: # Prop is not iterable
pass
return row
def get_enum_items(
@@ -372,11 +377,16 @@ def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip
setattr(property_group, prop_name, prop_value)
def draw_filter(layout: bpy.types.UILayout, filter_groups, data, module: str) -> None:
def draw_filter(
layout: bpy.types.UILayout,
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup],
data,
module: str,
) -> None:
if not data.is_loaded:
data.load()
sprops = bpy.context.scene.BIMSearchProperties
sprops = tool.Search.get_search_props()
if tool.Ifc.get():
row = layout.row(align=True)
+62 -26
View File
@@ -33,7 +33,7 @@ import bonsai.bim.handler
import bonsai.tool as tool
from pathlib import Path
from bonsai.tool.brick import BrickStore
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, cast
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
@@ -105,10 +105,8 @@ class IfcStore:
@staticmethod
def get_file():
if IfcStore.file is None:
IfcStore.path = cast(str, bpy.context.scene.BIMProperties.ifc_file)
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
props = tool.Blender.get_bim_props()
IfcStore.set_path(props.ifc_file)
if IfcStore.path:
try:
IfcStore.load_file(IfcStore.path)
@@ -116,13 +114,21 @@ class IfcStore:
print(f"Failed to load file {IfcStore.path}. Error details: {e}")
return IfcStore.file
@staticmethod
def set_path(value):
IfcStore.path = value
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
@staticmethod
def get_cache():
if IfcStore.cache is None and IfcStore.path:
props = tool.Blender.get_bim_props()
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
os.makedirs(bpy.context.scene.BIMProperties.cache_dir, exist_ok=True)
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
os.makedirs(props.cache_dir, exist_ok=True)
IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
cache_path = Path(IfcStore.cache_path)
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
@@ -162,7 +168,8 @@ class IfcStore:
assert IfcStore.file
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
props = tool.Blender.get_bim_props()
new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
IfcStore.cache = None
try:
shutil.move(IfcStore.cache_path, new_cache_path)
@@ -178,6 +185,7 @@ class IfcStore:
if not os.path.isfile(path):
return
extension = path.split(".")[-1]
props = tool.Project.get_project_props()
if extension.lower() == "ifczip":
with tempfile.TemporaryDirectory() as unzipped_path:
with zipfile.ZipFile(path, "r") as zip_ref:
@@ -187,7 +195,7 @@ class IfcStore:
return
elif extension.lower() == "ifcxml":
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
elif bpy.context.scene.BIMProjectProperties.should_stream:
elif props.should_stream:
IfcStore.file = ifcopenshell.open(path, should_stream=True)
else:
IfcStore.file = ifcopenshell.open(path)
@@ -195,9 +203,8 @@ class IfcStore:
@staticmethod
def get_schema() -> ifcopenshell.ifcopenshell_wrapper.schema_definition:
if IfcStore.file is None:
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(
bpy.context.scene.BIMProjectProperties.export_schema
)
props = tool.Project.get_project_props()
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(props.export_schema)
elif IfcStore.schema is None:
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema_identifier)
return IfcStore.schema
@@ -247,7 +254,7 @@ class IfcStore:
# refactor this class and deprecate usage of IfcStore in favour of
# tools.
if not isinstance(obj, (bpy.types.Object, bpy.types.Material)):
obj.BIMMeshProperties.ifc_definition_id = element.id()
tool.Geometry.get_mesh_props(obj).ifc_definition_id = element.id()
return
existing_obj = IfcStore.id_map.get(element.id(), None)
@@ -262,9 +269,11 @@ class IfcStore:
IfcStore.guid_map[global_id] = obj
if element.is_a("IfcSurfaceStyle"):
obj.BIMStyleProperties.ifc_definition_id = element.id()
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = element.id()
else:
obj.BIMObjectProperties.ifc_definition_id = element.id()
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = element.id()
tool.Ifc.setup_listeners(obj)
@@ -402,15 +411,24 @@ class IfcStore:
@staticmethod
def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None:
if isinstance(obj, bpy.types.Material):
obj.BIMStyleProperties.ifc_definition_id = 0
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = 0
else: # bpy.types.Object
obj.BIMObjectProperties.ifc_definition_id = 0
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = 0
@staticmethod
def execute_ifc_operator(operator: tool.Ifc.Operator, context: bpy.types.Context, is_invoke=False) -> set[str]:
def execute_ifc_operator(
operator: tool.Ifc.Operator,
context: bpy.types.Context,
event=None,
method: Literal["EXECUTE", "INVOKE", "MODAL"] = "EXECUTE",
) -> set[str]:
bonsai.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)
props = tool.Blender.get_bim_props()
props.is_dirty = True
# Modals don't nest, and Blender handles the loop that continuously calls modal()
is_top_level_operator = not bool(IfcStore.current_transaction) or (method == "MODAL")
if is_top_level_operator:
IfcStore.begin_transaction(operator)
@@ -436,10 +454,12 @@ class IfcStore:
bonsai.bim.handler.refresh_ui_data()
try:
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
if method == "EXECUTE":
result = getattr(operator, "_execute")(context)
elif method == "INVOKE":
result = getattr(operator, "_invoke")(context, event)
elif method == "MODAL":
result = getattr(operator, "_modal")(context, event)
except:
bonsai.last_error = traceback.format_exc()
# Try to ensure undo will work since Blender undo does work in case of errors.
@@ -451,12 +471,27 @@ class IfcStore:
end_top_level_operator()
raise
end_top_level_operator()
if method == "MODAL":
if result == {"FINISHED"}:
end_top_level_operator()
elif result == {"CANCELLED"}:
# Please read the docs: https://docs.blender.org/api/current/bpy.types.Operator.html
# > "when an operator returns {'CANCELLED'}, no undo step will be created".
# This means that if your modal edits IFC data, then the user
# cancels it, Blender's undo history will not be in sync with
# Bonsai / IfcOpenShell's undo history. Instead of hoping for
# Bonsai devs to remember to handle the "cancel" state (i.e.
# detect escape keypress) and return {"FINISHED"}, we instead
# always enforce an undo step.
bpy.ops.ed.undo_push(message=f"Cancel {operator.bl_idname}")
end_top_level_operator()
else:
end_top_level_operator()
return result
@staticmethod
def begin_transaction(operator: tool.Ifc.Operator) -> None:
IfcStore.current_transaction = str(uuid.uuid4())
IfcStore.current_transaction = str(uuid.uuid4()) + operator.__class__.__name__
operator.transaction_key = IfcStore.current_transaction
@staticmethod
@@ -470,7 +505,8 @@ class IfcStore:
) -> None:
key = getattr(operator, "transaction_key", None)
data = getattr(operator, "transaction_data", None)
bpy.context.scene.BIMProperties.last_transaction = key
props = tool.Blender.get_bim_props()
props.last_transaction = key
IfcStore.last_transaction = key
rollback = rollback or getattr(operator, "rollback", lambda data: True)
commit = commit or getattr(operator, "commit", lambda data: True)
+73 -40
View File
@@ -20,6 +20,7 @@ from __future__ import annotations
import bpy
import time
import json
import ifcpatch
import logging
import traceback
import mathutils
@@ -38,6 +39,7 @@ import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any, Literal
from ifcopenshell.util.shape import MatrixType
class MaterialCreator:
@@ -69,6 +71,9 @@ class MaterialCreator:
if isinstance(mesh, bpy.types.Curve):
return
if mesh.get("has_layer_styles", None) == True:
return
self.mesh = mesh
self.obj = obj
if element.is_a("IfcTypeProduct"):
@@ -82,7 +87,7 @@ class MaterialCreator:
def load_existing_materials(self) -> None:
for material in bpy.data.materials:
if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id:
if ifc_definition_id := tool.Blender.get_ifc_definition_id(material):
self.styles[ifc_definition_id] = material
def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None:
@@ -90,7 +95,8 @@ class MaterialCreator:
return # Already has materials assign to the representation itself
# Otherwise, we need to check for material styles on the element, since
# create_shape on types only works on representations.
context = tool.Ifc.get().by_id(self.mesh.BIMMeshProperties.ifc_definition_id).ContextOfItems
mprops = tool.Geometry.get_mesh_props(self.mesh)
context = tool.Ifc.get().by_id(mprops.ifc_definition_id).ContextOfItems
for material in ifcopenshell.util.element.get_materials(element):
if style := ifcopenshell.util.representation.get_material_style(material, context):
self.mesh["ios_materials"] = (style.id(),)
@@ -218,7 +224,7 @@ class IfcImporter:
self.progress = 0
self.material_creator = MaterialCreator(ifc_import_settings, self)
classes_to_wireframe_str = bpy.context.scene.DocProperties.classes_to_wireframe
classes_to_wireframe_str = tool.Drawing.get_document_props().classes_to_wireframe
self.classes_to_wireframe_list = [word.strip() for word in classes_to_wireframe_str.split(",")]
def profile_code(self, message: str) -> None:
@@ -294,6 +300,20 @@ class IfcImporter:
bpy.context.window_manager.progress_end()
def process_context_filter(self) -> None:
contexts = self.file.by_type("IfcGeometricRepresentationContext")
if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto.
print("Warning! Excessive contexts were found and merged where applicable.")
uniques = {}
i = 0
for element in contexts:
data = "-".join([str(a) for a in element])
if unique := uniques.get(data, None):
ifcopenshell.util.element.replace_element(element, unique)
self.file.remove(element)
i += 1
else:
uniques[data] = element
print(f"Replaced {i} IfcGeometricRepresentationContext")
tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(self.file)
tool.Loader.settings.context_settings = tool.Loader.create_settings()
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
@@ -434,7 +454,7 @@ class IfcImporter:
return False
def calculate_model_offset(self) -> None:
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if self.ifc_import_settings.false_origin_mode == "MANUAL":
tool.Loader.set_manual_blender_offset(self.file)
elif self.ifc_import_settings.false_origin_mode == "AUTOMATIC":
@@ -475,14 +495,16 @@ class IfcImporter:
if grid.WAxes:
self.create_grid_axes(grid.WAxes, grid_obj, grid_placement)
def create_grid_axes(self, axes, grid_obj, grid_placement):
def create_grid_axes(
self, axes: list[ifcopenshell.entity_instance], grid_obj: bpy.types.Object, grid_placement: MatrixType
) -> None:
for axis in axes:
shape = tool.Loader.create_generic_shape(axis.AxisCurve)
mesh = self.create_mesh(axis, shape)
obj = bpy.data.objects.new(tool.Loader.get_name(axis), mesh)
obj.show_in_front = True
self.link_element(axis, obj)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement.copy()))
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement))
def create_element_types(self):
for element_type in self.element_types:
@@ -507,8 +529,9 @@ class IfcImporter:
pass
elif shape:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
if mesh is not None:
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
break
@@ -738,12 +761,9 @@ class IfcImporter:
def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]:
result = set()
for product in products:
representation = self.get_pointcloud_representation(product)
if representation is not None:
pointcloud = self.create_pointcloud(product, representation)
if pointcloud is not None:
if representation := self.get_pointcloud_representation(product):
if pointcloud := self.create_pointcloud(product, representation):
result.add(pointcloud)
return result
def create_pointcloud(
@@ -791,8 +811,9 @@ class IfcImporter:
materials_updated = bool(mesh)
if mesh is None:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
if mesh is not None:
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
mesh = None
@@ -803,11 +824,10 @@ class IfcImporter:
if element.is_a(ifcclass):
obj.display_type = "WIRE"
if shape:
if shape and mesh:
# We use numpy here because Blender mathutils.Matrix is not accurate enough
mat = np.array(shape.transformation.matrix).reshape((4, 4), order="F")
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, mat))
assert mesh # Type checker.
if not materials_updated:
self.material_creator.create(element, obj, mesh, tool.Geometry.does_shape_has_openings(shape))
elif mesh: # When does this occur?
@@ -878,17 +898,19 @@ class IfcImporter:
def load_file(self):
self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file)
if not bpy.context.scene.BIMProperties.ifc_file:
bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file
self.file = IfcStore.get_file()
props = tool.Blender.get_bim_props()
if not props.ifc_file:
props.ifc_file = self.ifc_import_settings.input_file
self.file = tool.Ifc.get()
def calculate_unit_scale(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
tool.Loader.set_unit_scale(self.unit_scale)
def set_units(self):
def set_units(self) -> None:
if not (assignment := self.file.by_type("IfcProject")[0].UnitsInContext):
return # Geometry is optional in IFC
props = tool.Blender.get_bim_props()
for unit in assignment.Units:
if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT":
if unit.is_a("IfcSIUnit"):
@@ -908,26 +930,27 @@ class IfcImporter:
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "AREAUNIT":
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
try:
bpy.context.scene.BIMProperties.area_unit = "{}{}".format(
props.area_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
except: # Probably an invalid unit.
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
props.area_unit = "SQUARE_METRE"
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "VOLUMEUNIT":
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
try:
bpy.context.scene.BIMProperties.volume_unit = "{}{}".format(
props.volume_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
except: # Probably an invalid unit.
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
props.volume_unit = "CUBIC_METRE"
def create_project(self):
project = self.file.by_type("IfcProject")[0]
self.project = {"ifc": project}
obj = tool.Ifc.get_object(project)
if obj:
self.project["blender"] = obj.BIMObjectProperties.collection
props = tool.Blender.get_object_bim_props(obj)
self.project["blender"] = props.collection
self.has_existing_project = True
return
self.project["blender"] = bpy.data.collections.new(
@@ -937,10 +960,16 @@ class IfcImporter:
obj.hide_select = True
self.project["blender"].objects.link(obj)
self.project["blender"].BIMCollectionProperties.obj = obj
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
props = tool.Blender.get_object_bim_props(obj)
props.collection = self.collections[project.GlobalId] = self.project["blender"]
def create_styles(self) -> None:
for style in self.file.by_type("IfcSurfaceStyle"):
styles = self.file.by_type("IfcSurfaceStyle")
if len(styles) > self.ifc_import_settings.style_limit: # Probably something strange happening
print("Warning! Excessive styles were found and merged where applicable.")
ifcpatch.execute({"file": self.file, "recipe": "MergeStyles", "arguments": []})
styles = self.file.by_type("IfcSurfaceStyle")
for style in styles:
self.create_style(style)
def create_style(self, style: ifcopenshell.entity_instance) -> None:
@@ -953,10 +982,11 @@ class IfcImporter:
self.material_creator.styles[style.id()] = blender_material
style_elements = tool.Style.get_style_elements(blender_material)
props = tool.Style.get_material_style_props(blender_material)
if tool.Style.has_blender_external_style(style_elements):
blender_material.BIMStyleProperties.active_style_type = "External"
props.active_style_type = "External"
else:
blender_material.BIMStyleProperties.active_style_type = "Shading"
props.active_style_type = "Shading"
def place_objects_in_collections(self) -> None:
for ifc_definition_id, obj in self.added_data.items():
@@ -964,10 +994,10 @@ class IfcImporter:
tool.Collector.assign(obj, should_clean_users_collection=False)
def is_curve_annotation(self, element: ifcopenshell.entity_instance) -> bool:
object_type = element.ObjectType
object_type = ifcopenshell.util.element.get_predefined_type(element)
return (
object_type in tool.Drawing.ANNOTATION_TYPES_DATA
and tool.Drawing.ANNOTATION_TYPES_DATA[object_type][3] == "curve"
and tool.Drawing.ANNOTATION_TYPES_DATA[object_type].data_type == "curve"
)
def get_drawing_group(self, element):
@@ -1021,7 +1051,7 @@ class IfcImporter:
element: ifcopenshell.entity_instance,
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> bpy.types.Mesh:
) -> Union[bpy.types.Mesh, None]:
try:
if hasattr(shape, "geometry"):
# shape is ShapeElementType
@@ -1040,7 +1070,7 @@ class IfcImporter:
if cartesian_point_offset is False:
mesh["has_cartesian_point_offset"] = False
elif cartesian_point_offset is not None:
verts -= cartesian_point_offset
verts = verts - cartesian_point_offset
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = (
@@ -1048,21 +1078,21 @@ class IfcImporter:
)
elif verts.size and tool.Loader.is_point_far_away(verts[0], is_meters=True):
# Shift geometry close to the origin based off that first vert it found
verts_array = np.array(geometry.verts)
offset = verts[0]
verts -= offset
verts = verts - offset
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = f"{offset[0]},{offset[1]},{offset[2]}"
else:
mesh["has_cartesian_point_offset"] = False
return tool.Loader.convert_geometry_to_mesh(
mesh = tool.Loader.convert_geometry_to_mesh(
geometry,
mesh,
verts=verts,
load_indexed_maps=self.ifc_import_settings.load_indexed_maps,
)
return tool.Loader.slice_layerset_mesh(element, mesh)
except:
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
import traceback
@@ -1070,9 +1100,10 @@ class IfcImporter:
print(traceback.format_exc())
def set_default_context(self):
rprops = tool.Root.get_root_props()
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"):
if subcontext.ContextIdentifier == "Body":
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
rprops.contexts = str(subcontext.id())
break
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
@@ -1118,6 +1149,7 @@ class IfcImportSettings:
self.deflection_tolerance = 0.001
self.angular_tolerance = 0.5
self.void_limit = 30
self.style_limit = 300
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
self.distance_limit = 1000
@@ -1138,7 +1170,7 @@ class IfcImportSettings:
@staticmethod
def factory(context=None, input_file=None, logger=None):
scene_diff = bpy.context.scene.DiffProperties
scene_diff = tool.Blender.get_diff_props()
props = tool.Project.get_project_props()
settings = IfcImportSettings()
settings.input_file = input_file
@@ -1155,6 +1187,7 @@ class IfcImportSettings:
settings.deflection_tolerance = props.deflection_tolerance
settings.angular_tolerance = props.angular_tolerance
settings.void_limit = props.void_limit
settings.style_limit = props.style_limit
settings.distance_limit = props.distance_limit
settings.false_origin_mode = props.false_origin_mode
try:
@@ -23,7 +23,6 @@ import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
import bonsai.core.spatial
from bonsai.bim.ifc import IfcStore
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
@@ -102,6 +102,7 @@ class BIMObjectAggregateProperties(PropertyGroup):
class Objects(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
previous_hide_select: bpy.props.BoolProperty(default=False)
class BIMAggregateProperties(PropertyGroup):
+9 -9
View File
@@ -34,12 +34,12 @@ class BIM_PT_aggregate(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
@@ -66,9 +66,9 @@ class BIM_PT_aggregate(Panel):
col.enabled = False
op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK")
if props.relating_object:
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
op.relating_object = tool.Blender.get_object_bim_props(props.relating_object).ifc_definition_id
elif props.related_object:
op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id
op.related_object = tool.Blender.get_object_bim_props(props.related_object).ifc_definition_id
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
return
else:
@@ -115,14 +115,14 @@ class BIM_PT_linked_aggregate(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
return True
@@ -0,0 +1,36 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai 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.
#
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
# from . import ui, prop, operator
from . import operator
classes = (operator.ImportAlignmentCSV,)
def menu_func_import(self, context):
self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)")
def register():
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
@@ -0,0 +1,113 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2022 Yassine Oualid <yassine@sigmadimensions.com>
#
# This file is part of Bonsai.
#
# Bonsai 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.
#
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
# pyright: reportUnnecessaryTypeIgnoreComment=error
import os
import ifcopenshell.api.alignment
import ifcopenshell.api.alignment.add_stationing_to_alignment
import bpy
import json
import time
import calendar
import isodate
import bonsai.core.sequence as core
import bonsai.tool as tool
import bonsai.bim.module.sequence.helper as helper
import ifcopenshell.api.spatial
import ifcopenshell.geom
import ifcopenshell.util.sequence
import ifcopenshell.util.selector
from datetime import datetime
from dateutil import parser, relativedelta
from bpy_extras.io_utils import ImportHelper
from typing import get_args, TYPE_CHECKING
from typing_extensions import assert_never
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_alignment_csv"
bl_label = "Import Alignment CSV"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@classmethod
def poll(cls, context):
ifc_file = tool.Ifc.get()
if ifc_file is None:
cls.poll_message_set("No IFC file is loaded.")
return False
elif ifc_file.schema != "IFC4X3":
cls.poll_message_set("Schema must be IFC4x3.")
return False
return True
def _execute(self, context):
self.file = tool.Ifc.get()
start = time.time()
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment)
ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0)
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
sites = self.file.by_type("IfcSite")
for site in sites:
ifcopenshell.api.spatial.reference_structure(self.file, products=[alignment], relating_structure=site)
# process the generated IfcReferent for the alignment
for rel in alignment.IsNestedBy:
for referent in rel.RelatedObjects:
if referent.is_a("IfcReferent"):
referent_obj = bpy.data.objects.new(tool.Loader.get_name(referent), None)
tool.Geometry.link(referent, referent_obj)
tool.Collector.assign(referent_obj, should_clean_users_collection=False)
# an alignment can be an aggregation of multiple child alignments (ie. multiple verticals for a single horizontal)
# get all the alignment curves
curves = []
for rel in alignment.IsDecomposedBy:
for agg in rel.RelatedObjects:
if agg.is_a("IfcAlignment"):
curves.append(ifcopenshell.api.alignment.get_curve(agg)) # 3D curve
# if there aren't any curves from aggregation, then there is only a single vertical or no vertical
if len(curves) == 0:
curves.append(ifcopenshell.api.alignment.get_curve(alignment))
settings = ifcopenshell.geom.settings()
for curve in curves:
shape = ifcopenshell.geom.create_shape(settings, curve)
# create a new Blender mesh
mesh_name = tool.Loader.get_mesh_name_from_shape(shape)
mesh = bpy.data.meshes.new(mesh_name)
m = tool.Loader.convert_geometry_to_mesh(shape, mesh)
# create a new Blender object
alignment_obj = bpy.data.objects.new(tool.Loader.get_name(alignment), m)
# link the blender object to with the alignment element
tool.Geometry.link(alignment, alignment_obj)
# assign the object to the blender collections
tool.Collector.assign(alignment_obj, should_clean_users_collection=False)
self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start))
@@ -26,7 +26,6 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.attribute as core
import bonsai.core.spatial
from bonsai.bim.ifc import IfcStore
def get_objs_for_operation(operator_properties, context):
@@ -117,7 +116,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
obj = tool.Blender.get_active_object(is_selected=False)
if not (element := tool.Ifc.get_entity(obj)):
return
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -18,7 +18,6 @@
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.attribute.data import AttributesData
import bonsai.tool as tool
@@ -113,7 +113,7 @@ class AuginCreateNewModel(bpy.types.Operator):
context.scene.collection.objects.link(cam_obj)
context.scene.camera = cam_obj
tmpdir = tempfile.mkdtemp()
tmpdir = tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None)
thumb_path = os.path.join(tmpdir, "thumb.png")
context.scene.render.image_settings.file_format = "PNG"
context.scene.render.filepath = thumb_path
@@ -122,15 +122,16 @@ class AuginCreateNewModel(bpy.types.Operator):
context.scene.render.image_settings.file_format = old_file_format
context.scene.render.filepath = old_filepath
client.upload_file(context.scene.BIMProperties.ifc_file, result["s3_bucket"], result["model_path"])
bim_props = tool.Blender.get_bim_props()
client.upload_file(bim_props.ifc_file, result["s3_bucket"], result["model_path"])
client.upload_file(thumb_path, result["s3_bucket"], result["thumb_path"])
# Notify done
url = "https://server.auge.pro.br/API/v3/augin_rest.php/files_uploaded"
payload = {
"user_token": props.token,
"ifc_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file),
"model_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file),
"ifc_filesize": os.path.getsize(bim_props.ifc_file),
"model_filesize": os.path.getsize(bim_props.ifc_file),
"thumb_filesize": os.path.getsize(thumb_path),
"model_upload_path": result["model_path"],
"thumb_upload_path": result["thumb_path"],
+3 -1
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy.types
import bonsai.tool as tool
class BIM_PT_augin(bpy.types.Panel):
@@ -47,7 +48,8 @@ class BIM_PT_augin(bpy.types.Panel):
row = layout.row()
row.label(text="Logged in as " + props.username)
if not context.scene.BIMProperties.ifc_file:
bim_props = tool.Blender.get_bim_props()
if not bim_props.ifc_file:
row = layout.row()
row.label(text="No IFC Found")
return
+4 -2
View File
@@ -21,6 +21,7 @@ import bpy
import bcf
import bcf.bcfxml
import bcf.v2.bcfxml
import bonsai.tool as tool
from typing import Union
@@ -30,7 +31,8 @@ class BcfStore:
@classmethod
def get_bcfxml(cls) -> Union[bcf.bcfxml.BcfXml, None]:
if not cls.bcfxml:
bcf_filepath = bpy.context.scene.BCFProperties.bcf_file
props = tool.Bcf.get_bcf_props()
bcf_filepath = props.bcf_file
if not os.path.isabs(bcf_filepath):
bcf_filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), bcf_filepath))
if bcf_filepath:
@@ -46,7 +48,7 @@ class BcfStore:
@classmethod
def set(cls, bcfxml: Union[bcf.bcfxml.BcfXml, None], filepath: str) -> None:
cls.bcfxml = bcfxml
props = bpy.context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
props.bcf_file = filepath
# Set bcf_version prop on load.
+90 -83
View File
@@ -42,8 +42,8 @@ import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.bcf.prop as bcf_prop
import bonsai.bim.module.bcf.bcfstore as bcfstore
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from bonsai.bim.ifc import IfcStore
from math import radians, degrees, atan, tan, cos, sin
from mathutils import Vector, Matrix, Euler, geometry
from xsdata.models.datatype import XmlDateTime
@@ -55,7 +55,7 @@ class NewBcfProject(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
bcf_v2 = props.bcf_version == "2"
bcf_class = bcf.v2.bcfxml.BcfXml if bcf_v2 else bcf.v3.bcfxml.BcfXml
bcfxml = bcf_class.create_new("New Project")
@@ -64,12 +64,14 @@ class NewBcfProject(bpy.types.Operator):
return {"FINISHED"}
class LoadBcfProject(bpy.types.Operator):
class LoadBcfProject(bpy.types.Operator, ImportHelper):
bl_idname = "bim.load_bcf_project"
bl_label = "Load BCF Project"
bl_description = "Load the BCF file."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
filename_ext = ".bcf"
def execute(self, context):
# Operator is also used when new project is created by not yet saved.
@@ -103,15 +105,12 @@ class LoadBcfProject(bpy.types.Operator):
assert bcfxml.project
if bcfxml.project.name is None:
bcfxml.project.name = nameless
context.scene.BCFProperties.name = bcfxml.project.name
props = tool.Bcf.get_bcf_props()
props.name = bcfxml.project.name
bpy.ops.bim.load_bcf_topics()
self.report({"INFO"}, f"BCF Project '{Path(self.filepath).name}' is loaded.")
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class UnloadBcfProject(bpy.types.Operator):
bl_idname = "bim.unload_bcf_project"
@@ -131,7 +130,7 @@ class LoadBcfTopics(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
props.topics.clear()
# workaround, one non standard topic would break reading entire bcf
# ignored these topics ATM
@@ -163,7 +162,8 @@ class LoadBcfTopic(bpy.types.Operator):
assert bcfxml
topic = bcfxml.topics[self.topic_guid]
bcfxml.get_header(self.topic_guid)
new = context.scene.BCFProperties.topics[self.topic_index]
props = tool.Bcf.get_bcf_props()
new = props.topics[self.topic_index]
data_map = {
"name": topic.guid,
"title": topic.topic.title,
@@ -244,7 +244,8 @@ class LoadBcfComments(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
blender_topic = context.scene.BCFProperties.topics.get(self.topic_guid)
props = tool.Bcf.get_bcf_props()
blender_topic = props.topics.get(self.topic_guid)
blender_topic.comments.clear()
for comment in bcfxml.topics[self.topic_guid].comments:
new = blender_topic.comments.add()
@@ -274,7 +275,9 @@ class EditBcfProjectName(bpy.types.Operator):
# Bonsai creates default project on load.
assert bcfxml.project
bcfxml.project.name = context.scene.BCFProperties.name
props = tool.Bcf.get_bcf_props()
bcfxml.project.name = props.name
return {"FINISHED"}
@@ -284,7 +287,7 @@ class EditBcfTopicName(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
@@ -300,7 +303,7 @@ class EditBcfTopic(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
@@ -337,13 +340,15 @@ class EditBcfTopic(bpy.types.Operator):
return {"FINISHED"}
class SaveBcfProject(bpy.types.Operator):
class SaveBcfProject(bpy.types.Operator, ExportHelper):
bl_idname = "bim.save_bcf_project"
bl_label = "Save BCF Project"
bl_description = "Save active BCF project by the provided filepath."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
save_current_bcf: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
filename_ext = ".bcf"
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
@@ -360,8 +365,7 @@ class SaveBcfProject(bpy.types.Operator):
self.filepath = str(path)
return self.execute(context)
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
class AddBcfTopic(bpy.types.Operator):
@@ -371,13 +375,15 @@ class AddBcfTopic(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.author
props = tool.Bcf.get_bcf_props()
return props.author
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
bcfxml.add_topic("New Topic", "", context.scene.BCFProperties.author)
props = tool.Bcf.get_bcf_props()
bcfxml.add_topic("New Topic", "", props.author)
bpy.ops.bim.load_bcf_topics()
return {"FINISHED"}
@@ -389,7 +395,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
props_are_filled = all(
(getattr(props, attr) for attr in ("bim_snippet_reference", "bim_snippet_schema", "bim_snippet_type"))
)
@@ -403,7 +409,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
is_external = "://" in props.bim_snippet_reference
@@ -435,7 +441,7 @@ class AddBcfRelatedTopic(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
related_topics = tool.Bcf.get_topic_related_topics(topic)
@@ -462,14 +468,15 @@ class AddBcfHeaderFile(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.file_reference
props = tool.Bcf.get_bcf_props()
return props.file_reference
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -519,9 +526,10 @@ class ViewBcfTopic(bpy.types.Operator):
topic_guid: bpy.props.StringProperty()
def execute(self, context):
for index, topic in enumerate(context.scene.BCFProperties.topics):
props = tool.Bcf.get_bcf_props()
for index, topic in enumerate(props.topics):
if topic.name.lower() == self.topic_guid.lower():
context.scene.BCFProperties.active_topic_index = index
props.active_topic_index = index
break
return {"FINISHED"}
@@ -546,7 +554,7 @@ class AddBcfViewpoint(bpy.types.Operator):
blender_camera = context.scene.camera
assert blender_camera
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -675,7 +683,7 @@ class RemoveBcfViewpoint(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
if not bcfxml:
return False
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
topic = props.active_topic
if not topic:
return False
@@ -690,7 +698,7 @@ class RemoveBcfViewpoint(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
del topic.viewpoints[blender_topic.viewpoints]
@@ -715,7 +723,7 @@ class RemoveBcfFile(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
header_files = tool.Bcf.get_topic_header_files(topic)
@@ -733,13 +741,14 @@ class RemoveBcfTopic(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.topics
props = tool.Bcf.get_bcf_props()
return props.topics
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
del bcfxml.topics[props.active_topic.name]
bpy.ops.bim.load_bcf_topics()
return {"FINISHED"}
@@ -752,13 +761,14 @@ class AddBcfReferenceLink(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.reference_link
props = tool.Bcf.get_bcf_props()
return bool(props.reference_link)
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
reference_links = tool.Bcf.get_topic_reference_links(topic)
@@ -776,14 +786,15 @@ class AddBcfDocumentReference(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.document_reference
props = tool.Bcf.get_bcf_props()
return bool(props.document_reference)
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -855,13 +866,14 @@ class AddBcfLabel(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.label
props = tool.Bcf.get_bcf_props()
return bool(props.label)
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
new = blender_topic.labels.add()
@@ -883,7 +895,7 @@ class EditBcfReferenceLinks(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
reference_links = [r.name for r in blender_topic.reference_links]
@@ -900,7 +912,7 @@ class EditBcfLabels(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
labels = [l.name for l in blender_topic.labels]
@@ -918,7 +930,7 @@ class RemoveBcfReferenceLink(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
reference_links = tool.Bcf.get_topic_reference_links(topic)
@@ -938,7 +950,7 @@ class RemoveBcfLabel(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
labels = tool.Bcf.get_topic_labels(topic)
@@ -957,7 +969,7 @@ class RemoveBcfBimSnippet(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
tool.Bcf.set_topic_bim_snippet(topic, None)
@@ -978,7 +990,7 @@ class RemoveBcfDocumentReference(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -1043,7 +1055,7 @@ class RemoveBcfRelatedTopic(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
related_topics = tool.Bcf.get_topic_related_topics(topic)
@@ -1063,7 +1075,7 @@ class RemoveBcfComment(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
comments = topic.comments
@@ -1084,7 +1096,7 @@ class EditBcfComment(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
blender_comment = blender_topic.comments.get(self.comment_guid)
topic = bcfxml.topics[blender_topic.name]
@@ -1092,7 +1104,7 @@ class EditBcfComment(bpy.types.Operator):
if comment.guid == self.comment_guid:
comment.comment = blender_comment.comment
comment.modified_date = XmlDateTime.now()
comment.modified_author = context.scene.BCFProperties.author
comment.modified_author = props.author
bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid)
return {"FINISHED"}
@@ -1105,7 +1117,7 @@ class AddBcfComment(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
if not props.comment:
cls.poll_message_set("No comment to add.")
return False
@@ -1128,7 +1140,7 @@ class AddBcfComment(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
comments = topic.comments
@@ -1136,7 +1148,7 @@ class AddBcfComment(bpy.types.Operator):
if bcf_v2:
comment = bcf.v2.model.Comment(
date=XmlDateTime.now(),
author=context.scene.BCFProperties.author,
author=props.author,
comment=props.comment,
guid=str(uuid.uuid4()),
)
@@ -1149,7 +1161,7 @@ class AddBcfComment(bpy.types.Operator):
else:
comment = bcf.v3.model.Comment(
date=XmlDateTime.now(),
author=context.scene.BCFProperties.author,
author=props.author,
comment=props.comment,
guid=str(uuid.uuid4()),
)
@@ -1179,7 +1191,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
if blender_topic is None:
cls.poll_message_set("No topic is active.")
@@ -1193,11 +1205,11 @@ class ActivateBcfViewpoint(bpy.types.Operator):
return True
def execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
if self.viewpoint_guid:
@@ -1303,7 +1315,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
[0, 0, 0, 1],
)
)
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
matrix = ifcopenshell.util.geolocation.global2local(
@@ -1342,7 +1354,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
objs: list[bpy.types.Object] = []
for global_id in exception_global_ids:
obj = IfcStore.get_element(global_id)
obj = tool.Ifc.get_object_by_identifier(global_id)
if obj and context.view_layer.objects.get(obj.name):
assert isinstance(obj, bpy.types.Object)
objs.append(obj)
@@ -1401,7 +1413,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
return
bpy.ops.object.select_all(action="DESELECT")
for global_id in selected_global_ids:
obj = IfcStore.get_element(global_id)
obj = tool.Ifc.get_object_by_identifier(global_id)
if obj:
obj.select_set(True)
obj.hide_set(False)
@@ -1414,7 +1426,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
for acomponent in acoloring.component:
global_id_colours.setdefault(acomponent.ifc_guid, acoloring.color)
for global_id, color in global_id_colours.items():
obj = IfcStore.get_element(global_id)
obj = tool.Ifc.get_object_by_identifier(global_id)
if obj:
obj.color = self.hex_to_rgb(color)
@@ -1517,58 +1529,51 @@ class OpenBcfReferenceLink(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
webbrowser.open(context.scene.BCFProperties.topic_links[self.index].name)
props = tool.Bcf.get_bcf_props()
webbrowser.open(props.topic_links[self.index].name)
return {"FINISHED"}
class SelectBcfHeaderFile(bpy.types.Operator):
class SelectBcfHeaderFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_bcf_header_file"
bl_label = "Select BCF Header File"
bl_description = "Select filepath for BCF header reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
if self.filepath:
context.scene.BCFProperties.file_reference = self.filepath
props = tool.Bcf.get_bcf_props()
props.file_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectBcfBimSnippetReference(bpy.types.Operator):
class SelectBcfBimSnippetReference(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_bcf_bim_snippet_reference"
bl_label = "Select BCF BIM Snippet Reference"
bl_description = "Select filepath for BCF snippet reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
if self.filepath:
context.scene.BCFProperties.bim_snippet_reference = self.filepath
props = tool.Bcf.get_bcf_props()
props.bim_snippet_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectBcfDocumentReference(bpy.types.Operator):
class SelectBcfDocumentReference(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_bcf_document_reference"
bl_label = "Select BCF Document Reference"
bl_description = "Select filepath for BCF document reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
if self.filepath:
context.scene.BCFProperties.document_reference = self.filepath
props = tool.Bcf.get_bcf_props()
props.document_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class LoadBcfHeaderIfcFile(bpy.types.Operator):
bl_idname = "bim.load_bcf_header_ifc_file"
@@ -1585,7 +1590,8 @@ class LoadBcfHeaderIfcFile(bpy.types.Operator):
assert bcfxml
bcf_path = tool.Bcf.get_path()
topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name]
props = tool.Bcf.get_bcf_props()
topic = bcfxml.topics[props.active_topic.name]
entity = tool.Bcf.get_topic_header_files(topic)[self.index]
ifc_path = bcf.agnostic.topic.extract_file(topic, entity)
bpy.ops.bim.load_project(filepath=ifc_path)
@@ -1605,7 +1611,8 @@ class ExtractBcfFile(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name]
props = tool.Bcf.get_bcf_props()
topic = bcfxml.topics[props.active_topic.name]
if self.entity_type == "HEADER_FILE":
entity = tool.Bcf.get_topic_header_files(topic)[self.index]
+75 -4
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from . import bcfstore
from bonsai.bim.prop import StrProperty
from bpy.types import PropertyGroup
@@ -34,6 +35,7 @@ from functools import partial
from typing import Literal
from typing_extensions import assert_never
from bcf.agnostic.extensions import get_extensions_attributes
from typing import TYPE_CHECKING, Union
bcfviewpoints_enum = None
@@ -95,7 +97,7 @@ def getBcfViewpoints(self, context, force_update=False):
global bcfviewpoints_enum
if bcfviewpoints_enum is None or force_update: # Retrieving Viewpoints is slow. Make sure we only do when needed
bcfviewpoints_enum = []
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
topic = props.active_topic
@@ -110,6 +112,12 @@ class BcfBimSnippet(PropertyGroup):
type: StringProperty(name="Type")
is_external: BoolProperty(name="Is External")
if TYPE_CHECKING:
schema: str
reference: str
type: str
is_external: bool
class BcfDocumentReference(PropertyGroup):
reference: StringProperty(name="Reference")
@@ -117,6 +125,12 @@ class BcfDocumentReference(PropertyGroup):
guid: StringProperty(name="GUID")
is_external: BoolProperty(name="Is External")
if TYPE_CHECKING:
reference: str
description: str
guid: str
is_external: bool
class BcfComment(PropertyGroup):
name: StringProperty(name="GUID")
@@ -128,6 +142,16 @@ class BcfComment(PropertyGroup):
modified_author: StringProperty(name="Modified Author")
is_editable: BoolProperty(name="Is Editable", default=False, update=updateBcfCommentIsEditable)
if TYPE_CHECKING:
name: str
date: str
author: str
comment: str
viewpoint: str
modified_date: str
modified_author: str
is_editable: bool
def get_extensions_items(
self: "BCFProperties", context: bpy.types.Context, edit_text: str, extensions_attr: str
@@ -183,6 +207,30 @@ class BcfTopic(PropertyGroup):
comments: CollectionProperty(name="Comments", type=BcfComment)
is_editable: BoolProperty(name="Edit Topic Attributes", default=False, update=updateBcfTopicIsEditable)
if TYPE_CHECKING:
name: str
title: str
type: str
status: str
priority: str
stage: str
creation_date: str
creation_author: str
modified_date: str
modified_author: str
assigned_to: str
due_date: str
description: str
viewpoints: str
files: bpy.types.bpy_prop_collection_idprop[StrProperty]
reference_links: bpy.types.bpy_prop_collection_idprop[BcfReferenceLink]
labels: bpy.types.bpy_prop_collection_idprop[BcfLabel]
bim_snippet: BcfBimSnippet
document_references: bpy.types.bpy_prop_collection_idprop[BcfDocumentReference]
related_topics: bpy.types.bpy_prop_collection_idprop[StrProperty]
comments: bpy.types.bpy_prop_collection_idprop[BcfComment]
is_editable: bool
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
global RELATED_TOPICS_ENUM_ITEMS
@@ -235,7 +283,30 @@ class BCFProperties(PropertyGroup):
comment: StringProperty(default="", name="Comment")
has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False)
def clear_input_fields(self):
if TYPE_CHECKING:
bcf_file: str
bcf_version: str
comment_text_width: int
name: str
author: str
topics: bpy.types.bpy_prop_collection_idprop[BcfTopic]
active_topic_index: int
file_reference: str
file_ifc_project: str
file_ifc_spatial_structure_element: str
reference_link: str
label: str
bim_snippet_reference: str
bim_snippet_type: str
bim_snippet_schema: str
document_reference: str
document_reference_description: str
document_description: str
related_topic: str
comment: str
has_related_viewpoint: bool
def clear_input_fields(self) -> None:
self.file_reference = ""
self.file_ifc_project = ""
self.file_ifc_spatial_structure_element = ""
@@ -250,7 +321,7 @@ class BCFProperties(PropertyGroup):
self.has_related_viewpoint = False
@property
def active_topic(self):
def active_topic(self) -> Union[BcfTopic, None]:
if len(self.topics) == 0:
return None
if self.active_topic_index < 0:
@@ -259,5 +330,5 @@ class BCFProperties(PropertyGroup):
self.active_topic_index = len(self.topics) - 1
return self.topics[self.active_topic_index]
def refresh_topic(self, context):
def refresh_topic(self, context: bpy.types.Context) -> None:
refreshBcfTopic(self, context)
+6 -10
View File
@@ -37,8 +37,7 @@ class BIM_PT_bcf(Panel):
layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene
props = scene.BCFProperties
props = tool.Bcf.get_bcf_props()
if not bcfstore.BcfStore.get_bcfxml():
row = layout.row(align=True)
@@ -63,18 +62,17 @@ class BIM_PT_bcf(Panel):
row = layout.row()
row.prop(props, "author")
props = context.scene.BCFProperties
row = layout.row()
row.template_list("BIM_UL_topics", "", props, "topics", props, "active_topic_index")
col = row.column(align=True)
col.operator("bim.add_bcf_topic", icon="ADD", text="")
col.operator("bim.remove_bcf_topic", icon="REMOVE", text="")
if props.active_topic_index < len(props.topics):
topic = props.active_topic
topic = props.active_topic
if topic is not None:
is_editable = topic.is_editable
col.prop(topic, "is_editable", icon="CHECKMARK" if topic.is_editable else "GREASEPENCIL", icon_only=True)
topic = props.active_topic
row = layout.row()
row.enabled = is_editable
row.prop(topic, "description", text="")
@@ -131,8 +129,7 @@ class BIM_PT_bcf_metadata(Panel):
layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene
props = scene.BCFProperties
props = tool.Bcf.get_bcf_props()
bcfxml = bcfstore.BcfStore.get_bcfxml()
if not bcfxml or props.active_topic_index >= len(props.topics):
@@ -296,8 +293,7 @@ class BIM_PT_bcf_comments(Panel):
layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene
props = scene.BCFProperties
props = tool.Bcf.get_bcf_props()
if props.active_topic_index >= len(props.topics):
layout.label(text="No BCF project is loaded")
@@ -25,6 +25,7 @@ import mathutils
import numpy as np
import multiprocessing
import ifcopenshell.api
import ifcopenshell.api.boundary
import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.shape
@@ -40,6 +41,7 @@ from bonsai.bim.module.model.decorator import ProfileDecorator
from bonsai.bim.module.boundary.decorator import BoundaryDecorator
import bonsai.core
import bonsai.core.geometry
from typing import Union, Optional
def disable_editing_boundary_geometry(context):
@@ -57,7 +59,7 @@ def disable_editing_boundary_geometry(context):
class Loader:
def __init__(self, operator=None):
def __init__(self, operator: Optional[bpy.types.Operator] = None):
self.operator = operator
self.ifc_file = None
self.logger = None
@@ -66,7 +68,7 @@ class Loader:
self.fallback_settings = self.load_fallback_settings()
self.load_importer()
def create_mesh(self, boundary):
def create_mesh(self, boundary: ifcopenshell.entity_instance) -> Union[bpy.types.Mesh, None]:
# ConnectionGeometry is optional in IFC schema for some reasons.
if not boundary.ConnectionGeometry:
return None
@@ -113,7 +115,7 @@ class Loader:
bm.edges.new((verts[-1], verts[0]))
bm.to_mesh(mesh)
bm.free()
mesh.BIMMeshProperties.ifc_definition_id = surface.id()
tool.Ifc.link(surface, mesh)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
matrix = mathutils.Matrix(
ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist()
@@ -130,7 +132,7 @@ class Loader:
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
return settings
def load_importer(self):
def load_importer(self) -> None:
self.ifc_file = tool.Ifc.get()
self.logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, self.logger)
@@ -279,7 +281,7 @@ class SelectProjectBoundaries(bpy.types.Operator):
return {"FINISHED"}
def get_colour(ifc_boundary):
def get_colour(ifc_boundary: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
"""Return a color depending on IfcClass given"""
product_colors = {
"IfcWall": (0.7, 0.3, 0, 1),
@@ -315,7 +317,8 @@ class ColourByRelatedBuildingElement(bpy.types.Operator):
def _execute(self, context):
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
continue
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRelSpaceBoundary"):
@@ -416,7 +419,7 @@ class UpdateBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
tool.Boundary.move_origin_to_space_origin(context.active_object)
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings)
return {"FINISHED"}
+12 -11
View File
@@ -18,7 +18,6 @@
import bpy
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
import bonsai.tool as tool
from bonsai.bim.module.boundary.data import SpaceBoundariesData
@@ -34,7 +33,7 @@ class BIM_PT_SceneBoundaries(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
row = self.layout.row(align=True)
@@ -53,20 +52,20 @@ class BIM_PT_Boundary(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
entity = IfcStore.get_file().by_id(props.ifc_definition_id)
entity = tool.Ifc.get().by_id(props.ifc_definition_id)
return entity.is_a("IfcRelSpaceBoundary")
def draw(self, context):
obj = context.active_object
assert obj
props = obj.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
ifc_file = tool.Ifc.get()
boundary = ifc_file.by_id(props.ifc_definition_id)
self.bprops = tool.Boundary.get_object_boundary_props(obj)
@@ -129,12 +128,12 @@ class BIM_PT_SpaceBoundaries(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
element = tool.Ifc.get_entity(context.active_object)
for ifc_class in ("IfcSpace", "IfcExternalSpatialElement"):
@@ -146,7 +145,9 @@ class BIM_PT_SpaceBoundaries(Panel):
if not SpaceBoundariesData.is_loaded:
SpaceBoundariesData.load()
self.props = context.active_object.BIMObjectProperties
obj = context.active_object
assert obj
self.props = tool.Blender.get_object_bim_props(obj)
self.ifc_file = tool.Ifc.get()
row = self.layout.row()
row.operator("bim.load_space_boundaries")
+1 -1
View File
@@ -52,7 +52,7 @@ class BrickschemaData:
def active_relations(cls):
if BrickStore.graph is None:
return []
props = bpy.context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
try:
brick = props.bricks[props.active_brick_index]
except:
+19 -21
View File
@@ -22,29 +22,26 @@ import ifcopenshell.api
import bonsai.tool as tool
import bonsai.core.brick as core
import bonsai.bim.handler
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bonsai.bim.ifc import IfcStore
from bonsai.tool.brick import BrickStore
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator):
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.load_brick_project"
bl_label = "Load Brickschema Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load in a Brick project from a file"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
filename_ext = ".ttl"
def _execute(self, context):
if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower():
root = context.scene.BIMBrickProperties.brick_list_root
root = tool.Brick.get_brick_props().brick_list_root
core.load_brick_project(tool.Brick, filepath=self.filepath, brick_root=root)
else:
self.report({"ERROR"}, f"Failed to load {self.filepath}")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ViewBrickClass(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.view_brick_class"
@@ -111,10 +108,10 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Assign the selected Ifc entity to the selected Brick entity"
def _execute(self, context):
if not context.active_object:
if not (obj := context.active_object) or not (element := tool.Ifc.get_entity(obj)):
self.report({"ERROR"}, f"No Ifc selected")
return
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
try:
props.bricks[props.active_brick_index]
except:
@@ -123,7 +120,7 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
core.assign_brick_reference(
tool.Ifc,
tool.Brick,
element=tool.Ifc.get_entity(context.active_object),
element=element,
library=tool.Ifc.get().by_id(int(props.libraries)),
brick_uri=props.bricks[props.active_brick_index].uri,
)
@@ -136,7 +133,7 @@ class AddBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Create the Brick entity"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
core.add_brick(
tool.Ifc,
tool.Brick,
@@ -155,7 +152,7 @@ class AddBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Create the Brick relationship"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
brick = props.bricks[props.active_brick_index]
if props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label":
object = props.new_brick_relation_object
@@ -173,7 +170,7 @@ class ConvertIfcToBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Convert Ifc entities and relations to Brick entities and relations"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
library = None
if props.libraries:
library = tool.Ifc.get().by_id(int(props.libraries))
@@ -201,7 +198,8 @@ class NewBrickFile(bpy.types.Operator):
return {"FINISHED"}
def _execute(self, context):
root = context.scene.BIMBrickProperties.brick_list_root
props = tool.Brick.get_brick_props()
root = props.brick_list_root
core.new_brick_file(tool.Brick, brick_root=root)
def rollback(self, data):
@@ -230,7 +228,7 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Delete this entity"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
core.remove_brick(
tool.Ifc,
tool.Brick,
@@ -239,18 +237,18 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
)
class SerializeBrick(bpy.types.Operator):
class SerializeBrick(bpy.types.Operator, ExportHelper):
bl_idname = "bim.serialize_brick"
bl_label = "Serialize Brick"
# Prevents crash on Blender 4.4.0.
bl_description = "Save active Brick project by the provided filepath."
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
def invoke(self, context, event):
if self.should_save_as or not BrickStore.path:
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
else:
return self.execute(context)
@@ -273,7 +271,7 @@ class AddBrickNamespace(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Bind a new namespace to the Brick project"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
alias = props.new_brick_namespace_alias
uri = props.new_brick_namespace_uri
core.add_brick_namespace(tool.Brick, alias=alias, uri=uri)
@@ -288,6 +286,6 @@ class RemoveBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
object: bpy.props.StringProperty(name="Object")
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
brick = props.bricks[props.active_brick_index]
core.remove_brick_relation(tool.Brick, brick_uri=brick.uri, predicate=self.predicate, object=self.object)
+35 -4
View File
@@ -33,6 +33,7 @@ from bpy.props import (
import bonsai.core.brick as core
import bonsai.tool.brick as tool
from bonsai.tool.brick import BrickStore
from typing import TYPE_CHECKING
def update_active_brick_index(self, context):
@@ -74,13 +75,13 @@ def get_brick_relations(self, context):
return BRICK_RELATIONS_ENUM_ITEMS
def update_view(self, context):
root = context.scene.BIMBrickProperties.brick_list_root
def update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
root = self.brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False)
def split_screen_update_view(self, context):
root = context.scene.BIMBrickProperties.split_screen_brick_list_root
def split_screen_update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
root = self.split_screen_brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True)
@@ -90,6 +91,11 @@ class Brick(PropertyGroup):
uri: StringProperty(name="URI")
total_items: IntProperty(name="Total Items")
if TYPE_CHECKING:
label: str
uri: str
total_items: int
class BIMBrickProperties(PropertyGroup):
active_brick_class: StringProperty(name="Active Brick Class")
@@ -124,3 +130,28 @@ class BIMBrickProperties(PropertyGroup):
split_screen_brick_list_root: EnumProperty(
name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view
)
if TYPE_CHECKING:
active_brick_class: str
brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
bricks: bpy.types.bpy_prop_collection_idprop[Brick]
active_brick_index: int
libraries: str
set_list_root_toggled: bool
brick_list_root: str
namespace: str
new_brick_namespace_alias: str
new_brick_namespace_uri: str
new_brick_label: str
brick_entity_create_type: str
brick_entity_class: str
brick_create_relations_toggled: bool
brick_edit_relations_toggled: bool
new_brick_relation_type: str
new_brick_relation_object: str
split_screen_toggled: bool
split_screen_bricks: bpy.types.bpy_prop_collection_idprop[Brick]
split_screen_active_brick_index: int
split_screen_active_brick_class: str
split_screen_brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
split_screen_brick_list_root: str
+5 -5
View File
@@ -45,7 +45,7 @@ class BIM_PT_brickschema_project_info(Panel):
bl_parent_id = "BIM_PT_brickschema"
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
if not BrickschemaData.data["is_loaded"]:
row = self.layout.row(align=True)
@@ -87,7 +87,7 @@ class BIM_PT_brickschema_namespaces(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
row = self.layout.row(align=True)
row.label(text="Active Namespace:")
@@ -122,7 +122,7 @@ class BIM_PT_brickschema_create_entity(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
# TO DO: hide this if selected entity already has a reference, or something similar
row = self.layout.row(align=True)
@@ -156,7 +156,7 @@ class BIM_PT_brickschema_viewport(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
row = self.layout.row(align=True)
row.column().alignment = "RIGHT"
@@ -292,7 +292,7 @@ class BIM_PT_ifc_brickschema_references(Panel):
def draw(self, context):
if not BrickschemaReferencesData.is_loaded:
BrickschemaReferencesData.load()
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
if not BrickschemaReferencesData.data["is_loaded"]:
row = self.layout.row()
-1
View File
@@ -18,7 +18,6 @@
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
class BIM_PT_bsdd(Panel):
+51 -47
View File
@@ -128,9 +128,10 @@ class CadMitre(bpy.types.Operator):
class CadFillet(bpy.types.Operator):
bl_idname = "bim.cad_fillet"
bl_label = "CAD Fillet"
bl_description = "Add fillet to the 2 selected edges."
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=0, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
@classmethod
def poll(cls, context):
@@ -143,18 +144,15 @@ class CadFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
obj = bpy.context.active_object
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
mesh = obj.data
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
bm = bmesh.from_edit_mesh(mesh)
selected_edges = [e for e in bm.edges if e.select]
if len(selected_edges) != 2:
self.report({"ERROR"}, "Exactly 2 edges should be selected.")
return {"CANCELLED"}
# Assume the user has selected two edges sharing a vert, but merge verts
@@ -219,6 +217,7 @@ class CadFillet(bpy.types.Operator):
class CadArcFrom2Points(bpy.types.Operator):
bl_idname = "bim.cad_arc_from_2_points"
bl_label = "CAD Arc from 2 Points"
bl_description = "Add an arc to the active mesh based on 2 selected vertices."
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
@@ -235,6 +234,7 @@ class CadArcFrom2Points(bpy.types.Operator):
if bpy.context.mode != "EDIT_MESH":
return {"CANCELLED"}
obj = bpy.context.active_object
assert obj
region = bpy.context.region
region_3d = bpy.context.area.spaces.active.region_3d
cursor = bpy.context.scene.cursor.location
@@ -242,10 +242,12 @@ class CadArcFrom2Points(bpy.types.Operator):
if not center:
return {"CANCELLED"}
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
mw = obj.matrix_world
bm = bmesh.from_edit_mesh(mesh)
selected_verts = [v for v in bm.verts if v.select]
if len(selected_verts) != 2:
self.report({"ERROR"}, "Exactly 2 vertices should be selected.")
return {"CANCELLED"}
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co)
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
@@ -342,11 +344,12 @@ class CadArcFrom3Points(bpy.types.Operator):
class CadOffset(bpy.types.Operator):
bl_idname = "bim.cad_offset"
bl_label = "CAD Offset"
bl_description = "Copy selected mesh geometry at provided offset. Mesh copied based on the current viewport angle."
bl_options = {"REGISTER", "UNDO"}
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
@classmethod
def poll(self, context):
def poll(cls, context):
return context.mode == "EDIT_MESH"
def draw(self, context):
@@ -369,9 +372,6 @@ class CadOffset(bpy.types.Operator):
# effect of converting an unclosed loop into a closed loop which is also
# not what users expect.
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.distance = self.distance * si_conversion
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
@@ -543,7 +543,8 @@ class CadOffset(bpy.types.Operator):
class AddIfcCircle(bpy.types.Operator):
bl_idname = "bim.add_ifccircle"
bl_label = "Add IfcCircle"
radius: bpy.props.FloatProperty(name="Radius", default=0.5)
bl_description = "Add IfcCircle to the currently active mesh."
radius: bpy.props.FloatProperty(name="Radius", default=0.5, subtype="DISTANCE")
@classmethod
def poll(cls, context):
@@ -551,18 +552,19 @@ class AddIfcCircle(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
obj = context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
self.obj = obj
self.mesh = mesh
if self.has_selected_existing_circle(context):
self.change_radius(context)
else:
self.create_circle(context)
return {"FINISHED"}
def has_selected_existing_circle(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
def has_selected_existing_circle(self, context: bpy.types.Context) -> bool:
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 2:
return False
@@ -575,32 +577,32 @@ class AddIfcCircle(bpy.types.Operator):
for group in groups:
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
return True
return False
def change_radius(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
def change_radius(self, context: bpy.types.Object) -> None:
bm = bmesh.from_edit_mesh(self.mesh)
verts = [v for v in bm.verts if v.select and not v.hide]
center = verts[0].co.lerp(verts[1].co, 0.5)
verts[0].co = center + ((verts[0].co - center).normalized() * self.radius)
verts[1].co = center + ((verts[1].co - center).normalized() * self.radius)
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
bmesh.update_edit_mesh(self.mesh)
def create_circle(self, context):
obj = context.active_object
def create_circle(self, context: bpy.types.Context) -> None:
obj = self.obj
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
if len(obj.vertex_groups):
last_group = obj.vertex_groups[-1]
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
verts_in_group = [v for v in self.mesh.vertices if last_group.index in [vg.group for vg in v.groups]]
if "IFCCIRCLE" in last_group.name and len(verts_in_group) != 2:
obj.vertex_groups.remove(last_group)
group = obj.vertex_groups.new(name="IFCCIRCLE")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(obj.data)
bm = bmesh.from_edit_mesh(self.mesh)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
@@ -617,7 +619,7 @@ class AddIfcCircle(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
bmesh.update_edit_mesh(self.mesh)
class AddIfcArcIndexFillet(bpy.types.Operator):
@@ -638,18 +640,19 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
obj = context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
self.obj = obj
self.mesh = mesh
if self.has_selected_existing_arc(context):
self.change_radius(context)
else:
self.create_arc(context)
return self.create_arc(context)
return {"FINISHED"}
def has_selected_existing_arc(self, context: bpy.types.Context) -> bool:
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 3:
return False
@@ -668,13 +671,14 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
return False
def change_radius(self, context: bpy.types.Context) -> None:
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
edges = [e for e in bm.edges if e.select and not e.hide]
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
v1 = edges[0].other_vert(mid)
v2 = edges[1].other_vert(mid)
assert v1 and v2
center = tool.Cad.get_center_of_arc([v1.co, mid.co, v2.co])
if len(v1.link_edges) != 2 or len(v2.link_edges) != 2:
@@ -682,6 +686,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
v3 = v1.link_edges[1].other_vert(v1) if mid in v1.link_edges[0].verts else v1.link_edges[0].other_vert(v1)
v4 = v2.link_edges[1].other_vert(v2) if mid in v2.link_edges[0].verts else v2.link_edges[0].other_vert(v2)
assert v3 and v4
dir1 = (v3.co - v1.co).normalized()
dir2 = (v4.co - v2.co).normalized()
@@ -702,11 +707,10 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
def create_arc(self, context):
obj = bpy.context.active_object
bmesh.update_edit_mesh(self.mesh)
def create_arc(self, context: bpy.types.Context) -> set[str]:
obj = self.obj
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
@@ -718,7 +722,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
group = obj.vertex_groups.new(name="IFCARCINDEX")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(obj.data)
bm = bmesh.from_edit_mesh(self.mesh)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
@@ -737,6 +741,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
v1 = selected_edges[0].other_vert(shared_vert)
v2 = selected_edges[1].other_vert(shared_vert)
assert v1 and v2
dir1 = (v1.co - shared_vert.co).normalized()
dir2 = (v2.co - shared_vert.co).normalized()
edge_angle = dir1.angle(dir2)
@@ -770,7 +775,8 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
bmesh.update_edit_mesh(self.mesh)
return {"FINISHED"}
class AlignViewToProfile(bpy.types.Operator):
@@ -806,8 +812,9 @@ class AlignViewToProfile(bpy.types.Operator):
class AddRectangle(bpy.types.Operator):
bl_idname = "bim.add_rectangle"
bl_label = "Add Rectangle"
x: bpy.props.FloatProperty(name="X", default=1)
y: bpy.props.FloatProperty(name="Y", default=1)
bl_description = "Add rectangle shape to the active mesh."
x: bpy.props.FloatProperty(name="X", default=1, subtype="DISTANCE")
y: bpy.props.FloatProperty(name="Y", default=1, subtype="DISTANCE")
@classmethod
def poll(cls, context):
@@ -815,11 +822,8 @@ class AddRectangle(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.x = self.x * si_conversion
self.y = self.y * si_conversion
obj = context.active_object
assert obj and isinstance(obj.data, bpy.types.Mesh)
bm = bmesh.from_edit_mesh(obj.data)
cursor = obj.matrix_world.inverted() @ context.scene.cursor.location
new_verts = [
+9
View File
@@ -20,6 +20,7 @@ import bpy
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import PropertyGroup
from math import pi
from typing import TYPE_CHECKING
class BIMCadProperties(PropertyGroup):
@@ -31,3 +32,11 @@ class BIMCadProperties(PropertyGroup):
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
if TYPE_CHECKING:
resolution: int
radius: float
distance: float
x: float
y: float
gable_roof_edge_angle: float
+54 -59
View File
@@ -23,7 +23,7 @@ import bonsai.bim.module.type.prop as type_prop
import ifcopenshell.util.unit
from bpy.types import WorkSpaceTool
from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData
from typing import Union
from functools import partial
def load_custom_icons():
@@ -69,10 +69,12 @@ class CadTool(WorkSpaceTool):
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
)
def draw_settings(context, layout, workspace_tool):
def draw_settings(
context: bpy.types.Context, layout: bpy.types.UILayout, workspace_tool: bpy.types.WorkSpaceTool
) -> None:
ui_context = str(context.region.type)
obj = context.active_object
if not obj or not obj.data:
if not obj or not (data := obj.data):
return
is_profile = tool.Geometry.is_profile_object_active()
if is_profile:
@@ -112,17 +114,20 @@ class CadTool(WorkSpaceTool):
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Rectangle", "S_R", "Rectangle", ui_context)
add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Circle", "S_C", "Circle", ui_context)
add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context)
elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS":
elif (
isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES)
and tool.Geometry.get_mesh_props(data).subshape_type == "AXIS"
):
add_header_apply_button(
layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context
)
@@ -133,15 +138,15 @@ class CadTool(WorkSpaceTool):
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", "Fillet", ui_context)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
else:
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["pset_data"]
and context.active_object.BIMRailingProperties.is_editing_path
and tool.Model.get_railing_props(obj).is_editing_path
):
add_header_apply_button(
layout,
@@ -154,7 +159,7 @@ class CadTool(WorkSpaceTool):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and context.active_object.BIMRoofProperties.is_editing_path
and tool.Model.get_roof_props(obj).is_editing_path
):
add_header_apply_button(
layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context
@@ -171,17 +176,17 @@ class CadTool(WorkSpaceTool):
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", "2-Point Arc", ui_context)
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context)
class CadHotkey(bpy.types.Operator):
bl_idname = "bim.cad_hotkey"
bl_label = "CAD Hotkey"
bl_options = {"REGISTER", "UNDO"}
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -190,12 +195,14 @@ class CadHotkey(bpy.types.Operator):
return operator.description or ""
def execute(self, context):
self.props = context.scene.BIMCadProperties
self.props = tool.Cad.get_cad_props()
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def draw(self, context):
props = context.scene.BIMCadProperties
props = tool.Cad.get_cad_props()
obj = context.active_object
if self.hotkey == "S_C":
if tool.Geometry.is_profile_object_active():
row = self.layout.row()
@@ -221,7 +228,7 @@ class CadHotkey(bpy.types.Operator):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
and tool.Model.get_roof_props(obj).is_editing_path
):
self.layout.row().prop(props, "gable_roof_edge_angle")
@@ -231,9 +238,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 tool.Geometry.is_profile_object_active():
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()
@@ -241,36 +247,49 @@ 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 tool.Geometry.is_profile_object_active():
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)
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
obj = bpy.context.active_object
if not obj:
return
if not tool.Geometry.has_mesh_properties(data := obj.data):
return
element = tool.Ifc.get_entity(obj)
if not element:
return
mprops = tool.Geometry.get_mesh_props(data)
if mprops.subshape_type == "PROFILE":
if element.is_a("IfcProfileDef"):
bpy.ops.bim.edit_arbitrary_profile()
elif element.is_a("IfcRelSpaceBoundary"):
bpy.ops.bim.edit_boundary_geometry()
else:
bpy.ops.bim.edit_extrusion_profile()
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
elif mprops.subshape_type == "AXIS":
bpy.ops.bim.edit_extrusion_axis()
def hotkey_S_R(self):
obj = bpy.context.active_object
if not obj:
return
if tool.Geometry.is_profile_object_active():
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"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
and tool.Model.get_roof_props(obj).is_editing_path
):
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
@@ -288,7 +307,9 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.reset_vertex()
def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_context=""):
def add_header_apply_button(
layout: bpy.types.UILayout, text: str, apply_operator: str, cancel_operator: str, ui_context: str = ""
) -> None:
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
row = layout.row(align=True)
row.label(text=f"{text} Mode", icon_value=custom_icon)
@@ -307,34 +328,8 @@ def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_co
row.label(text="Tools")
def add_layout_hotkey_operator(
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None], ui_context: str = ""
) -> bpy.types.OperatorProperties:
parts = hotkey.split("_")
modifier, key = parts
op_text = "" if ui_context == "TOOL_HEADER" else text
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", ""))
row = layout if ui_context == "TOOL_HEADER" else layout.row(align=True)
op = row.operator("bim.cad_hotkey", text=op_text, icon_value=custom_icon)
if ui_context != "TOOL_HEADER" and len(parts) == 2:
layout = layout.row(align=True) # Create a new line for hotkey display
layout.label(text="", icon=modifier_icon)
layout.label(text="", icon=f"EVENT_{key}")
hotkey_description = f"Hotkey: {modifier_str} {key}"
description = "\n\n".join(filter(None, [hotkey_description if description else ""]))
op.hotkey = hotkey
op.description = description or hotkey_description
return op
add_layout_hotkey_operator = partial(tool.Blender.add_layout_hotkey_operator, tool_name="cad", module_name=__name__)
custom_icon_previews = None
display_mode = None
MODIFIERS = {
"S": ("EVENT_SHIFT", ""),
}
@@ -60,8 +60,9 @@ class ClashDecorator:
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)
props = tool.Clash.get_clash_props()
text = props.active_clash_text
p = props.p1.lerp(props.p2, 0.5)
font_id = 0
blf.size(font_id, 12)
@@ -92,7 +93,8 @@ class ClashDecorator:
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
props = tool.Clash.get_clash_props()
selected_vertices = [props.p1, props.p2]
selected_edges = []
if selected_vertices[0] != selected_vertices[1]:
selected_edges = [[0, 1]]
+72 -76
View File
@@ -24,26 +24,20 @@ import logging
import numpy as np
import ifcopenshell
import bonsai.tool as tool
from bpy_extras.io_utils import ExportHelper, ImportHelper
from math import radians
from mathutils import Matrix, Vector
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.clash.decorator import ClashDecorator
class ExportClashSets(bpy.types.Operator):
class ExportClashSets(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_clash_sets"
bl_label = "Export Clash Sets"
bl_description = "Export clash sets to a selected file"
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")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets = tool.Clash.export_clash_sets()
@@ -52,26 +46,24 @@ class ExportClashSets(bpy.types.Operator):
return {"FINISHED"}
class ImportClashSets(bpy.types.Operator):
class ImportClashSets(bpy.types.Operator, ImportHelper):
bl_idname = "bim.import_clash_sets"
bl_label = "Import Clash Sets"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Import clash sets from a selected file"
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")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ImportHelper.invoke(self, context, event)
def execute(self, context):
tool.Clash.load_clash_sets(self.filepath)
context.scene.BIMClashProperties.clash_sets.clear()
props = tool.Clash.get_clash_props()
props.clash_sets.clear()
for clash_set in tool.Clash.get_clash_sets():
new = context.scene.BIMClashProperties.clash_sets.add()
new = props.clash_sets.add()
new.name = clash_set["name"]
new.mode = clash_set["mode"]
if new.mode == "intersection":
@@ -106,7 +98,8 @@ class AddClashSet(bpy.types.Operator):
bl_description = "Add a clash set"
def execute(self, context):
new = context.scene.BIMClashProperties.clash_sets.add()
props = tool.Clash.get_clash_props()
new = props.clash_sets.add()
new.name = "New Clash Set"
return {"FINISHED"}
@@ -119,7 +112,8 @@ class RemoveClashSet(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
context.scene.BIMClashProperties.clash_sets.remove(self.index)
props = tool.Clash.get_clash_props()
props.clash_sets.remove(self.index)
return {"FINISHED"}
@@ -131,7 +125,8 @@ class AddClashSource(bpy.types.Operator):
group: bpy.props.StringProperty()
def execute(self, context):
clash_set = context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
source = getattr(clash_set, self.group).add()
return {"FINISHED"}
@@ -145,62 +140,54 @@ class RemoveClashSource(bpy.types.Operator):
group: bpy.props.StringProperty()
def execute(self, context):
clash_set = context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
getattr(clash_set, self.group).remove(self.index)
return {"FINISHED"}
class SelectClashSource(bpy.types.Operator):
class SelectClashSource(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_clash_source"
bl_label = "Select Clash Source"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC file to add as a clash source"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
index: bpy.props.IntProperty()
group: bpy.props.StringProperty()
filename_ext = ".ifc"
def execute(self, context):
clash_set = context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
getattr(clash_set, self.group)[self.index].name = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectClashResults(bpy.types.Operator):
class SelectClashResults(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_clash_results"
bl_label = "Select Clash Results"
bl_description = "Select filepath for clash results."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
context.scene.BIMClashProperties.clash_results_path = self.filepath
props = tool.Clash.get_clash_props()
props.clash_results_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectSmartGroupedClashesPath(bpy.types.Operator):
class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_smart_grouped_clashes_path"
bl_label = "Select Smart-Grouped Clashes Path"
bl_description = "Select filepath for smart-grouped clashes."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath
props = tool.Clash.get_clash_props()
props.smart_grouped_clashes_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcClash(bpy.types.Operator):
class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifc_clash"
bl_label = "Execute IFC Clash"
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
@@ -210,13 +197,12 @@ class ExecuteIfcClash(bpy.types.Operator):
def invoke(self, context, event):
if self.filepath:
return self.execute(context)
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
def execute(self, context):
from ifcclash import ifcclash
self.props = context.scene.BIMClashProperties
self.props = tool.Clash.get_clash_props()
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
@@ -284,27 +270,27 @@ class ExecuteIfcClash(bpy.types.Operator):
return {"FINISHED"}
class SelectIfcClashResults(bpy.types.Operator):
class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_ifc_clash_results"
bl_label = "Select IFC Clash Results"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select the clashing IFC geometry stored in a file"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
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"}
return ImportHelper.invoke(self, context, event)
def execute(self, context):
# TODO refactor into new clash results system
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
clash_sets = json.load(f)
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
clash_props = tool.Clash.get_clash_props()
assert clash_props.active_clash_set
clash_set_name = clash_props.active_clash_set.name
global_ids = []
for clash_set in clash_sets:
if clash_set["name"] != clash_set_name:
@@ -316,13 +302,15 @@ class SelectIfcClashResults(bpy.types.Operator):
global_ids.extend([clash["a_global_id"], clash["b_global_id"]])
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
continue
ifc_file = ""
for scene in obj.users_scene:
if scene.BIMProperties.ifc_file:
ifc_file = scene.BIMProperties.ifc_file
bim_props = tool.Blender.get_bim_props(scene)
if bim_props.ifc_file:
ifc_file = bim_props.ifc_file
if scene.library:
break
@@ -334,7 +322,7 @@ class SelectIfcClashResults(bpy.types.Operator):
element_file = self.file
try:
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = element_file.by_id(props.ifc_definition_id)
except:
continue
@@ -354,7 +342,7 @@ class SelectClash(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
self.props = context.scene.BIMClashProperties
self.props = tool.Clash.get_clash_props()
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)
@@ -388,13 +376,15 @@ class SmartClashGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BIMClashProperties.clash_results_path
props = tool.Clash.get_clash_props()
return bool(props.clash_results_path)
def execute(self, context):
from ifcclash import ifcclash
settings = ifcclash.ClashSettings()
self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json")
props = tool.Clash.get_clash_props()
self.filepath = bpy.path.ensure_ext(props.clash_results_path, ".json")
settings.output = self.filepath
settings.logger = logging.getLogger("Clash")
settings.logger.setLevel(logging.DEBUG)
@@ -404,19 +394,18 @@ class SmartClashGroup(bpy.types.Operator):
clash_sets = json.load(f)
# execute the smart grouping
save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes(
clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance
)
save_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, props.smart_clash_grouping_max_distance)
# save smart_groups to json
with open(save_path, "w") as f:
f.write(json.dumps(smart_grouped_clashes))
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
assert props.active_clash_set
clash_set_name = props.active_clash_set.name
# Reset the list of smart_clash_groups for the UI
context.scene.BIMClashProperties.smart_clash_groups.clear()
props.smart_clash_groups.clear()
for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -424,7 +413,7 @@ class SmartClashGroup(bpy.types.Operator):
continue
else:
for smart_group, global_id_pairs in smart_groups[0].items():
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
new_group = props.smart_clash_groups.add()
new_group.number = f"{smart_group}"
for pair in global_id_pairs:
@@ -442,18 +431,21 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
return bool(props.active_clash_set)
def execute(self, context):
smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
props = tool.Clash.get_clash_props()
smart_groups_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
assert props.active_clash_set
clash_set_name = props.active_clash_set.name
with open(smart_groups_path) as f:
smart_grouped_clashes = json.load(f)
# Reset the list of smart_clash_groups for the UI
context.scene.BIMClashProperties.smart_clash_groups.clear()
props.smart_clash_groups.clear()
for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -461,12 +453,12 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
continue
else:
for smart_group, global_id_pairs in smart_groups[0].items():
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
new_group = props.smart_clash_groups.add()
new_group.number = f"{smart_group}"
for pair in global_id_pairs:
for guid in pair:
new_global_id = new_group.global_ids.add()
new_global_id.guid = guid
new_global_id.name = guid
return {"FINISHED"}
@@ -478,15 +470,19 @@ class SelectSmartGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
return IfcStore.get_file() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
props = tool.Clash.get_clash_props()
return tool.Ifc.get() and context.visible_objects and props.active_smart_group
def execute(self, context):
selected_smart_group = context.scene.BIMClashProperties.active_smart_group
products = []
ifc_file = tool.Ifc.get()
props = tool.Clash.get_clash_props()
selected_smart_group = props.active_smart_group
assert selected_smart_group
products: list[ifcopenshell.entity_instance] = []
for global_id in selected_smart_group.global_ids:
try:
products.append(tool.Ifc.get().by_guid(global_id.guid))
except:
products.append(ifc_file.by_guid(global_id.name))
except RuntimeError:
continue
tool.Spatial.select_products(products, unhide=True)
context_override = tool.Blender.get_viewport_context()
+54 -11
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup
from bpy.types import PropertyGroup
from bpy.props import (
@@ -29,6 +30,8 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from mathutils import Vector
from typing import TYPE_CHECKING, Literal, Union
class ClashSource(PropertyGroup):
@@ -43,6 +46,10 @@ class ClashSource(PropertyGroup):
name="Mode",
)
if TYPE_CHECKING:
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
mode: Literal["a", "i", "e"]
class Clash(PropertyGroup):
a_global_id: StringProperty(name="A")
@@ -51,9 +58,15 @@ class Clash(PropertyGroup):
b_name: StringProperty(name="B Name")
status: BoolProperty(name="Status", default=False)
if TYPE_CHECKING:
a_global_id: str
b_global_id: str
a_name: str
b_name: str
status: bool
class ClashSet(PropertyGroup):
name: StringProperty(name="Name")
mode: EnumProperty(
items=[
(
@@ -76,11 +89,25 @@ class ClashSet(PropertyGroup):
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
if TYPE_CHECKING:
mode: Literal["intersection", "collision", "clearance"]
tolerance: float
clearance: float
allow_touching: bool
check_all: bool
a: bpy.types.bpy_prop_collection_idprop[ClashSource]
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
class SmartClashGroup(PropertyGroup):
number: StringProperty(name="Number")
global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty)
if TYPE_CHECKING:
number: str
global_ids: bpy.types.bpy_prop_collection_idprop[StrProperty]
class BIMClashProperties(PropertyGroup):
blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty)
@@ -107,17 +134,33 @@ class BIMClashProperties(PropertyGroup):
subtype="FILE_PATH",
)
@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 TYPE_CHECKING:
blender_clash_set_a: bpy.types.bpy_prop_collection_idprop[StrProperty]
blender_clash_set_b: bpy.types.bpy_prop_collection_idprop[StrProperty]
clash_sets: bpy.types.bpy_prop_collection_idprop[ClashSet]
should_create_clash_snapshots: bool
clash_results_path: str
smart_grouped_clashes_path: str
active_clash_set_index: int
active_clash_index: int
smart_clash_groups: bpy.types.bpy_prop_collection_idprop[SmartClashGroup]
active_smart_group_index: int
smart_clash_grouping_max_distance: int
p1: Vector
p2: Vector
active_clash_text: str
export_path: str
@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]
def active_clash_set(self) -> Union[ClashSet, None]:
return tool.Blender.get_active_uilist_element(self.clash_sets, self.active_clash_set_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]
def active_smart_group(self) -> Union[SmartClashGroup, None]:
return tool.Blender.get_active_uilist_element(self.smart_clash_groups, self.active_smart_group_index)
@property
def active_clash(self) -> Union[Clash, None]:
if not (clash_set := self.active_clash_set):
return None
return tool.Blender.get_active_uilist_element(clash_set.clashes, self.active_clash_index)
+3 -4
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.module.clash.data import ClashData
@@ -36,9 +37,7 @@ class BIM_PT_ifcclash(Panel):
ClashData.load()
layout = self.layout
scene = context.scene
props = scene.BIMClashProperties
props = tool.Clash.get_clash_props()
row = layout.row(align=True)
row.operator("bim.add_clash_set")
@@ -157,7 +156,7 @@ class BIM_PT_smart_clash_manager(Panel):
def draw(self, context):
layout = self.layout
props = context.scene.BIMClashProperties
props = tool.Clash.get_clash_props()
row = layout.row()
layout.label(text="Select clash results to group:")
@@ -118,7 +118,7 @@ class MaterialClassificationsData(ReferencesData):
def references(cls):
results = []
props = bpy.context.scene.BIMMaterialProperties
props = tool.Material.get_material_props()
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
@@ -25,19 +25,16 @@ import ifcopenshell.util.classification
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.bim.helper
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.ifc import IfcStore
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator):
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.load_classification_library"
bl_label = "Load Classification Library"
bl_description = "Load classification library from the provided filepath."
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _execute(self, context):
IfcStore.classification_file = ifcopenshell.open(self.filepath)
@@ -201,7 +198,8 @@ class EnableEditingClassification(bpy.types.Operator):
def execute(self, context):
def callback(name, prop, data):
if name == "ReferenceTokens":
new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add()
geo_props = tool.Georeference.get_georeference_props()
new = geo_props.projected_crs.add()
new.name = name
new.data_type = "string"
new.is_null = data[name] is None
@@ -239,7 +237,7 @@ class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
classification: bpy.props.IntProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"classification.remove_classification",
tool.Ifc.get(),
@@ -459,6 +457,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
for prop in blender_properties:
properties[prop.name] = prop.get_value()
# TODO: is this still the correct approach?
if classification_pset.name == "undefined_set":
if "ObjectType" in properties:
if hasattr(element, "ObjectType"):
@@ -21,7 +21,6 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.bim.module.classification.prop as classification_prop
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.classification.data import (
ClassificationsData,
ClassificationReferencesData,
@@ -124,7 +123,7 @@ class ReferenceUI:
self.sprops = context.scene.BIMClassificationProperties
self.bprops = context.scene.BIMBSDDProperties
self.props = context.scene.BIMClassificationReferenceProperties
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
self.draw_add_ui(context)
@@ -312,7 +311,7 @@ class BIM_PT_material_classifications(Panel, ReferenceUI):
def poll(cls, context):
if not tool.Ifc.get():
return False
props = context.scene.BIMMaterialProperties
props = tool.Material.get_material_props()
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
return True
return False
@@ -23,7 +23,6 @@ import ifcopenshell.api.constraint
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
class LoadObjectives(bpy.types.Operator):
@@ -84,7 +83,7 @@ class AddObjective(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
result = ifcopenshell.api.run("constraint.add_objective", IfcStore.get_file())
result = ifcopenshell.api.run("constraint.add_objective", tool.Ifc.get())
bpy.ops.bim.load_objectives()
bpy.ops.bim.enable_editing_constraint(constraint=result.id())
return {"FINISHED"}
@@ -116,7 +115,7 @@ class RemoveConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMConstraintProperties
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"constraint.remove_constraint", self.file, **{"constraint": self.file.by_id(self.constraint)}
)
@@ -162,8 +161,12 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
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)]
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
for obj in objs
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
if products:
ifcopenshell.api.run(
"constraint.assign_constraint",
@@ -185,8 +188,12 @@ class UnassignConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
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)]
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
for obj in objs
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
if products:
ifcopenshell.api.run(
"constraint.unassign_constraint",
@@ -16,8 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.constraint.data import ConstraintsData, ObjectConstraintsData
@@ -33,7 +33,7 @@ class BIM_PT_constraints(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not ConstraintsData.is_loaded:
@@ -79,21 +79,21 @@ class BIM_PT_object_constraints(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
props = tool.Blender.get_object_bim_props(obj)
return bool(tool.Ifc.get_object_by_identifier(props.ifc_definition_id))
def draw(self, context):
if not ObjectConstraintsData.is_loaded:
ObjectConstraintsData.load()
obj = context.active_object
self.oprops = obj.BIMObjectProperties
assert obj
self.oprops = tool.Blender.get_object_bim_props(obj)
self.sprops = context.scene.BIMConstraintProperties
self.props = obj.BIMObjectConstraintProperties
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
self.draw_add_ui()
@@ -21,7 +21,7 @@
import bpy
import ifcopenshell.api
import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper
from bpy_extras.io_utils import ImportHelper, ExportHelper
import bonsai.tool as tool
import bonsai.core.cost as core
from typing import get_args, TYPE_CHECKING
@@ -678,7 +678,7 @@ class CalculateCostItemResourceValue(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class ExportCostSchedules(bpy.types.Operator):
class ExportCostSchedules(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_cost_schedules"
bl_label = "Export Cost Schedule"
bl_options = {"REGISTER", "UNDO"}
@@ -701,9 +701,7 @@ class ExportCostSchedules(bpy.types.Operator):
return {"FINISHED"}
def invoke(self, context, event):
wm = context.window_manager
wm.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
def draw(self, context):
self.layout.label(text="Choose a format")
@@ -740,15 +738,14 @@ class LoadProductCostItems(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not tool.Ifc.get() or not (obj := context.active_object) or not (obj.BIMObjectProperties.ifc_definition_id):
if not tool.Ifc.get() or not (obj := context.active_object) or not (tool.Blender.get_ifc_definition_id(obj)):
cls.poll_message_set("No IFC object is active.")
return False
return True
def execute(self, context):
core.load_product_cost_items(
tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
obj = context.active_object
core.load_product_cost_items(tool.Cost, product=tool.Ifc.get_entity(obj))
return {"FINISHED"}
+2 -3
View File
@@ -19,7 +19,6 @@
import bpy
import ifcopenshell.api
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.classification.data import CostClassificationsData
from bonsai.bim.module.cost.data import CostSchedulesData, CostItemRatesData, CostItemQuantitiesData
from bonsai.bim.prop import StrProperty, Attribute
@@ -83,7 +82,7 @@ def update_cost_item_identification(self, context):
props = context.scene.BIMCostProperties
if not props.is_cost_update_enabled or self.identification == "XXX":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
@@ -98,7 +97,7 @@ def update_cost_item_name(self, context):
props = context.scene.BIMCostProperties
if not props.is_cost_update_enabled or self.name == "Unnamed":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
+5 -8
View File
@@ -21,7 +21,6 @@ import bonsai.bim.helper
import bonsai.bim.module.cost.prop as CostProp
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.cost.data import CostSchedulesData
from typing import Any
@@ -37,7 +36,7 @@ class BIM_PT_cost_schedules(Panel):
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
@@ -709,18 +708,16 @@ class BIM_UL_cost_items_trait:
class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList):
def __init__(self):
self.contract_operator = "bim.contract_cost_item"
self.expand_operator = "bim.expand_cost_item"
contract_operator = "bim.contract_cost_item"
expand_operator = "bim.expand_cost_item"
class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
# A schedule of rates UIList is identical to a regular cost items UIList but
# we want a separate UIList instance so that you can browse both lists
# independently in Blender. So we use a trait.
def __init__(self):
self.contract_operator = "bim.contract_cost_item_rate"
self.expand_operator = "bim.expand_cost_item_rate"
contract_operator = "bim.contract_cost_item_rate"
expand_operator = "bim.expand_cost_item_rate"
def draw_quantity_column(self, layout, cost_item):
self.draw_uom_column(layout, cost_item)
@@ -18,9 +18,13 @@
import bpy
from bpy.types import PropertyGroup
from typing import TYPE_CHECKING
class BIMCoveringProperties(PropertyGroup):
ceiling_height: bpy.props.FloatProperty(
name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height"
)
if TYPE_CHECKING:
ceiling_height: float
@@ -24,6 +24,7 @@ import bonsai.tool as tool
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import WorkSpaceTool
from functools import partial
class CoveringTool(WorkSpaceTool):
@@ -45,9 +46,7 @@ class CoveringTool(WorkSpaceTool):
CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
def add_layout_hotkey(layout: bpy.types.UILayout, text: str, hotkey: str, description: str) -> None:
args = ("covering", layout, text, hotkey, description)
tool.Blender.add_layout_hotkey_operator(*args)
add_layout_hotkey = partial(tool.Blender.add_layout_hotkey_operator, tool_name="covering", module_name=__name__)
class CoveringToolUI:
@@ -55,7 +54,7 @@ class CoveringToolUI:
def draw(cls, context, layout, ifc_element_type=None):
cls.layout = layout
cls.props = tool.Model.get_model_props()
cls.covering_props = context.scene.BIMCoveringProperties
cls.covering_props = tool.Covering.get_covering_props()
row = cls.layout.row(align=True)
if not tool.Ifc.get():
@@ -88,7 +87,7 @@ class CoveringToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
row.operator("bim.add_constr_type_instance", text="Add")
row.operator("bim.add_occurrence", text="Add")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -136,8 +135,8 @@ class CoveringToolUI:
if AuthoringData.data["ifc_classes"]:
if cls.props.ifc_class:
box = cls.layout.box()
if AuthoringData.data["type_thumbnail"]:
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
if thumbnail := AuthoringData.data["relating_type_data"].get("thumbnail"):
box.template_icon(icon_value=thumbnail, scale=5)
else:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = cls.props.ifc_class
@@ -145,8 +144,8 @@ class CoveringToolUI:
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.covering_hotkey"
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -159,12 +158,11 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return operator.description or ""
def _execute(self, context):
# self.props = context.scene.BIMCoveringProperties
self.props = tool.Covering.get_covering_props()
getattr(self, f"hotkey_{self.hotkey}")()
def invoke(self, context, event):
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
# self.props = context.scene.BIMSpatialProperties
return self.execute(context)
def draw(self, context):
@@ -175,22 +173,22 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(active_obj)
container = tool.Root.get_default_container()
if AuthoringData.data["predefined_type"] == "FLOORING":
if AuthoringData.data["relating_type_data"].get("predefined_type") == "FLOORING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
elif container:
bpy.ops.bim.add_instance_flooring_covering_from_cursor()
else:
bpy.ops.bim.add_constr_type_instance()
elif AuthoringData.data["predefined_type"] == "CEILING":
bpy.ops.bim.add_occurrence()
elif AuthoringData.data["relating_type_data"].get("predefined_type") == "CEILING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_ceiling_coverings_from_walls()
elif container:
bpy.ops.bim.add_instance_ceiling_covering_from_cursor()
else:
bpy.ops.bim.add_constr_type_instance()
bpy.ops.bim.add_occurrence()
else:
bpy.ops.bim.add_constr_type_instance()
bpy.ops.bim.add_occurrence()
def hotkey_S_G(self):
active_obj = bpy.context.active_object
@@ -20,8 +20,8 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.util.element
import bonsai.tool as tool
from math import degrees, atan2
from bonsai.bim.ifc import IfcStore
from .api import Api
@@ -92,7 +92,7 @@ class RunAnalysis(bpy.types.Operator):
bl_label = "Run Analysis"
def execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
self.inputs = {
"floors": [],
"walls": [],
@@ -181,12 +181,12 @@ class RunAnalysis(bpy.types.Operator):
if modifier.type == "TRIANGULATE":
return True
def get_covetool_category(self, obj):
def get_covetool_category(self, obj: bpy.types.Object):
if not hasattr(obj, "data") or not isinstance(obj.data, bpy.types.Mesh):
return
if not obj.BIMObjectProperties.ifc_definition_id:
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
return
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = self.file.by_id(ifc_id)
ifc_class = element.is_a()
if "IfcSlab" in ifc_class:
return "floors"
+39 -46
View File
@@ -27,7 +27,7 @@ import ifcopenshell
import ifcopenshell.util.selector
import bonsai.tool as tool
import bonsai.bim.module.drawing.scheduler as scheduler
from bonsai.bim.ifc import IfcStore
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bonsai.bim.handler import refresh_ui_data
from typing import TYPE_CHECKING
from collections import Counter
@@ -43,7 +43,8 @@ class AddCsvAttribute(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
attribute = context.scene.CsvProperties.csv_attributes.add()
props = tool.Blender.get_csv_props()
props.csv_attributes.add()
return {"FINISHED"}
@@ -54,7 +55,8 @@ class RemoveCsvAttribute(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
context.scene.CsvProperties.csv_attributes.remove(self.index)
props = tool.Blender.get_csv_props()
props.csv_attributes.remove(self.index)
return {"FINISHED"}
@@ -65,7 +67,8 @@ class RemoveAllCsvAttributes(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.scene.CsvProperties.csv_attributes.clear()
props = tool.Blender.get_csv_props()
props.csv_attributes.clear()
return {"FINISHED"}
@@ -77,8 +80,9 @@ class ReorderCsvAttribute(bpy.types.Operator):
new_index: bpy.props.IntProperty()
def execute(self, context):
old = context.scene.CsvProperties.csv_attributes[self.old_index]
new = context.scene.CsvProperties.csv_attributes[self.new_index]
props = tool.Blender.get_csv_props()
old = props.csv_attributes[self.old_index]
new = props.csv_attributes[self.new_index]
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
for prop in props:
value = getattr(new, prop)
@@ -87,16 +91,16 @@ class ReorderCsvAttribute(bpy.types.Operator):
return {"FINISHED"}
class ImportCsvAttributes(bpy.types.Operator):
class ImportCsvAttributes(bpy.types.Operator, ImportHelper):
bl_idname = "bim.import_csv_attributes"
bl_label = "Load CSV Settings"
bl_description = "Import a json template for CSV export"
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filename_ext = ".json"
def execute(self, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
data = json.load(open(self.filepath))
tool.Search.import_filter_query(data["query"], props.filter_groups)
@@ -122,22 +126,17 @@ class ImportCsvAttributes(bpy.types.Operator):
setattr(new, prop, attribute[prop])
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExportCsvAttributes(bpy.types.Operator):
class ExportCsvAttributes(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_csv_attributes"
bl_label = "Save CSV Settings"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save a json template for CSV export"
filename_ext = ".json"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
settings = {}
for prop in [
@@ -176,43 +175,42 @@ class ExportCsvAttributes(bpy.types.Operator):
return {"FINISHED"}
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"}
class ExportIfcCsv(bpy.types.Operator):
class ExportIfcCsv(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_ifccsv"
bl_label = "Export IFC"
bl_description = "Export IFC data as a spreadsheet."
filename_ext = ".csv"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if not props.should_load_from_memory and not props.csv_ifc_file:
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False
return True
@classmethod
def description(cls, context, properties):
props = tool.Blender.get_csv_props()
if props.format == "web":
return "Open Web UI for spreadsheet data export."
return f"Export IFC data as a spreadsheet by the provided filepath in '{props.format}' format ."
def invoke(self, context, event):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if props.format == "web":
return self.execute(context)
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
def execute(self, context):
import ifccsv
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}")
if props.should_load_from_memory:
ifc_file = IfcStore.get_file()
ifc_file = tool.Ifc.get()
else:
ifc_file = ifcopenshell.open(props.csv_ifc_file)
results = ifcopenshell.util.selector.filter_elements(
@@ -288,16 +286,16 @@ class ExportIfcCsv(bpy.types.Operator):
]
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_ifccsv"
bl_label = "Import to IFC"
bl_description = "Import IFC data from a spreadsheet."
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if not props.should_load_from_memory and not props.csv_ifc_file:
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False
@@ -305,16 +303,14 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".csv")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ImportHelper.invoke(self, context, event)
def _execute(self, context):
import ifccsv
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if props.should_load_from_memory:
ifc_file = IfcStore.get_file()
ifc_file = tool.Ifc.get()
else:
ifc_file = ifcopenshell.open(props.csv_ifc_file)
ifc_csv = ifccsv.IfcCsv()
@@ -338,18 +334,15 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class SelectCsvIfcFile(bpy.types.Operator):
class SelectCsvIfcFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_csv_ifc_file"
bl_label = "Select CSV IFC File"
bl_description = "Select IFC file for spreadsheet import/export."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
context.scene.CsvProperties.csv_ifc_file = self.filepath
props = tool.Blender.get_csv_props()
props.csv_ifc_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
+32
View File
@@ -29,6 +29,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
class CsvAttribute(PropertyGroup):
@@ -59,6 +60,14 @@ class CsvAttribute(PropertyGroup):
)
formatting: StringProperty(default="{{value}}", name="Formatting")
if TYPE_CHECKING:
header: str
sort: Literal["NONE", "ASC", "DESC"]
group: Literal["NONE", "GROUP", "CONCAT", "VARIES", "SUM", "AVERAGE", "MIN", "MAX"]
varies_value: str
summary: Literal["NONE", "SUM", "AVERAGE", "MIN", "MAX"]
formatting: str
class CsvProperties(PropertyGroup):
csv_ifc_file: StringProperty(default="", name="IFC File")
@@ -104,3 +113,26 @@ class CsvProperties(PropertyGroup):
name="Load from Memory",
description="Use IFC file currently loaded in Bonsai",
)
if TYPE_CHECKING:
csv_ifc_file: str
ifc_selector: str
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
csv_attributes: bpy.types.bpy_prop_collection_idprop[CsvAttribute]
should_generate_svg: bool
should_preserve_existing: bool
include_global_id: bool
null_value: str
empty_value: str
true_value: str
false_value: str
concat_value: str
csv_delimiter: Literal["NONE", "ASC", "DESC"]
format: Literal["csv", "xlsx", "ods", "web"]
csv_custom_delimiter: str
should_show_settings: bool
should_show_sort: bool
should_show_group: bool
should_show_summary: bool
should_show_formatting: bool
should_load_from_memory: bool
+8 -7
View File
@@ -17,8 +17,8 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.bim.helper
import bonsai.tool as tool
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.search.data import SearchData
@@ -33,11 +33,9 @@ class BIM_PT_ifccsv(Panel):
def draw(self, context):
layout = self.layout
props = tool.Blender.get_csv_props()
scene = context.scene
props = scene.CsvProperties
if IfcStore.get_file():
if tool.Ifc.get():
row = layout.row(align=True)
row.prop(props, "should_load_from_memory")
row.operator("bim.import_csv_attributes", icon="IMPORT", text="")
@@ -49,7 +47,7 @@ class BIM_PT_ifccsv(Panel):
row.operator("bim.export_csv_attributes", icon="EXPORT", text="")
row.prop(props, "should_show_settings", icon="PREFERENCES", text="")
if not IfcStore.get_file() or not props.should_load_from_memory:
if not tool.Ifc.get() or not props.should_load_from_memory:
row = layout.row(align=True)
row.prop(props, "csv_ifc_file")
row.operator("bim.select_csv_ifc_file", icon="FILE_FOLDER", text="")
@@ -126,5 +124,8 @@ class BIM_PT_ifccsv(Panel):
row.operator("bim.remove_csv_attribute", icon="X", text="").index = index
row = layout.row(align=True)
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
if props.format == "web":
row.operator("bim.export_ifccsv", icon="EXPORT", text="Open Web UI")
else:
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
row.operator("bim.import_ifccsv", icon="IMPORT")
+59 -42
View File
@@ -36,6 +36,7 @@ import bonsai.core.profile
import bonsai.core.type
import bonsai.bim.handler
import bonsai.bim.import_ifc as import_ifc
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from bonsai import get_debug_info, format_debug_info
from bonsai.bim.ifc import IfcStore
@@ -78,10 +79,10 @@ class PrintIfcFile(bpy.types.Operator):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def execute(self, context):
print(IfcStore.get_file().wrapped_data.to_string())
print(tool.Ifc.get().wrapped_data.to_string())
return {"FINISHED"}
@@ -101,13 +102,14 @@ class ConvertToBlender(bpy.types.Operator):
if tool.Geometry.has_mesh_properties(data):
if data.library:
continue
data.BIMMeshProperties.ifc_definition_id = 0
tool.Geometry.get_mesh_props(data).ifc_definition_id = 0
for material in bpy.data.materials:
if material.library:
continue
tool.Ifc.unlink(obj=material)
context.scene.BIMProperties.ifc_file = ""
context.scene.BIMDebugProperties.attributes.clear()
bim_props = tool.Blender.get_bim_props()
bim_props.ifc_file = ""
tool.Debug.get_debug_props().attributes.clear()
IfcStore.purge()
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
@@ -160,7 +162,8 @@ class ProfileImportIFC(bpy.types.Operator):
if not tool.Ifc.get():
cls.poll_message_set("No IFC file loaded.")
return False
if not context.scene.BIMProperties.ifc_file:
bim_props = tool.Blender.get_bim_props()
if not bim_props.ifc_file:
cls.poll_message_set("Current IFC file is not saved.")
return False
return True
@@ -184,10 +187,10 @@ class CreateAllShapes(bpy.types.Operator):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace")
total = len(elements)
@@ -256,7 +259,7 @@ class CreateShapeFromStepId(bpy.types.Operator):
logger = logging.getLogger("ImportIFC")
self.ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
self.file = tool.Ifc.get()
element = self.file.by_id(self.step_id or int(context.scene.BIMDebugProperties.step_id))
element = self.file.by_id(self.step_id or int(tool.Debug.get_debug_props().step_id))
settings = ifcopenshell.geom.settings()
settings.set("keep-bounding-boxes", True)
if self.should_include_curves:
@@ -309,7 +312,7 @@ class RewindInspector(bpy.types.Operator):
bl_description = "Rewind the Inspector to the previously inspected element"
def execute(self, context):
props = context.scene.BIMDebugProperties
props = tool.Debug.get_debug_props()
total_breadcrumbs = len(props.step_id_breadcrumb)
if total_breadcrumbs < 2:
return {"FINISHED"}
@@ -328,13 +331,13 @@ class InspectFromStepId(bpy.types.Operator):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def execute(self, context):
self.file = IfcStore.get_file()
debug_props = context.scene.BIMDebugProperties
self.file = tool.Ifc.get()
debug_props = tool.Debug.get_debug_props()
debug_props.active_step_id = self.step_id
crumb = context.scene.BIMDebugProperties.step_id_breadcrumb.add()
crumb = debug_props.step_id_breadcrumb.add()
crumb.name = str(self.step_id)
element = self.file.by_id(self.step_id)
debug_props.attributes.clear()
@@ -380,12 +383,12 @@ class InspectFromObject(bpy.types.Operator):
def get_active_object_ifc_definition(cls, context: bpy.types.Context) -> Union[int, None]:
obj = context.active_object
assert obj
if ifc_id := obj.BIMObjectProperties.ifc_definition_id:
if ifc_id := tool.Blender.get_ifc_definition_id(obj):
return ifc_id
if (
(data := obj.data)
and tool.Geometry.has_mesh_properties(data)
and (ifc_id := data.BIMMeshProperties.ifc_definition_id)
and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id)
):
return ifc_id
@@ -422,7 +425,7 @@ class PrintObjectPlacement(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
placement = ifcopenshell.util.placement.get_local_placement(IfcStore.get_file().by_id(self.step_id))
placement = ifcopenshell.util.placement.get_local_placement(tool.Ifc.get().by_id(self.step_id))
if self.create_empty_object:
bpy.ops.object.empty_add(type="ARROWS")
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -438,28 +441,25 @@ class ParseExpress(bpy.types.Operator):
bl_label = "Parse Express"
def execute(self, context):
core.parse_express(tool.Debug, context.scene.BIMDebugProperties.express_file)
props = tool.Debug.get_debug_props()
core.parse_express(tool.Debug, props.express_file)
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
class SelectExpressFile(bpy.types.Operator):
class SelectExpressFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_express_file"
bl_label = "Select Express File"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC EXPRESS definition"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"})
def execute(self, context):
props = tool.Debug.get_debug_props()
if os.path.exists(self.filepath) and "exp" in os.path.splitext(self.filepath)[1]:
context.scene.BIMDebugProperties.express_file = self.filepath
props.express_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class PurgeHdf5Cache(bpy.types.Operator):
bl_idname = "bim.purge_hdf5_cache"
@@ -497,7 +497,7 @@ class PrintUnusedElementStats(bpy.types.Operator):
ignore_styled_items: bpy.props.BoolProperty(name="Ignore Styled Items", default=True)
def execute(self, context):
props = context.scene.BIMDebugProperties
props = tool.Debug.get_debug_props()
# ignore some classes that could have zero 0 inverse references by their nature
ignore_classes = []
if self.ignore_contexts:
@@ -509,6 +509,7 @@ class PrintUnusedElementStats(bpy.types.Operator):
if self.ignore_styled_items:
ignore_classes += ["IfcStyledItem"]
ignore_classes += [
"IfcDocumentReference", # Document references for sheet elements (drawings, schedules, etc).
"IfcIndexedColourMap", # Only referenced by inverse attributes.
"IfcIndexedTextureMap", # Only referenced by inverse attributes.
]
@@ -518,12 +519,12 @@ class PrintUnusedElementStats(bpy.types.Operator):
return {"FINISHED"}
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHelper):
bl_idname = "bim.purge_unused_elements_by_class"
bl_label = "Purge Unused Elements By Class"
bl_description = (
"Will find all elements of class that have no inverse references and will remove them, use very carefully.\n"
"If IFC class is provided in neighbour field, will purge only elemnts of the provided class. Otherwise will purge all white-listed elements.\n"
"If IFC class is provided in neighbour field, will purge only elements of the provided class. Otherwise will purge all white-listed elements.\n"
"ALT+CLICK to provide a path where to save the IFC file with the removed elements (note changes will be applied to the current IFC too)"
)
bl_options = {"REGISTER", "UNDO"}
@@ -532,7 +533,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
context.window_manager.fileselect_add(self)
return ExportHelper.invoke(self, context, event)
return self.execute(context)
@classmethod
@@ -543,7 +544,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
return True
def _execute(self, context):
props = context.scene.BIMDebugProperties
props = tool.Debug.get_debug_props()
if props.ifc_class_purge:
purged_elements = core.purge_unused_elements(tool.Ifc, tool.Debug, props.ifc_class_purge)
self.report({"INFO"}, f"{purged_elements} unused elements found and removed.")
@@ -609,18 +610,30 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
"IfcUnitAssignment",
"IfcVirtualGridIntersection",
]
whitelist_exceptions = {
# Document references for sheet elements (drawings, schedules, etc).
"IfcExternalReference": ("IfcDocumentReference",),
}
total_purged = 0
schema = tool.Ifc.schema()
while True:
total_batches = 0
print("*" * 100)
total_batch_purged = 0
for ifc_class in whitelisted_classes:
total_class_purged = 0
# Ensure class is present in the schema.
try:
elements = tool.Ifc.get().by_type(ifc_class)
except:
continue # Probably not in this schema?
schema.declaration_by_name(ifc_class)
except RuntimeError:
continue
elements = tool.Ifc.get().by_type(ifc_class)
if ifc_class in whitelist_exceptions:
elements = [e for e in elements if not any(e.is_a(c) for c in whitelist_exceptions[ifc_class])]
to_purge = set()
for element in elements:
try:
@@ -687,15 +700,17 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
if purged == 0:
return
scene = context.scene
if object_type == "PROFILE":
if scene.BIMProfileProperties.is_editing:
props = tool.Profile.get_profile_props()
if props.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
if scene.BIMStylesProperties.is_editing:
props = tool.Style.get_style_props()
if props.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
if scene.BIMMaterialProperties.is_editing:
props = tool.Material.get_material_props()
if props.is_editing:
bpy.ops.bim.load_materials()
@@ -735,15 +750,17 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
if merged == 0:
return
scene = context.scene
if object_type == "PROFILE":
if scene.BIMProfileProperties.is_editing:
props = tool.Profile.get_profile_props()
if props.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
if scene.BIMStylesProperties.is_editing:
props = tool.Style.get_style_props()
if props.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
if scene.BIMMaterialProperties.is_editing:
props = tool.Material.get_material_props()
if props.is_editing:
bpy.ops.bim.load_materials()
@@ -799,7 +816,7 @@ class DebugActiveDrawing(bpy.types.Operator):
)
def execute(self, context: bpy.types.Context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
drawing_item = props.drawings[props.active_drawing_index]
drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id)
+19 -6
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bonsai.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -28,6 +29,9 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal, get_args
DisplayType = Literal["BOUNDS", "WIRE", "SOLID", "TEXTURED"]
class BIMDebugProperties(PropertyGroup):
@@ -41,14 +45,23 @@ class BIMDebugProperties(PropertyGroup):
inverse_references: CollectionProperty(name="Inverse References", type=Attribute)
express_file: StringProperty(name="Express File")
display_type: EnumProperty(
items=[
("BOUNDS", "Bounds", ""),
("WIRE", "Wire", ""),
("SOLID", "Solid", ""),
("TEXTURED", "Textured", ""),
],
items=[(display_type, display_type.capitalize(), "") for display_type in get_args(DisplayType)],
name="Display Type",
default="BOUNDS",
)
ifc_class_purge: StringProperty(name="Unused Elements IFC Class", default="")
package_name: StringProperty(name="Package Name", default="")
if TYPE_CHECKING:
step_id: int
number_of_polygons: int
percentile_of_polygons: int
active_step_id: int
step_id_breadcrumb: bpy.types.bpy_prop_collection_idprop[StrProperty]
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
inverse_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
inverse_references: bpy.types.bpy_prop_collection_idprop[Attribute]
express_file: str
display_type: str
ifc_class_purge: str
package_name: str
+11 -11
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bpy.types import Panel
@@ -32,10 +33,11 @@ class BIM_PT_debug(Panel):
def draw(self, context):
layout = self.layout
props = context.scene.BIMDebugProperties
props = tool.Debug.get_debug_props()
bim_props = tool.Blender.get_bim_props()
row = self.layout.row(align=True)
row.prop(context.scene.BIMProperties, "ifc_file", text="")
row.prop(bim_props, "ifc_file", text="")
row.operator("bim.validate_ifc_file", icon="CHECKMARK", text="")
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
@@ -87,27 +89,25 @@ class BIM_PT_debug(Panel):
row.prop(props, "step_id", text="")
row = layout.split(factor=0.7, align=True)
row.operator("bim.select_high_polygon_meshes").threshold = context.scene.BIMDebugProperties.number_of_polygons
row.operator("bim.select_high_polygon_meshes").threshold = props.number_of_polygons
row.prop(props, "number_of_polygons", text="")
row = layout.split(factor=0.7, align=True)
row.operator("bim.select_highest_polygon_meshes").percentile = (
context.scene.BIMDebugProperties.percentile_of_polygons
)
row.operator("bim.select_highest_polygon_meshes").percentile = props.percentile_of_polygons
row.prop(props, "percentile_of_polygons", text="")
row = layout.split(factor=0.5, align=True)
row.prop(props, "display_type", text="")
row.operator("bim.override_display_type").display = context.scene.BIMDebugProperties.display_type
row.operator("bim.override_display_type").display = props.display_type
layout.operator("bim.purge_unused_representations")
row = layout.row(align=True)
row.prop(context.scene.BIMDebugProperties, "ifc_class_purge", text="")
row.prop(props, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
if context.active_object and context.active_object.data:
mprops = context.active_object.data.BIMMeshProperties
if context.active_object and (data := context.active_object.data):
mprops = tool.Geometry.get_mesh_props(data)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
@@ -123,7 +123,7 @@ class BIM_PT_debug(Panel):
row.operator("bim.rewind_inspector", icon="FRAME_PREV", text="")
row.prop(props, "active_step_id", text="")
row = layout.row(align=True)
row.operator("bim.inspect_from_step_id").step_id = context.scene.BIMDebugProperties.active_step_id
row.operator("bim.inspect_from_step_id").step_id = props.active_step_id
row.operator("bim.inspect_from_object")
if props.attributes:
+1 -1
View File
@@ -43,7 +43,7 @@ class DiffData:
@classmethod
def diff_json(cls):
props = bpy.context.scene.DiffProperties
props = tool.Blender.get_diff_props()
if not props.diff_json_file:
cls.diff = None
return
+33 -40
View File
@@ -24,24 +24,23 @@ import ifcopenshell
import bonsai.bim.handler
import bonsai.bim.import_ifc
import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bonsai.bim.ifc import IfcStore
class SelectDiffJsonFile(bpy.types.Operator):
class SelectDiffJsonFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_diff_json_file"
bl_label = "Select Diff JSON File"
bl_description = "Select filepath for IFC diff results."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filename_ext = ".json"
def execute(self, context):
context.scene.DiffProperties.diff_json_file = self.filepath
props = tool.Blender.get_diff_props()
props.diff_json_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class VisualiseDiff(bpy.types.Operator):
bl_idname = "bim.visualise_diff"
@@ -50,7 +49,8 @@ class VisualiseDiff(bpy.types.Operator):
def execute(self, context):
ifc_file = tool.Ifc.get()
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
props = tool.Blender.get_diff_props()
with open(props.diff_json_file, "r") as file:
diff = json.load(file)
for obj in context.visible_objects:
obj.color = (1.0, 1.0, 1.0, 1.0)
@@ -65,13 +65,14 @@ class VisualiseDiff(bpy.types.Operator):
obj.color = (0.0, 0.0, 0.7, 1.0)
continue
if not obj.BIMObjectProperties.ifc_definition_id:
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
continue
ifc_file = ""
for scene in obj.users_scene:
if scene.BIMProperties.ifc_file:
ifc_file = scene.BIMProperties.ifc_file
bim_props = tool.Blender.get_bim_props(scene)
if bim_props.ifc_file:
ifc_file = bim_props.ifc_file
if scene.library:
break
@@ -83,7 +84,7 @@ class VisualiseDiff(bpy.types.Operator):
element_file = ifc_file
try:
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = element_file.by_id(ifc_id)
except:
continue
global_id = getattr(element, "GlobalId", None)
@@ -100,55 +101,45 @@ class VisualiseDiff(bpy.types.Operator):
return {"FINISHED"}
class SelectDiffOldFile(bpy.types.Operator):
class SelectDiffOldFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_diff_old_file"
bl_label = "Select Diff Old File"
bl_description = "Select filepath for an old IFC file to compare."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
context.scene.DiffProperties.old_file = self.filepath
props = tool.Blender.get_diff_props()
props.old_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectDiffNewFile(bpy.types.Operator):
class SelectDiffNewFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_diff_new_file"
bl_label = "Select Diff New File"
bl_description = "Select filepath for a new IFC file to compare."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
context.scene.DiffProperties.new_file = self.filepath
props = tool.Blender.get_diff_props()
props.new_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcDiff(bpy.types.Operator):
class ExecuteIfcDiff(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifc_diff"
bl_label = "Execute IFC Diff"
bl_description = "Compare two IFC files and save a json diff report by the provided filepath."
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")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
import ifcdiff
self.props = context.scene.DiffProperties
self.props = tool.Blender.get_diff_props()
if tool.Ifc.get():
if self.props.active_file == "NONE":
@@ -237,7 +228,8 @@ class SelectDiffObjects(bpy.types.Operator):
def execute(self, context):
ifc_file = tool.Ifc.get()
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
props = tool.Blender.get_diff_props()
with open(props.diff_json_file, "r") as file:
diff = json.load(file)
for obj in context.visible_objects:
obj.select_set(False)
@@ -252,13 +244,14 @@ class SelectDiffObjects(bpy.types.Operator):
obj.select_set(True)
continue
if not obj.BIMObjectProperties.ifc_definition_id:
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
continue
ifc_file = ""
for scene in obj.users_scene:
if scene.BIMProperties.ifc_file:
ifc_file = scene.BIMProperties.ifc_file
bim_props = tool.Blender.get_bim_props(scene)
if bim_props.ifc_file:
ifc_file = bim_props.ifc_file
if scene.library:
break
@@ -270,7 +263,7 @@ class SelectDiffObjects(bpy.types.Operator):
element_file = ifc_file
try:
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = element_file.by_id(ifc_id)
except:
continue
global_id = getattr(element, "GlobalId", None)
+18 -2
View File
@@ -30,18 +30,25 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal, get_args
def update_diff_json_file(self, context):
def update_diff_json_file(self: "DiffProperties", context: bpy.types.Context) -> None:
DiffData.data["diff_json"] = DiffData.diff_json()
RelationshipType = Literal["type", "property", "container", "aggregate", "classification"]
class Relationships(PropertyGroup):
relationship: EnumProperty(
name="Relationship",
items=[(r, r.capitalize(), r) for r in ["type", "property", "container", "aggregate", "classification"]],
items=[(r, r.capitalize(), r) for r in get_args(RelationshipType)],
)
if TYPE_CHECKING:
relationship: RelationshipType
class DiffProperties(PropertyGroup):
diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file)
@@ -59,3 +66,12 @@ class DiffProperties(PropertyGroup):
name="Active File",
default="NEW",
)
if TYPE_CHECKING:
diff_json_file: str
old_file: str
new_file: str
diff_relationships: bpy.types.bpy_prop_collection_idprop[Relationships]
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
should_load_changed_elements: bool
active_file: Literal["NONE", "OLD", "NEW"]
+1 -4
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.diff.data import DiffData
import bonsai.bim.helper
import bonsai.tool as tool
@@ -38,9 +37,7 @@ class BIM_PT_diff(Panel):
layout = self.layout
layout.use_property_split = True
scene = context.scene
props = scene.DiffProperties
props = tool.Blender.get_diff_props()
layout.label(text="IFC Diff Setup:")
@@ -52,7 +52,7 @@ class DocumentData:
@classmethod
def parent_document(cls):
props = bpy.context.scene.BIMDocumentProperties
props = tool.Document.get_document_props()
if len(props.breadcrumbs):
parent = tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name))
if tool.Ifc.get_schema() == "IFC2X3":
@@ -24,7 +24,6 @@ import ifcopenshell.util.element
import bonsai.bim.handler
import bonsai.tool as tool
import bonsai.core.document as core
from bonsai.bim.ifc import IfcStore
class LoadProjectDocuments(bpy.types.Operator):
@@ -116,7 +115,7 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMDocumentProperties
props = tool.Document.get_document_props()
core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id))
@@ -30,6 +30,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
@@ -57,6 +58,11 @@ class Document(PropertyGroup):
)
ifc_definition_id: IntProperty(name="IFC Definition ID")
if TYPE_CHECKING:
identification: str
is_information: bool
ifc_definition_id: int
class BIMDocumentProperties(PropertyGroup):
document_attributes: CollectionProperty(name="Document Attributes", type=Attribute)
@@ -65,3 +71,11 @@ class BIMDocumentProperties(PropertyGroup):
breadcrumbs: CollectionProperty(name="Breadcrumbs", type=StrProperty)
active_document_index: IntProperty(name="Active Document Index")
is_editing: BoolProperty(name="Is Editing", default=False)
if TYPE_CHECKING:
document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
active_document_id: int
documents: bpy.types.bpy_prop_collection_idprop[Document]
breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
active_document_index: int
is_editing: bool
+11 -9
View File
@@ -16,8 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData
@@ -33,13 +33,13 @@ class BIM_PT_documents(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not DocumentData.is_loaded:
DocumentData.load()
self.props = context.scene.BIMDocumentProperties
self.props = tool.Document.get_document_props()
row = self.layout.row(align=True)
row.label(text="{} Documents Found".format(DocumentData.data["total_information"]), icon="FILE")
@@ -90,20 +90,22 @@ class BIM_PT_object_documents(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
return True
def draw(self, context):
if not ObjectDocumentData.is_loaded:
ObjectDocumentData.load()
obj = context.active_object
self.oprops = obj.BIMObjectProperties
self.props = context.scene.BIMDocumentProperties
self.file = IfcStore.get_file()
self.oprops = tool.Blender.get_object_bim_props(obj)
self.props = tool.Document.get_document_props()
self.file = tool.Ifc.get()
self.draw_add_ui()
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os
import bpy
import math
@@ -50,7 +51,7 @@ class Annotator:
curve.font = font
props = tool.Drawing.get_text_props(obj)
props.font_size = "2.5"
collection = bpy.context.scene.camera.BIMObjectProperties.collection
collection = tool.Blender.get_object_bim_props(bpy.context.scene.camera).collection
collection.objects.link(obj)
Annotator.resize_text(obj)
return obj
@@ -125,12 +126,14 @@ class Annotator:
return obj
@staticmethod
def get_annotation_obj(drawing: ifcopenshell.entity_instance, object_type: str, data_type: str) -> bpy.types.Object:
def get_annotation_obj(
drawing: ifcopenshell.entity_instance, object_type: str, data_type: tool.Drawing.ANNOTATION_DATA_TYPE
) -> bpy.types.Object:
camera = tool.Ifc.get_object(drawing)
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = tool.Drawing.get_camera_matrix(camera)
matrix_world.translation = co1
collection = camera.BIMObjectProperties.collection
collection = tool.Blender.get_object_bim_props(camera).collection
if object_type == "TEXT":
obj = bpy.data.objects.new(object_type, None)
@@ -147,11 +150,17 @@ class Annotator:
collection.objects.link(obj)
return obj
if object_type != "ANGLE":
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if element and element.ObjectType == object_type and obj.type == object_type.upper():
return obj
# TODO: remove as outdated?
# Is reusing the same objects preventing the creation of new annotations.
# if object_type != "ANGLE":
# for obj in collection.objects:
# element = tool.Ifc.get_entity(obj)
# if (
# element
# and ifcopenshell.util.element.get_predefined_type(element) == object_type
# and obj.type == data_type.upper()
# ):
# return obj
if data_type == "mesh":
data = bpy.data.meshes.new(object_type)
+18 -7
View File
@@ -88,7 +88,8 @@ class SheetsData:
project = tool.Ifc.get().by_type("IfcProject")[0]
titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
if not titleblocks_dir:
titleblocks_dir = bpy.context.scene.DocProperties.titleblocks_dir
props = tool.Drawing.get_document_props()
titleblocks_dir = props.titleblocks_dir
titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir)
if os.path.exists(titleblocks_dir):
files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")])
@@ -120,23 +121,25 @@ class DrawingsData:
@classmethod
def location_hint(cls):
if bpy.context.scene.DocProperties.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
props = tool.Drawing.get_document_props()
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
results = [("0", "Origin", "")]
results.extend(
[(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcBuildingStorey")]
)
return results
elif bpy.context.scene.DocProperties.target_view in ["MODEL_VIEW"]:
elif props.target_view in ["MODEL_VIEW"]:
return [(h.upper(), h, "") for h in ["Orthographic", "Perspective"]]
return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]]
@classmethod
def active_drawing_pset_data(cls):
ifc_file = tool.Ifc.get()
drawing_id = bpy.context.scene.DocProperties.active_drawing_id
props = tool.Drawing.get_document_props()
drawing_id = props.active_drawing_id
if drawing_id == 0:
return {}
drawing = ifc_file.by_id(bpy.context.scene.DocProperties.active_drawing_id)
drawing = ifc_file.by_id(drawing_id)
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
@@ -233,7 +236,8 @@ class DecoratorData:
# stores 1 type of data per object
data = {}
cut_cache = {}
layerset_cache = {}
slice_cache = {}
fill_cache = {}
@classmethod
def get_batting_thickness(cls, obj):
@@ -361,7 +365,11 @@ class DecoratorData:
element = tool.Ifc.get_entity(obj)
supported_object_types = ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in supported_object_types:
if (
not element
or not element.is_a("IfcAnnotation")
or ifcopenshell.util.element.get_predefined_type(element) not in supported_object_types
):
return None
dimension_style = "arrow"
@@ -379,6 +387,8 @@ class DecoratorData:
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
text_prefix = pset_data.get("TextPrefix", None) or ""
text_suffix = pset_data.get("TextSuffix", None) or ""
custom_unit_list = pset_data.get("CustomUnit", None) or ""
custom_unit = custom_unit_list[0] if custom_unit_list else ""
dimension_data = {
"dimension_style": dimension_style,
@@ -387,6 +397,7 @@ class DecoratorData:
"text_prefix": text_prefix,
"text_suffix": text_suffix,
"fill_bg": fill_bg,
"custom_unit": custom_unit,
}
cls.data[obj.name] = dimension_data
return dimension_data
+221 -319
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import gpu
import bpy
import blf
@@ -29,7 +28,7 @@ import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.drawing.helper as helper
from pathlib import Path
from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil
from math import pi, sin, cos, acos, atan, degrees, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from bpy_extras.view3d_utils import location_3d_to_region_2d
@@ -423,7 +422,8 @@ class BaseDecorator:
# font_size = 16 <-- this is a good default
# TODO: need to synchronize it better with svg
magic_font_scale = bpy.context.scene.DocProperties.magic_font_scale
props = tool.Drawing.get_document_props()
magic_font_scale = props.magic_font_scale
font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5
pos = pos - line_no * font_size_px * rotation_matrix[1]
@@ -494,14 +494,20 @@ class BaseDecorator:
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
@lru_cache(maxsize=None)
def format_value(self, context, value):
def format_value(self, context, value, custom_unit=None):
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
precision = drawing_pset_data.get("MetricPrecision", None)
if not precision:
precision = drawing_pset_data.get("ImperialPrecision", None)
decimal_places = drawing_pset_data.get("DecimalPlaces", None)
return format_distance(value, precision=precision, decimal_places=decimal_places)
return format_distance(
value,
precision=precision,
decimal_places=decimal_places,
suppress_zero_inches=True,
custom_unit=custom_unit,
)
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
"""`pos` is a world space position\n
@@ -588,7 +594,8 @@ class BaseDecorator:
region3d = context.region_data
text_dir = self.get_annotation_direction(context, obj)
pos = location_3d_to_region_2d(region, region3d, text_world_position)
if not (pos := location_3d_to_region_2d(region, region3d, text_world_position)):
return
props = tool.Drawing.get_text_props(obj)
text_data = DecoratorData.get_ifc_text_data(obj)
if props.is_editing:
@@ -721,6 +728,8 @@ class DimensionDecorator(BaseDecorator):
v1 = Vector(vertices[i1])
p0 = location_3d_to_region_2d(region, region3d, v0)
p1 = location_3d_to_region_2d(region, region3d, v1)
if not p0 or not p1:
return
text_dir = p1 - p0
if text_dir.length < 1:
continue
@@ -736,7 +745,7 @@ class DimensionDecorator(BaseDecorator):
if not show_description_only:
length = (v1 - v0).length
text = self.format_value(context, length)
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
if isinstance(self, DiameterDecorator):
text = "D" + text
text = text_prefix + text + text_suffix
@@ -871,6 +880,8 @@ class AngleDecorator(BaseDecorator):
# calculate angle position
p0, p1, p2 = [location_3d_to_region_2d(region, region3d, p) for p in vertices[i0 : i1 + 2]]
if not p0 or not p1 or not p2:
continue
edge0 = p0 - p1
edge1 = p2 - p1
radius = min(edge0.length_squared, edge1.length_squared) ** 0.5
@@ -935,6 +946,8 @@ class RadiusDecorator(BaseDecorator):
spline_points = self.get_spline_points(obj)
p0, p1 = [location_3d_to_region_2d(region, region3d, p) for p in self.get_spline_points(obj)]
if not p0 or not p1:
return
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
@@ -945,7 +958,7 @@ class RadiusDecorator(BaseDecorator):
def get_text():
length = (spline_points[-1] - spline_points[-2]).length
return "R" + self.format_value(context, length)
return "R" + self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
self.draw_dimension_text(
context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center"
@@ -967,7 +980,8 @@ class FallDecorator(BaseDecorator):
region = context.region
region3d = context.region_data
dir = Vector((1, 0))
pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))
if not (pos := location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))):
return
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
@@ -976,6 +990,7 @@ class FallDecorator(BaseDecorator):
# same function as in svgwriter.py
def get_label_text():
element = tool.Ifc.get_entity(obj)
assert element
B, A = [v.co.xyz for v in spline_points[:2]]
rise = abs(A.z - B.z)
O = A.copy()
@@ -988,13 +1003,14 @@ class FallDecorator(BaseDecorator):
angle = 90
# ues SLOPE_ANGLE as default
if element.ObjectType in ("FALL", "SLOPE_ANGLE"):
object_type = ifcopenshell.util.element.get_predefined_type(element)
if object_type in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
elif element.ObjectType == "SLOPE_FRACTION":
elif object_type == "SLOPE_FRACTION":
if angle == 90:
return "-"
return f"{self.format_value(context, rise)} / {self.format_value(context, run)}"
elif element.ObjectType == "SLOPE_PERCENT":
elif object_type == "SLOPE_PERCENT":
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
@@ -1156,7 +1172,6 @@ class PlanLevelDecorator(BaseDecorator):
self.draw_labels(context, obj, self.get_splines(obj))
def draw_labels(self, context, obj, splines):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
region = context.region
region3d = context.region_data
@@ -1166,6 +1181,8 @@ class PlanLevelDecorator(BaseDecorator):
for verts in splines:
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
if not p0 or not p1:
continue
text_dir = p1 - p0
if text_dir.length < 1:
continue
@@ -1432,8 +1449,10 @@ class GridDecorator(BaseDecorator):
p1 = location_3d_to_region_2d(region, region3d, v1)
dir = Vector((1, 0))
text = obj.name.split("/")[1].split(".")[0]
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
if p0:
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
if p1:
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
class ElevationDecorator(BaseDecorator):
@@ -1586,6 +1605,9 @@ class CutDecorator:
@classmethod
def install(cls, context):
DecoratorData.cut_cache.clear()
DecoratorData.slice_cache.clear()
DecoratorData.fill_cache.clear()
if cls.installed:
cls.uninstall()
handler = cls()
@@ -1600,38 +1622,50 @@ class CutDecorator:
cls.installed = None
def __call__(self, context):
if not context.scene.camera:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
self.fallback_colour = (0.3, 0.3, 0.3, 1)
all_vertices = []
all_edges = []
selected_vertices = []
selected_edges = []
layer_vertices = []
layer_edges = []
fills = {}
all_vertex_i_offset = 0
selected_vertex_i_offset = 0
layer_vertex_i_offset = 0
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
verts, edges = self.decorate(context, obj)
if not verts:
if not (element := tool.Ifc.get_entity(obj)):
continue
self.decorate(context, obj, element)
obj_layer_verts, obj_layer_edges = self.slice_layersets(context, obj, verts, edges)
if obj_layer_verts:
layer_vertices.extend(obj_layer_verts)
layer_edges.extend([[vi + layer_vertex_i_offset for vi in e] for e in obj_layer_edges])
layer_vertex_i_offset += len(obj_layer_verts)
verts, edges = DecoratorData.cut_cache[element.id()]
if verts:
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
verts, edges = DecoratorData.slice_cache.get(element.id(), (None, None))
if verts:
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
for colour, element_fills in DecoratorData.fill_cache[element.id()].items():
fills.setdefault(colour, []).append(element_fills)
gpu.state.point_size_set(1)
gpu.state.blend_set("ALPHA")
@@ -1649,9 +1683,10 @@ class CutDecorator:
black = (0, 0, 0, 1)
if layer_vertices:
self.draw_batch("LINES", layer_vertices, black, layer_edges)
self.draw_batch("POINTS", layer_vertices, black)
for colour, element_fills in fills.items():
for verts_tris in element_fills:
for verts, tris in verts_tris:
self.draw_batch("TRIS", verts, colour, tris)
gpu.state.point_size_set(2)
self.line_shader.uniform_float("lineWidth", 3.0)
@@ -1669,310 +1704,176 @@ class CutDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def decorate(self, context, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return None, None
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
has_cut_cache = element.id() in DecoratorData.cut_cache
has_fill_cache = element.id() in DecoratorData.fill_cache
# Currently selected objects shall not be cached as they may be being moved / edited.
# If the camera is selected, we also disable the cache as the user may be moving the camera.
if obj.select_get() or context.scene.camera.select_get():
verts, edges = None, None
else:
verts, edges = DecoratorData.cut_cache.get(element.id(), (None, None))
# Currently selected objects must be recalculated as they may be being moved / edited.
# If the camera is selected, we also recalculate as the user may be moving the camera.
if verts is False:
return None, None
elif verts:
return verts, edges
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
self.recalculate_cut(context, obj, element)
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
self.recalculate_fill(context, obj, element)
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.cut_cache[element.id()] = (False, False)
return None, None
if verts is None:
else:
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
DecoratorData.cut_cache[element.id()] = (verts, edges)
return verts, edges
def slice_layersets(self, context, obj, cut_verts, cut_edges):
element = tool.Ifc.get_entity(obj)
# Currently selected objects shall not be cached as they may be being moved / edited.
# If the camera is selected, we also disable the cache as the user may be moving the camera.
if obj.select_get() or context.scene.camera.select_get():
verts, edges = None, None
else:
verts, edges = DecoratorData.layerset_cache.get(element.id(), (None, None))
if verts is False:
return None, None
elif verts is not None:
return verts, edges
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
element_id = element.id()
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
DecoratorData.fill_cache[element_id] = {}
return
if tool.Model.get_usage_type(element) != "LAYER2":
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
DecoratorData.fill_cache[element_id] = {}
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layers = self.get_layer_data(element)
mesh = obj.data
bm_original = bmesh.new()
bm_original.from_mesh(mesh)
if not layers:
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
minx = min([co[0] for co in obj.bound_box])
maxx = max([co[0] for co in obj.bound_box])
min_edge = [Vector((minx, layers["offset"])), Vector((maxx, layers["offset"]))]
max_edge = [
Vector((minx, layers["offset"] + layers["thickness"])),
Vector((maxx, layers["offset"] + layers["thickness"])),
]
centerline = [
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
]
connections = self.get_connections(element, obj, centerline, min_edge, max_edge)
start_point = None
end_point = None
if connections["ATSTART"]:
boundary_edge = min_edge if connections["ATSTART"]["angle"] > 0 else max_edge
rel_dir = connections["ATSTART"]["centerline"][1] - connections["ATSTART"]["centerline"][0]
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[0], centerline[0] + rel_dir])
offset_x = (offset - centerline[0]).length + abs(offset.x)
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATSTART"]["centerline"])
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
centerline[0] = centerline[0] + Vector((offset_x, 0))
start_point = centerline[0] + rel_dir
if connections["ATEND"]:
boundary_edge = min_edge if connections["ATEND"]["angle"] > 0 else max_edge
rel_dir = connections["ATEND"]["centerline"][1] - connections["ATEND"]["centerline"][0]
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[1], centerline[1] + rel_dir])
offset_x = (offset - centerline[1]).length + abs((maxx - abs(offset.x)))
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATEND"]["centerline"])
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
centerline[1] = centerline[1] - Vector((offset_x, 0))
end_point = centerline[1] + rel_dir
min_segments = self.get_segments(connections["MINPATH"], centerline, start_point, end_point)
max_segments = self.get_segments(connections["MAXPATH"], centerline, start_point, end_point)
final_segments = []
for segment in min_segments:
segment_line = shapely.LineString([co for co in segment])
for distance in layers["min_layers"]:
distance *= -1
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
final_segments.append(list(layer_line.coords))
for segment in max_segments:
segment_line = shapely.LineString([co for co in segment])
for distance in layers["max_layers"]:
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
final_segments.append(list(layer_line.coords))
# Extrude and bisect layers
bm = bmesh.new()
verts = []
edges = []
offset = 0
for segment in final_segments:
if not segment:
# Why does this occur?
continue
if isinstance(segment[0], tuple):
segment = [Vector(co) for co in segment]
verts.extend([bm.verts.new(co.to_3d()) for co in segment])
[bm.edges.new((verts[i + offset], verts[i + 1 + offset])) for i in range(0, len(segment) - 1)]
offset += len(segment)
extrusion_dir = (0, 0, 3)
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=bm.edges[:])
bmesh.ops.translate(
bm, vec=extrusion_dir, verts=[v for v in extruded_geom["geom"] if isinstance(v, bmesh.types.BMVert)]
)
verts, edges = self.bisect_mesh(obj, bm, context.scene.camera)
layer_linestrings = [shapely.LineString((verts[e[0]], verts[e[1]])) for e in edges]
clipped_linestrings = []
polygons = shapely.polygonize([shapely.LineString((cut_verts[e[0]], cut_verts[e[1]])) for e in cut_edges])
for polygon in polygons.geoms:
clipped_linestrings.extend([polygon.intersection(ls) for ls in layer_linestrings])
verts = []
edges = []
offset = 0
for linestring in clipped_linestrings:
try:
linestring = list(linestring.coords)
except:
print("Failed ... ", linestring)
continue
verts.extend(linestring)
edges.extend([(i + offset, i + 1 + offset) for i in range(0, len(linestring) - 1)])
offset += len(linestring)
DecoratorData.layerset_cache[element.id()] = (verts, edges)
return verts, edges
def bisect_mesh(self, obj, bm, camera):
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
# Slice our mesh into a 2D drawing cut (2D is always easier)
camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world
global_offset = context.scene.camera.matrix_world.col[2].xyz * -context.scene.camera.data.clip_start
plane_co = camera_matrix.translation
plane_no = camera_matrix.col[2].xyz
geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:]
bmesh.ops.bisect_plane(
bm_original,
geom=geom,
dist=0.0001,
plane_co=plane_co,
plane_no=plane_no,
clear_outer=True,
clear_inner=True,
)
bmesh.ops.remove_doubles(bm_original, verts=bm_original.verts, dist=0.000001)
bmesh.ops.triangle_fill(bm_original, use_dissolve=True, edges=bm_original.edges)
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
if not (material := ifcopenshell.util.element.get_material(element)):
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
DecoratorData.fill_cache[element_id].setdefault(self.fallback_colour, []).append((verts, tris))
return
# Run the bisect operation
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
# Constituents, lists, and item styles not supported yet
material = ifcopenshell.util.element.get_materials(element)[0]
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
colour = self.get_material_colour(material)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
return
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if material.is_a("IfcMaterialLayerSetUsage"):
usage = material
layer_set = material.ForLayerSet
offset = usage.OffsetFromReferenceLine * self.unit_scale
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
elif material.is_a("IfcMaterialLayerSet"):
usage = None
layer_set = material
offset = 0
sense_factor = 1
if len(layer_set.MaterialLayers) == 1:
material = layer_set.MaterialLayers[0].Material
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
colour = self.get_material_colour(material)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
return
bm = bm_original.copy()
prev_co = None
if not usage:
sense_factor = 1 # Assume the extrusion vector points in the direction sense
no = tool.Drawing.get_extrusion_vector(element).normalized()
co = Vector((0.0, 0.0, offset))
elif usage.LayerSetDirection == "AXIS2":
co = Vector((0.0, offset, 0.0))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = no.cross(Vector([1.0, 0.0, 0.0]))
elif usage.LayerSetDirection == "AXIS3":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([0.0, 0.0, 1.0])
elif usage.LayerSetDirection == "AXIS1":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([1.0, 0.0, 0.0])
no *= sense_factor
last_i = len(layer_set.MaterialLayers) - 1
vert_map = {}
verts = []
edges = []
j = 0
for i, layer in enumerate(layer_set.MaterialLayers):
prev_co = co.copy()
co += no * layer.LayerThickness * self.unit_scale
if i != last_i:
bisect = bmesh.ops.bisect_plane(
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
)
for geom in bisect["geom_cut"]:
if isinstance(geom, bmesh.types.BMVert):
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
vert_map[geom.index] = j
j += 1
else:
# It seems as though edges always appear after verts
edges.append([vert_map[v.index] for v in geom.verts])
colour = self.get_material_colour(layer.Material)
bm_fill = bm_original.copy()
if i != last_i:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True)
if i != 0:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append(
self.get_bmesh_tris(obj, bm_fill, context.scene.camera)
)
DecoratorData.slice_cache[element.id()] = (verts, edges)
bm_original.free()
def get_material_colour(self, material: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
style = ifcopenshell.util.representation.get_material_style(material, body)
if not style:
return self.fallback_colour
if not (styles := [s for s in style.Styles if s.is_a("IfcSurfaceStyleShading")]):
return self.fallback_colour
colour = styles[0].SurfaceColour
return (colour.Red, colour.Green, colour.Blue, 1)
def get_bmesh_tris(self, obj, bm, camera):
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
triangulate = bmesh.ops.triangulate(bm, faces=bm.faces)
vert_map = {}
verts = []
tris = []
i = 0
for geom in results["geom_cut"]:
if isinstance(geom, bmesh.types.BMVert):
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
vert_map[geom.index] = i
i += 1
else:
# It seems as though edges always appear after verts
edges.append([vert_map[v.index] for v in geom.verts])
bm.free()
return verts, edges
def get_layer_data(self, element):
usage = ifcopenshell.util.element.get_material(element)
offset = usage.OffsetFromReferenceLine * self.unit_scale
layer_set = usage.ForLayerSet
if len(layer_set.MaterialLayers) == 1:
return # No use slicing if there's only one layer
total_thickness = layer_set.TotalThickness
half_thickness = total_thickness / 2
min_layers = []
max_layers = []
current_thickness = 0
for layer in layer_set.MaterialLayers:
current_thickness += layer.LayerThickness
if current_thickness / total_thickness < 0.5:
min_layers.append((half_thickness - current_thickness) * self.unit_scale)
else:
max_layers.append((current_thickness - half_thickness) * self.unit_scale)
min_layers.reverse()
max_layers.pop()
if usage.DirectionSense == "NEGATIVE":
total_thickness *= -1
offset *= -1
min_layers = [l * -1 for l in min_layers]
max_layers = [l * -1 for l in max_layers]
return {
"offset": offset,
"min_layers": min_layers,
"max_layers": max_layers,
"thickness": total_thickness * self.unit_scale,
}
def get_segments(self, connections, centerline, start_point, end_point):
segments = []
total_connections = len(connections)
if total_connections:
for i in range(0, total_connections + 1):
if i == 0:
connection = connections[i]
segments.append([centerline[0], connection["intersection"], connection["out_point"]])
elif i == total_connections:
segments.append([segments[-1][-1], segments[-1][-2], centerline[1]])
for face in triangulate["faces"]:
tri = []
for vert in face.verts:
if index := vert_map.get(vert.index, None):
tri.append(index)
else:
connection = connections[i]
segments.append(
[segments[-1][-1], segments[-1][-2], connection["intersection"], connection["out_point"]]
)
else:
segments.append([centerline[0], centerline[1]])
if start_point:
segments[0].insert(0, start_point)
if end_point:
segments[-1].append(end_point)
return segments
def get_connections(self, wall, obj, centerline, min_edge, max_edge):
connections = {"ATEND": None, "ATSTART": None, "ATPATH": [], "MINPATH": [], "MAXPATH": []}
for rel in wall.ConnectedTo:
# How do you join to a non layered element? Not sure.
if tool.Model.get_usage_type(rel.RelatedElement) != "LAYER2":
continue
if rel.RelatingConnectionType == "ATPATH":
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections["ATPATH"].append(metadata)
else:
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections[rel.RelatingConnectionType] = metadata
for rel in wall.ConnectedFrom:
if tool.Model.get_usage_type(rel.RelatingElement) != "LAYER2":
continue
# We only consider ATPATH since in this situation, we have the
# priority. The non-priority wall never has any layers that need to
# "turn a corner".
if rel.RelatedConnectionType == "ATPATH":
metadata = self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections["ATPATH"].append(metadata)
connections["ATPATH"] = sorted(connections["ATPATH"], key=lambda c: c["intersection"].x)
for connection in connections["ATPATH"]:
if connection["angle"] > 0:
connections["MINPATH"].append(connection)
else:
connections["MAXPATH"].append(connection)
return connections
def get_connection_metadata(self, obj, rel_element, centerline, min_edge, max_edge):
rel_obj = tool.Ifc.get_object(rel_element)
layers = self.get_layer_data(rel_element)
if not layers:
return
minx = min([co[0] for co in rel_obj.bound_box])
maxx = max([co[0] for co in rel_obj.bound_box])
rel_centerline = [
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
]
rel_centerline = [obj.matrix_world.inverted() @ rel_obj.matrix_world @ v.to_3d() for v in rel_centerline]
rel_centerline = [v.to_2d() for v in rel_centerline]
intersection = tool.Cad.intersect_edges(centerline, rel_centerline)
if intersection:
intersection, _ = intersection
else:
return
closest_centerline_point = tool.Cad.closest_vector(intersection, tuple(rel_centerline))
if closest_centerline_point == rel_centerline[1]:
rel_centerline = [rel_centerline[1], rel_centerline[0]]
angle = tool.Cad.angle_edges(centerline, rel_centerline, signed=True)
# A little extreme, but maybe it's OK?
out_point = tool.Cad.furthest_vector(intersection, tuple(rel_centerline))
return {
"element": rel_element,
"obj": rel_obj,
"centerline": rel_centerline,
"intersection": intersection,
"angle": angle,
"out_point": out_point,
}
verts.append(tuple((obj.matrix_world @ vert.co) + global_offset))
vert_map[vert.index] = i
tri.append(i)
i += 1
tris.append(tri)
return verts, tris
class DecorationsHandler:
@@ -2022,7 +1923,8 @@ class DecorationsHandler:
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
props = tool.Drawing.get_document_props()
if drawing_font := props.drawing_font:
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
if drawing_font_path.is_file():
font_id = blf.load(drawing_font_path.__str__())
@@ -2045,7 +1947,7 @@ class DecorationsHandler:
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = element.ObjectType
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
+23 -15
View File
@@ -19,6 +19,7 @@
import bpy
import blf
import gpu
import bonsai.tool as tool
from bpy import types
from mathutils import Vector
from mathutils import geometry
@@ -478,21 +479,25 @@ class ExtrusionWidget(types.GizmoGroup):
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
@classmethod
def poll(cls, ctx):
obj = ctx.object
def poll(cls, context):
obj = context.active_object
return (
obj
and obj.type == "MESH"
and obj.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None
and (data := obj.data)
and isinstance(data, bpy.types.Mesh)
and tool.Geometry.get_mesh_props(data).ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None
)
def setup(self, ctx):
target = ctx.object
prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
def setup(self, context: bpy.types.Context) -> None:
target = context.object
assert target
mesh = target.data
assert isinstance(mesh, bpy.types.Mesh)
prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
basis = target.matrix_world.normalized()
theme = ctx.preferences.themes[0].user_interface
scale_value = self.get_scale_value(ctx.scene.unit_settings.system, ctx.scene.unit_settings.length_unit)
theme = context.preferences.themes[0].user_interface
scale_value = self.get_scale_value(context.scene.unit_settings.system, context.scene.unit_settings.length_unit)
# setup handle
gz = self.handle = self.gizmos.new("BIM_GT_uglydot_3d")
@@ -521,23 +526,26 @@ class ExtrusionWidget(types.GizmoGroup):
# gz.use_draw_modal = True
# gz.target_set_prop('value', target.demo, 'depth')
def refresh(self, ctx):
def refresh(self, context: bpy.types.Context) -> None:
"""updating gizmos"""
target = ctx.object
target = context.active_object
basis = target.matrix_world.normalized()
self.handle.matrix_basis = basis
self.guides.matrix_basis = basis
def update(self, ctx):
def update(self, context: bpy.types.Context) -> None:
"""updating object"""
bpy.ops.bim.update_parametric_representation()
target = ctx.object
prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
target = context.active_object
assert target
mesh = target.data
assert isinstance(mesh, bpy.types.Mesh)
prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
self.handle.target_set_prop("offset", prop, "value")
self.guides.target_set_prop("depth", prop, "value")
@staticmethod
def get_scale_value(system, length_unit):
def get_scale_value(system: str, length_unit: str) -> float:
scale_value = 1
if system == "METRIC":
if length_unit == "KILOMETERS":
@@ -24,7 +24,8 @@ from bpy.app.handlers import persistent
@persistent
def load_post(*args):
if bpy.context.scene.DocProperties.should_draw_decorations:
props = tool.Drawing.get_document_props()
if props.should_draw_decorations:
decoration.DecorationsHandler.install(bpy.context)
else:
decoration.DecorationsHandler.uninstall()
@@ -35,9 +36,11 @@ def depsgraph_update_pre_handler(scene):
set_active_camera_resolution(scene)
def set_active_camera_resolution(scene):
if not scene.camera or "/" not in scene.camera.name or not scene.DocProperties.drawings:
def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
props = tool.Drawing.get_document_props()
if not scene.camera or "/" not in scene.camera.name or not props.drawings:
return
assert isinstance(scene.camera.data, bpy.types.Camera)
props = scene.camera.data.BIMCameraProperties
ortho_scale = max((props.width, props.height))
aspect_ratio = props.width / props.height
@@ -60,5 +63,3 @@ def set_active_camera_resolution(scene):
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
current_drawing = scene.DocProperties.drawings[scene.DocProperties.current_drawing_index]
+112 -12
View File
@@ -19,8 +19,11 @@
import bpy
import math
import mathutils.geometry
import ifcopenshell
import ifcopenshell.util.unit
import bonsai.tool as tool
from mathutils import Vector
from typing import Union
# Code taken and updated from https://blenderartists.org/t/detecting-intersection-of-bounding-boxes/457520/2
@@ -128,15 +131,53 @@ def format_distance(
decimal_places=None,
suppress_zero_inches=False,
in_unit_length=False,
custom_unit=None,
):
s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented)
# Get Scene Unit Settings
scaleFactor = bpy.context.scene.unit_settings.scale_length
# Get Blender Scene Unit Settings
unit_scale = bpy.context.scene.unit_settings.scale_length
unit_system = bpy.context.scene.unit_settings.system
unit_length = bpy.context.scene.unit_settings.length_unit
area_unit_symbol = " m2" if unit_system == "METRIC" else " ft2"
value *= scaleFactor
# Get IFC Unit Settings
if tool.Ifc.get():
unit_scale = 1
if length_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT"):
unit_system = "METRIC" if length_unit.Name == "METRE" else "IMPERIAL"
unit_length = length_unit.Name.upper()
if hasattr(length_unit, "Prefix") and length_unit.Prefix:
unit_length = length_unit.Prefix + length_unit.Name
unit_length_mapping = {
"FOOT": "FEET",
"INCH": "INCHES",
"METRE": "METERS",
"DECIMETRE": "DECIMETERS",
"CENTIMETRE": "CENTIMETERS",
"MILLIMETRE": "MILLIMETERS",
}
unit_length = unit_length_mapping[unit_length]
# For now we only format area in IFC Units
if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"):
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit)
unit_fraction = True if unit_system == "IMPERIAL" else False
# Custom Unit Settings
if custom_unit:
unit_mapping = {
"Feet and Inches - Fractional": ("IMPERIAL", "FEET", True),
"Feet - Decimal": ("IMPERIAL", "FEET", False),
"Inches - Fractional": ("IMPERIAL", "INCHES", True),
"Inches - Decimal": ("IMPERIAL", "INCHES", False),
"Meters": ("METRIC", "METERS", False),
"Decimeters": ("METRIC", "DECIMETERS", False),
"Centimeters": ("METRIC", "CENTIMETERS", False),
"Millimeters": ("METRIC", "MILLIMETERS", False),
}
if custom_unit in unit_mapping:
unit_system, unit_length, unit_fraction = unit_mapping[custom_unit]
value *= unit_scale
# Imperial Formatting
if unit_system == "IMPERIAL":
@@ -161,10 +202,14 @@ def format_distance(
base = int(precision)
decInches = value * toInches
decFeet = decInches / 12
# Separate ft and inches
# Unless Inches are the specified Length Unit
if unit_length != "INCHES":
# Unless Inches are the specified Length Unit or unit_fraction is False
if unit_length == "FEET" and not unit_fraction:
feet = round(decInches / inPerFoot, 3) # keep decimal
decInches = 0
elif unit_length != "INCHES":
feet = int(decInches / inPerFoot) # remove decimal
decInches -= feet * inPerFoot
else:
@@ -199,6 +244,10 @@ def format_distance(
feet += 1
inches = 0
# Check whether decimal or fractional
if not unit_fraction:
inches = round(decInches, 3)
frac = None
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac)
tx_dist = ""
@@ -224,12 +273,33 @@ def format_distance(
tx_dist += str(frac) + "/" + str(base)
if add_inches or frac:
tx_dist += '"'
if precision == "12" and unit_system == "IMPERIAL":
tx_dist = str(round(decFeet)) + "'"
else:
tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
fmt = "%1.3f"
sq_feet = round(value * toInches / inPerFoot, 4)
tx_dist = ""
if area_unit_symbol == " ft2":
fmt += area_unit_symbol
tx_dist = fmt % sq_feet
if area_unit_symbol == " in2":
sq_inch = sq_feet * 144
fmt += area_unit_symbol
tx_dist = fmt % sq_inch
if area_unit_symbol == " yd2":
sq_yard = sq_feet / 9
fmt += area_unit_symbol
tx_dist = fmt % sq_yard
if area_unit_symbol == " mi2":
sq_mile = sq_feet / 27878400
fmt += area_unit_symbol
tx_dist = fmt % sq_mile
# METRIC FORMATTING
elif unit_system == "METRIC":
if in_unit_length:
if unit_length == "DECIMETERS":
value = value / 10
if unit_length == "CENTIMETERS":
value = value / 100
if unit_length == "MILLIMETERS":
@@ -248,6 +318,14 @@ def format_distance(
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
# Decimeters
elif unit_length == "DECIMETERS":
if decimal_places is None:
fmt = "%1.1f"
if hide_units is False:
fmt += " dm"
d_dm = value * (10)
tx_dist = fmt % d_dm
# Centimeters
elif unit_length == "CENTIMETERS":
if decimal_places is None:
@@ -287,19 +365,41 @@ def format_distance(
d_mm = value * (1000)
tx_dist = fmt % d_mm
if isArea:
tx_dist += s_code
if area_unit_symbol == " m2":
if decimal_places is None:
fmt = "%1.3f"
if hide_units is False:
fmt += area_unit_symbol
tx_dist = fmt % value
if area_unit_symbol == " cm2":
if decimal_places is None:
fmt = "%1.1f"
if hide_units is False:
fmt += area_unit_symbol
d_cm = value * (10000)
tx_dist = fmt % d_cm
if area_unit_symbol == " mm2":
if decimal_places is None:
fmt = "%1.0f"
if hide_units is False:
fmt += area_unit_symbol
d_cm = value * (1000000)
tx_dist = fmt % d_cm
else:
tx_dist = fmt % value
return tx_dist
def get_active_drawing(scene):
def get_active_drawing(
scene: bpy.types.Scene,
) -> Union[tuple[bpy.types.Collection, bpy.types.Camera], tuple[None, None]]:
"""Get active drawing collection and camera"""
props = scene.DocProperties
props = tool.Drawing.get_document_props()
try:
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
return camera.BIMObjectProperties.collection, camera
return tool.Blender.get_object_bim_props(camera).collection, camera
except:
return None, None
+306 -124
View File
@@ -43,6 +43,7 @@ import bonsai.bim.module.drawing.svgwriter as svgwriter
import bonsai.bim.module.drawing.annotation as annotation
import bonsai.bim.module.drawing.sheeter as sheeter
import bonsai.bim.export_ifc
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.module.drawing.decoration import CutDecorator
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List, Union, Optional, Literal
@@ -134,19 +135,19 @@ class AddDrawing(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Add a drawing view to the IFC project"
def _execute(self, context):
self.props = context.scene.DocProperties
hint = self.props.location_hint
if self.props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
props = tool.Drawing.get_document_props()
hint = props.location_hint
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
hint = int(hint)
core.add_drawing(
tool.Ifc,
tool.Collector,
tool.Drawing,
target_view=self.props.target_view,
target_view=props.target_view,
location_hint=hint,
)
try:
drawing = tool.Ifc.get().by_id(self.props.active_drawing_id)
drawing = tool.Ifc.get().by_id(props.active_drawing_id)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
except:
pass
@@ -162,7 +163,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -176,7 +177,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
row.prop(self, "should_duplicate_annotations")
def _execute(self, context):
self.props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
core.duplicate_drawing(
tool.Ifc,
tool.Drawing,
@@ -184,7 +185,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
should_duplicate_annotations=self.should_duplicate_annotations,
)
try:
drawing = tool.Ifc.get().by_id(self.props.active_drawing_id)
drawing = tool.Ifc.get().by_id(props.active_drawing_id)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
except:
pass
@@ -244,9 +245,9 @@ class CreateDrawing(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id
active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera)
if self.print_all:
original_drawing_id = active_drawing_id
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
@@ -261,7 +262,7 @@ class CreateDrawing(bpy.types.Operator):
self.camera = context.scene.camera
self.camera_element = tool.Ifc.get_entity(self.camera)
self.camera_document = tool.Drawing.get_drawing_document(self.camera_element)
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
with profile("Drawing generation process"):
with profile("Initialize drawing generation process"):
@@ -385,13 +386,13 @@ class CreateDrawing(bpy.types.Operator):
obj.hide_render = obj.name not in visible_object_names
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
drawing_style = self.props.drawing_styles[self.cprops.active_drawing_style_index]
if drawing_style.render_type == "DEFAULT":
bpy.ops.render.render(write_still=True)
else:
previous_visibility = {}
for obj in self.camera.BIMObjectProperties.collection.objects:
for obj in tool.Blender.get_object_bim_props(self.camera).collection.objects:
if bpy.context.view_layer.objects.get(obj.name):
previous_visibility[obj.name] = obj.hide_get()
obj.hide_set(True)
@@ -562,6 +563,112 @@ class CreateDrawing(bpy.types.Operator):
path.attrib["d"] = d
group.append(g)
def generate_wall_layers(self, context: bpy.types.Context, root):
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" in el.get("class", "").split():
continue
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
if not (obj := tool.Ifc.get_object(element)):
continue
if not (material := ifcopenshell.util.element.get_material(element)):
continue
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
continue
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if material.is_a("IfcMaterialLayerSetUsage"):
usage = material
layer_set = material.ForLayerSet
offset = usage.OffsetFromReferenceLine * self.unit_scale
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
elif material.is_a("IfcMaterialLayerSet"):
usage = None
layer_set = material
offset = 0
sense_factor = 1
camera_matrix_i = context.scene.camera.matrix_world.inverted()
group = root.find("{http://www.w3.org/2000/svg}g")
raw_width, raw_height = self.get_camera_dimensions()
x_offset = raw_width / 2
y_offset = raw_height / 2
svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
mesh = obj.data
bm = bmesh.new()
bm.from_mesh(mesh)
# Slice our mesh into a 2D drawing cut (2D is always easier)
camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world
plane_co = camera_matrix.translation
plane_no = camera_matrix.col[2].xyz
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
bmesh.ops.bisect_plane(
bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no, clear_outer=True, clear_inner=True
)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
bmesh.ops.triangle_fill(bm, use_dissolve=True, edges=bm.edges)
prev_co = None
if not usage:
sense_factor = 1 # Assume the extrusion vector points in the direction sense
no = tool.Drawing.get_extrusion_vector(element).normalized()
co = Vector((0.0, 0.0, offset))
elif usage.LayerSetDirection == "AXIS2":
co = Vector((0.0, offset, 0.0))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = no.cross(Vector([1.0, 0.0, 0.0]))
elif usage.LayerSetDirection == "AXIS3":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([0.0, 0.0, 1.0])
elif usage.LayerSetDirection == "AXIS1":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([1.0, 0.0, 0.0])
no *= sense_factor
last_i = len(layer_set.MaterialLayers) - 1
for i, layer in enumerate(layer_set.MaterialLayers):
prev_co = co.copy()
co += no * layer.LayerThickness * self.unit_scale
bm_fill = bm.copy()
if i != last_i:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True)
if i != 0:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(
bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True
)
bm_fill.verts.ensure_lookup_table()
bm_fill.edges.ensure_lookup_table()
verts = [tuple(obj.matrix_world @ v.co) for v in bm_fill.verts]
edges = [[v.index for v in e.verts] for e in bm_fill.edges]
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
g.attrib["{http://www.ifcopenshell.org/ns}layer-id"] = str(layer.id())
lines = []
for edge in edges:
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
coords = [start, end]
d = " ".join(
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
)
d = "M{}".format(d[1:])
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
path.attrib["d"] = d
group.append(g)
bm.free()
def generate_freestyle_linework(self, context: bpy.types.Context) -> str | None:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
@@ -712,9 +819,11 @@ class CreateDrawing(bpy.types.Operator):
}
cached_linework -= edited_guids
files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()}
bim_props = tool.Blender.get_bim_props()
files = {bim_props.ifc_file: tool.Ifc.get()}
for link in context.scene.BIMProjectProperties.links:
props = tool.Project.get_project_props()
for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
files[link.name] = IfcStore.session_files[link.name]
@@ -729,7 +838,7 @@ class CreateDrawing(bpy.types.Operator):
# 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.cache_dir, f"{ifc_hash}.h5")
ifc_cache_path = os.path.join(bim_props.cache_dir, f"{ifc_hash}.h5")
self.serialiser.setFile(ifc)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
@@ -767,10 +876,12 @@ class CreateDrawing(bpy.types.Operator):
if self.camera.data.BIMCameraProperties.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
elif self.camera.data.BIMCameraProperties.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
@@ -1099,7 +1210,7 @@ class CreateDrawing(bpy.types.Operator):
# the style of the face when running tree.select_ray()
# tree.enable_face_styles(True)
def get_svg_classes(self, element):
def get_svg_classes(self, element, layer=None):
classes = [element.is_a()]
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
material_name = ""
@@ -1111,7 +1222,13 @@ class CreateDrawing(bpy.types.Operator):
material_name = tool.Drawing.canonicalise_class_name(material_name)
classes.append(f"material-{material_name}")
else:
classes.append(f"material-null")
classes.append("material-null")
if layer:
classes.append(layer.is_a())
material_name = layer.Material.Name or "null"
material_name = tool.Drawing.canonicalise_class_name(material_name)
classes.append(f"layer-material-{material_name}")
for key in self.metadata:
value = ifcopenshell.util.selector.get_element_value(element, key)
@@ -1141,7 +1258,8 @@ class CreateDrawing(bpy.types.Operator):
try:
return tool.Ifc.get().by_guid(guid)
except:
for link in bpy.context.scene.BIMProjectProperties.links:
props = tool.Project.get_project_props()
for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
try:
@@ -1149,6 +1267,23 @@ class CreateDrawing(bpy.types.Operator):
except:
continue
def get_element_by_id(self, step_id):
try:
step_id = int(step_id)
except:
return
try:
return tool.Ifc.get().by_id(step_id)
except:
props = tool.Project.get_project_props()
for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
try:
return IfcStore.session_files[link.name].by_id(step_id)
except:
continue
def remove_cut_linework(self, root):
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" not in el.get("class", "").split():
@@ -1160,15 +1295,15 @@ class CreateDrawing(bpy.types.Operator):
join_criteria = join_criteria.split(",")
else:
# Drawing convention states that same objects classes with the same material are merged when cut.
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status"]
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"]
group = root.find("{http://www.w3.org/2000/svg}g")
joined_paths = {}
self.is_manifold_cache = {}
ifc = tool.Ifc.get()
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
layer = self.get_element_by_id(el.get("{http://www.ifcopenshell.org/ns}layer-id"))
if "projection" in el.get("class", "").split():
classes = self.get_svg_classes(element)
@@ -1176,7 +1311,7 @@ class CreateDrawing(bpy.types.Operator):
el.set("class", " ".join(classes))
continue
else:
classes = self.get_svg_classes(element)
classes = self.get_svg_classes(element, layer)
classes.append("cut")
el.set("class", " ".join(classes))
@@ -1185,7 +1320,12 @@ class CreateDrawing(bpy.types.Operator):
if not obj: # This is a linked model object. For now, do nothing.
continue
if not self.is_manifold(obj):
if (material := ifcopenshell.util.element.get_material(element)) and material.is_a() in (
"IfcMaterialLayerSet",
"IfcMaterialLayerSetUsage",
):
pass # These are always manifold
elif not self.is_manifold(obj):
continue
# An element group will contain a bunch of paths representing the
@@ -1270,6 +1410,15 @@ class CreateDrawing(bpy.types.Operator):
else:
keys.append(key)
if layer:
for query in join_criteria:
key = ifcopenshell.util.selector.get_element_value(layer, query)
print("got layer key", query, key)
if isinstance(key, (list, tuple)):
keys.extend(key)
else:
keys.append(key)
hash_keys = hash(tuple(keys))
if el.findall("{http://www.w3.org/2000/svg}path"):
@@ -1286,7 +1435,8 @@ class CreateDrawing(bpy.types.Operator):
for path in el.findall("{http://www.w3.org/2000/svg}path"):
for subpath in path.attrib["d"].split("M")[1:]:
subpath_co = "M" + subpath.strip(" Z")
coords = [[float(o) for o in co[1:].split(",")] for co in subpath_co.split()]
# Round due to inaccuracies from Blender meshes and bisection
coords = [[round(float(o), 3) for o in co[1:].split(",")] for co in subpath_co.split()]
if subpath.strip().lower().endswith("z"):
coords.append(coords[0])
if len(coords) > 2 and coords[0] == coords[-1]:
@@ -1367,7 +1517,11 @@ class CreateDrawing(bpy.types.Operator):
elements = list(elements | filtered_drawing_annotations)
annotations = sorted(
elements, key=lambda a: (tool.Drawing.get_annotation_z_index(a), 1 if a.ObjectType == "TEXT" else 0)
elements,
key=lambda a: (
tool.Drawing.get_annotation_z_index(a),
1 if ifcopenshell.util.element.get_predefined_type(a) == "TEXT" else 0,
),
)
precision = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "MetricPrecision")
@@ -1458,7 +1612,8 @@ class AddSheet(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Add a sheet to the project"
def _execute(self, context):
core.add_sheet(tool.Ifc, tool.Drawing, titleblock=context.scene.DocProperties.titleblock)
props = tool.Drawing.get_document_props()
core.add_sheet(tool.Ifc, tool.Drawing, titleblock=props.titleblock)
class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -1474,7 +1629,7 @@ class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator):
cls.poll_message_set("Not implemented yet.")
return False
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -1483,7 +1638,7 @@ class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
pass
"""
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
core.duplicate_sheet(
tool.Ifc,
tool.Drawing,
@@ -1508,7 +1663,7 @@ class OpenLayout(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.update_sheet_drawing_sizes(sheet)
@@ -1530,7 +1685,8 @@ class SelectAllSheets(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
for sheet in context.scene.DocProperties.sheets:
props = tool.Drawing.get_document_props()
for sheet in props.sheets:
if sheet.is_selected != self.select_all:
sheet.is_selected = self.select_all
return {"FINISHED"}
@@ -1550,7 +1706,7 @@ class OpenSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
cls.poll_message_set("No sheet selected.")
return False
@@ -1564,7 +1720,7 @@ class OpenSheet(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context)
def execute(self, context):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command
if self.open_all:
@@ -1612,9 +1768,10 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
# Won't be visible in UI anyway.
if not props.sheets or not context.scene.BIMProperties.data_dir:
bim_props = tool.Blender.get_bim_props()
if not props.sheets or not bim_props.data_dir:
return False
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
@@ -1622,8 +1779,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
return True
def _execute(self, context):
props = context.scene.DocProperties
active_drawing = tool.Drawing.get_active_drawing_item()
props = tool.Drawing.get_document_props()
active_drawing = props.drawings[props.active_drawing_index]
assert active_drawing
active_sheet = tool.Drawing.get_active_sheet(context)
@@ -1680,7 +1837,7 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
active_item = tool.Drawing.get_active_sheet_item()
if active_item is None:
return False
@@ -1714,11 +1871,12 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
cls.poll_message_set("No sheet selected.")
return False
return props.sheets and context.scene.BIMProperties.data_dir
bim_props = tool.Blender.get_bim_props()
return props.sheets and bim_props.data_dir
def invoke(self, context, event):
# opening all sheets on shift+click
@@ -1731,7 +1889,7 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
scene = context.scene
props = scene.DocProperties
props = tool.Drawing.get_document_props()
svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command
svg2dxf_command = tool.Blender.get_addon_preferences().svg2dxf_command
@@ -1838,7 +1996,8 @@ class SelectAllDrawings(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
for drawing in context.scene.DocProperties.drawings:
props = tool.Drawing.get_document_props()
for drawing in props.drawings:
if drawing.is_selected != self.select_all:
drawing.is_selected = self.select_all
return {"FINISHED"}
@@ -1857,7 +2016,7 @@ class OpenDrawing(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -1871,7 +2030,7 @@ class OpenDrawing(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if self.open_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id) for d in self.props.drawings if d.is_drawing and d.is_selected
@@ -1907,7 +2066,7 @@ class ActivateModel(bpy.types.Operator):
bl_description = "Activate the model view, hide all annotations"
def execute(self, context):
dprops = bpy.context.scene.DocProperties
dprops = tool.Drawing.get_document_props()
dprops.active_drawing_id = 0
CutDecorator.uninstall()
@@ -1971,11 +2130,12 @@ class ActivateDrawingBase:
return self.execute(context)
def execute(self, context):
if bpy.context.scene.DocProperties.is_editing_drawings == False:
props = tool.Drawing.get_document_props()
if props.is_editing_drawings == False:
bpy.ops.bim.load_drawings()
drawing = tool.Ifc.get().by_id(self.drawing)
dprops = bpy.context.scene.DocProperties
dprops = tool.Drawing.get_document_props()
if self.use_quick_preview:
tool.Blender.activate_camera(tool.Drawing.import_temporary_drawing_camera(drawing))
@@ -2007,6 +2167,7 @@ class ActivateDrawingBase:
camera_props = camera.data.BIMCameraProperties
if camera_props.update_representation(camera):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
return {"FINISHED"}
@@ -2027,7 +2188,6 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -2050,29 +2210,27 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_sheet_item(reference_type="DRAWING"):
cls.poll_message_set("No drawing selected.")
return False
return True
class SelectDocIfcFile(bpy.types.Operator):
# TODO: not exposed to the UI.
class SelectDocIfcFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_doc_ifc_file"
bl_label = "Select Documentation IFC File"
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filename_ext = ".ifc"
index: bpy.props.IntProperty()
def execute(self, context):
context.scene.DocProperties.ifc_files[self.index].name = self.filepath
props = tool.Drawing.get_document_props()
props.ifc_files[self.index].name = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ResizeText(bpy.types.Operator):
bl_idname = "bim.resize_text"
@@ -2081,7 +2239,7 @@ class ResizeText(bpy.types.Operator):
# TODO: check undo redo
def execute(self, context):
for obj in context.scene.camera.BIMObjectProperties.collection.objects:
for obj in tool.Blender.get_object_bim_props(context.scene.camera).collection.objects:
if isinstance(obj.data, bpy.types.TextCurve):
annotation.Annotator.resize_text(obj)
return {"FINISHED"}
@@ -2098,7 +2256,7 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -2112,12 +2270,10 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context)
def _execute(self, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if self.remove_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id)
for d in context.scene.DocProperties.drawings
if d.is_drawing and d.is_selected
tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected
]
else:
if not self.drawing:
@@ -2187,7 +2343,8 @@ class ReloadDrawingStyles(bpy.types.Operator):
with open(json_path, "r") as fi:
shading_styles_json = json.load(fi)
drawing_styles = context.scene.DocProperties.drawing_styles
props = tool.Drawing.get_document_props()
drawing_styles = props.drawing_styles
drawing_styles.clear()
styles = [style for style in shading_styles_json]
for style_name in styles:
@@ -2212,7 +2369,8 @@ class AddDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
drawing_styles = context.scene.DocProperties.drawing_styles
props = tool.Drawing.get_document_props()
drawing_styles = props.drawing_styles
new = drawing_styles.add()
# drawing style is saved to ifc on rename
new.name = tool.Blender.ensure_unique_name("New Drawing Style", drawing_styles)
@@ -2227,7 +2385,8 @@ class RemoveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
context.scene.DocProperties.drawing_styles.remove(self.index)
props = tool.Drawing.get_document_props()
props.drawing_styles.remove(self.index)
context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = max(self.index - 1, 0)
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
@@ -2283,7 +2442,8 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
index = int(self.index)
else:
index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index
scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style)
props = tool.Drawing.get_document_props()
props.drawing_styles[index].raster_style = json.dumps(style)
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
@@ -2309,7 +2469,8 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
drawing_styles = context.scene.DocProperties.drawing_styles
props = tool.Drawing.get_document_props()
drawing_styles = props.drawing_styles
rel_path = drawing_pset_data["ShadingStyles"]
current_style = drawing_pset_data.get("CurrentShadingStyle", None)
@@ -2338,7 +2499,7 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
new_style_name = None
ifc_file = tool.Ifc.get()
drawing = ifc_file.by_id(context.scene.DocProperties.active_drawing_id)
drawing = ifc_file.by_id(props.active_drawing_id)
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
ifcopenshell.api.run(
"pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": new_style_name}
@@ -2357,17 +2518,18 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
scene = context.scene
ifc_file = tool.Ifc.get()
active_drawing_style_index = scene.camera.data.BIMCameraProperties.active_drawing_style_index
props = tool.Drawing.get_document_props()
if active_drawing_style_index >= len(scene.DocProperties.drawing_styles):
if active_drawing_style_index >= len(props.drawing_styles):
self.report({"ERROR"}, "Could not find active drawing style")
return {"CANCELLED"}
self.drawing_style = scene.DocProperties.drawing_styles[active_drawing_style_index]
self.drawing_style = props.drawing_styles[active_drawing_style_index]
self.set_raster_style(context)
self.set_query(context)
drawing = ifc_file.by_id(scene.DocProperties.active_drawing_id)
drawing = ifc_file.by_id(props.active_drawing_id)
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
ifcopenshell.api.run(
"pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": self.drawing_style.name}
@@ -2392,7 +2554,8 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
def set_query(self, context: bpy.types.Context) -> None:
self.include_global_ids = []
self.exclude_global_ids = []
for ifc_file in context.scene.DocProperties.ifc_files:
props = tool.Drawing.get_document_props()
for ifc_file in props.ifc_files:
try:
ifc = ifcopenshell.open(ifc_file.name)
except:
@@ -2446,13 +2609,12 @@ class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator):
core.remove_sheet(tool.Ifc, tool.Drawing, sheet=tool.Ifc.get().by_id(self.sheet))
class AddSchedule(bpy.types.Operator, tool.Ifc.Operator):
class AddSchedule(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.add_schedule"
bl_label = "Add Schedule"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add an .ods, .xls or .xlsx file as a schedule"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
@@ -2460,10 +2622,6 @@ class AddSchedule(bpy.types.Operator, tool.Ifc.Operator):
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class RemoveSchedule(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_schedule"
@@ -2507,14 +2665,15 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not props.schedules:
cls.poll_message_set("No schedule selected.")
return False
return props.schedules and props.sheets and context.scene.BIMProperties.data_dir
bim_props = tool.Blender.get_bim_props()
return props.schedules and props.sheets and bim_props.data_dir
def _execute(self, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
active_schedule = props.schedules[props.active_schedule_index]
active_sheet = tool.Drawing.get_active_sheet(context)
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
@@ -2573,14 +2732,15 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not props.references:
cls.poll_message_set("No reference selected.")
return False
return props.references and props.sheets and context.scene.BIMProperties.data_dir
bim_props = tool.Blender.get_bim_props()
return props.references and props.sheets and bim_props.data_dir
def _execute(self, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
active_reference = props.references[props.active_reference_index]
active_sheet = tool.Drawing.get_active_sheet(context)
extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id)
@@ -2631,24 +2791,20 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
tool.Drawing.import_sheets()
class AddReference(bpy.types.Operator, tool.Ifc.Operator):
class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.add_reference"
bl_label = "Add Reference"
bl_description = "Import a .svg file to the project as a reference"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
filename_ext = ".svg"
def _execute(self, context):
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class RemoveReference(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_reference"
@@ -2682,7 +2838,8 @@ class AddDrawingStyleAttribute(bpy.types.Operator):
def execute(self, context):
props = context.scene.camera.data.BIMCameraProperties
context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.add()
dprops = tool.Drawing.get_document_props()
dprops.drawing_styles[props.active_drawing_style_index].attributes.add()
return {"FINISHED"}
@@ -2695,7 +2852,8 @@ class RemoveDrawingStyleAttribute(bpy.types.Operator):
def execute(self, context):
props = context.scene.camera.data.BIMCameraProperties
context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index)
dprops = tool.Drawing.get_document_props()
dprops.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index)
return {"FINISHED"}
@@ -2979,7 +3137,7 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.load_sheets(tool.Drawing)
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
sheets_not_found = []
for sheet_prop in props.sheets:
if not sheet_prop.is_sheet:
@@ -3009,7 +3167,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
document_type: Literal["SHEET", "TITLEBLOCK", "EMBEDDED"]
def invoke(self, context, event):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
@@ -3023,6 +3181,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
props = tool.Drawing.get_document_props()
if self.document_type == "SHEET":
row = self.layout.row()
row.prop(self, "identification", text="Identification")
@@ -3030,13 +3189,13 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
row.prop(self, "name", text="Name")
elif self.document_type == "TITLEBLOCK":
row = self.layout.row()
row.prop(context.scene.DocProperties, "titleblock", text="Titleblock")
row.prop(props, "titleblock", text="Titleblock")
elif self.document_type == "EMBEDDED":
row = self.layout.row()
row.prop(self, "identification", text="Identification")
def _execute(self, context):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
@@ -3128,14 +3287,22 @@ class DisableEditingDrawings(bpy.types.Operator, tool.Ifc.Operator):
class ExpandTargetView(bpy.types.Operator):
bl_idname = "bim.expand_target_view"
bl_label = "Expand Target View"
bl_description = "Show views in this category"
bl_description = "\nSHIFT+CLICK to expand all view categories "
bl_options = {"REGISTER", "UNDO"}
target_view: bpy.props.StringProperty()
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# Expanding all categories on shift+click.
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck (copied from #4771).
if event.type == "LEFTMOUSE" and event.shift:
self.expand_all = True
return self.execute(context)
def execute(self, context):
props = context.scene.DocProperties
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
props = tool.Drawing.get_document_props()
for drawing in [d for d in props.drawings if self.expand_all or d.target_view == self.target_view]:
drawing.is_expanded = True
core.load_drawings(tool.Drawing)
return {"FINISHED"}
@@ -3144,14 +3311,22 @@ class ExpandTargetView(bpy.types.Operator):
class ContractTargetView(bpy.types.Operator):
bl_idname = "bim.contract_target_view"
bl_label = "Contract Target View"
bl_description = "Hide views in this category"
bl_description = "\n\nSHIFT+CLICK to hide all view categories"
bl_options = {"REGISTER", "UNDO"}
target_view: bpy.props.StringProperty()
contract_all: bpy.props.BoolProperty(name="Contract All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# Contracting all categories on shift+click.
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck (copied from #4771).
if event.type == "LEFTMOUSE" and event.shift:
self.contract_all = True
return self.execute(context)
def execute(self, context):
props = context.scene.DocProperties
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
props = tool.Drawing.get_document_props()
for drawing in [d for d in props.drawings if self.contract_all or d.target_view == self.target_view]:
drawing.is_expanded = False
core.load_drawings(tool.Drawing)
return {"FINISHED"}
@@ -3160,14 +3335,20 @@ class ContractTargetView(bpy.types.Operator):
class ExpandSheet(bpy.types.Operator):
bl_idname = "bim.expand_sheet"
bl_label = "Expand Sheet"
bl_description = "Show views, schedules, references etc\nplaced on this sheet"
bl_description = "Show views, schedules, references etc\nplaced on this sheet.\n\nShift+click to expand all sheets."
bl_options = {"REGISTER", "UNDO"}
sheet: bpy.props.IntProperty()
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.shift:
self.expand_all = True
return self.execute(context)
def execute(self, context):
props = context.scene.DocProperties
for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]:
props = tool.Drawing.get_document_props()
for sheet in [s for s in props.sheets if self.expand_all or s.ifc_definition_id == self.sheet]:
sheet.is_expanded = True
core.load_sheets(tool.Drawing)
return {"FINISHED"}
@@ -3176,14 +3357,22 @@ class ExpandSheet(bpy.types.Operator):
class ContractSheet(bpy.types.Operator):
bl_idname = "bim.contract_sheet"
bl_label = "Contract Sheet"
bl_description = "Hide views, schedules, references etc\nplaced on this sheet"
bl_description = (
"Hide views, schedules, references etc\nplaced on this sheet.\n\nShift+click to contract all sheets."
)
bl_options = {"REGISTER", "UNDO"}
sheet: bpy.props.IntProperty()
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.shift:
self.expand_all = True
return self.execute(context)
def execute(self, context):
props = context.scene.DocProperties
for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]:
props = tool.Drawing.get_document_props()
for sheet in [s for s in props.sheets if self.expand_all or s.ifc_definition_id == self.sheet]:
sheet.is_expanded = False
core.load_sheets(tool.Drawing)
return {"FINISHED"}
@@ -3244,7 +3433,7 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.add_reference_image"
bl_label = "Add Reference Image"
bl_description = "Add or import reference image to the IFC project"
@@ -3252,9 +3441,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
filepath: bpy.props.StringProperty(
name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH"
)
filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
@@ -3282,10 +3468,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
layout.prop(self, "override_existing_image")
layout.prop(self, "use_existing_object_by_name")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _execute(self, context):
abs_path = Path(self.filepath).absolute().resolve()
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
@@ -3329,7 +3511,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
obj.material_slots[0].material = material
bpy.ops.bim.add_style()
style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id)
style = tool.Ifc.get_entity(material)
assert style
tool.Style.assign_style_to_object(style, obj)
# TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because
@@ -3373,7 +3556,7 @@ class ConvertSVGToDXF(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -3387,14 +3570,13 @@ class ConvertSVGToDXF(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
props = tool.Drawing.get_document_props()
if self.convert_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id)
for d in context.scene.DocProperties.drawings
if d.is_drawing and d.is_selected
tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected
]
else:
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
drawings = [tool.Ifc.get().by_id(props.drawings.get(self.view).ifc_definition_id)]
drawing_uris: list[Path] = []
drawings_not_found: list[str] = []
+6 -6
View File
@@ -76,7 +76,7 @@ def update_diagram_scale(self, context):
try:
element = (
tool.Ifc.get()
.by_id(self.id_data.BIMMeshProperties.ifc_definition_id)
.by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id)
.OfProductRepresentation[0]
.ShapeOfProduct[0]
)
@@ -93,7 +93,7 @@ def update_diagram_scale(self, context):
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale)
def update_is_nts(self, context):
def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
@@ -104,7 +104,7 @@ def update_is_nts(self, context):
try:
element = (
tool.Ifc.get()
.by_id(self.id_data.BIMMeshProperties.ifc_definition_id)
.by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id)
.OfProductRepresentation[0]
.ShapeOfProduct[0]
)
@@ -192,8 +192,8 @@ def get_drawing_style_name(self: "DrawingStyle"):
def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None:
"""ensure the name is unique"""
scene = bpy.context.scene
drawing_styles = [s.name for s in scene.DocProperties.drawing_styles if s.name != self.name]
props = tool.Drawing.get_document_props()
drawing_styles = [s.name for s in props.drawing_styles if s.name != self.name]
new_value = tool.Blender.ensure_unique_name(new_value, drawing_styles)
old_value = self.name
self["name"] = new_value
@@ -266,7 +266,7 @@ def update_titleblocks(self, context):
def update_should_draw_decorations(self, context: bpy.types.Context) -> None:
if self.should_draw_decorations:
# TODO: design a proper text variable templating renderer
collection = context.scene.camera.BIMObjectProperties.collection
collection = tool.Blender.get_object_bim_props(context.scene.camera).collection
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
@@ -56,7 +56,7 @@ def a1_to_rc(cell):
class Scheduler:
def schedule(self, infile, outfile):
def schedule(self, infile: str, outfile: str) -> None:
self.svg = svgwrite.Drawing(
outfile,
debug=False,
@@ -71,11 +71,12 @@ class Scheduler:
elif infile.endswith("xlsx"):
self.schedule_xlsx(infile, outfile)
def parse_css(self, infile):
def parse_css(self, infile: str) -> None:
props = tool.Drawing.get_document_props()
stylesheet_path = os.path.splitext(infile)[0] + ".css"
if not os.path.exists(stylesheet_path):
stylesheet_rel_path = getattr(bpy.context.scene.DocProperties, "schedules_stylesheet_path")
ifc_file_path = os.path.dirname(IfcStore.path)
stylesheet_rel_path = props.schedules_stylesheet_path
ifc_file_path = os.path.dirname(tool.Ifc.get_path())
stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path
if not os.path.exists(stylesheet_path):
stylesheet_path = tool.Blender.get_data_dir_path(Path("assets") / "schedule.css")
@@ -91,7 +92,7 @@ class Scheduler:
self.svg.defs.add(self.svg.style(css))
def schedule_xlsx(self, infile, outfile):
def schedule_xlsx(self, infile: str, outfile: str) -> None:
workbook = openpyxl.open(infile, data_only=True)
sheet = workbook.active
@@ -236,7 +237,7 @@ class Scheduler:
self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height)
self.svg.save(pretty=True)
def schedule_ods(self, infile, outfile):
def schedule_ods(self, infile: str, outfile: str) -> None:
doc = load_ods(infile)
# useful for debugging ods
@@ -495,11 +496,11 @@ class Scheduler:
self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height)
self.svg.save(pretty=True)
def get_style(self, style_name, styles):
def get_style(self, style_name: str, styles: dict) -> dict:
style = styles[style_name] if style_name else {}
return style
def get_box_alignment(self, style):
def get_box_alignment(self, style: dict) -> str:
if style and "vertical-align" in style and style["vertical-align"] != "automatic":
vertical_align = style["vertical-align"]
else:
+48 -12
View File
@@ -32,6 +32,7 @@ from mathutils import Vector
import re
VIEW_TITLE_OFFSET_Y = 5
DRAWING_PADDING = 10
DEFAULT_POSITION = Vector((30, 30))
SVG = "{http://www.w3.org/2000/svg}"
XLINK = "{http://www.w3.org/1999/xlink}"
@@ -113,13 +114,15 @@ class SheetBuilder:
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
x, y = self.next_drawing_location(layout_root, view_width)
# add background
if os.path.isfile(underlay_path):
background = ET.SubElement(view, "image")
background.attrib["data-type"] = "background"
background.attrib["xlink:href"] = os.path.relpath(underlay_path, layout_dir)
background.attrib["x"] = str(DEFAULT_POSITION.x)
background.attrib["y"] = str(DEFAULT_POSITION.y)
background.attrib["x"] = str(x)
background.attrib["y"] = str(y)
background.attrib["width"] = str(view_width)
background.attrib["height"] = str(view_height)
@@ -128,16 +131,49 @@ class SheetBuilder:
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "foreground"
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, layout_dir)
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
foreground.attrib["x"] = str(x)
foreground.attrib["y"] = str(y)
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
)
self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir)
layout_tree.write(layout_path)
def next_drawing_location(self, layout_root: ET.Element, next_width: float) -> list:
titleblocks = layout_root.findall(f'{SVG}g[@data-type="titleblock"]')
drawings = layout_root.findall(f'{SVG}g[@data-type="drawing"]')
# how wide is the title block frame
try:
titleblock_width = self.convert_to_mm(titleblocks[0][0].attrib.get("width"))
except (IndexError, AttributeError):
titleblock_width = 840.0
# where does the last drawing finish
try:
last = drawings[-1][0]
last_width = self.convert_to_mm(last.attrib.get("width"))
last_x = self.convert_to_mm(last.attrib.get("x"))
last_y = self.convert_to_mm(last.attrib.get("y"))
except (IndexError, AttributeError):
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
# check if the new drawing fits in the current row
if last_x + last_width + DRAWING_PADDING + next_width + DEFAULT_POSITION.x < titleblock_width:
return [last_x + last_width + DRAWING_PADDING, last_y]
# start a new row, find the y
for drawing in drawings:
for image in drawing:
try:
image_y = self.convert_to_mm(image.attrib.get("y"))
image_height = self.convert_to_mm(image.attrib.get("height"))
except AttributeError:
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
if image_y + image_height + DRAWING_PADDING > last_y:
last_y = image_y + image_height + DRAWING_PADDING
return [DEFAULT_POSITION.x, last_y]
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
ET.register_namespace("", "http://www.w3.org/2000/svg")
@@ -221,6 +257,8 @@ class SheetBuilder:
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
x, y = self.next_drawing_location(layout_root, view_width)
view = ET.SubElement(layout_root, "g")
view.attrib["data-id"] = str(reference.id())
view.attrib["data-type"] = document.Scope.lower()
@@ -229,14 +267,12 @@ class SheetBuilder:
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "content"
foreground.attrib["xlink:href"] = os.path.relpath(view_path, layout_dir)
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
foreground.attrib["x"] = str(x)
foreground.attrib["y"] = str(y)
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
)
self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir)
layout_tree.write(layout_path)
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
@@ -181,35 +181,35 @@ class SvgWriter:
self.decimal_places = decimal_places
for element in annotations:
obj = tool.Ifc.get_object(element)
if not obj or element.ObjectType == "DRAWING":
if not obj or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING":
continue
elif element.ObjectType == "GRID":
elif object_type == "GRID":
self.draw_grid_annotation(obj)
elif element.ObjectType == "TEXT_LEADER":
elif object_type == "TEXT_LEADER":
self.draw_leader_annotation(obj)
elif element.ObjectType == "STAIR_ARROW":
elif object_type == "STAIR_ARROW":
self.draw_stair_annotation(obj)
elif element.ObjectType == "DIMENSION":
elif object_type == "DIMENSION":
self.draw_dimension_annotations(obj)
elif element.ObjectType == "ANGLE":
elif object_type == "ANGLE":
self.draw_angle_annotations(obj)
elif element.ObjectType == "RADIUS":
elif object_type == "RADIUS":
self.draw_radius_annotations(obj)
elif element.ObjectType == "DIAMETER":
elif object_type == "DIAMETER":
self.draw_diameter_annotations(obj)
elif element.ObjectType == "ELEVATION":
elif object_type == "ELEVATION":
self.draw_elevation_annotation(obj)
elif element.ObjectType == "SECTION":
elif object_type == "SECTION":
self.draw_section_annotation(obj)
elif element.ObjectType == "BREAKLINE":
elif object_type == "BREAKLINE":
self.draw_break_annotations(obj)
elif element.ObjectType == "PLAN_LEVEL":
elif object_type == "PLAN_LEVEL":
self.draw_plan_level_annotation(obj)
elif element.ObjectType == "SECTION_LEVEL":
elif object_type == "SECTION_LEVEL":
self.draw_section_level_annotation(obj)
elif element.ObjectType == "TEXT":
elif object_type == "TEXT":
self.draw_text_annotation(obj, obj.location)
elif element.ObjectType in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
elif object_type in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.draw_fall_annotations(obj)
else:
self.draw_misc_annotation(obj)
@@ -1128,7 +1128,12 @@ class SvgWriter:
def get_text():
radius = (points[-1].co - points[-2].co).length
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
radius = helper.format_distance(
radius,
precision=self.precision,
decimal_places=self.decimal_places,
custom_unit=dimension_data["custom_unit"],
)
text = f"R{radius}"
return text
@@ -1207,13 +1212,14 @@ class SvgWriter:
angle = 90
# ues SLOPE_ANGLE as default
if element.ObjectType in ("FALL", "SLOPE_ANGLE"):
object_type = ifcopenshell.util.element.get_predefined_type(element)
if object_type in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
elif element.ObjectType == "SLOPE_FRACTION":
elif object_type == "SLOPE_FRACTION":
if angle == 90:
return "-"
return f"{helper.format_distance(rise, precision=self.precision, decimal_places=self.decimal_places)} / {helper.format_distance(run, precision=self.precision, decimal_places=self.decimal_places)}"
elif element.ObjectType == "SLOPE_PERCENT":
elif object_type == "SLOPE_PERCENT":
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
@@ -1255,6 +1261,7 @@ class SvgWriter:
text_prefix=dimension_data["text_prefix"],
text_suffix=dimension_data["text_suffix"],
fill_bg=dimension_data["fill_bg"],
custom_unit=dimension_data["custom_unit"],
)
def draw_dimension_annotations(self, obj):
@@ -1279,6 +1286,7 @@ class SvgWriter:
text_prefix=dimension_data["text_prefix"],
text_suffix=dimension_data["text_suffix"],
fill_bg=dimension_data["fill_bg"],
custom_unit=dimension_data["custom_unit"],
)
def draw_measureit_arch_dimension_annotations(self):
@@ -1305,6 +1313,7 @@ class SvgWriter:
text_prefix="",
text_suffix="",
fill_bg=False,
custom_unit=None,
):
offset = Vector([self.raw_width, self.raw_height]) / 2
v0 = self.project_point_onto_camera(v0_global)
@@ -1338,6 +1347,7 @@ class SvgWriter:
precision=self.precision,
decimal_places=self.decimal_places,
suppress_zero_inches=suppress_zero_inches,
custom_unit=custom_unit,
)
text = text_prefix + str(dimension) + text_suffix
else:
+7 -7
View File
@@ -49,7 +49,7 @@ class BIM_PT_camera(Panel):
return
self.layout.use_property_split = True
dprops = context.scene.DocProperties
dprops = tool.Drawing.get_document_props()
props = context.scene.camera.data.BIMCameraProperties
col = self.layout.column(align=True)
@@ -161,7 +161,7 @@ class BIM_PT_drawing_underlay(Panel):
layout.use_property_split = True
camera = context.scene.camera
assert camera
dprops = context.scene.DocProperties
dprops = tool.Drawing.get_document_props()
props = camera.data.BIMCameraProperties
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
@@ -229,7 +229,7 @@ class BIM_PT_drawings(Panel):
draw_project_not_saved_ui(self)
return
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_drawings:
row = self.layout.row(align=True)
@@ -302,7 +302,7 @@ class BIM_PT_schedules(Panel):
draw_project_not_saved_ui(self)
return
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_schedules:
row = self.layout.row(align=True)
@@ -352,7 +352,7 @@ class BIM_PT_references(Panel):
draw_project_not_saved_ui(self)
return
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_references:
row = self.layout.row(align=True)
@@ -394,7 +394,7 @@ class BIM_PT_sheets(Panel):
draw_project_not_saved_ui(self)
return
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_sheets:
row = self.layout.row(align=True)
@@ -601,7 +601,7 @@ class BIM_UL_drawinglist(bpy.types.UIList):
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False)
row.prop(item, "name", text="", emboss=False)
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if (
self.props.drawings
and self.props.active_drawing_id
@@ -19,6 +19,7 @@
import os
import bpy
import ifcopenshell.util.element
import bonsai.core.type
import bonsai.core.drawing as core
import bonsai.tool as tool
@@ -26,7 +27,7 @@ import ifcopenshell.util.representation
from bonsai.bim.module.drawing.data import DecoratorData, AnnotationData
from bonsai.bim.helper import prop_with_search
from bpy.types import WorkSpaceTool
from typing import Union
from functools import partial
class LaunchAnnotationTypeManager(bpy.types.Operator):
@@ -134,23 +135,9 @@ class AnnotationTool(WorkSpaceTool):
AnnotationToolUI.draw(context, layout)
def add_layout_hotkey_operator(
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None]
) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]:
modifiers = {
"A": "EVENT_ALT",
"S": "EVENT_SHIFT",
}
modifier, key = hotkey.split("_")
row = layout.row(align=True)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
op = row.operator("bim.annotation_hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op, row
add_layout_hotkey_operator = partial(
tool.Blender.add_layout_hotkey_operator, tool_name="annotation", module_name=__name__
)
# TODO: move to operator
@@ -197,6 +184,8 @@ def create_annotation_occurrence(context):
class AnnotationToolUI:
layout: bpy.types.UILayout
@classmethod
def draw(cls, context, layout):
cls.layout = layout
@@ -224,7 +213,8 @@ class AnnotationToolUI:
@classmethod
def draw_create_object_interface(cls):
row = cls.layout.row(align=True)
row.prop(bpy.context.scene.DocProperties, "should_draw_decorations", text="Viewport Annotations")
props = tool.Drawing.get_document_props()
row.prop(props, "should_draw_decorations", text="Viewport Annotations")
@classmethod
def draw_edit_object_interface(cls, context):
@@ -261,8 +251,8 @@ class AnnotationToolUI:
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.annotation_hotkey"
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -330,7 +320,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not element or not element.is_a("IfcAnnotation"):
continue
annotation_type = element.ObjectType
annotation_type = ifcopenshell.util.element.get_predefined_type(element)
if annotation_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
self.report({"ERROR"}, f"Annotation type {annotation_type} is not supported for readjustment.")
continue
+9 -16
View File
@@ -24,14 +24,15 @@ import logging
import tempfile
import ifcopenshell
import bonsai.tool as tool
from bpy_extras.io_utils import ExportHelper, ImportHelper
class ExecuteIfcFM(bpy.types.Operator):
class ExecuteIfcFM(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifcfm"
bl_label = "Execute IfcFM"
bl_description = "Export IfcFM data as a spreadsheet."
file_format: bpy.props.StringProperty()
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
@@ -44,9 +45,7 @@ class ExecuteIfcFM(bpy.types.Operator):
def invoke(self, context, event):
props = context.scene.BIMFMProperties
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
def execute(self, context):
props = context.scene.BIMFMProperties
@@ -82,12 +81,12 @@ class ExecuteIfcFM(bpy.types.Operator):
return {"FINISHED"}
class SelectFMSpreadsheetFiles(bpy.types.Operator):
class SelectFMSpreadsheetFiles(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_fm_spreadsheet_files"
bl_label = "Select FM Spreadsheet Files"
bl_description = "Select FM spreadsheets to merge."
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
def execute(self, context):
@@ -99,16 +98,12 @@ class SelectFMSpreadsheetFiles(bpy.types.Operator):
new.name = os.path.join(dirname, f.name)
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcFMFederate(bpy.types.Operator):
class ExecuteIfcFMFederate(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifcfm_federate"
bl_label = "Merge IfcFM SpreadSheets"
bl_description = "Merge added IfcFM spreadsheets."
filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
@@ -121,9 +116,7 @@ class ExecuteIfcFMFederate(bpy.types.Operator):
def invoke(self, context, event):
props = context.scene.BIMFMProperties
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
def execute(self, context):
props = context.scene.BIMFMProperties
+2 -3
View File
@@ -18,7 +18,6 @@
import bonsai.tool as tool
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.fm.data import FMData
@@ -40,11 +39,11 @@ class BIM_PT_fm(Panel):
scene = context.scene
props = scene.BIMFMProperties
if IfcStore.get_file():
if tool.Ifc.get():
row = layout.row()
row.prop(props, "should_load_from_memory")
if not IfcStore.get_file() or not props.should_load_from_memory:
if not tool.Ifc.get() or not props.should_load_from_memory:
row = layout.row()
props.ifc_files.layout_file_select(row, "*.ifc;*.ifczip;*.ifcxml", "IFC File(s)")
@@ -19,6 +19,7 @@
import bpy
from . import ui, prop, operator
from bpy.app.handlers import persistent
import ifcopenshell.util.element
classes = (
operator.AddCurvelikeItem,
@@ -98,12 +99,19 @@ addon_keymaps = []
@persistent
def block_scale(scene):
def block_scale(scene: bpy.types.Scene) -> None:
import bonsai.tool as tool
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
elif isinstance(obj, bpy.types.Mesh) and obj.BIMMeshProperties.ifc_definition_id:
if isinstance(obj, bpy.types.Object) and tool.Blender.get_ifc_definition_id(obj):
if obj.type == "CAMERA":
camera = tool.Ifc.get_entity(obj)
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
obj.scale = (-1, -1, -1)
else:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
elif isinstance(obj, bpy.types.Mesh) and tool.Geometry.get_mesh_props(obj).ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
@@ -116,6 +124,7 @@ def register():
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_object_duplicate_move_linked_aggregate")
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_override_move")
operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate")
operator.OverrideMoveMacro.define("BIM_OT_override_move")
operator.OverrideMoveMacro.define("TRANSFORM_OT_translate")
+15 -11
View File
@@ -53,15 +53,16 @@ class ViewportData:
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
modes = [obj_mode]
if bpy.context.scene.BIMGeometryProperties.representation_obj:
modes: list[tuple[str, str, str, str, int]] = [obj_mode]
gprops = tool.Geometry.get_geometry_props()
if gprops.representation_obj:
modes.append(item_mode)
if not obj:
return modes
if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
pprops = tool.Project.get_project_props()
if obj in pprops.clipping_planes_objs:
pass
elif element:
if tool.Geometry.is_locked(element):
@@ -107,8 +108,9 @@ class RepresentationsData:
element = tool.Ifc.get_entity(obj)
active_representation_id = None
if obj.data and hasattr(obj.data, "BIMMeshProperties"):
active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id
active_representation = tool.Geometry.get_active_representation(obj)
if active_representation:
active_representation_id = active_representation.id()
for representation in tool.Geometry.get_representations_iter(element):
representation_type = representation.RepresentationType
@@ -155,15 +157,17 @@ class RepresentationsData:
if not cls.data["representations"]:
return []
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
if not obj.data:
return []
element = tool.Ifc.get_entity(obj)
if not (active_representation_id := obj.data.BIMMeshProperties.ifc_definition_id):
assert element
base_representation = tool.Geometry.get_active_representation(obj)
if not base_representation:
return [] # Maybe in profile editing mode
base_representation = tool.Ifc.get().by_id(active_representation_id)
# shape aspects matching context of the active representation
matching_shape_aspects = []
matching_shape_aspects: list[ifcopenshell.entity_instance] = []
for shape_aspect in ifcopenshell.util.element.get_shape_aspects(element):
matching_representation = tool.Geometry.get_shape_aspect_representation(shape_aspect, base_representation)
if matching_representation:
@@ -390,7 +394,7 @@ class PlacementData:
def load(cls):
cls.data = {"has_placement": cls.has_placement()}
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
obj = bpy.context.active_object
if obj and props.has_blender_offset:
xyz = cls.original_xyz(obj)
@@ -413,7 +417,7 @@ class PlacementData:
@classmethod
def original_xyz(cls, obj):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
xyz = ifcopenshell.util.geolocation.xyz2enh(
obj.matrix_world[0][3],
obj.matrix_world[1][3],
@@ -46,12 +46,14 @@ class ItemDecorator:
obj_is_boolean: dict[str, list[ifcopenshell.entity_instance]] = {}
objs: dict[str, dict[str, list]] = {}
obj_matrix: dict[str, Matrix] = {}
for item_obj in context.scene.BIMGeometryProperties.item_objs:
props = tool.Geometry.get_geometry_props()
for item_obj in props.item_objs:
if obj := item_obj.obj:
obj: bpy.types.Object
objs[obj.name] = cls.get_obj_data(obj)
obj_is_selected[obj.name] = obj.select_get()
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
item = tool.Geometry.get_active_representation(obj)
assert item
obj_is_boolean[obj.name] = [i for i in tool.Ifc.get().get_inverse(item) if i.is_a("IfcBooleanResult")]
obj_matrix[obj.name] = obj.matrix_world.copy()
@@ -143,7 +145,8 @@ class ItemDecorator:
color = selected_elements_color
blf.color(font_id, *color)
for item in context.scene.BIMGeometryProperties.item_objs:
props = tool.Geometry.get_geometry_props()
for item in props.item_objs:
if (obj := item.obj) and obj.hide_get() == False:
if obj.select_get():
centroid = obj.matrix_world @ Vector(obj.bound_box[0]).lerp(Vector(obj.bound_box[6]), 0.5)
+214 -120
View File
@@ -23,12 +23,14 @@ import numpy as np
import numpy.typing as npt
import ifcopenshell
import ifcopenshell.api.layer
import ifcopenshell.api.style
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
import ifcopenshell.api
import ifcopenshell.api.boundary
import ifcopenshell.api.grid
import bonsai.core.geometry
import bonsai.core.geometry as core
@@ -77,7 +79,8 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
self.separate_element(element)
def separate_item(self, context, obj):
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
item = tool.Geometry.get_active_representation(obj)
assert item
if tool.Geometry.is_meshlike_item(item):
previous_selected_objects = context.selected_objects
bpy.ops.mesh.separate(type=self.type)
@@ -85,7 +88,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
if obj in previous_selected_objects:
continue
self.add_meshlike_item(obj)
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.reload_representation(tool.Geometry.get_geometry_props().representation_obj)
else:
self.report({"INFO"}, f"Separating an {item.is_a()} is not supported")
@@ -113,14 +116,14 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
representation_type = representation.RepresentationType
if representation_type in ("Brep", "AdvancedBrep"):
item = builder.faceted_brep(verts, faces)
elif representation_type in ("Tessellation"):
elif representation_type == "Tessellation":
item = builder.mesh(verts, faces)
else:
assert False, f"Unexpected representation type: '{representation_type}'."
representation.Items = list(representation.Items) + [item]
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Ifc.link(item, obj)
props.add_item_object(obj, item)
def separate_element(self, element):
@@ -301,11 +304,12 @@ class AddRepresentation(bpy.types.Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
props = tool.Geometry.get_geometry_props()
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="")
row.prop(props, "representation_from_object", text="")
class SelectConnection(bpy.types.Operator, tool.Ifc.Operator):
@@ -456,13 +460,18 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def update_obj_mesh_representation(self, context: bpy.types.Context, obj: bpy.types.Object) -> None:
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
data = obj.data
assert tool.Geometry.is_data_supported_for_adding_representation(data)
mprops = tool.Geometry.get_mesh_props(data)
product = tool.Ifc.get_entity(obj)
assert product
material = ifcopenshell.util.element.get_material(product, should_skip_usage=True)
# NOTE: Currently iterator doesn't detect whether opening is actually affected the representation
# or it's just present on the element. In theory, we can also allow editing representations
# if we know that representation wasn't affected by existing openings.
has_openings = tool.Geometry.has_openings(product) and obj.data.BIMMeshProperties.has_openings_applied
has_openings = tool.Geometry.has_openings(product) and tool.Geometry.get_mesh_props(data).has_openings_applied
if has_openings and not self.apply_openings:
# Meshlike things with openings can only be updated without openings applied.
if self.from_ui:
@@ -474,18 +483,19 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
if product.is_a("IfcGridAxis"):
# Grid geometry does not follow the "representation" paradigm and needs to be treated specially
ifcopenshell.api.grid.create_axis_curve(self.file, axis_curve=obj, grid_axis=product)
tool.Model.create_axis_curve(obj, product)
return
elif product.is_a("IfcRelSpaceBoundary"):
# TODO refactor
settings = tool.Boundary.get_assign_connection_geometry_settings(obj)
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings)
return
if tool.Ifc.is_moved(obj) or tool.Geometry.is_scaled(obj):
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
old_representation = tool.Geometry.get_active_representation(obj)
assert old_representation
if material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]:
if self.ifc_representation_class == "IfcTessellatedFaceSet":
# We are explicitly casting to a tessellation, so remove all parametric materials.
@@ -544,12 +554,12 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry.run_style_add_style(obj=mat)
for mat in tool.Geometry.get_object_materials_without_styles(obj)
]
ifcopenshell.api.run(
"style.assign_representation_styles",
props = tool.Geometry.get_geometry_props()
ifcopenshell.api.style.assign_representation_styles(
self.file,
shape_representation=new_representation,
styles=tool.Geometry.get_styles(obj, only_assigned_to_faces=True),
should_use_presentation_style_assignment=context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
should_use_presentation_style_assignment=props.should_use_presentation_style_assignment,
)
tool.Geometry.record_object_materials(obj)
@@ -568,8 +578,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
continue
representation.RepresentationIdentifier = "Reference"
obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id())
obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}"
tool.Ifc.link(new_representation, data)
data.name = tool.Loader.get_mesh_name(new_representation)
# TODO: In simple scenarios, a type has a ShapeRepresentation of ID
# 123. This is then mapped through mapped representations by
@@ -585,7 +595,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
# transformations.
core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation)
if obj.data.BIMMeshProperties.ifc_parameters:
if mprops.ifc_parameters:
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
@@ -597,12 +607,13 @@ class UpdateParametricRepresentation(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.mode == "OBJECT"
return (obj := context.active_object) and obj.mode == "OBJECT" and tool.Geometry.has_mesh_properties(obj.data)
def execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
obj = context.active_object
props = obj.data.BIMMeshProperties
assert obj and tool.Geometry.has_mesh_properties(obj.data)
props = tool.Geometry.get_mesh_props(obj.data)
parameter = props.ifc_parameters[self.index]
self.file.by_id(parameter.step_id)[parameter.index] = parameter.value
show_representation_parameters = bool(props.ifc_parameters)
@@ -626,8 +637,10 @@ class GetRepresentationIfcParameters(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.get_representation_ifc_parameters(tool.Geometry, obj=context.active_object)
parameters = context.active_object.data.BIMMeshProperties.ifc_parameters
obj = context.active_object
assert obj and tool.Geometry.has_mesh_properties((data := obj.data))
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
parameters = tool.Geometry.get_mesh_props(data).ifc_parameters
self.report({"INFO"}, f"{len(parameters)} parameters found.")
@@ -671,7 +684,7 @@ class CopyRepresentation(bpy.types.Operator, tool.Ifc.Operator):
)
def lock_error_message(name):
def lock_error_message(name: str) -> str:
return f"'{name}' is locked. Unlock it via the Spatial panel in the Project Overview tab."
@@ -719,8 +732,10 @@ class OverrideDelete(bpy.types.Operator):
if self.is_batch:
row = self.layout.row()
row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR")
row = self.layout.row()
row.label(text="See system console for deletion progress.")
def _execute(self, context):
def _execute(self, context: bpy.types.Context):
start_time = time()
if self.is_batch:
@@ -728,10 +743,19 @@ class OverrideDelete(bpy.types.Operator):
self.process_arrays(context)
clear_active_object = True
for obj in context.selected_objects:
try:
obj.name
except:
objects_to_remove = context.selected_objects
for i, obj in enumerate(objects_to_remove, 1):
# Log time.
time_since_start = time() - start_time
is_valid_data_block = tool.Blender.is_valid_data_block(obj)
if time_since_start > 10:
obj_name = f" ({obj.name})" if is_valid_data_block else ""
print(
f"Removing object {i}/{len(objects_to_remove)}{obj_name}. "
f"Time since start: {time_since_start:.2f} seconds."
)
if not is_valid_data_block:
continue
element = tool.Ifc.get_entity(obj)
if element:
@@ -781,7 +805,7 @@ class OverrideDelete(bpy.types.Operator):
data["old_file"].redo()
tool.Ifc.set(data["new_file"])
def process_arrays(self, context):
def process_arrays(self, context: bpy.types.Context) -> None:
selected_objects = set(context.selected_objects)
array_parents = set()
for obj in context.selected_objects:
@@ -824,7 +848,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
# unintended IFC spatial modifications. To make life less confusing for
# the user, Delete means Delete. End of story.
# Deep magick from the dawn of time
if IfcStore.get_file():
if tool.Ifc.get():
return IfcStore.execute_ifc_operator(self, context)
# https://blender.stackexchange.com/questions/203729/python-get-selected-objects-in-outliner
objects_to_delete = set()
@@ -880,7 +904,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
if element := tool.Ifc.get_entity(obj):
if tool.Geometry.is_locked(element):
self.report({"ERROR"}, lock_error_message(obj.name))
if collection := obj.BIMObjectProperties.collection:
if collection := tool.Blender.get_object_bim_props(obj).collection:
collections_to_delete.discard(collection)
continue
tool.Geometry.delete_ifc_object(obj)
@@ -948,7 +972,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
@staticmethod
def execute_duplicate_operator(self, context, linked=False):
# Deep magick from the dawn of time
if IfcStore.get_file():
if tool.Ifc.get():
IfcStore.execute_ifc_operator(self, context)
if self.new_active_obj:
context.view_layer.objects.active = self.new_active_obj
@@ -1029,7 +1053,8 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Unlink from previous boolean element
# and keep object tracked for decorations.
if is_tracked_opening:
new_obj.data.BIMMeshProperties.ifc_boolean_id = 0
mprops = tool.Geometry.get_mesh_props(new_obj.data)
mprops.ifc_boolean_id = 0
tool.Root.add_tracked_opening(new_obj, tracked_opening_type)
if obj == context.active_object:
@@ -1043,7 +1068,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
continue
# clear object's collection so it will be able to have it's own
new_obj.BIMObjectProperties.collection = None
tool.Blender.get_object_bim_props(new_obj).collection = None
# copy the actual class
new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
@@ -1053,7 +1078,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
if new.is_a("IfcRelSpaceBoundary"):
surface = new.ConnectionGeometry.SurfaceOnRelatingElement
temp_data.name = f"0/{surface.id()}"
temp_data.BIMMeshProperties.ifc_definition_id = surface.id()
tool.Ifc.link(surface, temp_data)
else:
tool.Blender.remove_data_block(temp_data)
@@ -1089,12 +1114,14 @@ class OverrideDuplicateMove(bpy.types.Operator):
@staticmethod
def duplicate_item(obj: bpy.types.Object) -> None:
props = tool.Geometry.get_geometry_props()
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
item = tool.Geometry.get_active_representation(obj)
assert item
new_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item)
new_obj = obj.copy()
assert tool.Geometry.has_mesh_properties(obj.data)
temp_data = obj.data.copy()
new_obj.data = temp_data
new_obj.data.BIMMeshProperties.ifc_definition_id = new_item.id()
tool.Ifc.link(new_item, temp_data)
new_obj.name = obj.data.name = f"Item/{new_item.is_a()}/{new_item.id()}"
props.add_item_object(new_obj, new_item)
@@ -1670,7 +1697,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
def join_item(self) -> None:
props = tool.Geometry.get_geometry_props()
ifc_file = tool.Ifc.get()
item = tool.Ifc.get().by_id(self.target.data.BIMMeshProperties.ifc_definition_id)
item = tool.Geometry.get_active_representation(self.target)
assert item
if tool.Geometry.is_meshlike_item(item):
tool.Geometry.dissolve_triangulated_edges(self.target)
item_objs = [i.obj for i in props.item_objs if i.obj]
@@ -1693,7 +1721,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
for item_data in items_data:
props.add_item_object(item_data["obj"], ifc_file.by_id(item_data["ifc_definition_id"]))
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.reload_representation(props.representation_obj)
bpy.context.view_layer.update()
tool.Root.reload_item_decorator()
@@ -1701,7 +1729,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
ifc_file = tool.Ifc.get()
builder = ShapeBuilder(ifc_file)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
representation = ifc_file.by_id(self.target.data.BIMMeshProperties.ifc_definition_id)
representation = tool.Geometry.get_active_representation(self.target)
assert representation
representation_type = representation.RepresentationType
if representation_type in ("Tessellation", "Brep"):
for obj in bpy.context.selected_objects:
@@ -1740,7 +1769,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
continue
# Only objects of the same representation type can be joined
obj_rep = ifc_file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
obj_rep = tool.Geometry.get_active_representation(obj)
assert obj_rep
if obj_rep.RepresentationType != representation_type:
obj.select_set(False)
self.report(
@@ -1891,9 +1921,10 @@ class OverrideEscape(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
if context.scene.BIMGeometryProperties.mode == "ITEM":
props = tool.Geometry.get_geometry_props()
if props.mode == "ITEM":
tool.Geometry.disable_item_mode()
elif context.scene.BIMGeometryProperties.mode == "EDIT":
elif props.mode == "EDIT":
bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT", should_save=False)
tool.Geometry.disable_item_mode()
elif tool.Model.get_model_props().openings:
@@ -1913,7 +1944,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
def _invoke(self, context, event):
if not tool.Ifc.get():
@@ -1949,6 +1980,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
def handle_single_object(self, context: bpy.types.Context, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
props = tool.Geometry.get_geometry_props()
pprops = tool.Project.get_project_props()
if obj == props.representation_obj:
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
@@ -1956,7 +1988,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
self.report(
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
)
elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
elif obj in pprops.clipping_planes_objs:
self.report({"ERROR"}, "Clipping planes cannot be edited")
elif element:
if not obj.data:
@@ -2004,12 +2036,15 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
self.handle_single_object(context, obj)
def enable_editing_representation_item(self, context: bpy.types.Context, obj: bpy.types.Object) -> None:
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
element = tool.Ifc.get_entity(context.scene.BIMGeometryProperties.representation_obj)
item = tool.Geometry.get_active_representation(obj)
assert item
element = tool.Ifc.get_entity(tool.Geometry.get_geometry_props().representation_obj)
if tool.Geometry.is_meshlike_item(item):
tool.Geometry.dissolve_triangulated_edges(obj)
tool.Blender.select_and_activate_single_object(context, obj)
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
assert isinstance(mesh := obj.data, bpy.types.Mesh)
props = tool.Geometry.get_mesh_props(mesh)
props.mesh_checksum = tool.Geometry.get_mesh_checksum(mesh)
self.enable_edit_mode(context)
elif (
item.is_a("IfcSweptAreaSolid")
@@ -2026,21 +2061,21 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
f"Couldn't import profile, editing it directly is not yet supported. Failing profile: {profile}.",
)
return
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Ifc.link(item, obj.data)
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
elif item.is_a("IfcAnnotationFillArea"):
tool.Model.import_annotation_fill_area(item, obj=obj)
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Ifc.link(item, obj.data)
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
elif tool.Geometry.is_curvelike_item(item):
tool.Model.import_curve(item, obj=obj)
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Ifc.link(item, obj.data)
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
@@ -2051,10 +2086,11 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
def enable_edit_mode(self, context: bpy.types.Context) -> Union[None, set[str]]:
if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}:
return {"CANCELLED"}
context.scene.BIMGeometryProperties.is_changing_mode = True
if context.scene.BIMGeometryProperties.mode != "EDIT":
context.scene.BIMGeometryProperties.mode = "EDIT"
context.scene.BIMGeometryProperties.is_changing_mode = False
props = tool.Geometry.get_geometry_props()
props.is_changing_mode = True
if props.mode != "EDIT":
props.mode = "EDIT"
props.is_changing_mode = False
def has_aggregates(self, objs):
for obj in objs:
@@ -2089,7 +2125,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
should_save: bpy.props.BoolProperty(name="Should Save", default=True)
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
def _invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
if not tool.Ifc.get():
@@ -2101,14 +2137,15 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.toggle_edit_mode(context)
context.scene.BIMGeometryProperties.is_changing_mode = True
if context.scene.BIMGeometryProperties.representation_obj:
if context.scene.BIMGeometryProperties.mode != "ITEM":
context.scene.BIMGeometryProperties.mode = "ITEM"
props = tool.Geometry.get_geometry_props()
props.is_changing_mode = True
if props.representation_obj:
if props.mode != "ITEM":
props.mode = "ITEM"
else:
if context.scene.BIMGeometryProperties.mode != "OBJECT":
context.scene.BIMGeometryProperties.mode = "OBJECT"
context.scene.BIMGeometryProperties.is_changing_mode = False
if props.mode != "OBJECT":
props.mode = "OBJECT"
props.is_changing_mode = False
if context.active_object and self.should_save:
element = tool.Ifc.get_entity(context.active_object)
@@ -2136,7 +2173,8 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
continue
if tool.Profile.is_editing_profile():
profile_id = context.scene.BIMProfileProperties.active_profile_id
props = tool.Profile.get_profile_props()
profile_id = props.active_profile_id
if profile_id:
profile = tool.Ifc.get().by_id(profile_id)
if tool.Ifc.get_object(profile): # We are editing an arbitrary profile
@@ -2148,29 +2186,32 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
elif tool.Model.get_usage_type(element) == "PROFILE":
bpy.ops.bim.edit_extrusion_axis()
# if in the process of editing arbitrary profile
elif context.scene.BIMProfileProperties.active_arbitrary_profile_id:
elif props.active_arbitrary_profile_id:
bpy.ops.bim.edit_arbitrary_profile()
else:
bpy.ops.bim.edit_extrusion_profile()
return self.execute(context)
elif obj.data.BIMMeshProperties.ifc_definition_id:
if not tool.Geometry.has_geometric_data(obj):
elif representation := tool.Geometry.get_active_representation(obj):
if not tool.Geometry.is_geometric_data(obj.data):
self.is_valid = False
self.should_save = False
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
assert tool.Geometry.has_mesh_properties(obj.data)
mesh_props = tool.Geometry.get_mesh_props(obj.data)
if tool.Geometry.is_meshlike(
representation
) and obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
) and mesh_props.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
self.edited_objs.append(obj)
elif getattr(element, "HasOpenings", None):
self.unchanged_objs_with_openings.append(obj)
else:
tool.Ifc.finish_edit(obj)
elif element.is_a("IfcGridAxis"):
if not tool.Geometry.has_geometric_data(obj):
if not tool.Geometry.is_geometric_data(obj.data):
self.is_valid = False
self.should_save = False
if obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
assert tool.Geometry.has_mesh_properties(obj.data)
mesh_props = tool.Geometry.get_mesh_props(obj.data)
if mesh_props.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
self.edited_objs.append(obj)
else:
tool.Ifc.finish_edit(obj)
@@ -2195,9 +2236,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def edit_representation_item(self, obj: bpy.types.Object) -> None:
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
props = tool.Geometry.get_geometry_props()
item = tool.Geometry.get_active_representation(obj)
assert item
if tool.Geometry.is_meshlike_item(item):
if tool.Geometry.has_geometric_data(obj) and obj.data.polygons:
if tool.Geometry.is_geometric_data(obj.data) and obj.data.polygons:
tool.Geometry.edit_meshlike_item(obj)
else:
tool.Geometry.import_item(obj)
@@ -2214,15 +2257,17 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
return
old_profile = item.SweptArea
profile.ProfileName = old_profile.ProfileName
for inverse in tool.Ifc.get().get_inverse(old_profile):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
element = tool.Ifc.get_entity(bpy.context.scene.BIMGeometryProperties.representation_obj)
element = tool.Ifc.get_entity(props.representation_obj)
# Only certain classes should have a footprint
if element.is_a() in ("IfcSlab", "IfcRamp"):
footprint_context = ifcopenshell.util.representation.get_context(
@@ -2275,9 +2320,9 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
for inverse in tool.Ifc.get().get_inverse(item):
ifcopenshell.util.element.replace_attribute(inverse, item, profile)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
obj.data.BIMMeshProperties.ifc_definition_id = profile.id()
tool.Ifc.link(profile, obj.data)
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
elif tool.Geometry.is_curvelike_item(item):
@@ -2303,10 +2348,9 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.util.element.replace_attribute(inverse, item, new)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
obj.data.BIMMeshProperties.ifc_definition_id = new.id()
tool.Ifc.link(new, obj.data)
tool.Geometry.import_item(obj)
props = tool.Geometry.get_geometry_props()
for item in additional_curves:
representation = tool.Geometry.get_active_representation(props.representation_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
@@ -2315,7 +2359,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
name = f"Item/{item.is_a()}/{item.id()}"
mesh = bpy.data.meshes.new(name)
new_obj = bpy.data.objects.new(name, mesh)
new_obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Ifc.link(item, new_obj.data)
bpy.context.collection.objects.link(new_obj)
props.add_item_object(new_obj, item)
new_obj.matrix_world = obj.matrix_world
@@ -2328,10 +2372,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
def enable_edit_mode(self, context):
if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}:
return {"CANCELLED"}
context.scene.BIMGeometryProperties.is_changing_mode = True
if context.scene.BIMGeometryProperties.mode != "EDIT":
context.scene.BIMGeometryProperties.mode = "EDIT"
context.scene.BIMGeometryProperties.is_changing_mode = False
props = tool.Geometry.get_geometry_props()
props.is_changing_mode = True
if props.mode != "EDIT":
props.mode = "EDIT"
props.is_changing_mode = False
class FlipObject(bpy.types.Operator):
@@ -2368,13 +2413,33 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
item.tags += ","
item.tags += tag
if obj.data and hasattr(obj.data, "BIMMeshProperties"):
active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id
element = tool.Ifc.get().by_id(active_representation_id)
if tool.Geometry.has_mesh_properties((data := obj.data)):
representation = tool.Geometry.get_data_representation(data)
assert representation
# Shape aspects must be considered from the PartOfProductDefinitionShape level
element = tool.Ifc.get_entity(obj)
assert element
product_reps = []
if element.is_a("IfcProduct"):
product_reps = [element.Representation]
if element_type := ifcopenshell.util.element.get_type(element):
product_reps.extend(element_type.RepresentationMaps or [])
elif element.is_a("IfcTypeProduct"):
product_reps = element.RepresentationMaps
item_aspect = {}
for product_rep in product_reps:
for aspect in product_rep.HasShapeAspects:
for aspect_rep in aspect.ShapeRepresentations:
if aspect_rep.ContextOfItems != representation.ContextOfItems:
continue
for item in aspect_rep.Items:
item_aspect[item] = aspect
# IfcShapeRepresentation or IfcTopologyRepresentation.
if not element.is_a("IfcShapeModel"):
if not representation.is_a("IfcShapeModel"):
return
queue = list(element.Items)
queue = list(representation.Items)
while queue:
item = queue.pop()
if item.is_a("IfcMappedItem"):
@@ -2397,16 +2462,15 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
elif inverse.is_a("IfcPresentationLayerAssignment"):
new.layer = inverse.Name or "Unnamed"
new.layer_id = inverse.id()
elif inverse.is_a("IfcShapeRepresentation"):
if inverse.OfShapeAspect:
shape_aspect = inverse.OfShapeAspect[0]
new.shape_aspect = shape_aspect.Name
new.shape_aspect_id = shape_aspect.id()
elif inverse.is_a("IfcIndexedTextureMap"):
add_tag(new, "UV")
elif inverse.is_a("IfcIndexedColourMap"):
add_tag(new, "Colour")
if aspect := item_aspect.get(item, None):
new.shape_aspect = aspect.Name
new.shape_aspect_id = aspect.id()
# sort created items
sorted_items = sorted(props.items[:], key=lambda i: (not i.shape_aspect, i.shape_aspect))
for i, item in enumerate(sorted_items[:-1]): # last item is sorted automatically
@@ -2420,7 +2484,8 @@ class DisableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
obj.BIMGeometryProperties.is_editing = False
assert obj
tool.Geometry.get_object_geometry_props(obj).is_editing = False
class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
@@ -2431,9 +2496,12 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
if context.scene.BIMGeometryProperties.representation_obj:
if tool.Geometry.get_geometry_props().representation_obj:
return False # Artificial restriction for now to prevent removing when in item mode
if not (obj := tool.Geometry.get_active_or_representation_obj()) or len(obj.BIMGeometryProperties.items) <= 1:
if (
not (obj := tool.Geometry.get_active_or_representation_obj())
or len(tool.Geometry.get_object_geometry_props(obj).items) <= 1
):
cls.poll_message_set(
"Active object need to have more than 1 representation items to keep representation valid"
)
@@ -2468,13 +2536,17 @@ class SelectRepresentationItem(bpy.types.Operator):
def execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
item = tool.Ifc.get().by_id(obj.BIMGeometryProperties.active_item.ifc_definition_id)
obj_props = tool.Geometry.get_object_geometry_props(obj)
assert obj_props.active_item
item = tool.Ifc.get().by_id(obj_props.active_item.ifc_definition_id)
item_ids = self.get_nested_item_ids(item)
props = tool.Geometry.get_geometry_props()
for item_obj in props.item_objs:
if item_obj.obj.data.BIMMeshProperties.ifc_definition_id in item_ids:
tool.Blender.select_object(item_obj.obj)
obj_ = item_obj.obj
props = tool.Geometry.get_mesh_props(obj_.data)
if props.ifc_definition_id in item_ids:
tool.Blender.select_object(obj_)
return {"FINISHED"}
def get_nested_item_ids(self, item):
@@ -2492,7 +2564,7 @@ class SelectRepresentationItem(bpy.types.Operator):
def poll_editing_representation_item_style(cls, context):
if not (obj := tool.Geometry.get_active_or_representation_obj()):
return False
props = obj.BIMGeometryProperties
props = tool.Geometry.get_object_geometry_props(obj)
if not props.is_editing:
return False
if not (item := props.active_item):
@@ -2527,7 +2599,8 @@ class EnableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
props = obj.BIMGeometryProperties
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_style = True
ifc_file = tool.Ifc.get()
@@ -2546,7 +2619,8 @@ class EditRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
props = obj.BIMGeometryProperties
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_style = False
ifc_file = tool.Ifc.get()
@@ -2568,7 +2642,7 @@ class DisableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operato
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
props = obj.BIMGeometryProperties
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_style = False
@@ -2584,7 +2658,8 @@ class UnassignRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
active_obj = tool.Geometry.get_active_or_representation_obj()
active_props = active_obj.BIMGeometryProperties
assert active_obj
active_props = tool.Geometry.get_object_geometry_props(active_obj)
active_props.is_editing_item_style = False
# Get active representation item
@@ -2651,7 +2726,8 @@ class EnableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Op
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
props = obj.BIMGeometryProperties
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_shape_aspect = True
# set dropdown to currently active shape aspect
@@ -2667,11 +2743,13 @@ class EditRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
element = tool.Ifc.get_entity(obj)
props = obj.BIMGeometryProperties
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_shape_aspect = False
ifc_file = tool.Ifc.get()
assert props.active_item
representation_item_id = props.active_item.ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
@@ -2724,7 +2802,8 @@ class DisableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.O
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
props = obj.BIMGeometryProperties
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_shape_aspect = False
@@ -2735,10 +2814,12 @@ class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Opera
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
element = tool.Ifc.get_entity(obj)
props = obj.BIMGeometryProperties
props = tool.Geometry.get_object_geometry_props(obj)
ifc_file = tool.Ifc.get()
assert props.active_item
representation_item_id = props.active_item.ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
shape_aspect = ifc_file.by_id(props.active_item.shape_aspect_id)
@@ -2807,6 +2888,15 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
else:
assert False, "Unexpected mesh type."
if not item_ids:
# It is possible that the user has created a shape that
# IfcOpenShell cannot render (i.e. boolean clipped everything), but
# we still want to edit items. I'm not sure the best way to handle
# this, but for now perhaps we can detect when there are no
# item_ids at all.
representation = tool.Ifc.get_entity(data)
item_ids = [i["item"].id() for i in ifcopenshell.util.representation.resolve_items(representation)]
queue = list(set(item_ids))
processed_ids = set()
boolean_ids = set()
@@ -2821,7 +2911,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
boolean_ids.add(item.SecondOperand.id())
continue
item_mesh = bpy.data.meshes.new(f"Item/{item.is_a()}/{item_id}")
item_mesh.BIMMeshProperties.ifc_definition_id = item_id
tool.Ifc.link(item, item_mesh)
item_obj = bpy.data.objects.new(f"Item/{item.is_a()}/{item_id}", item_mesh)
item_obj.matrix_world = obj.matrix_world
@@ -2851,7 +2941,7 @@ class UpdateItemAttributes(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
tool.Geometry.sync_item_positions()
tool.Geometry.update_item_attributes(obj)
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.reload_representation(tool.Geometry.get_geometry_props().representation_obj)
tool.Geometry.import_item(obj)
tool.Root.reload_item_decorator()
@@ -2868,6 +2958,10 @@ class NameProfile(bpy.types.Operator, tool.Ifc.Operator):
options={"SKIP_SAVE"},
)
if TYPE_CHECKING:
extrusion_item_obj: str
profile_name: str
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -2882,7 +2976,7 @@ class NameProfile(bpy.types.Operator, tool.Ifc.Operator):
ifc_file = tool.Ifc.get()
extrusion_item_obj = bpy.data.objects[self.extrusion_item_obj]
mesh_props = extrusion_item_obj.data.BIMMeshProperties
mesh_props = tool.Geometry.get_mesh_props(extrusion_item_obj.data)
extrusion = ifc_file.by_id(mesh_props.ifc_definition_id)
assert extrusion.is_a("IfcSweptAreaSolid")
profile = extrusion.SweptArea
@@ -2952,9 +3046,9 @@ class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator):
props.add_item_object(obj, item)
representation.Items = list(representation.Items) + [item]
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.reload_representation(props.representation_obj)
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id()
tool.Root.reload_item_decorator()
@@ -3000,10 +3094,10 @@ class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator):
props.add_item_object(obj, item)
representation.Items = list(representation.Items) + [item]
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.reload_representation(props.representation_obj)
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id()
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
tool.Root.reload_item_decorator()
@@ -3071,10 +3165,10 @@ class AddCurvelikeItem(bpy.types.Operator, tool.Ifc.Operator):
props.add_item_object(obj, item)
representation.Items = list(representation.Items) + [item]
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.reload_representation(props.representation_obj)
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id()
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
@@ -3120,10 +3214,10 @@ class AddHalfSpaceSolidItem(bpy.types.Operator, tool.Ifc.Operator):
representation = ifcopenshell.util.representation.resolve_representation(representation)
representation.Items = list(representation.Items) + [item]
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
tool.Geometry.reload_representation(props.representation_obj)
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id()
tool.Geometry.import_item(obj)
# TODO refactor to core and not rely on selection
@@ -3149,7 +3243,7 @@ class OverrideMove(bpy.types.Operator):
def execute(self, context):
# Deep magick from the dawn of time
if IfcStore.get_file():
if tool.Ifc.get():
IfcStore.execute_ifc_operator(self, context)
if self.new_active_obj:
context.view_layer.objects.active = self.new_active_obj
@@ -32,7 +32,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import Optional, TYPE_CHECKING, Union
from typing import Optional, TYPE_CHECKING, Union, Literal
def get_contexts(self, context):
@@ -121,7 +121,7 @@ def get_layers_no_active(self, context):
return LayersData.data["layers_enum_no_active"]
def update_shape_aspect(self, context):
def update_shape_aspect(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> None:
shape_aspect_id = self.representation_item_shape_aspect
attrs = self.shape_aspect_attrs
@@ -250,9 +250,8 @@ class BIMObjectGeometryProperties(PropertyGroup):
representation_item_layer: EnumProperty(items=get_layers, name="Representation Item's Layer")
@property
def active_item(self):
if 0 <= self.active_item_index < len(self.items):
return self.items[self.active_item_index]
def active_item(self) -> Union[RepresentationItem, None]:
return tool.Blender.get_active_uilist_element(self.items, self.active_item_index)
if TYPE_CHECKING:
contexts: str
@@ -270,6 +269,9 @@ class BIMObjectGeometryProperties(PropertyGroup):
representation_item_layer: str
GeometryMode = Literal["OBJECT", "ITEM", "EDIT"]
class BIMGeometryProperties(PropertyGroup):
# Revit workaround
should_use_presentation_style_assignment: BoolProperty(name="Force Presentation Style Assignment", default=False)
@@ -308,7 +310,7 @@ class BIMGeometryProperties(PropertyGroup):
should_force_faceted_brep: bool
should_force_triangulation: bool
is_changing_mode: bool
mode: str
mode: GeometryMode
representation_obj: Union[bpy.types.Object, None]
item_objs: bpy.types.bpy_prop_collection_idprop[RepresentationItemObject]
representation_from_object: Union[bpy.types.Object, None]
+31 -25
View File
@@ -20,7 +20,6 @@ import bpy
import bonsai.bim
import bonsai.tool as tool
from bpy.types import Panel, Menu, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.geometry.data import (
RepresentationsData,
@@ -53,9 +52,10 @@ def mode_menu(self, context):
UIData.load()
ifc_icon = f"{UIData.data['menu_icon_color_mode']}_ifc"
row = self.layout.row(align=True)
if context.scene.BIMGeometryProperties.mode == "EDIT":
props = tool.Geometry.get_geometry_props()
if props.mode == "EDIT":
row.operator("bim.override_mode_set_object", icon="CANCEL", text="Discard Changes").should_save = False
row.prop(context.scene.BIMGeometryProperties, "mode", text="", icon_value=bonsai.bim.icons[ifc_icon].icon_id)
row.prop(props, "mode", text="", icon_value=bonsai.bim.icons[ifc_icon].icon_id)
def object_menu(self, context):
@@ -334,18 +334,17 @@ class BIM_PT_connections(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(tool.Blender.get_ifc_definition_id(obj)):
return False
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not ConnectionsData.is_loaded:
ConnectionsData.load()
layout = self.layout
props = context.active_object.BIMObjectProperties
if not ConnectionsData.data["connections"] and not ConnectionsData.data["is_connection_realization"]:
layout.label(text="No connections found")
@@ -411,15 +410,18 @@ class BIM_PT_mesh(Panel):
@classmethod
def poll(cls, context):
return (
context.active_object is not None
and context.active_object.type == "MESH"
and hasattr(context.active_object.data, "BIMMeshProperties")
and context.active_object.data.BIMMeshProperties.ifc_definition_id
(obj := context.active_object) is not None
and (mesh := obj.data)
and isinstance(mesh, bpy.types.Mesh)
and tool.Geometry.get_mesh_props(mesh).ifc_definition_id
)
def draw(self, context):
if not context.active_object.data:
return
obj = context.active_object
assert obj
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
row = self.layout.row()
row.label(text="Advanced Users Only", icon="ERROR")
@@ -427,7 +429,7 @@ class BIM_PT_mesh(Panel):
row = layout.row()
text = "Manually Save Representation"
if tool.Ifc.is_edited(context.active_object):
if tool.Ifc.is_edited(obj):
text += "*"
row.operator("bim.update_representation", text=text)
@@ -454,8 +456,8 @@ class BIM_PT_mesh(Panel):
op = row.operator("bim.update_representation", text="Convert To Arbitrary Extrusion With Voids")
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids"
if context.active_object and context.active_object.data:
mprops = context.active_object.data.BIMMeshProperties
if True:
mprops = tool.Geometry.get_mesh_props(mesh)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
@@ -477,12 +479,16 @@ class BIM_PT_placement(Panel):
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.BIMObjectProperties.ifc_definition_id
return (obj := context.active_object) and tool.Blender.get_ifc_definition_id(obj)
def draw(self, context):
if not PlacementData.is_loaded:
PlacementData.load()
obj = context.active_object
assert obj
props = tool.Blender.get_object_bim_props(obj)
if not PlacementData.data["has_placement"]:
row = self.layout.row()
row.label(text="No Object Placement Found")
@@ -493,12 +499,12 @@ class BIM_PT_placement(Panel):
row = self.layout.row()
row.prop(context.active_object, "rotation_euler", text="Rotation")
if context.active_object.BIMObjectProperties.blender_offset_type != "NONE":
if props.blender_offset_type != "NONE":
row = self.layout.row(align=True)
row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS")
row.label(text=context.active_object.BIMObjectProperties.blender_offset_type)
row.label(text=props.blender_offset_type)
if context.active_object.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE":
if props.blender_offset_type != "NOT_APPLICABLE":
row = self.layout.row(align=True)
row.label(text=PlacementData.data["original_x"], icon="EMPTY_AXIS")
row.label(text=PlacementData.data["original_y"])
@@ -571,10 +577,10 @@ class BIM_PT_workarounds(Panel):
@classmethod
def poll(cls, context):
return (
context.active_object is not None
and context.active_object.type == "MESH"
and hasattr(context.active_object.data, "BIMMeshProperties")
and context.active_object.data.BIMMeshProperties.ifc_definition_id
(obj := context.active_object) is not None
and (mesh := obj.data)
and isinstance(mesh, bpy.types.Mesh)
and tool.Geometry.get_mesh_props(mesh).ifc_definition_id
)
def draw(self, context):
@@ -588,7 +594,7 @@ class BIM_PT_workarounds(Panel):
class BIM_UL_representation_items(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
def draw_item(self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_propname):
if item:
icon = "MATERIAL" if item.surface_style else "MESH_UVSPHERE"
row = layout.row(align=True)
@@ -167,7 +167,7 @@ class GeoreferenceData:
result["rotation"] = str(round(ifcopenshell.util.geolocation.yaxis2angle(*wcs[:, 1][:2]), 3))
result["x"], result["y"], result["z"] = wcs[:, 3][:3]
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
blender_xyz = ifcopenshell.util.geolocation.enh2xyz(
result["x"],
@@ -193,7 +193,7 @@ class GeoreferenceData:
@classmethod
def local_origin(cls):
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if not props.has_blender_offset:
return
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -21,6 +21,7 @@ import blf
import gpu
import bmesh
import ifcopenshell
import ifcopenshell.util.geolocation
import bonsai.tool as tool
from math import radians
from bpy.types import SpaceView3D
@@ -53,7 +54,8 @@ class GeoreferenceDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
self.scale = bpy.context.scene.BIMGeoreferenceProperties.visualization_scale
props = tool.Georeference.get_georeference_props()
self.scale = props.visualization_scale
content_pos = [v * self.scale for v in content_pos]
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
@@ -64,7 +66,7 @@ class GeoreferenceDecorator:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if not props.model_origin: # If this is empty, no georeferencing data has been loaded.
return
@@ -177,7 +179,7 @@ class GeoreferenceDecorator:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if not props.model_origin: # If this is empty, no georeferencing data has been loaded.
return
@@ -350,7 +352,7 @@ class GeoreferenceDecorator:
self.gn_angle = float(GeoreferenceData.data["map_derived_angle"] or 0)
self.tn_angle = float(GeoreferenceData.data["true_derived_angle"] or 0)
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
blender_angle = ifcopenshell.util.geolocation.xaxis2angle(
float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate)
@@ -20,6 +20,7 @@ import bpy
import bonsai.tool as tool
import bonsai.core.georeference as core
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
@@ -87,22 +88,18 @@ class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator):
core.get_cursor_location(tool.Georeference)
class ImportPlot(bpy.types.Operator, tool.Ifc.Operator):
class ImportPlot(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_plot"
bl_label = "Import Plot"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Import plot"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
bl_description = "Import plot from a csv file."
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
filename_ext = ".csv"
def execute(self, context):
def _execute(self, context):
core.import_plot(tool.Georeference, filepath=self.filepath)
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class EnableEditingWCS(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_wcs"
@@ -33,15 +33,18 @@ from bpy.props import (
)
from bonsai.bim.module.georeference.data import GeoreferenceData
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
from typing import TYPE_CHECKING
def get_coordinate_operation_class(self, context):
def get_coordinate_operation_class(
self: "BIMGeoreferenceProperties", context: bpy.types.Context
) -> list[tuple[str, str, str]]:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
return GeoreferenceData.data["coordinate_operation_class"]
def update_true_north_angle(self, context):
def update_true_north_angle(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -54,7 +57,7 @@ def update_true_north_angle(self, context):
self.is_changing_angle = False
def update_true_north_vector(self, context):
def update_true_north_vector(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -67,7 +70,7 @@ def update_true_north_vector(self, context):
self.is_changing_angle = False
def update_grid_north_angle(self, context):
def update_grid_north_angle(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -81,7 +84,7 @@ def update_grid_north_angle(self, context):
self.is_changing_angle = False
def update_grid_north_vector(self, context):
def update_grid_north_vector(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -95,15 +98,15 @@ def update_grid_north_vector(self, context):
self.is_changing_angle = False
def update_should_visualise(self, context):
def update_should_visualise(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.should_visualise:
GeoreferenceDecorator.install(bpy.context)
else:
GeoreferenceDecorator.uninstall()
def update_blender_coordinates(self, context):
props = bpy.context.scene.BIMGeoreferenceProperties
def update_blender_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
props = self
if props.is_updating_coordinates:
return
props.is_updating_coordinates = True
@@ -123,8 +126,8 @@ def update_blender_coordinates(self, context):
props.is_updating_coordinates = False
def update_local_coordinates(self, context):
props = bpy.context.scene.BIMGeoreferenceProperties
def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
props = self
if props.is_updating_coordinates:
return
props.is_updating_coordinates = True
@@ -147,8 +150,8 @@ def update_local_coordinates(self, context):
props.is_updating_coordinates = False
def update_map_coordinates(self, context):
props = bpy.context.scene.BIMGeoreferenceProperties
def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
props = self
if props.is_updating_coordinates:
return
props.is_updating_coordinates = True
@@ -248,3 +251,45 @@ class BIMGeoreferenceProperties(PropertyGroup):
wcs_y: StringProperty(name="WCS Y", default="0")
wcs_z: StringProperty(name="WCS Z", default="0")
wcs_rotation: StringProperty(name="WCS Rotation", default="0")
if TYPE_CHECKING:
coordinate_operation_class: str
is_changing_angle: bool
is_editing: bool
is_editing_wcs: bool
is_editing_true_north: bool
coordinate_operation: bpy.types.bpy_prop_collection_idprop[Attribute]
projected_crs: bpy.types.bpy_prop_collection_idprop[Attribute]
is_updating_coordinates: bool
blender_coordinates: str
local_coordinates: str
map_coordinates: str
should_visualise: bool
visualization_scale: float
grid_north_angle: str
x_axis_abscissa: str
x_axis_ordinate: str
x_axis_is_null: bool
host_model_origin: str
host_model_origin_si: str
host_model_project_north: str
model_origin: str
model_origin_si: str
model_project_north: str
has_blender_offset: bool
blender_offset_x: str
blender_offset_y: str
blender_offset_z: str
blender_x_axis_abscissa: str
blender_x_axis_ordinate: str
true_north_angle: str
true_north_abscissa: str
true_north_ordinate: str
wcs_x: str
wcs_y: str
wcs_z: str
wcs_rotation: str
@@ -32,7 +32,7 @@ class BIM_PT_gis(Panel):
bl_parent_id = "BIM_PT_tab_geometry"
def draw_header(self, context):
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
row = self.layout.row(align=True)
icon = "HIDE_OFF" if props.should_visualise else "HIDE_ON"
row.label(text="") # empty text occupies the left of the row
@@ -43,7 +43,7 @@ class BIM_PT_gis(Panel):
def draw(self, context):
self.layout.use_property_split = True
self.layout.use_property_decorate = False
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
@@ -54,7 +54,7 @@ class BIM_PT_gis(Panel):
self.draw_ui(context)
def draw_editable_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
row = self.layout.row(align=True)
row.label(text="Projected CRS", icon="WORLD")
row.operator("bim.edit_georeferencing", icon="CHECKMARK", text="")
@@ -87,7 +87,7 @@ class BIM_PT_gis(Panel):
draw_attribute(attribute, self.layout.row())
def draw_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if tool.Ifc.get_schema() == "IFC2X3":
row = self.layout.row()
@@ -149,7 +149,7 @@ class BIM_PT_gis_true_north(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
self.props = context.scene.BIMGeoreferenceProperties
self.props = tool.Georeference.get_georeference_props()
if self.props.is_editing_true_north:
self.draw_editable_ui(context)
@@ -200,7 +200,7 @@ class BIM_PT_gis_blender(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
row = self.layout.row()
@@ -233,7 +233,7 @@ class BIM_PT_gis_wcs(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.is_editing_wcs:
self.draw_editable_ui(context)
@@ -263,7 +263,7 @@ class BIM_PT_gis_wcs(Panel):
row.operator("bim.enable_editing_wcs", icon="GREASEPENCIL", text="")
def draw_editable_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
row = self.layout.row(align=True)
row.label(text="World Coordinate System", icon="EMPTY_ARROWS")
@@ -293,7 +293,7 @@ class BIM_PT_gis_calculator(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
row = self.layout.row(align=True)
+21 -14
View File
@@ -22,7 +22,6 @@ import ifcopenshell.api.group
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
import json
@@ -36,9 +35,13 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
self.expanded_groups = json.loads(context.scene.ExpandedGroups.json_string)
self.props.groups.clear()
for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False):
if not group.HasAssignments:
self.load_group(group)
groups = [
group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.HasAssignments
]
sorted_groups = sorted(groups, key=lambda group: group.Name or "Unnamed")
for group in sorted_groups:
self.load_group(group)
self.props.is_editing = True
bpy.ops.bim.disable_editing_group()
@@ -52,14 +55,19 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
new.has_children = False
new.is_expanded = group.id() in self.expanded_groups
for rel in group.IsGroupedBy or []:
for related_object in rel.RelatedObjects:
if not related_object.is_a("IfcGroup"):
continue
new.has_children = True
if not new.is_expanded:
return
self.load_group(related_object, tree_depth=tree_depth + 1)
related_groups = [
related_object
for rel in group.IsGroupedBy or []
for related_object in rel.RelatedObjects
if related_object.is_a("IfcGroup")
]
sorted_related_groups = sorted(related_groups, key=lambda group: group.Name or "Unnamed")
for related_group in sorted_related_groups:
new.has_children = True
if not new.is_expanded:
return
self.load_group(related_group, tree_depth=tree_depth + 1)
class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator):
@@ -129,8 +137,7 @@ class RemoveGroup(bpy.types.Operator, tool.Ifc.Operator):
group: bpy.props.IntProperty()
def _execute(self, context):
props = context.scene.BIMGroupProperties
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run("group.remove_group", self.file, **{"group": self.file.by_id(self.group)})
bpy.ops.bim.load_groups()
return {"FINISHED"}
+5 -4
View File
@@ -17,8 +17,8 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.group.data import GroupsData, ObjectGroupsData
@@ -34,7 +34,7 @@ class BIM_PT_groups(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not GroupsData.is_loaded:
@@ -91,9 +91,10 @@ class BIM_PT_object_groups(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
return IfcStore.get_file() and context.active_object.BIMObjectProperties.ifc_definition_id
props = tool.Blender.get_object_bim_props(obj)
return tool.Ifc.get() and props.ifc_definition_id
def draw(self, context):
if not ObjectGroupsData.is_loaded:
@@ -374,16 +374,17 @@ class ObjectLog(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
cls.poll_message_set("No Active Object")
elif not context.active_object.BIMObjectProperties.ifc_definition_id:
elif not tool.Blender.get_ifc_definition_id(obj):
cls.poll_message_set("Active Object doesn't have an IFC definition")
else:
return True
def execute(self, context):
step_id = context.active_object.BIMObjectProperties.ifc_definition_id
obj = context.active_object
assert obj
step_id = tool.Blender.get_ifc_definition_id(obj)
core.entity_log(tool.IfcGit, tool.Ifc, step_id, self)
return {"FINISHED"}
+8 -14
View File
@@ -24,7 +24,6 @@ import ifcopenshell.util.element
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
class LoadLayers(bpy.types.Operator):
@@ -33,7 +32,7 @@ class LoadLayers(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
props = context.scene.BIMLayerProperties
props.layers.clear()
for layer in tool.Ifc.get().by_type("IfcPresentationLayerAssignment"):
@@ -126,7 +125,7 @@ class RemovePresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMLayerProperties
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run("layer.remove_layer", self.file, **{"layer": self.file.by_id(self.layer)})
bpy.ops.bim.load_layers()
return {"FINISHED"}
@@ -142,12 +141,12 @@ class AssignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
item = bpy.data.meshes.get(self.item) if self.item else context.active_object.data
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"layer.assign_layer",
self.file,
**{
"items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
"items": [self.file.by_id(tool.Geometry.get_mesh_props(item).ifc_definition_id)],
"layer": self.file.by_id(self.layer),
},
)
@@ -164,15 +163,10 @@ class UnassignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
item = bpy.data.meshes.get(self.item) if self.item else context.active_object.data
self.file = IfcStore.get_file()
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),
},
)
ifc_file = tool.Ifc.get()
representation = tool.Geometry.get_data_representation(item)
assert representation
ifcopenshell.api.layer.unassign_layer(ifc_file, items=[representation], layer=ifc_file.by_id(self.layer))
return {"FINISHED"}

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