From 542ae6b44aaa4295bdbc0f653a398489c9070dde Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 20 Sep 2024 18:08:16 +1000 Subject: [PATCH] Basic support for editing swept profiles and positions --- src/bonsai/bonsai/bim/handler.py | 1 + .../bonsai/bim/module/geometry/operator.py | 129 ++++++++++++++++-- src/bonsai/bonsai/bim/module/geometry/prop.py | 1 + src/bonsai/bonsai/tool/geometry.py | 30 ++++ src/bonsai/bonsai/tool/ifc.py | 4 +- src/bonsai/bonsai/tool/model.py | 2 +- 6 files changed, 153 insertions(+), 14 deletions(-) diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index ed50d32921..feeff780dc 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -88,6 +88,7 @@ def name_callback(obj, data): def active_object_callback(): refresh_ui_data() update_bim_tool_props() + tool.Geometry.sync_item_positions() def update_bim_tool_props(): diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 149ffb51ee..c3253c5511 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -41,6 +41,7 @@ from time import time from bonsai.bim.ifc import IfcStore from ifcopenshell.util.shape_builder import ShapeBuilder from typing import Any +from bonsai.bim.module.model.decorator import ProfileDecorator class EditObjectPlacement(bpy.types.Operator, tool.Ifc.Operator): @@ -1706,16 +1707,26 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): if tool.Geometry.is_meshlike_item(item): tool.Blender.select_and_activate_single_object(context, obj) obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data) - 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 - return {"FINISHED"} + self.enable_edit_mode(context) + elif item.is_a("IfcSweptAreaSolid"): + tool.Geometry.sync_item_positions() + tool.Model.import_profile(item.SweptArea, obj=obj) + obj.data.BIMMeshProperties.ifc_definition_id = item.id() + self.enable_edit_mode(context) + ProfileDecorator.install(context) + if not bpy.app.background: + tool.Blender.set_viewport_tool("bim.cad_tool") else: self.report({"INFO"}, f"Editing {item.is_a()} geometry is not supported") + 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 + def invoke(self, context, event): return IfcStore.execute_ifc_operator(self, context, is_invoke=True) @@ -1753,7 +1764,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): if tool.Geometry.is_meshlike_item(item): if tool.Geometry.has_geometric_data(obj) and obj.data.polygons: if obj.data.BIMMeshProperties.mesh_checksum == tool.Geometry.get_mesh_checksum(obj.data): - return {"FINISHED"} + return builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) verts = [v.co / unit_scale for v in obj.data.vertices] @@ -1767,11 +1778,93 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item) obj.data.BIMMeshProperties.ifc_definition_id = new_item.id() tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) - return {"FINISHED"} + return else: self.reload_item(obj) - return {"FINISHED"} - return {"FINISHED"} + return + elif item.is_a("IfcSweptAreaSolid"): + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + ProfileDecorator.uninstall() + + profile = tool.Model.export_profile(obj) + print("got profile", profile) + + if not profile: + + def msg(self, context): + self.layout.label(text="INVALID PROFILE") + + bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") + ProfileDecorator.install(bpy.context) + self.enable_edit_mode(bpy.context) + return + + old_profile = item.SweptArea + for inverse in tool.Ifc.get().get_inverse(old_profile): + ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile) + ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile) + + tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + + tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(tool.Ifc.get()) + tool.Loader.settings.context_settings = tool.Loader.create_settings() + tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True) + geometry = tool.Loader.create_generic_shape(item) + obj.data.clear_geometry() + tool.Loader.convert_geometry_to_mesh(geometry, obj.data) + if item.Position: + position = Matrix(ifcopenshell.util.placement.get_axis2placement(item.Position).tolist()) + position.translation *= unit_scale + position_i = position.inverted() + for vert in obj.data.vertices: + vert.co = position_i @ vert.co + + element = tool.Ifc.get_entity(bpy.context.scene.BIMGeometryProperties.representation_obj) + # Only certain classes should have a footprint + if element.is_a() in ("IfcSlab", "IfcRamp"): + footprint_context = ifcopenshell.util.representation.get_context( + tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW" + ) + if footprint_context: + if profile.is_a("IfcCompositeProfileDef"): + profiles = profile.Profiles + else: + profiles = [profile] + curves = [] + for profile in profiles: + curves.append(profile.OuterCurve) + if profile.is_a("IfcArbitraryProfileDefWithVoids"): + curves.extend(profile.InnerCurves) + new_footprint = ifcopenshell.api.run( + "geometry.add_footprint_representation", + tool.Ifc.get(), + context=footprint_context, + curves=curves, + ) + old_footprint = ifcopenshell.util.representation.get_representation( + element, "Plan", "FootPrint", "SKETCH_VIEW" + ) + if old_footprint: + for inverse in tool.Ifc.get().get_inverse(old_footprint): + ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint) + bonsai.core.geometry.remove_representation( + tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint + ) + else: + ifcopenshell.api.run( + "geometry.assign_representation", + tool.Ifc.get(), + product=element, + representation=new_footprint, + ) + + 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 def reload_item(self, obj): tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(tool.Ifc.get()) @@ -1809,7 +1902,8 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): if element and element.is_a("IfcRelSpaceBoundary"): return bpy.ops.bim.edit_boundary_geometry() elif tool.Geometry.is_representation_item(context.active_object): - return self.edit_representation_item(context.active_object) + self.edit_representation_item(context.active_object) + return {"FINISHED"} objs = context.selected_objects or [context.active_object] @@ -2307,3 +2401,14 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): new = props.item_objs.add() new.obj = item_obj + + if item.is_a("IfcSweptAreaSolid"): + tool.Geometry.record_object_position(item_obj) + if item.Position: + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + position = Matrix(ifcopenshell.util.placement.get_axis2placement(item.Position).tolist()) + position.translation *= unit_scale + position_i = position.inverted() + item_obj.matrix_world = item_obj.matrix_world @ position + for vert in item_obj.data.vertices: + vert.co = position_i @ vert.co diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py index 0f2168579c..01b55f68e8 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -45,6 +45,7 @@ def update_mode(self, context): if self.mode == "OBJECT": if self.representation_obj: tool.Geometry.unlock_object(self.representation_obj) + tool.Geometry.sync_item_positions() self.representation_obj = None bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT") elif self.mode == "ITEM": diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index a3908f2cd9..3cd1db1cd9 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1408,3 +1408,33 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def get_elements_by_representation(cls, representation): return ifcopenshell.util.element.get_elements_by_representation(tool.Ifc.get(), representation) + + @classmethod + def sync_item_positions(cls): + props = bpy.context.scene.BIMGeometryProperties + if not props.representation_obj: + return + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + rep_obj = props.representation_obj + rep_matrix_i = rep_obj.matrix_world.inverted() + builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) + has_changed = False + for item_obj in props.item_objs: + obj = item_obj.obj + if not tool.Ifc.is_moved(obj): + continue + has_changed = True + item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + old_position = item.Position + if np.allclose(np.array(obj.matrix_world), np.array(rep_obj.matrix_world), atol=1e-4): + if old_position: + item.Position = None + ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_position) + continue + rel_matrix = rep_matrix_i @ obj.matrix_world + rel_matrix.translation /= unit_scale + item.Position = builder.create_axis2_placement_3d_from_matrix(np.array(rel_matrix)) + if old_position: + ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_position) + if has_changed: + cls.reload_representation(rep_obj) diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index e31b0bfc1d..10fb71cf3b 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -72,7 +72,9 @@ class Ifc(bonsai.core.tool.Ifc): @classmethod def is_moved(cls, obj: bpy.types.Object) -> bool: element = cls.get_entity(obj) - if not element or element.is_a("IfcTypeProduct") or element.is_a("IfcProject"): + if not element and not tool.Geometry.is_representation_item(obj): + return False + if element and (element.is_a("IfcTypeProduct") or element.is_a("IfcProject")): return False if not obj.BIMObjectProperties.location_checksum: return True # Let's be conservative diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 465994d74d..75dc27ce77 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -132,7 +132,7 @@ class Model(bonsai.core.tool.Model): helper = Helper(tool.Ifc.get()) result = helper.auto_detect_profiles(obj, obj.data, position) - if result["profile_def"]: + if isinstance(result, dict) and result["profile_def"]: return tool.Ifc.get().add(result["profile_def"]) @classmethod