Add external SVG reference support to manual drawing reference tags

Extends manual drawing reference annotations (elevation and section) to
also support external SVG references imported via bim.add_reference.

- Add "Is a Reference" checkbox to the annotation tool sidebar, shown
  when Elevation or Section is the active type; checking it and pressing
  Add opens a dialog to optionally link the tag to a Bonsai drawing or
  an external SVG reference
- The MANUAL_DRAWING_REFERENCE dropdown type is retained for backwards
  compatibility; selecting it shows a style picker (Elevation/Section)
  and the same linking dialog
- External-reference annotations are flagged with IsDocumentReference
  in EPset_Annotation and linked to their IfcDocumentInformation via
  IfcRelAssociatesDocument; drawing-reference annotations continue to
  use IfcRelAssignsToProduct as before
- SVG export resolves the correct reference/sheet IDs for both link
  types via get_reference_and_sheet_id_from_annotation
- Add IsDocumentReference to the EPset_Annotation pset template
This commit is contained in:
Ryan Schultz
2026-04-08 13:59:14 -05:00
parent 4dd82cad9b
commit 565414cf51
9 changed files with 173 additions and 29 deletions
@@ -5,7 +5,7 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',$,$,'Psets_BBIM_Anno
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2,#34));
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2,#34,#35));
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -39,5 +39,6 @@ DATA;
#32=IFCSIMPLEPROPERTYTEMPLATE('0gjJzDYBX8P85qn1xcAOOo',$,'Reverse_List','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#33=IFCSIMPLEPROPERTYTEMPLATE('22TrcxF8jFNB4buSmzjGEF',$,'List_Separator','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#34=IFCSIMPLEPROPERTYTEMPLATE('0FauxIsAnnotFaux0001aB',$,'IsManualDrawingReference','Marks this annotation as a manually placed drawing reference, exempt from automatic drawing regeneration.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#35=IFCSIMPLEPROPERTYTEMPLATE('0FauxIsDocRefFaux001aB',$,'IsDocumentReference','Marks this annotation as pointing to an external document reference (not a Bonsai drawing camera).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -55,6 +55,14 @@ class ProductAssignmentsData:
element = tool.Ifc.get_entity(bpy.context.active_object)
if not element or not element.is_a("IfcAnnotation"):
return
# Document-reference annotations link to an IfcDocumentInformation, not a product.
if tool.Drawing.is_document_reference(element):
for rel in element.HasAssociations:
if rel.is_a("IfcRelAssociatesDocument"):
doc = rel.RelatingDocument
if doc.is_a("IfcDocumentInformation"):
return doc.Name or "Unnamed"
return None
for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToProduct"):
name = rel.RelatingProduct.Name or "Unnamed"
@@ -1737,20 +1737,32 @@ def _get_drawing_enum_items(self, context):
return items
def _get_reference_doc_enum_items(self, context):
items = [("0", "None", "Clear the reference assignment")]
if ifc := tool.Ifc.get():
for d in ifc.by_type("IfcDocumentInformation"):
if d.Scope == "REFERENCE":
items.append((str(d.id()), d.Name or f"Reference {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",
drawing_id: bpy.props.EnumProperty(name="Drawing", items=_get_drawing_enum_items)
reference_doc_id: bpy.props.EnumProperty(name="Reference", items=_get_reference_doc_enum_items)
is_document_reference: bpy.props.BoolProperty(name="External Reference", default=False)
# Used only when placing via the legacy MANUAL_DRAWING_REFERENCE dropdown type.
manual_style: bpy.props.EnumProperty(
name="Style",
items=[
("ELEVATION", "Elevation", "Place a manual elevation tag"),
("SECTION", "Section", "Place a manual section tag"),
("ELEVATION", "Elevation", "Use the elevation tag visual (circle with arrow)"),
("SECTION", "Section", "Use the section tag visual (line with end circles)"),
],
default="ELEVATION",
)
drawing_id: bpy.props.EnumProperty(name="Drawing", items=_get_drawing_enum_items)
@classmethod
def poll(cls, context):
@@ -1761,14 +1773,27 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
return operator.description or ""
def invoke(self, context, event):
if tool.Drawing.get_annotation_props().object_type == "MANUAL_DRAWING_REFERENCE":
props = tool.Drawing.get_annotation_props()
needs_dialog = (
(props.is_manual_reference and props.object_type in ("ELEVATION", "SECTION"))
or props.object_type == "MANUAL_DRAWING_REFERENCE"
)
if needs_dialog:
self.drawing_id = "0"
self.reference_doc_id = "0"
self.is_document_reference = False
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
def draw(self, context):
self.layout.prop(self, "annotation_subtype", expand=True)
prop_with_search(self.layout, self, "drawing_id", should_click_ok=True, search_threshold=0, original_operator_path=f"{__name__}.AddAnnotation")
props = tool.Drawing.get_annotation_props()
if props.object_type == "MANUAL_DRAWING_REFERENCE":
self.layout.prop(self, "manual_style", expand=True)
self.layout.prop(self, "is_document_reference")
if self.is_document_reference:
prop_with_search(self.layout, self, "reference_doc_id", should_click_ok=True, search_threshold=0, original_operator_path=f"{__name__}.AddAnnotation")
else:
prop_with_search(self.layout, self, "drawing_id", should_click_ok=True, search_threshold=0, original_operator_path=f"{__name__}.AddAnnotation")
def _execute(self, context):
props = tool.Drawing.get_annotation_props()
@@ -1777,7 +1802,15 @@ 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
# MANUAL_DRAWING_REFERENCE is a legacy/convenience dropdown entry; resolve
# it to the chosen style so create_annotation_object knows what to build.
if props.object_type == "MANUAL_DRAWING_REFERENCE":
object_type = self.manual_style
is_manual = True
else:
object_type = props.object_type
is_manual = props.is_manual_reference and object_type in ("ELEVATION", "SECTION")
obj = core.add_annotation(
tool.Ifc,
tool.Collector,
@@ -1790,12 +1823,18 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
# item edit mode for these types.
enable_editing=object_type not in ("ELEVATION", "SECTION"),
)
if props.object_type == "IMAGE":
if object_type == "IMAGE":
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name)
if object_type in ("ELEVATION", "SECTION"):
if is_manual:
element = tool.Ifc.get_entity(obj)
tool.Drawing.set_manual_drawing_reference(element)
if self.drawing_id != "0":
if self.is_document_reference:
tool.Drawing.set_document_reference_flag(element)
if self.reference_doc_id != "0":
core.assign_manual_reference_document(
tool.Drawing, element=element, document=tool.Ifc.get().by_id(int(self.reference_doc_id))
)
elif 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))
)
@@ -1804,9 +1843,10 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
class AssignManualDrawingReference(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_manual_drawing_reference"
bl_label = "Assign Drawing"
bl_description = "Assign a target drawing to this manual drawing reference tag"
bl_description = "Assign a target drawing or reference to this manual drawing reference tag"
bl_options = {"REGISTER", "UNDO"}
drawing_id: bpy.props.EnumProperty(name="Drawing", items=_get_drawing_enum_items)
reference_doc_id: bpy.props.EnumProperty(name="Reference", items=_get_reference_doc_enum_items)
@classmethod
def poll(cls, context):
@@ -1825,17 +1865,29 @@ class AssignManualDrawingReference(bpy.types.Operator, tool.Ifc.Operator):
def invoke(self, context, event):
element = tool.Ifc.get_entity(context.active_object)
current = tool.Drawing.get_annotation_element(element)
self.drawing_id = str(current.id()) if current else "0"
if tool.Drawing.is_document_reference(element):
doc = tool.Drawing.get_annotation_reference_doc(element)
self.reference_doc_id = str(doc.id()) if doc else "0"
else:
current = tool.Drawing.get_annotation_element(element)
self.drawing_id = str(current.id()) if current else "0"
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
prop_with_search(self.layout, self, "drawing_id", search_threshold=0, original_operator_path=f"{__name__}.AssignManualDrawingReference")
element = tool.Ifc.get_entity(bpy.context.active_object)
if element and tool.Drawing.is_document_reference(element):
prop_with_search(self.layout, self, "reference_doc_id", search_threshold=0, original_operator_path=f"{__name__}.AssignManualDrawingReference")
else:
prop_with_search(self.layout, self, "drawing_id", search_threshold=0, original_operator_path=f"{__name__}.AssignManualDrawingReference")
def _execute(self, context):
element = tool.Ifc.get_entity(context.active_object)
drawing = tool.Ifc.get().by_id(int(self.drawing_id)) if self.drawing_id != "0" else None
core.assign_manual_drawing_reference(tool.Ifc, tool.Drawing, element=element, drawing=drawing)
if tool.Drawing.is_document_reference(element):
document = tool.Ifc.get().by_id(int(self.reference_doc_id)) if self.reference_doc_id != "0" else None
core.assign_manual_reference_document(tool.Drawing, element=element, document=document)
else:
drawing = tool.Ifc.get().by_id(int(self.drawing_id)) if self.drawing_id != "0" else None
core.assign_manual_drawing_reference(tool.Ifc, tool.Drawing, element=element, drawing=drawing)
for area in context.screen.areas:
if area.type == "PROPERTIES":
area.tag_redraw()
@@ -1000,6 +1000,12 @@ class BIMAnnotationProperties(PropertyGroup):
)
is_adding_type: bpy.props.BoolProperty(default=False)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
is_manual_reference: bpy.props.BoolProperty(
name="Is a Reference",
default=False,
description="Place as a manual reference tag (IsManualDrawingReference). "
"Exempt from automatic drawing regeneration. Optionally link to a drawing or external reference.",
)
tag_rotation_mode: bpy.props.EnumProperty(
name="Tag Rotation Mode",
description="How to orient the tag relative to the tagged object",
@@ -1020,3 +1026,4 @@ class BIMAnnotationProperties(PropertyGroup):
create_representation_for_type: bool
is_adding_type: bool
type_name: str
is_manual_reference: bool
@@ -896,8 +896,32 @@ class SvgWriter:
)
def get_reference_and_sheet_id_from_annotation(self, element: ifcopenshell.entity_instance) -> tuple[str, str]:
reference_id = "-"
sheet_id = "-"
is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3"
# Document-reference annotations link to an IfcDocumentInformation via
# IfcRelAssociatesDocument rather than to a drawing product.
if tool.Drawing.is_document_reference(element):
doc_info = tool.Drawing.get_annotation_reference_doc(element)
if not doc_info:
return ("-", "-")
ext_location = tool.Drawing.get_path_with_ext(
(doc_info.DocumentReferences[0].Location if is_ifc2x3 else doc_info.HasDocumentReferences[0].Location),
"svg",
) if (doc_info.DocumentReferences if is_ifc2x3 else doc_info.HasDocumentReferences) else None
if not ext_location:
return ("-", "-")
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
if tool.Drawing.get_reference_description(sheet_reference) != "REFERENCE":
continue
if sheet_reference.Location != ext_location:
continue
sheet = tool.Drawing.get_reference_document(sheet_reference)
if sheet:
if is_ifc2x3:
return (sheet_reference.ItemReference or "-", sheet.DocumentId or "-")
return (sheet_reference.Identification or "-", sheet.Identification or "-")
return ("-", "-")
drawing = tool.Drawing.get_annotation_element(element)
if not drawing:
return ("-", "-")
@@ -909,13 +933,9 @@ class SvgWriter:
continue
sheet = tool.Drawing.get_reference_document(sheet_reference)
if sheet:
if tool.Ifc.get_schema() == "IFC2X3":
reference_id = sheet_reference.ItemReference or "-"
sheet_id = sheet.DocumentId or "-"
else:
reference_id = sheet_reference.Identification or "-"
sheet_id = sheet.Identification or "-"
return (reference_id, sheet_id)
if is_ifc2x3:
return (sheet_reference.ItemReference or "-", sheet.DocumentId or "-")
return (sheet_reference.Identification or "-", sheet.Identification or "-")
break
return ("-", "-")
+2 -1
View File
@@ -539,8 +539,9 @@ class BIM_PT_product_assignments(Panel):
element = tool.Ifc.get_entity(obj)
if element and tool.Drawing.is_manual_drawing_reference(element):
row = self.layout.row(align=True)
fallback = "No Reference Assigned" if element.ObjectType == "REFERENCE" else "No Drawing Assigned"
row.label(
text=ProductAssignmentsData.data["relating_product"] or "No Drawing Assigned", icon="IMAGE_DATA"
text=ProductAssignmentsData.data["relating_product"] or fallback, icon="IMAGE_DATA"
)
row.operator("bim.assign_manual_drawing_reference", icon="GREASEPENCIL", text="")
return
@@ -249,6 +249,10 @@ class AnnotationToolUI:
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
if object_type in ("ELEVATION", "SECTION"):
row = cls.layout.row(align=True)
row.prop(cls.props, "is_manual_reference")
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
row = cls.layout.row(align=True)
row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE")
+8
View File
@@ -518,6 +518,14 @@ def assign_manual_drawing_reference(
ifc.run("drawing.assign_product", relating_product=drawing, related_object=element)
def assign_manual_reference_document(
drawing_tool: type[tool.Drawing],
element: ifcopenshell.entity_instance,
document: Optional[ifcopenshell.entity_instance],
) -> None:
drawing_tool.set_annotation_reference_doc(element, document)
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"))
+43
View File
@@ -1889,6 +1889,49 @@ class Drawing(bonsai.core.tool.Drawing):
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 is_document_reference(cls, element: ifcopenshell.entity_instance) -> bool:
"""Return True if this annotation links to an external document (not a Bonsai drawing camera)."""
return bool(ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "IsDocumentReference"))
@classmethod
def set_document_reference_flag(cls, element: ifcopenshell.entity_instance) -> None:
"""Mark this annotation as pointing to an external document reference."""
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={"IsDocumentReference": True})
@classmethod
def get_annotation_reference_doc(
cls, element: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
"""Return the IfcDocumentInformation linked to a document-reference annotation."""
for rel in element.HasAssociations:
if rel.is_a("IfcRelAssociatesDocument"):
doc = rel.RelatingDocument
if doc.is_a("IfcDocumentInformation"):
return doc
return None
@classmethod
def set_annotation_reference_doc(
cls,
element: ifcopenshell.entity_instance,
document: Union[ifcopenshell.entity_instance, None],
) -> None:
"""Associate (or clear) an IfcDocumentInformation on a document-reference annotation."""
ifc_file = tool.Ifc.get()
# Remove existing document associations on this annotation.
for rel in list(element.HasAssociations):
if rel.is_a("IfcRelAssociatesDocument"):
ifcopenshell.api.document.unassign_document(
ifc_file, products=[element], document=rel.RelatingDocument
)
if document:
ifcopenshell.api.document.assign_document(ifc_file, products=[element], document=document)
@classmethod
def regenerate_elevation_reference_annotation(
cls,