From 02fb09ff7e3d325c0aa2a793d032a0986fb8f44f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 27 Mar 2023 14:21:59 +1100 Subject: [PATCH] Implement faux-mirroring. --- .../bim/module/geometry/__init__.py | 1 + .../bim/module/geometry/operator.py | 7 ++- .../blenderbim/bim/module/model/__init__.py | 1 + .../blenderbim/bim/module/model/product.py | 58 +++++++++++++++++++ .../blenderbim/bim/module/model/workspace.py | 4 ++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py index 06262b00b0..dd959f3274 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py @@ -59,6 +59,7 @@ def register(): if wm.keyconfigs.addon: km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY") kmi = km.keymap_items.new("bim.override_object_duplicate_move", "D", "PRESS", shift=True) + kmi.properties.is_interactive = True kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked", "D", "PRESS", alt=True) kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True) kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS") diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 86b89d67c4..097c3bbce0 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -502,6 +502,7 @@ class OverrideDuplicateMove(bpy.types.Operator): bl_idname = "bim.override_object_duplicate_move" bl_label = "IFC Duplicate Objects" bl_options = {"REGISTER", "UNDO"} + is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True) @classmethod def poll(cls, context): @@ -528,7 +529,8 @@ class OverrideDuplicateMove(bpy.types.Operator): new_obj.select_set(True) if new_active_obj: context.view_layer.objects.active = new_active_obj - bpy.ops.transform.translate("INVOKE_DEFAULT") + if self.is_interactive: + bpy.ops.transform.translate("INVOKE_DEFAULT") return {"FINISHED"} def _execute(self, context): @@ -552,7 +554,8 @@ class OverrideDuplicateMove(bpy.types.Operator): old_to_new[tool.Ifc.get_entity(obj)] = [new] # Recreate decompositions tool.Root.recreate_decompositions(relationships, old_to_new) - bpy.ops.transform.translate("INVOKE_DEFAULT") + if self.is_interactive: + bpy.ops.transform.translate("INVOKE_DEFAULT") blenderbim.bim.handler.purge_module_data() diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 5d4937cf72..f29b99ab32 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -52,6 +52,7 @@ classes = ( product.AlignProduct, product.ChangeTypePage, product.LoadTypeThumbnails, + product.MirrorElements, workspace.Hotkey, wall.AlignWall, wall.ChangeExtrusionDepth, diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index f50a785824..b7c112564b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -398,6 +398,64 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} +class MirrorElements(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.mirror_elements" + bl_label = "Mirror Elements" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Faux-mirrors the selected object by an active empty along a mirror plane." + + @classmethod + def poll(cls, context): + return context.selected_objects + + def _execute(self, context): + # This is not a true mirror operation. In BIM, objects that are + # mirrored are not the same type. (i.e. a mirrored asymmetric desk is a + # completely different product). To preserve types, we calculate + # bounding box centroids, and mirror the position of the object. The + # mirrored object performs the necessary rotation to preserve the + # mirrored intention, but never actually truly mirror anything (i.e. + # scale = -1). + + # Objects are mirrored along the YZ plane of the mirror object. + # Mirrored objects have their relative local Y and Z axes preserved, + # and the new X axis is calculated. + + # In theory, untyped objects may be truly mirrored, but this is not yet + # implemented. + mirror = context.active_object + reflection = Matrix() + reflection[0][0] = -1 + + mirror.select_set(False) + + bpy.ops.bim.override_object_duplicate_move(is_interactive=False) + + for obj in context.selected_objects: + if obj == mirror: + continue + + objmat = mirror.matrix_world.inverted() @ obj.matrix_world.copy() + x, y, z = objmat.to_3x3().col + centroid = Vector(obj.bound_box[0]).lerp(Vector(obj.bound_box[6]), 0.5) + c = mirror.matrix_world.inverted() @ obj.matrix_world @ centroid + newy = mirror.matrix_world.to_quaternion() @ (y @ reflection) + newz = mirror.matrix_world.to_quaternion() @ (z @ reflection) + newx = newy.cross(newz) + newc = mirror.matrix_world @ (c @ reflection) + + newmat = Matrix((newx.to_4d(), newy.to_4d(), newz.to_4d(), newc.to_4d())).transposed() + newmat.col[3][0:3] = Vector(newmat.col[3][0:3]) - (newmat.to_quaternion() @ centroid) + + obj.matrix_world = newmat + + def copy_obj(self, obj): + new = obj.copy() + new.data = wall2.data.copy() + wall1.users_collection[0].objects.link(wall2) + blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2) + + def generate_box(usecase_path, ifc_file, settings): box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW") if not box_context: diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 9eb3ab72c0..2ceaab3e6a 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -368,6 +368,8 @@ class BimToolUI: row.label(text="", icon="EVENT_SHIFT") row.label(text="Align Interior", icon="EVENT_V") row = cls.layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Mirror", icon="EVENT_M") row = cls.layout.row(align=True) row.label(text="Mode") @@ -606,6 +608,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return if self.active_material_usage == "LAYER2": bpy.ops.bim.merge_wall() + else: + bpy.ops.bim.mirror_elements() def hotkey_S_R(self): if not bpy.context.selected_objects: