Intergrate explorer UI to support editing some props like #6869

Example - https://files.catbox.moe/ujhlwv.mp4
This commit is contained in:
Andrej730
2025-07-07 18:38:24 +05:00
parent 2ec2ac0e5c
commit 5866994b84
7 changed files with 153 additions and 20 deletions
+16 -7
View File
@@ -137,6 +137,14 @@ def draw_attribute(
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if attribute.use_explorer_ui:
op = layout.operator("bim.explorer_show_ui_popup", text="", icon="ZOOM_SELECTED")
op.ifc_class = attribute.ifc_class
op.attribute_name = attribute.name
op.data_path = tool.Blender.get_full_data_path(attribute, value_name)
if ifc_id := attribute.get_value():
op.preselect_ifc_id = int(ifc_id)
if attribute.name == "GlobalId":
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
elif copy_operator:
@@ -314,13 +322,14 @@ def export_attributes(
return attributes
def process_exported_entity_attribute(attributes: dict[str, Any], attribute_name: str) -> None:
entity_id = attributes[attribute_name]
if entity_id is None:
# Maybe it was removed by now and enum is invalid.
del attributes[attribute_name]
else:
attributes[attribute_name] = tool.Ifc.get().by_id(int(entity_id))
def process_exported_entity_attribute(attributes: dict[str, Any], attribute_names: list[str]) -> None:
for attribute_name in attribute_names:
entity_id = attributes[attribute_name]
if entity_id is None:
# Maybe it was removed by now and enum is invalid.
del attributes[attribute_name]
else:
attributes[attribute_name] = tool.Ifc.get().by_id(int(entity_id))
ENUM_ITEMS_DATA = Union[bpy.types.PropertyGroup, bpy.types.ID, bpy.types.Operator, bpy.types.OperatorProperties]
@@ -29,6 +29,7 @@ classes = (
operator.ExplorerEnableEditingEntity,
operator.ExplorerDisableEditingEntity,
operator.ExplorerEditEntity,
operator.ExplorerShowUIPopup,
prop.BIMAttributeProperties,
prop.ExplorerEntity,
prop.BIMExplorerProperties,
@@ -79,6 +79,8 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
None,
)
lookup_attrs = tool.Attribute.does_ifc_class_support_explorer_lookup(element.is_a())
def callback(name: str, prop: Union["Attribute", None], data: dict[str, Any]) -> None | Literal[True]:
if name in ("RefLatitude", "RefLongitude"):
new = props.attributes.add()
@@ -93,6 +95,17 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type:
props.attributes.remove(len(props.attributes) - 1)
return True
if lookup_attrs and (name in lookup_attrs):
new = props.attributes.add()
new.name = name
new.ifc_class = data["type"]
new.data_type = "enum"
new.is_optional = True
new.enum_items_dynamic = lookup_attrs[name]
new.use_explorer_ui = True
value: Union[ifcopenshell.entity_instance, None] = data[name]
if value is not None:
new.enum_value = str(value.id())
bonsai.bim.helper.import_attributes2(element, props.attributes, callback=callback)
props.is_editing_attributes = True
@@ -143,6 +156,9 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Blender.get_object_attribute_props(obj)
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
lookup_attrs = tool.Attribute.does_ifc_class_support_explorer_lookup(element.is_a())
if lookup_attrs:
bonsai.bim.helper.process_exported_entity_attribute(attributes, list(lookup_attrs))
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
tool.Root.set_object_name(obj, element)
@@ -268,3 +284,80 @@ class ExplorerEditEntity(bpy.types.Operator, tool.Ifc.Operator):
setattr(entity, attr, value)
tool.Attribute.refresh_uilist_entities()
tool.Attribute.disable_editing_entity()
class ExplorerShowUIPopup(bpy.types.Operator):
bl_idname = "bim.explorer_show_ui_popup"
bl_label = "Show Explorer UI"
bl_description = "Show Explorer UI to select element as attribute value or edit it."
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""Element IFC class."""
attribute_name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""IFC class attribute name."""
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""Full data path"""
preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
"""IFC id to preselect in the popup."""
if TYPE_CHECKING:
ifc_class: str
attribute_name: str
data_path: str
preselect_ifc_id: int
def invoke(self, context, event) -> "set[rna_enums.OperatorReturnItems]":
assert context.window_manager
assert self.ifc_class and self.attribute_name and self.data_path
props = tool.Attribute.get_explorer_props()
props.is_loaded = True
props.ifc_class = self.get_attribute_type()
if self.preselect_ifc_id:
props.active_entity_index = next(
i for i, e in enumerate(props.entities) if e.ifc_definition_id == self.preselect_ifc_id
)
return context.window_manager.invoke_props_dialog(self, width=400)
def get_attribute_type(self) -> str:
schema = tool.Ifc.schema()
entity = schema.declaration_by_name(self.ifc_class).as_entity()
assert entity
i = entity.attribute_index(self.attribute_name)
attr = entity.all_attributes()[i]
named_type = attr.type_of_attribute().as_named_type()
assert named_type
declared = named_type.declared_type()
return declared.name()
def draw(self, context) -> None:
from bonsai.bim.module.attribute.ui import BIM_PT_explorer
BIM_PT_explorer.draw(self, context, is_popup=True)
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
props = tool.Attribute.get_explorer_props()
active_entity = props.active_entity
if active_entity is None:
self.report({"WARNING"}, "No entity selected.")
return {"FINISHED"}
# Apply pending changes for convenience.
if props.editing_entity_id:
if props.editing_entity_id == active_entity.ifc_definition_id:
bpy.ops.bim.explorer_edit_entity()
else:
bpy.ops.bim.explorer_disable_editing_entity()
# Very important to do it after changes applied, otherwise enum might update
# and index will be pointing to a different element.
exec(f"{self.data_path} = '{active_entity.ifc_definition_id}'")
return {"FINISHED"}
def cancel(self, context: bpy.types.Context) -> None:
props = tool.Attribute.get_explorer_props()
if props.editing_entity_id:
bpy.ops.bim.explorer_disable_editing_entity()
+18 -13
View File
@@ -82,22 +82,24 @@ class BIM_PT_explorer(Panel):
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
def draw(self, context, *, is_popup=False):
assert (layout := self.layout)
props = tool.Attribute.get_explorer_props()
if not props.is_loaded:
if is_popup:
layout.label(text=props.ifc_class)
else:
if not props.is_loaded:
row = layout.row(align=True)
row.label(text="Explorer UI is not Loaded.")
row.prop(props, "is_loaded", text="", icon="IMPORT")
return
row = layout.row(align=True)
row.label(text="Explorer UI is not Loaded.")
row.prop(props, "is_loaded", text="", icon="IMPORT")
return
row.prop(props, "ifc_class", text="")
row.prop(props, "is_loaded", text="", icon="CANCEL")
active_entity = props.active_entity
row = layout.row(align=True)
row.prop(props, "ifc_class", text="")
row.prop(props, "is_loaded", text="", icon="CANCEL")
row = layout.row(align=True)
row.label(text=f"{len(props.entities)} entities found")
row.operator("bim.explorer_add_entity", text="", icon="ADD")
@@ -113,9 +115,12 @@ class BIM_PT_explorer(Panel):
if props.editing_entity_id:
box = self.layout.box()
row = box.row(align=True)
row.operator("bim.explorer_edit_entity", icon="CHECKMARK")
row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="")
# In popup we accept edits automatically for the convenience.
# Othrewise it seems very unintuitive, when you need to click confirmation twice.
if not is_popup:
row = box.row(align=True)
row.operator("bim.explorer_edit_entity", icon="CHECKMARK")
row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="")
bonsai.bim.helper.draw_attributes(props.entity_attributes, box)
+2
View File
@@ -344,6 +344,7 @@ class Attribute(PropertyGroup):
value_max: FloatProperty(description="This is used to validate int_value and float_value")
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
special_type: StringProperty(name="Special Value Type", default="")
use_explorer_ui: BoolProperty() # pyright: ignore[reportRedeclaration]
metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute")
update: StringProperty(name="Update", description="Custom update function to be executed")
@@ -374,6 +375,7 @@ class Attribute(PropertyGroup):
value_min_constraint: bool
value_max: float
value_max_constraint: bool
use_explorer_ui: bool
metadata: str
update: str
+7
View File
@@ -1186,6 +1186,13 @@ class EnumData:
organizations = tool.Ifc.get().by_type("IfcOrganization")
return natsorted(((str(e.id()), e.Name, "") for e in organizations), key=lambda x: x[1])
@classmethod
def postal_addresses(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
addresses = tool.Ifc.get().by_type("IfcPostalAddress")
items = ((str(e.id()), (e.Description or "Undescribed"), "") for e in addresses)
items = natsorted(items, key=lambda x: x[1])
return items
class UIData:
data = {}
+16
View File
@@ -75,3 +75,19 @@ class Attribute(bonsai.core.tool.Attribute):
props = cls.get_explorer_props()
attributes = helper.export_attributes(props.entity_attributes)
return attributes
EXPLORER_UI_LOOKUP_PROPS: dict[str, dict[str, str]] = {
"IfcSite": {"SiteAddress": "postal_addresses"},
"IfcBuilding": {"BuildingAddress": "postal_addresses"},
}
"""``ifc_class -> {attribute_name -> enum_items_dynamic}``"""
@classmethod
def does_ifc_class_support_explorer_lookup(cls, ifc_class: str) -> Union[dict[str, str], None]:
"""
:return: ``{attribute_name -> enum_items_dynamic}``
if IFC class does support explorer UI lookup, otherwise returns ``None``.
"""
if not (lookup_attrs := cls.EXPLORER_UI_LOOKUP_PROPS.get(ifc_class)):
return
return lookup_attrs