Skip to content

Commit 565414c

Browse files
committed
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
1 parent 4dd82ca commit 565414c

File tree

9 files changed

+173
-29
lines changed

9 files changed

+173
-29
lines changed

src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',$,$,'Psets_BBIM_Anno
55
FILE_SCHEMA(('IFC4'));
66
ENDSEC;
77
DATA;
8-
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2,#34));
8+
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2,#34,#35));
99
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
1010
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
1111
#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;
3939
#32=IFCSIMPLEPROPERTYTEMPLATE('0gjJzDYBX8P85qn1xcAOOo',$,'Reverse_List','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
4040
#33=IFCSIMPLEPROPERTYTEMPLATE('22TrcxF8jFNB4buSmzjGEF',$,'List_Separator','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
4141
#34=IFCSIMPLEPROPERTYTEMPLATE('0FauxIsAnnotFaux0001aB',$,'IsManualDrawingReference','Marks this annotation as a manually placed drawing reference, exempt from automatic drawing regeneration.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
42+
#35=IFCSIMPLEPROPERTYTEMPLATE('0FauxIsDocRefFaux001aB',$,'IsDocumentReference','Marks this annotation as pointing to an external document reference (not a Bonsai drawing camera).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
4243
ENDSEC;
4344
END-ISO-10303-21;

src/bonsai/bonsai/bim/module/drawing/data.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ def relating_product(cls):
5555
element = tool.Ifc.get_entity(bpy.context.active_object)
5656
if not element or not element.is_a("IfcAnnotation"):
5757
return
58+
# Document-reference annotations link to an IfcDocumentInformation, not a product.
59+
if tool.Drawing.is_document_reference(element):
60+
for rel in element.HasAssociations:
61+
if rel.is_a("IfcRelAssociatesDocument"):
62+
doc = rel.RelatingDocument
63+
if doc.is_a("IfcDocumentInformation"):
64+
return doc.Name or "Unnamed"
65+
return None
5866
for rel in element.HasAssignments:
5967
if rel.is_a("IfcRelAssignsToProduct"):
6068
name = rel.RelatingProduct.Name or "Unnamed"

src/bonsai/bonsai/bim/module/drawing/operator.py

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1737,20 +1737,32 @@ def _get_drawing_enum_items(self, context):
17371737
return items
17381738

17391739

1740+
def _get_reference_doc_enum_items(self, context):
1741+
items = [("0", "None", "Clear the reference assignment")]
1742+
if ifc := tool.Ifc.get():
1743+
for d in ifc.by_type("IfcDocumentInformation"):
1744+
if d.Scope == "REFERENCE":
1745+
items.append((str(d.id()), d.Name or f"Reference {d.id()}", ""))
1746+
return items
1747+
1748+
17401749
class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
17411750
bl_idname = "bim.add_annotation"
17421751
bl_label = "Add Annotation"
17431752
bl_options = {"REGISTER", "UNDO"}
17441753
description: bpy.props.StringProperty()
1745-
annotation_subtype: bpy.props.EnumProperty(
1746-
name="Type",
1754+
drawing_id: bpy.props.EnumProperty(name="Drawing", items=_get_drawing_enum_items)
1755+
reference_doc_id: bpy.props.EnumProperty(name="Reference", items=_get_reference_doc_enum_items)
1756+
is_document_reference: bpy.props.BoolProperty(name="External Reference", default=False)
1757+
# Used only when placing via the legacy MANUAL_DRAWING_REFERENCE dropdown type.
1758+
manual_style: bpy.props.EnumProperty(
1759+
name="Style",
17471760
items=[
1748-
("ELEVATION", "Elevation", "Place a manual elevation tag"),
1749-
("SECTION", "Section", "Place a manual section tag"),
1761+
("ELEVATION", "Elevation", "Use the elevation tag visual (circle with arrow)"),
1762+
("SECTION", "Section", "Use the section tag visual (line with end circles)"),
17501763
],
17511764
default="ELEVATION",
17521765
)
1753-
drawing_id: bpy.props.EnumProperty(name="Drawing", items=_get_drawing_enum_items)
17541766

17551767
@classmethod
17561768
def poll(cls, context):
@@ -1761,14 +1773,27 @@ def description(cls, context, operator):
17611773
return operator.description or ""
17621774

17631775
def invoke(self, context, event):
1764-
if tool.Drawing.get_annotation_props().object_type == "MANUAL_DRAWING_REFERENCE":
1776+
props = tool.Drawing.get_annotation_props()
1777+
needs_dialog = (
1778+
(props.is_manual_reference and props.object_type in ("ELEVATION", "SECTION"))
1779+
or props.object_type == "MANUAL_DRAWING_REFERENCE"
1780+
)
1781+
if needs_dialog:
17651782
self.drawing_id = "0"
1783+
self.reference_doc_id = "0"
1784+
self.is_document_reference = False
17661785
return context.window_manager.invoke_props_dialog(self)
17671786
return self.execute(context)
17681787

17691788
def draw(self, context):
1770-
self.layout.prop(self, "annotation_subtype", expand=True)
1771-
prop_with_search(self.layout, self, "drawing_id", should_click_ok=True, search_threshold=0, original_operator_path=f"{__name__}.AddAnnotation")
1789+
props = tool.Drawing.get_annotation_props()
1790+
if props.object_type == "MANUAL_DRAWING_REFERENCE":
1791+
self.layout.prop(self, "manual_style", expand=True)
1792+
self.layout.prop(self, "is_document_reference")
1793+
if self.is_document_reference:
1794+
prop_with_search(self.layout, self, "reference_doc_id", should_click_ok=True, search_threshold=0, original_operator_path=f"{__name__}.AddAnnotation")
1795+
else:
1796+
prop_with_search(self.layout, self, "drawing_id", should_click_ok=True, search_threshold=0, original_operator_path=f"{__name__}.AddAnnotation")
17721797

17731798
def _execute(self, context):
17741799
props = tool.Drawing.get_annotation_props()
@@ -1777,7 +1802,15 @@ def _execute(self, context):
17771802
self.report({"WARNING"}, "No active drawing.")
17781803
return
17791804

1780-
object_type = self.annotation_subtype if props.object_type == "MANUAL_DRAWING_REFERENCE" else props.object_type
1805+
# MANUAL_DRAWING_REFERENCE is a legacy/convenience dropdown entry; resolve
1806+
# it to the chosen style so create_annotation_object knows what to build.
1807+
if props.object_type == "MANUAL_DRAWING_REFERENCE":
1808+
object_type = self.manual_style
1809+
is_manual = True
1810+
else:
1811+
object_type = props.object_type
1812+
is_manual = props.is_manual_reference and object_type in ("ELEVATION", "SECTION")
1813+
17811814
obj = core.add_annotation(
17821815
tool.Ifc,
17831816
tool.Collector,
@@ -1790,12 +1823,18 @@ def _execute(self, context):
17901823
# item edit mode for these types.
17911824
enable_editing=object_type not in ("ELEVATION", "SECTION"),
17921825
)
1793-
if props.object_type == "IMAGE":
1826+
if object_type == "IMAGE":
17941827
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name)
1795-
if object_type in ("ELEVATION", "SECTION"):
1828+
if is_manual:
17961829
element = tool.Ifc.get_entity(obj)
17971830
tool.Drawing.set_manual_drawing_reference(element)
1798-
if self.drawing_id != "0":
1831+
if self.is_document_reference:
1832+
tool.Drawing.set_document_reference_flag(element)
1833+
if self.reference_doc_id != "0":
1834+
core.assign_manual_reference_document(
1835+
tool.Drawing, element=element, document=tool.Ifc.get().by_id(int(self.reference_doc_id))
1836+
)
1837+
elif self.drawing_id != "0":
17991838
core.assign_manual_drawing_reference(
18001839
tool.Ifc, tool.Drawing, element=element, drawing=tool.Ifc.get().by_id(int(self.drawing_id))
18011840
)
@@ -1804,9 +1843,10 @@ def _execute(self, context):
18041843
class AssignManualDrawingReference(bpy.types.Operator, tool.Ifc.Operator):
18051844
bl_idname = "bim.assign_manual_drawing_reference"
18061845
bl_label = "Assign Drawing"
1807-
bl_description = "Assign a target drawing to this manual drawing reference tag"
1846+
bl_description = "Assign a target drawing or reference to this manual drawing reference tag"
18081847
bl_options = {"REGISTER", "UNDO"}
18091848
drawing_id: bpy.props.EnumProperty(name="Drawing", items=_get_drawing_enum_items)
1849+
reference_doc_id: bpy.props.EnumProperty(name="Reference", items=_get_reference_doc_enum_items)
18101850

18111851
@classmethod
18121852
def poll(cls, context):
@@ -1825,17 +1865,29 @@ def poll(cls, context):
18251865

18261866
def invoke(self, context, event):
18271867
element = tool.Ifc.get_entity(context.active_object)
1828-
current = tool.Drawing.get_annotation_element(element)
1829-
self.drawing_id = str(current.id()) if current else "0"
1868+
if tool.Drawing.is_document_reference(element):
1869+
doc = tool.Drawing.get_annotation_reference_doc(element)
1870+
self.reference_doc_id = str(doc.id()) if doc else "0"
1871+
else:
1872+
current = tool.Drawing.get_annotation_element(element)
1873+
self.drawing_id = str(current.id()) if current else "0"
18301874
return context.window_manager.invoke_props_dialog(self)
18311875

18321876
def draw(self, context):
1833-
prop_with_search(self.layout, self, "drawing_id", search_threshold=0, original_operator_path=f"{__name__}.AssignManualDrawingReference")
1877+
element = tool.Ifc.get_entity(bpy.context.active_object)
1878+
if element and tool.Drawing.is_document_reference(element):
1879+
prop_with_search(self.layout, self, "reference_doc_id", search_threshold=0, original_operator_path=f"{__name__}.AssignManualDrawingReference")
1880+
else:
1881+
prop_with_search(self.layout, self, "drawing_id", search_threshold=0, original_operator_path=f"{__name__}.AssignManualDrawingReference")
18341882

18351883
def _execute(self, context):
18361884
element = tool.Ifc.get_entity(context.active_object)
1837-
drawing = tool.Ifc.get().by_id(int(self.drawing_id)) if self.drawing_id != "0" else None
1838-
core.assign_manual_drawing_reference(tool.Ifc, tool.Drawing, element=element, drawing=drawing)
1885+
if tool.Drawing.is_document_reference(element):
1886+
document = tool.Ifc.get().by_id(int(self.reference_doc_id)) if self.reference_doc_id != "0" else None
1887+
core.assign_manual_reference_document(tool.Drawing, element=element, document=document)
1888+
else:
1889+
drawing = tool.Ifc.get().by_id(int(self.drawing_id)) if self.drawing_id != "0" else None
1890+
core.assign_manual_drawing_reference(tool.Ifc, tool.Drawing, element=element, drawing=drawing)
18391891
for area in context.screen.areas:
18401892
if area.type == "PROPERTIES":
18411893
area.tag_redraw()

src/bonsai/bonsai/bim/module/drawing/prop.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,12 @@ class BIMAnnotationProperties(PropertyGroup):
10001000
)
10011001
is_adding_type: bpy.props.BoolProperty(default=False)
10021002
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
1003+
is_manual_reference: bpy.props.BoolProperty(
1004+
name="Is a Reference",
1005+
default=False,
1006+
description="Place as a manual reference tag (IsManualDrawingReference). "
1007+
"Exempt from automatic drawing regeneration. Optionally link to a drawing or external reference.",
1008+
)
10031009
tag_rotation_mode: bpy.props.EnumProperty(
10041010
name="Tag Rotation Mode",
10051011
description="How to orient the tag relative to the tagged object",
@@ -1020,3 +1026,4 @@ class BIMAnnotationProperties(PropertyGroup):
10201026
create_representation_for_type: bool
10211027
is_adding_type: bool
10221028
type_name: str
1029+
is_manual_reference: bool

src/bonsai/bonsai/bim/module/drawing/svgwriter.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -896,8 +896,32 @@ def draw_elevation_annotation(self, obj: bpy.types.Object) -> None:
896896
)
897897

898898
def get_reference_and_sheet_id_from_annotation(self, element: ifcopenshell.entity_instance) -> tuple[str, str]:
899-
reference_id = "-"
900-
sheet_id = "-"
899+
is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3"
900+
901+
# Document-reference annotations link to an IfcDocumentInformation via
902+
# IfcRelAssociatesDocument rather than to a drawing product.
903+
if tool.Drawing.is_document_reference(element):
904+
doc_info = tool.Drawing.get_annotation_reference_doc(element)
905+
if not doc_info:
906+
return ("-", "-")
907+
ext_location = tool.Drawing.get_path_with_ext(
908+
(doc_info.DocumentReferences[0].Location if is_ifc2x3 else doc_info.HasDocumentReferences[0].Location),
909+
"svg",
910+
) if (doc_info.DocumentReferences if is_ifc2x3 else doc_info.HasDocumentReferences) else None
911+
if not ext_location:
912+
return ("-", "-")
913+
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
914+
if tool.Drawing.get_reference_description(sheet_reference) != "REFERENCE":
915+
continue
916+
if sheet_reference.Location != ext_location:
917+
continue
918+
sheet = tool.Drawing.get_reference_document(sheet_reference)
919+
if sheet:
920+
if is_ifc2x3:
921+
return (sheet_reference.ItemReference or "-", sheet.DocumentId or "-")
922+
return (sheet_reference.Identification or "-", sheet.Identification or "-")
923+
return ("-", "-")
924+
901925
drawing = tool.Drawing.get_annotation_element(element)
902926
if not drawing:
903927
return ("-", "-")
@@ -909,13 +933,9 @@ def get_reference_and_sheet_id_from_annotation(self, element: ifcopenshell.entit
909933
continue
910934
sheet = tool.Drawing.get_reference_document(sheet_reference)
911935
if sheet:
912-
if tool.Ifc.get_schema() == "IFC2X3":
913-
reference_id = sheet_reference.ItemReference or "-"
914-
sheet_id = sheet.DocumentId or "-"
915-
else:
916-
reference_id = sheet_reference.Identification or "-"
917-
sheet_id = sheet.Identification or "-"
918-
return (reference_id, sheet_id)
936+
if is_ifc2x3:
937+
return (sheet_reference.ItemReference or "-", sheet.DocumentId or "-")
938+
return (sheet_reference.Identification or "-", sheet.Identification or "-")
919939
break
920940
return ("-", "-")
921941

src/bonsai/bonsai/bim/module/drawing/ui.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,8 +539,9 @@ def draw(self, context):
539539
element = tool.Ifc.get_entity(obj)
540540
if element and tool.Drawing.is_manual_drawing_reference(element):
541541
row = self.layout.row(align=True)
542+
fallback = "No Reference Assigned" if element.ObjectType == "REFERENCE" else "No Drawing Assigned"
542543
row.label(
543-
text=ProductAssignmentsData.data["relating_product"] or "No Drawing Assigned", icon="IMAGE_DATA"
544+
text=ProductAssignmentsData.data["relating_product"] or fallback, icon="IMAGE_DATA"
544545
)
545546
row.operator("bim.assign_manual_drawing_reference", icon="GREASEPENCIL", text="")
546547
return

src/bonsai/bonsai/bim/module/drawing/workspace.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ def draw_type_selection_interface(cls):
249249

250250
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
251251

252+
if object_type in ("ELEVATION", "SECTION"):
253+
row = cls.layout.row(align=True)
254+
row.prop(cls.props, "is_manual_reference")
255+
252256
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
253257
row = cls.layout.row(align=True)
254258
row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE")

src/bonsai/bonsai/core/drawing.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,14 @@ def assign_manual_drawing_reference(
518518
ifc.run("drawing.assign_product", relating_product=drawing, related_object=element)
519519

520520

521+
def assign_manual_reference_document(
522+
drawing_tool: type[tool.Drawing],
523+
element: ifcopenshell.entity_instance,
524+
document: Optional[ifcopenshell.entity_instance],
525+
) -> None:
526+
drawing_tool.set_annotation_reference_doc(element, document)
527+
528+
521529
def build_schedule(drawing: type[tool.Drawing], schedule: ifcopenshell.entity_instance) -> None:
522530
drawing.create_svg_schedule(schedule)
523531
drawing.open_svg(drawing.get_path_with_ext(drawing.get_document_uri(schedule), "svg"))

src/bonsai/bonsai/tool/drawing.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,6 +1889,49 @@ def set_manual_drawing_reference(cls, element: ifcopenshell.entity_instance) ->
18891889
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="EPset_Annotation")
18901890
ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"IsManualDrawingReference": True})
18911891

1892+
@classmethod
1893+
def is_document_reference(cls, element: ifcopenshell.entity_instance) -> bool:
1894+
"""Return True if this annotation links to an external document (not a Bonsai drawing camera)."""
1895+
return bool(ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "IsDocumentReference"))
1896+
1897+
@classmethod
1898+
def set_document_reference_flag(cls, element: ifcopenshell.entity_instance) -> None:
1899+
"""Mark this annotation as pointing to an external document reference."""
1900+
ifc_file = tool.Ifc.get()
1901+
pset = tool.Pset.get_element_pset(element, "EPset_Annotation")
1902+
if not pset:
1903+
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="EPset_Annotation")
1904+
ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"IsDocumentReference": True})
1905+
1906+
@classmethod
1907+
def get_annotation_reference_doc(
1908+
cls, element: ifcopenshell.entity_instance
1909+
) -> Union[ifcopenshell.entity_instance, None]:
1910+
"""Return the IfcDocumentInformation linked to a document-reference annotation."""
1911+
for rel in element.HasAssociations:
1912+
if rel.is_a("IfcRelAssociatesDocument"):
1913+
doc = rel.RelatingDocument
1914+
if doc.is_a("IfcDocumentInformation"):
1915+
return doc
1916+
return None
1917+
1918+
@classmethod
1919+
def set_annotation_reference_doc(
1920+
cls,
1921+
element: ifcopenshell.entity_instance,
1922+
document: Union[ifcopenshell.entity_instance, None],
1923+
) -> None:
1924+
"""Associate (or clear) an IfcDocumentInformation on a document-reference annotation."""
1925+
ifc_file = tool.Ifc.get()
1926+
# Remove existing document associations on this annotation.
1927+
for rel in list(element.HasAssociations):
1928+
if rel.is_a("IfcRelAssociatesDocument"):
1929+
ifcopenshell.api.document.unassign_document(
1930+
ifc_file, products=[element], document=rel.RelatingDocument
1931+
)
1932+
if document:
1933+
ifcopenshell.api.document.assign_document(ifc_file, products=[element], document=document)
1934+
18921935
@classmethod
18931936
def regenerate_elevation_reference_annotation(
18941937
cls,

0 commit comments

Comments
 (0)