mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
Intergrate explorer UI to support editing some props like #6869
Example - https://files.catbox.moe/ujhlwv.mp4
This commit is contained in:
@@ -137,6 +137,14 @@ def draw_attribute(
|
|||||||
if attribute.is_optional:
|
if attribute.is_optional:
|
||||||
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
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":
|
if attribute.name == "GlobalId":
|
||||||
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
|
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
|
||||||
elif copy_operator:
|
elif copy_operator:
|
||||||
@@ -314,13 +322,14 @@ def export_attributes(
|
|||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
def process_exported_entity_attribute(attributes: dict[str, Any], attribute_name: str) -> None:
|
def process_exported_entity_attribute(attributes: dict[str, Any], attribute_names: list[str]) -> None:
|
||||||
entity_id = attributes[attribute_name]
|
for attribute_name in attribute_names:
|
||||||
if entity_id is None:
|
entity_id = attributes[attribute_name]
|
||||||
# Maybe it was removed by now and enum is invalid.
|
if entity_id is None:
|
||||||
del attributes[attribute_name]
|
# Maybe it was removed by now and enum is invalid.
|
||||||
else:
|
del attributes[attribute_name]
|
||||||
attributes[attribute_name] = tool.Ifc.get().by_id(int(entity_id))
|
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]
|
ENUM_ITEMS_DATA = Union[bpy.types.PropertyGroup, bpy.types.ID, bpy.types.Operator, bpy.types.OperatorProperties]
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ classes = (
|
|||||||
operator.ExplorerEnableEditingEntity,
|
operator.ExplorerEnableEditingEntity,
|
||||||
operator.ExplorerDisableEditingEntity,
|
operator.ExplorerDisableEditingEntity,
|
||||||
operator.ExplorerEditEntity,
|
operator.ExplorerEditEntity,
|
||||||
|
operator.ExplorerShowUIPopup,
|
||||||
prop.BIMAttributeProperties,
|
prop.BIMAttributeProperties,
|
||||||
prop.ExplorerEntity,
|
prop.ExplorerEntity,
|
||||||
prop.BIMExplorerProperties,
|
prop.BIMExplorerProperties,
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
|
|||||||
None,
|
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]:
|
def callback(name: str, prop: Union["Attribute", None], data: dict[str, Any]) -> None | Literal[True]:
|
||||||
if name in ("RefLatitude", "RefLongitude"):
|
if name in ("RefLatitude", "RefLongitude"):
|
||||||
new = props.attributes.add()
|
new = props.attributes.add()
|
||||||
@@ -93,6 +95,17 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
|
|||||||
if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type:
|
if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type:
|
||||||
props.attributes.remove(len(props.attributes) - 1)
|
props.attributes.remove(len(props.attributes) - 1)
|
||||||
return True
|
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)
|
bonsai.bim.helper.import_attributes2(element, props.attributes, callback=callback)
|
||||||
props.is_editing_attributes = True
|
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)
|
props = tool.Blender.get_object_attribute_props(obj)
|
||||||
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
|
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)
|
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
|
||||||
|
|
||||||
tool.Root.set_object_name(obj, element)
|
tool.Root.set_object_name(obj, element)
|
||||||
@@ -268,3 +284,80 @@ class ExplorerEditEntity(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
setattr(entity, attr, value)
|
setattr(entity, attr, value)
|
||||||
tool.Attribute.refresh_uilist_entities()
|
tool.Attribute.refresh_uilist_entities()
|
||||||
tool.Attribute.disable_editing_entity()
|
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()
|
||||||
|
|||||||
@@ -82,22 +82,24 @@ class BIM_PT_explorer(Panel):
|
|||||||
bl_context = "scene"
|
bl_context = "scene"
|
||||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context, *, is_popup=False):
|
||||||
assert (layout := self.layout)
|
assert (layout := self.layout)
|
||||||
props = tool.Attribute.get_explorer_props()
|
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 = layout.row(align=True)
|
||||||
row.label(text="Explorer UI is not Loaded.")
|
row.prop(props, "ifc_class", text="")
|
||||||
row.prop(props, "is_loaded", text="", icon="IMPORT")
|
row.prop(props, "is_loaded", text="", icon="CANCEL")
|
||||||
return
|
|
||||||
|
|
||||||
active_entity = props.active_entity
|
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 = layout.row(align=True)
|
||||||
row.label(text=f"{len(props.entities)} entities found")
|
row.label(text=f"{len(props.entities)} entities found")
|
||||||
row.operator("bim.explorer_add_entity", text="", icon="ADD")
|
row.operator("bim.explorer_add_entity", text="", icon="ADD")
|
||||||
@@ -113,9 +115,12 @@ class BIM_PT_explorer(Panel):
|
|||||||
|
|
||||||
if props.editing_entity_id:
|
if props.editing_entity_id:
|
||||||
box = self.layout.box()
|
box = self.layout.box()
|
||||||
row = box.row(align=True)
|
# In popup we accept edits automatically for the convenience.
|
||||||
row.operator("bim.explorer_edit_entity", icon="CHECKMARK")
|
# Othrewise it seems very unintuitive, when you need to click confirmation twice.
|
||||||
row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="")
|
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)
|
bonsai.bim.helper.draw_attributes(props.entity_attributes, box)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ class Attribute(PropertyGroup):
|
|||||||
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")
|
||||||
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
|
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
|
||||||
special_type: StringProperty(name="Special Value Type", default="")
|
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")
|
metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute")
|
||||||
update: StringProperty(name="Update", description="Custom update function to be executed")
|
update: StringProperty(name="Update", description="Custom update function to be executed")
|
||||||
|
|
||||||
@@ -374,6 +375,7 @@ class Attribute(PropertyGroup):
|
|||||||
value_min_constraint: bool
|
value_min_constraint: bool
|
||||||
value_max: float
|
value_max: float
|
||||||
value_max_constraint: bool
|
value_max_constraint: bool
|
||||||
|
use_explorer_ui: bool
|
||||||
metadata: str
|
metadata: str
|
||||||
update: str
|
update: str
|
||||||
|
|
||||||
|
|||||||
@@ -1186,6 +1186,13 @@ class EnumData:
|
|||||||
organizations = tool.Ifc.get().by_type("IfcOrganization")
|
organizations = tool.Ifc.get().by_type("IfcOrganization")
|
||||||
return natsorted(((str(e.id()), e.Name, "") for e in organizations), key=lambda x: x[1])
|
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:
|
class UIData:
|
||||||
data = {}
|
data = {}
|
||||||
|
|||||||
@@ -75,3 +75,19 @@ class Attribute(bonsai.core.tool.Attribute):
|
|||||||
props = cls.get_explorer_props()
|
props = cls.get_explorer_props()
|
||||||
attributes = helper.export_attributes(props.entity_attributes)
|
attributes = helper.export_attributes(props.entity_attributes)
|
||||||
return 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
|
||||||
|
|||||||
Reference in New Issue
Block a user