From 3f67620cc572945d15f960bbea6f19b01103c499 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sun, 15 Mar 2026 13:24:56 -0500 Subject: [PATCH] Integrate manual drawing references into annotation tool Adds MANUAL_DRAWING_REFERENCE to the annotation type dropdown. Selecting it shows a dialog to choose elevation or section and optionally assign a target drawing before placement. Tags are protected from regeneration via EPset_Annotation.IsManualDrawingReference. Generated with the assistance of an AI coding tool. --- .../bonsai/bim/module/drawing/__init__.py | 1 - .../bonsai/bim/module/drawing/operator.py | 90 ++++++++----------- src/bonsai/bonsai/bim/module/drawing/ui.py | 10 +++ .../bonsai/bim/module/drawing/workspace.py | 2 +- src/bonsai/bonsai/core/drawing.py | 22 ----- src/bonsai/bonsai/tool/drawing.py | 50 ++++------- 6 files changed, 64 insertions(+), 111 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/__init__.py b/src/bonsai/bonsai/bim/module/drawing/__init__.py index 3777115a84..a6a76439c1 100644 --- a/src/bonsai/bonsai/bim/module/drawing/__init__.py +++ b/src/bonsai/bonsai/bim/module/drawing/__init__.py @@ -30,7 +30,6 @@ classes = ( operator.ActivateModel, operator.AddAnnotation, operator.AddAnnotationType, - operator.AddManualDrawingReference, operator.AssignManualDrawingReference, operator.AddDrawing, operator.AddDrawingStyle, diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 5ea74b8133..85efdada02 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -1727,11 +1727,29 @@ class CreateDrawing(bpy.types.Operator): return drawing_path +def _get_drawing_enum_items(self, context): + items = [("0", "None", "Clear the drawing assignment")] + if ifc := tool.Ifc.get(): + for d in ifc.by_type("IfcAnnotation"): + if d.ObjectType == "DRAWING": + items.append((str(d.id()), d.Name or f"Drawing {d.id()}", "")) + return items + + class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_annotation" bl_label = "Add Annotation" bl_options = {"REGISTER", "UNDO"} description: bpy.props.StringProperty() + annotation_subtype: bpy.props.EnumProperty( + name="Type", + items=[ + ("ELEVATION", "Elevation", "Place a manual elevation tag"), + ("SECTION", "Section", "Place a manual section tag"), + ], + default="ELEVATION", + ) + drawing_id: bpy.props.EnumProperty(name="Drawing", items=_get_drawing_enum_items) @classmethod def poll(cls, context): @@ -1741,6 +1759,16 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator): def description(cls, context, operator): return operator.description or "" + def invoke(self, context, event): + if tool.Drawing.get_annotation_props().object_type == "MANUAL_DRAWING_REFERENCE": + self.drawing_id = "0" + return context.window_manager.invoke_props_dialog(self) + return self.execute(context) + + def draw(self, context): + self.layout.prop(self, "annotation_subtype", expand=True) + self.layout.prop(self, "drawing_id") + def _execute(self, context): props = tool.Drawing.get_annotation_props() dprops = tool.Drawing.get_document_props() @@ -1748,69 +1776,25 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator): self.report({"WARNING"}, "No active drawing.") return + object_type = self.annotation_subtype if props.object_type == "MANUAL_DRAWING_REFERENCE" else props.object_type obj = core.add_annotation( tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing, - object_type=props.object_type, + object_type=object_type, relating_type=tool.Ifc.get().by_id(int(props.relating_type_id)) if props.relating_type_id != "0" else None, enable_editing=True, ) if props.object_type == "IMAGE": bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name) - - -class AddManualDrawingReference(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.add_manual_drawing_reference" - bl_label = "Add Manual Drawing Reference" - bl_description = "Place a manual elevation or section tag that will not be moved or deleted during drawing regeneration" - bl_options = {"REGISTER", "UNDO"} - annotation_type: bpy.props.EnumProperty( - name="Type", - items=[ - ("ELEVATION", "Elevation", "Place a manual elevation tag"), - ("SECTION", "Section", "Place a manual section tag"), - ], - default="ELEVATION", - ) - - @classmethod - def poll(cls, context): - if not tool.Ifc.get(): - return False - if not context.scene.camera: - cls.poll_message_set("No active drawing.") - return False - return True - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - self.layout.prop(self, "annotation_type") - - def _execute(self, context): - dprops = tool.Drawing.get_document_props() - if not (drawing := dprops.get_active_drawing()): - self.report({"WARNING"}, "No active drawing.") - return - core.add_manual_drawing_reference( - tool.Ifc, - tool.Collector, - tool.Drawing, - drawing=drawing, - annotation_type=self.annotation_type, - ) - - -def _get_drawing_enum_items(self, context): - items = [("0", "None", "Clear the drawing assignment")] - if ifc := tool.Ifc.get(): - for d in ifc.by_type("IfcAnnotation"): - if d.ObjectType == "DRAWING": - items.append((str(d.id()), d.Name or f"Drawing {d.id()}", "")) - return items + if object_type in ("ELEVATION", "SECTION"): + element = tool.Ifc.get_entity(obj) + tool.Drawing.set_manual_drawing_reference(element) + if self.drawing_id != "0": + core.assign_manual_drawing_reference( + tool.Ifc, tool.Drawing, element=element, drawing=tool.Ifc.get().by_id(int(self.drawing_id)) + ) class AssignManualDrawingReference(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index 796c36b484..89d3c98459 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -535,6 +535,16 @@ class BIM_PT_product_assignments(Panel): assert self.layout assert (obj := context.active_object) + + element = tool.Ifc.get_entity(obj) + if element and tool.Drawing.is_manual_drawing_reference(element): + row = self.layout.row(align=True) + row.label( + text=ProductAssignmentsData.data["relating_product"] or "No Drawing Assigned", icon="IMAGE_DATA" + ) + row.operator("bim.assign_manual_drawing_reference", icon="GREASEPENCIL", text="") + return + props = tool.Drawing.get_object_assigned_product_props(obj) if props.is_editing_product: diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py index bc7463073e..9b7098417d 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -332,7 +332,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): def hotkey_S_A(self): if bpy.ops.bim.add_annotation.poll(): - bpy.ops.bim.add_annotation() + bpy.ops.bim.add_annotation("INVOKE_DEFAULT") def hotkey_S_E(self): if not bpy.context.active_object: diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index 5279cd55a7..394d2edf40 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -518,28 +518,6 @@ def assign_manual_drawing_reference( ifc.run("drawing.assign_product", relating_product=drawing, related_object=element) -def add_manual_drawing_reference( - ifc: type[tool.Ifc], - collector: type[tool.Collector], - drawing_tool: type[tool.Drawing], - drawing: ifcopenshell.entity_instance, - annotation_type: str, -) -> bpy.types.Object: - if annotation_type == "ELEVATION": - element = drawing_tool.create_manual_elevation_reference(drawing) - else: # SECTION - target_view = drawing_tool.get_drawing_target_view(drawing) - context = drawing_tool.get_annotation_context(target_view) - if not context: - context = drawing_tool.create_annotation_context(target_view, annotation_type) - element = drawing_tool.create_manual_section_reference(drawing, context) - drawing_tool.set_manual_drawing_reference(element) - ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), products=[element]) - obj = ifc.get_object(element) - collector.assign(obj) - return obj - - def build_schedule(drawing: type[tool.Drawing], schedule: ifcopenshell.entity_instance) -> None: drawing.create_svg_schedule(schedule) drawing.open_svg(drawing.get_path_with_ext(drawing.get_document_uri(schedule), "svg")) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index ca490221d0..7b58fa0018 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -111,6 +111,7 @@ class Drawing(bonsai.core.tool.Drawing): "FILL_AREA": AnnotationObjectType("Fill Area", "", "NODE_TEXTURE", "mesh"), "FALL": AnnotationObjectType("Fall", "", "SORT_ASC", "curve"), "IMAGE": AnnotationObjectType("Image", "Add reference image attached to the drawing", "TEXTURE", "mesh"), + "MANUAL_DRAWING_REFERENCE": AnnotationObjectType("Manual Drawing Reference", "Add manual elevation or section reference tag that will not be moved or deleted during drawing regeneration", "EMPTY_ARROWS", "empty"), } # fmt: on @@ -177,6 +178,10 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def get_annotation_data_type(cls, object_type: str) -> ANNOTATION_DATA_TYPE: + if object_type == "ELEVATION": + return "empty" + if object_type == "SECTION": + return "mesh" return cls.ANNOTATION_TYPES_DATA[object_type].data_type @classmethod @@ -207,6 +212,13 @@ class Drawing(bonsai.core.tool.Drawing): co_end = co1 + vec * scaled_length obj = annotation.Annotator.add_line_to_annotation(obj, co_end, co1) obj.matrix_world = obj.matrix_world @ Matrix.Rotation(math.radians(-90), 4, "Z") + elif object_type == "ELEVATION": + obj.matrix_world = Matrix.Translation(bpy.context.scene.cursor.location.copy()) @ Matrix.Rotation( + math.radians(90), 4, "X" + ) + elif object_type == "SECTION": + obj.matrix_world = Matrix.Translation(bpy.context.scene.cursor.location.copy()) + obj = annotation.Annotator.add_line_to_annotation(obj) elif object_type != "TEXT": obj = annotation.Annotator.add_line_to_annotation(obj) @@ -1530,6 +1542,10 @@ class Drawing(bonsai.core.tool.Drawing): elements.append(element) return elements + @classmethod + def is_manual_drawing_reference(cls, element: ifcopenshell.entity_instance) -> bool: + return bool(ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "IsManualDrawingReference")) + @classmethod def is_auto_annotation(cls, element: ifcopenshell.entity_instance): if not (element.is_a("IfcAnnotation") and element.ObjectType in ("GRID", "SECTION", "ELEVATION", "SECTION_LEVEL")): @@ -1864,40 +1880,6 @@ class Drawing(bonsai.core.tool.Drawing): element.Name = elevation.Name or "Unnamed" return element - @classmethod - def create_manual_elevation_reference(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: - cursor_location = bpy.context.scene.cursor.location.copy() - obj = bpy.data.objects.new("Unnamed", None) - obj.empty_display_size = 0.1 - obj.matrix_world = Matrix.Translation(cursor_location) @ Matrix.Rotation(math.radians(90), 4, "X") - element = cls.run_root_assign_class( - obj=obj, ifc_class="IfcAnnotation", predefined_type="ELEVATION", should_add_representation=False - ) - element.Name = "Unnamed" - return element - - @classmethod - def create_manual_section_reference( - cls, drawing: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance - ) -> ifcopenshell.entity_instance: - cursor_location = bpy.context.scene.cursor.location.copy() - mesh = bpy.data.meshes.new("Mesh") - obj = bpy.data.objects.new("Unnamed", mesh) - obj.matrix_world = Matrix.Translation(cursor_location) - element = cls.run_root_assign_class( - obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION", should_add_representation=False - ) - element.Name = "Unnamed" - builder = ShapeBuilder(tool.Ifc.get()) - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - p1 = cursor_location + Vector((-0.5, 0, 0)) - p2 = cursor_location + Vector((0.5, 0, 0)) - points = [p1 / unit_scale, p2 / unit_scale] - representation = builder.get_representation(context, [builder.polyline(points)]) - ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation) - bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation) - return element - @classmethod def set_manual_drawing_reference(cls, element: ifcopenshell.entity_instance) -> None: ifc_file = tool.Ifc.get()