diff --git a/src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc b/src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc index d1fedddef8..f3c2c0277e 100644 --- a/src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc +++ b/src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc @@ -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; diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index 23ee7c1987..9bc6e98a53 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -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" diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index c7833b3d27..b8ea0efb39 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -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() diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 6019c480b9..702f04258e 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -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 diff --git a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py index 711fb51624..37c79474d0 100644 --- a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py +++ b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py @@ -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 ("-", "-") diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index 89d3c98459..6d008f5e6b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -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 diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py index 4cd615e890..0d3470134d 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -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") diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index 394d2edf40..80ecc33d88 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -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")) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 60f860ff2e..00bb96a267 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -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,