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.
This commit is contained in:
Ryan Schultz
2025-10-01 22:44:34 -05:00
parent d684d47dc8
commit ec2d0bfa4a
8 changed files with 64 additions and 21 deletions
@@ -5,10 +5,10 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',$,$,'Psets_BBIM_Anno
FILE_SCHEMA(('IFC4')); FILE_SCHEMA(('IFC4'));
ENDSEC; ENDSEC;
DATA; 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.); #2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.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)); #5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/BATTING,IfcTypeProduct',(#6,#7));
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.); #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.); #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.); #29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.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')),$); #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; ENDSEC;
END-ISO-10303-21; END-ISO-10303-21;
+13 -6
View File
@@ -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_type for font_size_type in FONT_SIZES if font_size_type in classes_split), "regular"
) )
font_size = FONT_SIZES[font_size_type] font_size = FONT_SIZES[font_size_type]
# get symbol
symbol = tool.Drawing.get_annotation_symbol(element) symbol = tool.Drawing.get_annotation_symbol(element)
# get newline_at
newline_at = pset_data.get("Newline_At", 0) 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 # other attributes
literals = tool.Drawing.get_text_literal(obj, return_list=True) literals = tool.Drawing.get_text_literal(obj, return_list=True)
@@ -364,11 +362,20 @@ class DecoratorData:
literal_data = { literal_data = {
"Literal": literal_value, "Literal": literal_value,
"BoxAlignment": literal.BoxAlignment, "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) 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 @classmethod
def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]: def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
@@ -744,6 +744,11 @@ class BIMTextProperties(PropertyGroup):
name="Font Size", name="Font Size",
) )
newline_at: IntProperty(name="Newline At") 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] symbol: EnumProperty( # pyright: ignore[reportRedeclaration]
name="Symbol", name="Symbol",
description="Symbol from symbols.svg to use for this text.", 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] literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
font_size: str font_size: str
newline_at: int newline_at: int
reverse_list: bool
list_separator: str
symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]] symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]]
custom_symbol: str custom_symbol: str
@@ -796,6 +803,8 @@ class BIMTextProperties(PropertyGroup):
"FontSize": float(self.font_size), "FontSize": float(self.font_size),
"Newline_At": int(self.newline_at), "Newline_At": int(self.newline_at),
"Symbol": self.get_symbol(), "Symbol": self.get_symbol(),
"Reverse_List": self.reverse_list,
"List_Separator": self.list_separator or ", ",
} }
return text_data return text_data
@@ -590,6 +590,10 @@ class BIM_PT_text(Panel):
row.prop(props, "font_size") row.prop(props, "font_size")
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.prop(props, "newline_at") 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 = self.layout.row(align=True)
row.prop(props, "symbol") row.prop(props, "symbol")
@@ -648,6 +652,12 @@ class BIM_PT_text(Panel):
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text="Newline_At") row.label(text="Newline_At")
row.label(text=str(text_data["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"]: for literal_data in text_data["Literals"]:
box = self.layout.box() box = self.layout.box()
+1 -1
View File
@@ -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: def edit_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
drawing.synchronise_ifc_and_text_attributes(obj) drawing.synchronise_ifc_and_text_attributes(obj)
drawing.update_text_size_pset(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) drawing.disable_editing_text(obj)
+1 -1
View File
@@ -394,7 +394,7 @@ class Drawing:
def sync_object_placement(cls, obj): pass def sync_object_placement(cls, obj): pass
def synchronise_ifc_and_text_attributes(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_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 def update_text_size_pset(cls, obj): pass
+25 -10
View File
@@ -1098,6 +1098,8 @@ class Drawing(bonsai.core.tool.Drawing):
props.font_size = str(text_data["FontSize"]) props.font_size = str(text_data["FontSize"])
props.newline_at = text_data["Newline_At"] props.newline_at = text_data["Newline_At"]
props.set_symbol(text_data["Symbol"]) props.set_symbol(text_data["Symbol"])
props.reverse_list = text_data["Reverse_List"]
props.list_separator = text_data["List_Separator"]
@classmethod @classmethod
def import_assigned_product(cls, obj: bpy.types.Object) -> None: def import_assigned_product(cls, obj: bpy.types.Object) -> None:
@@ -1235,25 +1237,29 @@ class Drawing(bonsai.core.tool.Drawing):
) )
@classmethod @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) props = cls.get_text_props(obj)
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
assert element assert element
newline_at = int(props.newline_at)
symbol = props.get_symbol()
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
pset = tool.Pset.get_element_pset(element, "EPset_Annotation") pset = tool.Pset.get_element_pset(element, "EPset_Annotation")
if not pset: if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="EPset_Annotation") pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="EPset_Annotation")
ifcopenshell.api.pset.edit_pset( ifcopenshell.api.pset.edit_pset(
ifc_file, ifc_file,
pset=pset, pset=pset,
properties={ properties={
"Newline_At": newline_at, "Newline_At": int(props.newline_at),
"Symbol": symbol, "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 # TODO below this point is highly experimental prototype code with no tests
class SheetWarningType(NamedTuple): class SheetWarningType(NamedTuple):
@@ -1834,10 +1840,18 @@ class Drawing(bonsai.core.tool.Drawing):
return bool(a_tree.overlap(b_tree)) return bool(a_tree.overlap(b_tree))
@classmethod @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: if not product:
return text return text
if list_separator:
list_separator = list_separator.encode().decode('unicode_escape')
for command in re.findall("``.*?``", text): for command in re.findall("``.*?``", text):
original_command = command original_command = command
for variable in re.findall("{{.*?}}", command): for variable in re.findall("{{.*?}}", command):
@@ -1845,13 +1859,14 @@ class Drawing(bonsai.core.tool.Drawing):
value = '"' + str(value).replace('"', '\\"') + '"' value = '"' + str(value).replace('"', '\\"') + '"'
command = command.replace(variable, value) command = command.replace(variable, value)
text = text.replace(original_command, ifcopenshell.util.selector.format(command[2:-2])) text = text.replace(original_command, ifcopenshell.util.selector.format(command[2:-2]))
for variable in re.findall("{{.*?}}", text): for variable in re.findall("{{.*?}}", text):
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2]) value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
if isinstance(value, (list, tuple)): 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)) text = text.replace(variable, str(value))
return text return text
@classmethod @classmethod
+1 -1
View File
@@ -37,7 +37,7 @@ class TestEditText:
def test_run(self, drawing): def test_run(self, drawing):
drawing.synchronise_ifc_and_text_attributes("obj").should_be_called() drawing.synchronise_ifc_and_text_attributes("obj").should_be_called()
drawing.update_text_size_pset("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() drawing.disable_editing_text("obj").should_be_called()
subject.edit_text(drawing, obj="obj") subject.edit_text(drawing, obj="obj")