mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Attributes UI to support attributes with string subitems
Example - https://files.catbox.moe/oew1ue.mp4
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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`."""
|
||||||
|
|||||||
Reference in New Issue
Block a user