Attributes UI to support attributes with string subitems

Example - https://files.catbox.moe/oew1ue.mp4
This commit is contained in:
Andrej730
2025-07-08 14:59:13 +05:00
parent c7555652d7
commit 2ec2ac0e5c
5 changed files with 107 additions and 7 deletions
+2
View File
@@ -105,6 +105,8 @@ classes = [
operator.BIM_OT_select_object, operator.BIM_OT_select_object,
operator.BIM_OT_show_description, operator.BIM_OT_show_description,
operator.BIM_OT_multiple_file_selector, operator.BIM_OT_multiple_file_selector,
operator.BIM_OT_attribute_add_subitem,
operator.BIM_OT_attribute_remove_subitem,
operator.ClippingPlaneCutWithCappings, operator.ClippingPlaneCutWithCappings,
operator.CloseBlendWarning, operator.CloseBlendWarning,
operator.CloseError, operator.CloseError,
+25
View File
@@ -83,6 +83,22 @@ def draw_attribute(
prop_with_search(layout, attribute, "enum_value", text=attribute.name) prop_with_search(layout, attribute, "enum_value", text=attribute.name)
elif value_name == "filepath_value": elif value_name == "filepath_value":
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name) attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
elif value_name == "subitems_values":
col = layout.column()
layout = col.row(align=True)
layout.label(text=f"{attribute.name}:")
data_path = tool.Blender.get_full_data_path(attribute, value_name)
for i, item in enumerate(attribute.subitems_values, 1):
row = col.row(align=True)
row.alignment = "EXPAND"
row.prop(item, "name", text=f"# {i}")
op = row.operator("bim.attribute_remove_subitem", text="", icon="X")
op.data_path = data_path
op.index = i - 1
op = layout.operator("bim.attribute_add_subitem", icon="ADD", text="")
op.data_path = data_path
elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"): elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
props = tool.Sequence.get_work_schedule_props() props = tool.Sequence.get_work_schedule_props()
for item in props.durations_attributes: for item in props.durations_attributes:
@@ -167,6 +183,8 @@ def import_attribute(
) -> None: ) -> None:
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
# Complex data types (aggregates and entities) are handled only by callback. # Complex data types (aggregates and entities) are handled only by callback.
if data_type == ("list", "string"):
data_type = "list[string]"
if isinstance(data_type, tuple) or data_type == "entity": if isinstance(data_type, tuple) or data_type == "entity":
callback(attribute.name(), None, data) if callback else None callback(attribute.name(), None, data) if callback else None
return return
@@ -177,6 +195,7 @@ def import_attribute(
new.is_optional = attribute.optional() new.is_optional = attribute.optional()
new.data_type = data_type if isinstance(data_type, str) else "" new.data_type = data_type if isinstance(data_type, str) else ""
new.ifc_class = data["type"] new.ifc_class = data["type"]
is_handled_by_callback = callback(attribute.name(), new, data) if callback else None is_handled_by_callback = callback(attribute.name(), new, data) if callback else None
data_type = new.data_type # Allow callback to override data type. data_type = new.data_type # Allow callback to override data type.
@@ -222,6 +241,12 @@ def import_attribute(
if enum_value is not None: if enum_value is not None:
new.enum_value = enum_value new.enum_value = enum_value
elif data_type == "list[string]":
value: Union[list[str], None] = data[attribute.name()]
if value:
for item in value:
new.subitems_values.add().name = str(item).replace("\n", "\\n")
add_attribute_description(new, data) add_attribute_description(new, data)
add_attribute_min_max(attribute, new) add_attribute_min_max(attribute, new)
+41
View File
@@ -48,6 +48,7 @@ from natsort import natsorted
if TYPE_CHECKING: if TYPE_CHECKING:
from bonsai.bim.prop import MultipleFileSelect, Attribute from bonsai.bim.prop import MultipleFileSelect, Attribute
from bpy.stub_internal import rna_enums
class SetTab(bpy.types.Operator): class SetTab(bpy.types.Operator):
@@ -1407,3 +1408,43 @@ class BIM_OT_attribute_search_values(bpy.types.Operator):
def execute(self, context): def execute(self, context):
return {"FINISHED"} return {"FINISHED"}
class BIM_OT_attribute_add_subitem(bpy.types.Operator):
bl_idname = "bim.attribute_add_subitem"
bl_label = "Add Subitem"
bl_description = "Add subitem to the current attribute"
bl_options = {"REGISTER", "UNDO"}
data_path: bpy.props.StringProperty()
"""Full data path."""
if TYPE_CHECKING:
data_path: str
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
col: "bpy.types.bpy_prop_collection_idprop[StrProperty]"
col = eval(self.data_path)
col.add()
return {"FINISHED"}
class BIM_OT_attribute_remove_subitem(bpy.types.Operator):
bl_idname = "bim.attribute_remove_subitem"
bl_label = "Add Subitem"
bl_description = "Add subitem to the current attribute"
bl_options = {"REGISTER", "UNDO"}
data_path: bpy.props.StringProperty()
"""Full data path."""
index: bpy.props.IntProperty()
if TYPE_CHECKING:
data_path: str
index: int
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
col: "bpy.types.bpy_prop_collection_idprop[StrProperty]"
col = eval(self.data_path)
col.remove(self.index)
return {"FINISHED"}
+23 -7
View File
@@ -234,7 +234,9 @@ def update_attribute_value(self: "Attribute", context: bpy.types.Context) -> Non
def update_is_null(self: "Attribute", context: bpy.types.Context) -> None: def update_is_null(self: "Attribute", context: bpy.types.Context) -> None:
if self.is_null: if self.is_null:
if self.data_type != "enum" and self.get_value() != (default := self.get_value_default()): if self.data_type == "list[string]":
self.subitems_values.clear()
elif self.data_type != "enum" and self.get_value() != (default := self.get_value_default()):
self.set_value(default) self.set_value(default)
if self.is_null is not True: if self.is_null is not True:
self.is_null = True self.is_null = True
@@ -286,7 +288,7 @@ def get_display_name(self: "Attribute") -> str:
return f"{name}, {unit_symbol}" return f"{name}, {unit_symbol}"
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file"] AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file", "list[string]"]
AttributeSpecialType = Literal["", "DATE", "DATETIME", "LENGTH", "AREA", "VOLUME", "FORCE", "LOGICAL", "URI"] AttributeSpecialType = Literal["", "DATE", "DATETIME", "LENGTH", "AREA", "VOLUME", "FORCE", "LOGICAL", "URI"]
@@ -300,6 +302,8 @@ class Attribute(PropertyGroup):
name="Data Type", name="Data Type",
items=[(i, i, "") for i in get_args(AttributeDataType)], items=[(i, i, "") for i in get_args(AttributeDataType)],
) )
# Value containers.
string_value: StringProperty(name="Value", update=update_attribute_value, description=tooltip) string_value: StringProperty(name="Value", update=update_attribute_value, description=tooltip)
bool_value: BoolProperty(name="Value", update=update_attribute_value, description=tooltip) bool_value: BoolProperty(name="Value", update=update_attribute_value, description=tooltip)
int_value: IntProperty( int_value: IntProperty(
@@ -330,8 +334,11 @@ class Attribute(PropertyGroup):
filepath_value: PointerProperty(type=MultipleFileSelect) filepath_value: PointerProperty(type=MultipleFileSelect)
filter_glob: StringProperty() filter_glob: StringProperty()
is_null: BoolProperty(name="Is Null", update=update_is_null) is_null: BoolProperty(name="Is Null", update=update_is_null)
is_optional: BoolProperty(name="Is Optional")
is_selected: BoolProperty(name="Is Selected", default=False) is_selected: BoolProperty(name="Is Selected", default=False)
subitems_values: CollectionProperty(type=StrProperty) # pyright: ignore[reportRedeclaration]
# Attribute parameters.
is_optional: BoolProperty(name="Is Optional")
value_min: FloatProperty(description="This is used to validate int_value and float_value") value_min: FloatProperty(description="This is used to validate int_value and float_value")
value_min_constraint: BoolProperty(default=False, description="True if the numerical value has a lower bound") value_min_constraint: BoolProperty(default=False, description="True if the numerical value has a lower bound")
value_max: FloatProperty(description="This is used to validate int_value and float_value") value_max: FloatProperty(description="This is used to validate int_value and float_value")
@@ -359,8 +366,10 @@ class Attribute(PropertyGroup):
filepath_value: MultipleFileSelect filepath_value: MultipleFileSelect
filter_glob: str filter_glob: str
is_null: bool is_null: bool
is_optional: bool
is_selected: bool is_selected: bool
subitems_values: bpy.types.bpy_prop_collection_idprop[StrProperty]
is_optional: bool
value_min: float value_min: float
value_min_constraint: bool value_min_constraint: bool
value_max: float value_max: float
@@ -368,13 +377,16 @@ class Attribute(PropertyGroup):
metadata: str metadata: str
update: str update: str
def get_value(self) -> Union[str, float, int, bool, None]: def get_value(self) -> Union[str, float, int, bool, list[str], None]:
if self.is_optional and self.is_null: if self.is_optional and self.is_null:
return None return None
if self.data_type == "string": if self.data_type == "string":
return self.string_value.replace("\\n", "\n") return self.string_value.replace("\\n", "\n")
if self.data_type == "file": if self.data_type == "file":
return [f.name for f in self.filepath_value.file_list] return [f.name for f in self.filepath_value.file_list]
elif self.data_type == "list[string]":
return [s.name for s in self.subitems_values]
value_name = self.get_value_name() value_name = self.get_value_name()
if value_name == "enum_value": if value_name == "enum_value":
value = tool.Blender.get_enum_safe(self, "enum_value") value = tool.Blender.get_enum_safe(self, "enum_value")
@@ -385,7 +397,7 @@ class Attribute(PropertyGroup):
value = value == "TRUE" value = value == "TRUE"
return value return value
def get_value_default(self) -> Union[str, float, int, bool]: def get_value_default(self) -> Union[str, float, int, bool, list[str]]:
data_type = self.data_type data_type = self.data_type
if data_type == "string": if data_type == "string":
return "" return ""
@@ -399,10 +411,12 @@ class Attribute(PropertyGroup):
return "0" return "0"
elif data_type == "file": elif data_type == "file":
return "" return ""
elif data_type == "list[string]":
return []
else: else:
assert_never(data_type) assert_never(data_type)
def get_value_name(self, display_only: bool = False) -> str: def get_value_name(self, display_only: bool = False):
"""Get name of the value attribute. """Get name of the value attribute.
:param display_only: Should be `True` if the value won't be accessed directly :param display_only: Should be `True` if the value won't be accessed directly
@@ -423,6 +437,8 @@ class Attribute(PropertyGroup):
return "enum_value" return "enum_value"
elif data_type == "file": elif data_type == "file":
return "filepath_value" return "filepath_value"
elif data_type == "list[string]":
return "subitems_values"
else: else:
assert_never(data_type) assert_never(data_type)
+16
View File
@@ -1534,6 +1534,22 @@ class Blender(bonsai.core.tool.Blender):
obj.matrix_world = matrix obj.matrix_world = matrix
return True return True
@classmethod
def get_full_data_path(cls, bpy_struct: bpy.types.bpy_struct, path: str = "") -> str:
"""Get full data path to Blender entity or it's attributes.
:param bpy_struct: Blender entity.
:param path: Additional path to add to entity.
:return: Path in a format
``bpy.data.scenes['Scene'].BIMExplorerProperties.entity_attributes[4].enum_value``
"""
if path:
bpy_prop: bpy.types.bpy_prop # pyright: ignore[reportAttributeAccessIssue]
bpy_prop = bpy_struct.path_resolve(path, False)
return repr(bpy_prop)
return repr(bpy_struct)
@classmethod @classmethod
def set_prop_from_path(cls, bpy_object: bpy.types.bpy_struct, prop_path: str, value: Any) -> None: def set_prop_from_path(cls, bpy_object: bpy.types.bpy_struct, prop_path: str, value: Any) -> None:
"""Set `data_block` property value using path from `path_from_id`.""" """Set `data_block` property value using path from `path_from_id`."""