From ce010f1e567385c3e1b5c5e047971300a1bbc133 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 21 Sep 2024 16:34:41 +1000 Subject: [PATCH] You can now add new mesh representation items --- .../bonsai/bim/module/geometry/__init__.py | 1 + .../bonsai/bim/module/geometry/operator.py | 232 +++++++++++------- .../bonsai/bim/module/model/__init__.py | 1 + src/bonsai/bonsai/bim/module/model/data.py | 25 ++ .../bonsai/bim/module/model/workspace.py | 25 +- src/bonsai/bonsai/tool/geometry.py | 25 +- 6 files changed, 210 insertions(+), 99 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index d2d82e450e..76a56373e2 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -20,6 +20,7 @@ import bpy from . import ui, prop, operator classes = ( + operator.AddMeshlikeItem, operator.AddRepresentation, operator.CopyRepresentation, operator.DisableEditingRepresentationItemShapeAspect, diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index b6fa28afdd..666663eed2 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -1742,6 +1742,93 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} 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) + + def _invoke(self, context, event): + if not context.active_object: + return {"FINISHED"} + self.is_valid = True + + if context.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + + if not tool.Ifc.get(): + return {"FINISHED"} + + context.scene.BIMGeometryProperties.is_changing_mode = True + if context.scene.BIMGeometryProperties.representation_obj: + if context.scene.BIMGeometryProperties.mode != "ITEM": + context.scene.BIMGeometryProperties.mode = "ITEM" + else: + if context.scene.BIMGeometryProperties.mode != "OBJECT": + context.scene.BIMGeometryProperties.mode = "OBJECT" + context.scene.BIMGeometryProperties.is_changing_mode = False + + if context.active_object: + element = tool.Ifc.get_entity(context.active_object) + if element and element.is_a("IfcRelSpaceBoundary"): + return bpy.ops.bim.edit_boundary_geometry() + elif tool.Geometry.is_representation_item(context.active_object): + self.edit_representation_item(context.active_object) + return {"FINISHED"} + + objs = context.selected_objects or [context.active_object] + print("objs are", objs) + + self.edited_objs = [] + self.unchanged_objs_with_openings = [] + + for obj in objs: + if not obj: + continue + + element = tool.Ifc.get_entity(obj) + if not element: + continue + + if tool.Profile.is_editing_profile(): + profile_id = context.scene.BIMProfileProperties.active_profile_id + if profile_id: + profile = tool.Ifc.get().by_id(profile_id) + if tool.Ifc.get_object(profile): # We are editing an arbitrary profile + bpy.ops.bim.edit_arbitrary_profile() + elif tool.Blender.Modifier.is_railing(element): + bpy.ops.bim.finish_editing_railing_path() + elif tool.Blender.Modifier.is_roof(element): + bpy.ops.bim.finish_editing_roof_path() + elif tool.Model.get_usage_type(element) == "PROFILE": + bpy.ops.bim.edit_extrusion_axis() + # if in the process of editing arbitrary profile + elif context.scene.BIMProfileProperties.active_arbitrary_profile_id: + bpy.ops.bim.edit_arbitrary_profile() + else: + bpy.ops.bim.edit_extrusion_profile() + return self.execute(context) + elif obj.data.BIMMeshProperties.ifc_definition_id: + if not tool.Geometry.has_geometric_data(obj): + self.is_valid = False + self.should_save = False + representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + if tool.Geometry.is_meshlike( + representation + ) and obj.data.BIMMeshProperties.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): + self.is_valid = False + self.should_save = False + if obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data): + self.edited_objs.append(obj) + else: + tool.Ifc.finish_edit(obj) + + return self.execute(context) + def _execute(self, context): if not context.active_object: return {"FINISHED"} @@ -1866,91 +1953,6 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): 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) - - def _invoke(self, context, event): - if not context.active_object: - return {"FINISHED"} - self.is_valid = True - - bpy.ops.object.mode_set(mode="EDIT", toggle=True) - - context.scene.BIMGeometryProperties.is_changing_mode = True - if context.scene.BIMGeometryProperties.representation_obj: - if context.scene.BIMGeometryProperties.mode != "ITEM": - context.scene.BIMGeometryProperties.mode = "ITEM" - else: - if context.scene.BIMGeometryProperties.mode != "OBJECT": - context.scene.BIMGeometryProperties.mode = "OBJECT" - context.scene.BIMGeometryProperties.is_changing_mode = False - - if not tool.Ifc.get(): - return {"FINISHED"} - - if context.active_object: - element = tool.Ifc.get_entity(context.active_object) - if element and element.is_a("IfcRelSpaceBoundary"): - return bpy.ops.bim.edit_boundary_geometry() - elif tool.Geometry.is_representation_item(context.active_object): - self.edit_representation_item(context.active_object) - return {"FINISHED"} - - objs = context.selected_objects or [context.active_object] - - self.edited_objs = [] - self.unchanged_objs_with_openings = [] - - for obj in objs: - if not obj: - continue - - element = tool.Ifc.get_entity(obj) - if not element: - continue - - if tool.Profile.is_editing_profile(): - profile_id = context.scene.BIMProfileProperties.active_profile_id - if profile_id: - profile = tool.Ifc.get().by_id(profile_id) - if tool.Ifc.get_object(profile): # We are editing an arbitrary profile - bpy.ops.bim.edit_arbitrary_profile() - elif tool.Blender.Modifier.is_railing(element): - bpy.ops.bim.finish_editing_railing_path() - elif tool.Blender.Modifier.is_roof(element): - bpy.ops.bim.finish_editing_roof_path() - elif tool.Model.get_usage_type(element) == "PROFILE": - bpy.ops.bim.edit_extrusion_axis() - # if in the process of editing arbitrary profile - elif context.scene.BIMProfileProperties.active_arbitrary_profile_id: - bpy.ops.bim.edit_arbitrary_profile() - else: - bpy.ops.bim.edit_extrusion_profile() - return self.execute(context) - elif obj.data.BIMMeshProperties.ifc_definition_id: - if not tool.Geometry.has_geometric_data(obj): - self.is_valid = False - self.should_save = False - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) - if tool.Geometry.is_meshlike( - representation - ) and obj.data.BIMMeshProperties.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): - self.is_valid = False - self.should_save = False - if obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data): - self.edited_objs.append(obj) - else: - tool.Ifc.finish_edit(obj) - - return self.execute(context) - class FlipObject(bpy.types.Operator): bl_idname = "bim.flip_object" @@ -2409,3 +2411,63 @@ class UpdateItemAttributes(bpy.types.Operator, tool.Ifc.Operator): item.Depth = attribute.float_value tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) tool.Geometry.import_item(obj) + + +class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_meshlike_item" + bl_label = "Add Meshlike Item" + bl_options = {"REGISTER", "UNDO"} + shape: bpy.props.StringProperty(name="Shape") + + def _execute(self, context): + props = context.scene.BIMGeometryProperties + mesh = bpy.data.meshes.new("Tmp") + obj = bpy.data.objects.new("Tmp", mesh) + scene = bpy.context.scene + scene.collection.objects.link(obj) + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bm = bmesh.new() + matrix = props.representation_obj.matrix_world.copy() + matrix.translation = context.scene.cursor.location + matrix = props.representation_obj.matrix_world.inverted() @ matrix + if self.shape == "CUBE": + bmesh.ops.create_cube(bm, matrix=matrix, size=0.5) + elif self.shape == "PLANE": + bmesh.ops.create_grid(bm, matrix=matrix, size=0.5) + elif self.shape == "CIRCLE": + bmesh.ops.create_circle(bm, matrix=matrix, radius=0.25, segments=16, cap_ends=True) + elif self.shape == "UVSPHERE": + bmesh.ops.create_uvsphere(bm, matrix=matrix, radius=0.25, u_segments=16, v_segments=16) + elif self.shape == "ICOSPHERE": + bmesh.ops.create_icosphere(bm, matrix=matrix, radius=0.25, subdivisions=2) + elif self.shape == "CYLINDER": + # Cone is legitimate. + bmesh.ops.create_cone(bm, matrix=matrix, radius1=0.25, radius2=0.25, depth=0.5, segments=16, cap_ends=True) + + bm.to_mesh(mesh) + bm.free() + mesh.update() + obj.matrix_world = props.representation_obj.matrix_world + new = props.item_objs.add() + new.obj = obj + + tool.Geometry.lock_object(obj) + + 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] + faces = [p.vertices[:] for p in obj.data.polygons] + + representation = tool.Geometry.get_active_representation(props.representation_obj) + representation = ifcopenshell.util.representation.resolve_representation(representation) + + if representation.RepresentationType in ("Brep", "AdvancedBrep"): + item = builder.faceted_brep(verts, faces) + elif representation.RepresentationType in ("Tessellation"): + item = builder.mesh(verts, faces) + + representation.Items = list(representation.Items) + [item] + tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" + obj.data.BIMMeshProperties.ifc_definition_id = item.id() diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 9636d97d67..a4b55fbe53 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -64,6 +64,7 @@ classes = ( product.LoadTypeThumbnails, product.MirrorElements, workspace.Hotkey, + workspace.BIM_MT_add_meshlike_item, wall.AlignWall, wall.ChangeExtrusionDepth, wall.ChangeExtrusionXAngle, diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py index 63f548d02f..7d391fbd8f 100644 --- a/src/bonsai/bonsai/bim/module/model/data.py +++ b/src/bonsai/bonsai/bim/module/model/data.py @@ -30,6 +30,7 @@ from natsort import natsorted def refresh(): AuthoringData.is_loaded = False + ItemData.is_loaded = False ArrayData.is_loaded = False StairData.is_loaded = False SverchokData.is_loaded = False @@ -620,3 +621,27 @@ class RoofData: general_params[prop_readable_name] = prop_value return general_params + + +class ItemData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.data = {} + cls.data["representation_identifier"] = cls.representation_identifier() + cls.data["representation_type"] = cls.representation_type() + + @classmethod + def representation_identifier(cls): + props = bpy.context.scene.BIMGeometryProperties + rep = tool.Geometry.get_active_representation(props.representation_obj) + return rep.RepresentationIdentifier + + @classmethod + def representation_type(cls): + props = bpy.context.scene.BIMGeometryProperties + rep = tool.Geometry.get_active_representation(props.representation_obj) + return rep.RepresentationType diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 531e54c7be..4f80b0fa46 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -24,8 +24,8 @@ import bonsai.tool as tool import bonsai.core.model as core from bonsai.bim.module.model.wall import DumbWallJoiner from bonsai.bim.helper import prop_with_search -from bpy.types import WorkSpaceTool -from bonsai.bim.module.model.data import AuthoringData +from bpy.types import WorkSpaceTool, Menu +from bonsai.bim.module.model.data import AuthoringData, ItemData from bonsai.bim.module.system.data import PortData from bonsai.bim.module.model.prop import get_ifc_class @@ -262,9 +262,17 @@ def format_ifc_camel_case(string): class EditItemUI: @classmethod def draw(cls, context, layout): + if not ItemData.is_loaded: + ItemData.load() + cls.layout = layout row = cls.layout.row() row.label(text="Item Mode", icon="MESH_DATA") + row = cls.layout.row() + row.label(text="Context: " + ItemData.data["representation_identifier"], icon="SCENE_DATA") + row = cls.layout.row() + row.label(text="Type: " + ItemData.data["representation_type"], icon="OUTLINER_OB_MESH") + cls.layout.menu("BIM_MT_add_meshlike_item", icon="ADD") if not (obj := context.active_object) or not tool.Geometry.is_representation_item(obj): return for item_attribute in obj.data.BIMMeshProperties.item_attributes: @@ -275,6 +283,19 @@ class EditItemUI: row.operator("bim.update_item_attributes", icon="FILE_REFRESH", text="") +class BIM_MT_add_meshlike_item(Menu): + bl_idname = "BIM_MT_add_meshlike_item" + bl_label = "Add Item" + + def draw(self, context): + self.layout.operator("bim.add_meshlike_item", icon="MESH_PLANE", text="Mesh Plane").shape = "PLANE" + self.layout.operator("bim.add_meshlike_item", icon="MESH_CUBE", text="Mesh Cube").shape = "CUBE" + self.layout.operator("bim.add_meshlike_item", icon="MESH_CIRCLE", text="Mesh Circle").shape = "CIRCLE" + self.layout.operator("bim.add_meshlike_item", icon="MESH_UVSPHERE", text="Mesh UV Sphere").shape = "UVSPHERE" + self.layout.operator("bim.add_meshlike_item", icon="MESH_ICOSPHERE", text="Mesh Icosphere").shape = "ICOSPHERE" + self.layout.operator("bim.add_meshlike_item", icon="MESH_CYLINDER", text="Mesh Cylinder").shape = "CYLINDER" + + class CreateObjectUI: @classmethod def draw(cls, context, layout, ifc_element_type=None): diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 11f2b6270b..caca0843c0 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1422,21 +1422,22 @@ class Geometry(bonsai.core.tool.Geometry): 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 item.is_a("IfcSweptAreaSolid"): + if not tool.Ifc.is_moved(obj): + continue + has_changed = True + 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: - 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)