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.
This commit is contained in:
Ryan Schultz
2026-03-15 13:24:56 -05:00
parent d37ff6fe83
commit 3f67620cc5
6 changed files with 64 additions and 111 deletions
@@ -30,7 +30,6 @@ classes = (
operator.ActivateModel,
operator.AddAnnotation,
operator.AddAnnotationType,
operator.AddManualDrawingReference,
operator.AssignManualDrawingReference,
operator.AddDrawing,
operator.AddDrawingStyle,
@@ -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):
@@ -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:
@@ -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:
-22
View File
@@ -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"))
+16 -34
View File
@@ -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()