From 47f89373f08cb4fb23078788f7bed5168e2dcf1e Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sun, 15 Mar 2026 12:02:33 -0500 Subject: [PATCH] Add manual drawing reference annotation operator Adds operator to place manual elevation/section drawing reference tags that survive drawing regeneration. Includes core function, tool methods, type-selection dialog, default horizontal rotation for elevation tags, and SVG null guard for unassigned references. Generated with the assistance of an AI coding tool. --- .../bonsai/bim/module/drawing/__init__.py | 1 + .../bonsai/bim/module/drawing/operator.py | 43 +++++++++++++++++++ .../bonsai/bim/module/drawing/svgwriter.py | 2 + src/bonsai/bonsai/core/drawing.py | 22 ++++++++++ src/bonsai/bonsai/tool/drawing.py | 42 ++++++++++++++++++ 5 files changed, 110 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/drawing/__init__.py b/src/bonsai/bonsai/bim/module/drawing/__init__.py index 9f172ce2bb..b88344f76c 100644 --- a/src/bonsai/bonsai/bim/module/drawing/__init__.py +++ b/src/bonsai/bonsai/bim/module/drawing/__init__.py @@ -30,6 +30,7 @@ classes = ( operator.ActivateModel, operator.AddAnnotation, operator.AddAnnotationType, + operator.AddManualDrawingReference, operator.AddDrawing, operator.AddDrawingStyle, operator.AddDrawingToSheet, diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 58474d8ce5..8105100307 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -1761,6 +1761,49 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator): 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, + ) + + class AddSheet(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_sheet" bl_label = "Add Sheet" diff --git a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py index 9b2910b9a0..4bd3e72171 100644 --- a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py +++ b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py @@ -895,6 +895,8 @@ class SvgWriter: reference_id = "-" sheet_id = "-" drawing = tool.Drawing.get_annotation_element(element) + if not drawing: + return ("-", "-") reference = tool.Drawing.get_drawing_reference(drawing) if reference: for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"): diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index 42b0f77fbb..48d0b3e095 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -506,6 +506,28 @@ def add_annotation( return obj +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 1f0aa43c85..ca490221d0 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -1864,6 +1864,48 @@ 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() + pset = tool.Pset.get_element_pset(element, "EPset_Annotation") + if not pset: + pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="EPset_Annotation") + ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"IsManualDrawingReference": True}) + @classmethod def regenerate_elevation_reference_annotation( cls,