From ec2d0bfa4a89f8b611ea0107cff3c12a5c18b335 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Wed, 1 Oct 2025 22:44:34 -0500 Subject: [PATCH] Add support for reversing and customizing separators in text annotation lists Text annotations can now reverse the order of list values (e.g., materials) and use custom separators instead of the default comma-space. Two new properties have been added to EPset_Annotation: Reverse_List (boolean) and List_Separator (string). When text literals contain IFC list/tuple values, they are now detected during variable replacement and can be reversed and joined with the specified separator. The list separator supports escape sequences like \n for newlines, enabling multi-line list displays. These properties are exposed in the text editing UI and properly persist to the IFC file through the new update_text_annotation_properties method, which consolidates all annotation property updates into a single efficient operation. --- .../bim/data/pset/Psets_BBIM_Annotation.ifc | 6 ++-- src/bonsai/bonsai/bim/module/drawing/data.py | 19 ++++++---- src/bonsai/bonsai/bim/module/drawing/prop.py | 9 +++++ src/bonsai/bonsai/bim/module/drawing/ui.py | 10 ++++++ src/bonsai/bonsai/core/drawing.py | 2 +- src/bonsai/bonsai/core/tool.py | 2 +- src/bonsai/bonsai/tool/drawing.py | 35 +++++++++++++------ src/bonsai/test/core/test_drawing.py | 2 +- 8 files changed, 64 insertions(+), 21 deletions(-) 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 dd4851f9c5..451c890536 100644 --- a/src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc +++ b/src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc @@ -5,10 +5,10 @@ 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',(#2,#3,#4,#29)); +#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2)); #2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.); #3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); -#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated 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.); +#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.); #5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/BATTING,IfcTypeProduct',(#6,#7)); #6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.); #7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); @@ -36,5 +36,7 @@ DATA; #29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.); #30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.); #31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$); +#32=IFCSIMPLEPROPERTYTEMPLATE('0gjJzDYBX8P85qn1xcAOOo',$,'Reverse_List','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#33=IFCSIMPLEPROPERTYTEMPLATE('22TrcxF8jFNB4buSmzjGEF',$,'List_Separator','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.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 c4b6c1c7c3..b31b1371f4 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -347,12 +347,10 @@ class DecoratorData: (font_size_type for font_size_type in FONT_SIZES if font_size_type in classes_split), "regular" ) font_size = FONT_SIZES[font_size_type] - - # get symbol symbol = tool.Drawing.get_annotation_symbol(element) - - # get newline_at newline_at = pset_data.get("Newline_At", 0) + reverse_list = pset_data.get("Reverse_List", False) + list_separator = pset_data.get("List_Separator") or ", " # other attributes literals = tool.Drawing.get_text_literal(obj, return_list=True) @@ -364,11 +362,20 @@ class DecoratorData: literal_data = { "Literal": literal_value, "BoxAlignment": literal.BoxAlignment, - "CurrentValue": tool.Drawing.replace_text_literal_variables(literal_value, product), + "CurrentValue": tool.Drawing.replace_text_literal_variables( + literal_value, product, reverse_list, list_separator + ), } literals_data.append(literal_data) - return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at} + return { + "Literals": literals_data, + "FontSize": font_size, + "Symbol": symbol, + "Newline_At": newline_at, + "Reverse_List": reverse_list, + "List_Separator": list_separator, + } @classmethod def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]: diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 49e39551eb..fb5c05679a 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -744,6 +744,11 @@ class BIMTextProperties(PropertyGroup): name="Font Size", ) newline_at: IntProperty(name="Newline At") + reverse_list: BoolProperty(name="Reverse List", description="Reverses the order of any list.", default=False) + list_separator: StringProperty( # pyright: ignore[reportRedeclaration] + name="List Separator", + description="Text used to separate lists. Uses a comma (, ) if empty.", + ) symbol: EnumProperty( # pyright: ignore[reportRedeclaration] name="Symbol", description="Symbol from symbols.svg to use for this text.", @@ -760,6 +765,8 @@ class BIMTextProperties(PropertyGroup): literals: bpy.types.bpy_prop_collection_idprop[LiteralProps] font_size: str newline_at: int + reverse_list: bool + list_separator: str symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]] custom_symbol: str @@ -796,6 +803,8 @@ class BIMTextProperties(PropertyGroup): "FontSize": float(self.font_size), "Newline_At": int(self.newline_at), "Symbol": self.get_symbol(), + "Reverse_List": self.reverse_list, + "List_Separator": self.list_separator or ", ", } return text_data diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index 8fb0c5743e..905bc8e287 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -590,6 +590,10 @@ class BIM_PT_text(Panel): row.prop(props, "font_size") row = self.layout.row(align=True) row.prop(props, "newline_at") + row = self.layout.row(align=True) + row.prop(props, "reverse_list") + row = self.layout.row(align=True) + row.prop(props, "list_separator") row = self.layout.row(align=True) row.prop(props, "symbol") @@ -648,6 +652,12 @@ class BIM_PT_text(Panel): row = self.layout.row(align=True) row.label(text="Newline_At") row.label(text=str(text_data["Newline_At"])) + row = self.layout.row(align=True) + row.label(text="Reverse_List") + row.label(text=str(text_data["Reverse_List"])) + row = self.layout.row(align=True) + row.label(text="List_Separator") + row.label(text=str(text_data["List_Separator"])) for literal_data in text_data["Literals"]: box = self.layout.box() diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index fd3f679270..2763f72ea0 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -39,7 +39,7 @@ def disable_editing_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> def edit_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None: drawing.synchronise_ifc_and_text_attributes(obj) drawing.update_text_size_pset(obj) - drawing.update_newline_at_and_symbol(obj) + drawing.update_text_annotation_properties(obj) drawing.disable_editing_text(obj) diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index abef38836e..0124ec1430 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -394,7 +394,7 @@ class Drawing: def sync_object_placement(cls, obj): pass def synchronise_ifc_and_text_attributes(cls, obj): pass def update_embedded_svg_location(cls, uri, old_location, new_location): pass - def update_newline_at_and_symbol(cls, obj): pass + def update_text_annotation_properties(cls, obj): pass def update_text_size_pset(cls, obj): pass diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 8227b9dbfd..5a01923103 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -1098,6 +1098,8 @@ class Drawing(bonsai.core.tool.Drawing): props.font_size = str(text_data["FontSize"]) props.newline_at = text_data["Newline_At"] props.set_symbol(text_data["Symbol"]) + props.reverse_list = text_data["Reverse_List"] + props.list_separator = text_data["List_Separator"] @classmethod def import_assigned_product(cls, obj: bpy.types.Object) -> None: @@ -1235,25 +1237,29 @@ class Drawing(bonsai.core.tool.Drawing): ) @classmethod - def update_newline_at_and_symbol(cls, obj: bpy.types.Object) -> None: + def update_text_annotation_properties(cls, obj: bpy.types.Object) -> None: + """Update all EPset_Annotation properties from the text props""" props = cls.get_text_props(obj) element = tool.Ifc.get_entity(obj) assert element - newline_at = int(props.newline_at) - symbol = props.get_symbol() + 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={ - "Newline_At": newline_at, - "Symbol": symbol, + "Newline_At": int(props.newline_at), + "Symbol": props.get_symbol(), + "Reverse_List": props.reverse_list, + "List_Separator": props.list_separator or "", }, ) + # TODO below this point is highly experimental prototype code with no tests class SheetWarningType(NamedTuple): @@ -1834,10 +1840,18 @@ class Drawing(bonsai.core.tool.Drawing): return bool(a_tree.overlap(b_tree)) @classmethod - def replace_text_literal_variables(cls, text: str, product: Optional[ifcopenshell.entity_instance] = None) -> str: + def replace_text_literal_variables( + cls, + text: str, + product: Optional[ifcopenshell.entity_instance] = None, + reverse_list: bool = False, + list_separator: str = ", " + ) -> str: if not product: return text - + if list_separator: + list_separator = list_separator.encode().decode('unicode_escape') + for command in re.findall("``.*?``", text): original_command = command for variable in re.findall("{{.*?}}", command): @@ -1845,13 +1859,14 @@ class Drawing(bonsai.core.tool.Drawing): value = '"' + str(value).replace('"', '\\"') + '"' command = command.replace(variable, value) text = text.replace(original_command, ifcopenshell.util.selector.format(command[2:-2])) - for variable in re.findall("{{.*?}}", text): value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2]) if isinstance(value, (list, tuple)): - value = ", ".join(str(v) for v in value) + if reverse_list: + value = list_separator.join(str(v) for v in reversed(value)) + else: + value = list_separator.join(str(v) for v in value) text = text.replace(variable, str(value)) - return text @classmethod diff --git a/src/bonsai/test/core/test_drawing.py b/src/bonsai/test/core/test_drawing.py index 875efba93c..e12c7dd0f8 100644 --- a/src/bonsai/test/core/test_drawing.py +++ b/src/bonsai/test/core/test_drawing.py @@ -37,7 +37,7 @@ class TestEditText: def test_run(self, drawing): drawing.synchronise_ifc_and_text_attributes("obj").should_be_called() drawing.update_text_size_pset("obj").should_be_called() - drawing.update_newline_at_and_symbol("obj").should_be_called() + drawing.update_text_annotation_properties("obj").should_be_called() drawing.disable_editing_text("obj").should_be_called() subject.edit_text(drawing, obj="obj")