diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 0388f0387a..b80652811d 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -864,6 +864,19 @@ class BIMAnnotationProperties(PropertyGroup): ) is_adding_type: bpy.props.BoolProperty(default=False) type_name: bpy.props.StringProperty(name="Name", default="TYPEX") + tag_rotation_mode: bpy.props.EnumProperty( + name="Tag Rotation Mode", + description="How to orient the tag relative to the tagged object", + items=[ + ("NONE", "No Rotation", "Keep tag in default orientation"), + ("LOCAL_X", "Local X Axis", "Align tag with object's local X axis"), + ("LOCAL_Y", "Local Y Axis", "Align tag with object's local Y axis"), + ("LOCAL_Z", "Local Z Axis", "Align tag with object's local Z axis"), + ("CAMERA_Horizontal", "Camera Horizontal", "Align tag with camera X axis"), + ("CAMERA_Vertical", "Camera Vertical", "Align tag with camera Y axis"), + ], + default="NONE", + ) if TYPE_CHECKING: object_type: str diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py index 901c67342c..3512560c14 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -228,7 +228,6 @@ class AnnotationToolUI: def draw_type_selection_interface(cls): # shared by both sidebar and header object_type = cls.props.object_type - row = cls.layout.row(align=True) row.label(text="", icon="FILE_VOLUME") prop_with_search(row, cls.props, "object_type", text="") @@ -248,6 +247,9 @@ class AnnotationToolUI: add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation") if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP: + row = cls.layout.row(align=True) + row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE") + row.prop(cls.props, "tag_rotation_mode", text="") add_layout_hotkey_operator( cls.layout, "Bulk Tag", @@ -313,7 +315,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): ), enable_editing=False, ) - tool.Drawing.setup_annotation_object(obj, object_type, related_object) + tool.Drawing.setup_annotation_object(obj, object_type, related_object, props.tag_rotation_mode) created_objects.append(obj) # Select the created annotation objects @@ -323,6 +325,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): if created_objects: bpy.context.view_layer.objects.active = created_objects[-1] + def hotkey_S_A(self): if bpy.ops.bim.add_annotation.poll(): bpy.ops.bim.add_annotation() @@ -351,4 +354,5 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): continue related_object = tool.Ifc.get_object(related_product) - tool.Drawing.setup_annotation_object(obj, annotation_type, related_object) + rotation_mode = tool.Drawing.get_annotation_props().tag_rotation_mode + tool.Drawing.setup_annotation_object(obj, annotation_type, related_object, rotation_mode) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index c770a9fc8f..86712d19c6 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -251,7 +251,11 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def setup_annotation_object( - cls, obj: bpy.types.Object, object_type: str, related_object: Optional[bpy.types.Object] = None + cls, + obj: bpy.types.Object, + object_type: str, + related_object: Optional[bpy.types.Object] = None, + rotation_mode: str = "NONE", ) -> None: """Finish object's adjustments after both object and entity are created""" @@ -323,6 +327,55 @@ class Drawing(bonsai.core.tool.Drawing): ifc_file, relating_product=related_entity, related_object=obj_entity ) + if rotation_mode != "NONE" and related_object: + cls.apply_annotation_rotation(obj, related_object, rotation_mode) + + @classmethod + def apply_annotation_rotation( + cls, tag_obj: bpy.types.Object, related_object: bpy.types.Object, rotation_mode: str + ) -> None: + """Apply rotation to annotation based on the selected rotation mode""" + camera = bpy.context.scene.camera + camera_right = camera.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0)) + + if rotation_mode == "CAMERA_Horizontal": + location = tag_obj.location.copy() + camera_matrix = camera.matrix_world.copy() + camera_matrix.translation = location + tag_obj.matrix_world = camera_matrix + + elif rotation_mode == "CAMERA_Vertical": + location = tag_obj.location.copy() + camera_matrix = camera.matrix_world.copy() + camera_matrix.translation = location + rotation_90z = mathutils.Matrix.Rotation(math.pi / 2, 4, "Z") + camera_matrix = camera_matrix @ rotation_90z + tag_obj.matrix_world = camera_matrix + + elif rotation_mode == "LOCAL_X": + local_x = related_object.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0)) + local_x = local_x.normalized() + if local_x.dot(camera_right) < 0: + local_x = -local_x + + tag_obj.rotation_euler = local_x.to_track_quat("X", "Z").to_euler() + + elif rotation_mode == "LOCAL_Y": + local_y = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 1, 0)) + local_y = local_y.normalized() + if local_y.dot(camera_right) < 0: + local_y = -local_y + + tag_obj.rotation_euler = local_y.to_track_quat("X", "Z").to_euler() + + elif rotation_mode == "LOCAL_Z": + local_z = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1)) + local_z = local_z.normalized() + if local_z.dot(camera_right) < 0: + local_z = -local_z + + tag_obj.rotation_euler = local_z.to_track_quat("X", "Z").to_euler() + @classmethod def is_annotation_object_type( cls, element: ifcopenshell.entity_instance, object_types: Union[str, Sequence[str]]