mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 14:31:39 +00:00
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:
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user