Structural UI - refactor common code to tool, fix bugs

Also revert previous change in UI - `draw_attributes` doesn't cover it and there indeed should be special handling.
This commit is contained in:
Andrej730
2025-07-10 17:59:21 +05:00
parent 76f0b57ff1
commit 0fcc47508a
4 changed files with 125 additions and 92 deletions
@@ -23,14 +23,13 @@ import ifcopenshell.api
import ifcopenshell.api.aggregate
import ifcopenshell.api.group
import ifcopenshell.api.structural
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.core.structural as core
import bonsai.tool as tool
from math import degrees
from mathutils import Vector, Matrix
from bonsai.bim.module.structural.decorator import LoadsDecorator
from typing import Literal
from typing import Literal, Any, TYPE_CHECKING
class ShowLoads(bpy.types.Operator):
@@ -175,39 +174,17 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator):
bl_idname = "bim.enable_editing_structural_boundary_condition"
bl_label = "Enable Editing Structural Boundary Condition"
bl_options = {"REGISTER", "UNDO"}
boundary_condition: bpy.props.IntProperty()
boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
boundary_condition: int
def execute(self, context):
obj = context.active_object
assert obj
props = tool.Structural.get_object_structural_props(obj)
props.boundary_condition_attributes.clear()
condition = tool.Ifc.get().by_id(self.boundary_condition)
schema = tool.Ifc.schema()
for attribute in schema.declaration_by_name(condition.is_a()).all_attributes():
value = getattr(condition, attribute.name(), None)
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
new = props.boundary_condition_attributes.add()
new.name = attribute.name()
new.is_null = value is None
new.is_optional = attribute.optional()
if isinstance(data_type, tuple) and data_type[0] == "select":
enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)]
new.enum_items = json.dumps(enum_items)
if isinstance(value, bool):
new.bool_value = False if new.is_null else value
new.data_type = "bool"
new.enum_value = "IfcBoolean"
elif isinstance(value, float):
new.float_value = 0.0 if new.is_null else value
new.data_type = "float"
new.enum_value = next(i for i in enum_items if i != "IfcBoolean")
elif data_type == "string":
new.string_value = "" if new.is_null else value
new.data_type = "string"
tool.Structural.import_boundary_condition_attributes(condition, props)
props.active_boundary_condition = self.boundary_condition
return {"FINISHED"}
@@ -216,7 +193,10 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_structural_boundary_condition"
bl_label = "Edit Structural Boundary Condition"
bl_options = {"REGISTER", "UNDO"}
connection: bpy.props.IntProperty()
connection: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
connection: int
def _execute(self, context):
obj = context.active_object
@@ -227,18 +207,7 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
connection = file.by_id(self.connection)
condition = connection.AppliedCondition
attributes = {}
for attribute in props.boundary_condition_attributes:
if attribute.is_null:
attributes[attribute.name] = {"value": None, "type": "null"}
elif attribute.data_type == "string":
attributes[attribute.name] = {"value": attribute.string_value, "type": "string"}
elif attribute.enum_value == "IfcBoolean":
attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value}
else:
attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value}
ifcopenshell.api.structural.edit_structural_boundary_condition(file, condition=condition, attributes=attributes)
tool.Structural.export_and_apply_boundary_condition_attributes(condition, props)
bpy.ops.bim.disable_editing_structural_boundary_condition()
return {"FINISHED"}
@@ -955,36 +924,15 @@ class EnableEditingBoundaryCondition(bpy.types.Operator):
bl_idname = "bim.enable_editing_boundary_condition"
bl_label = "Enable Editing Boundary Condition"
bl_options = {"REGISTER", "UNDO"}
boundary_condition: bpy.props.IntProperty()
boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
boundary_condition: int
def execute(self, context):
props = tool.Structural.get_structural_props()
props.boundary_condition_attributes.clear()
boundary_condition = tool.Ifc.get().by_id(self.boundary_condition)
# bonsai.bim.helper.import_attributes(data["type"], props.boundary_condition_attributes, data)
schema = tool.Ifc.schema()
for attribute in schema.declaration_by_name(boundary_condition.is_a()).all_attributes():
value = getattr(boundary_condition, attribute.name(), None)
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
new = props.boundary_condition_attributes.add()
new.name = attribute.name()
new.is_null = value is None
new.is_optional = attribute.optional()
if isinstance(data_type, tuple) and data_type[0] == "select":
enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)]
new.enum_items = json.dumps(enum_items)
if isinstance(value, bool):
new.bool_value = False if new.is_null else value
new.data_type = "bool"
new.enum_value = "IfcBoolean"
elif isinstance(value, float):
new.float_value = 0.0 if new.is_null else value
new.data_type = "float"
new.enum_value = next(i for i in enum_items if i != "IfcBoolean")
elif data_type == "string":
new.string_value = "" if new.is_null else value
new.data_type = "string"
props = tool.Structural.get_structural_props()
tool.Structural.import_boundary_condition_attributes(boundary_condition, props)
props.active_boundary_condition_id = self.boundary_condition
return {"FINISHED"}
@@ -1023,22 +971,8 @@ class EditBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = tool.Structural.get_structural_props()
self.file = tool.Ifc.get()
# attributes = bonsai.bim.helper.export_attributes(props.boundary_condition_attributes)
attributes = {}
for attribute in props.boundary_condition_attributes:
if attribute.is_null:
attributes[attribute.name] = {"value": None, "type": "null"}
elif attribute.data_type == "string":
attributes[attribute.name] = {"value": attribute.string_value, "type": "string"}
elif attribute.enum_value == "IfcBoolean":
attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value}
else:
attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value}
ifcopenshell.api.structural.edit_structural_boundary_condition(
self.file,
condition=self.file.by_id(props.active_boundary_condition_id),
attributes=attributes,
)
ifc_file = tool.Ifc.get()
condition = ifc_file.by_id(props.active_boundary_condition_id)
tool.Structural.export_and_apply_boundary_condition_attributes(condition, props)
bpy.ops.bim.load_boundary_conditions()
return {"FINISHED"}
+17 -4
View File
@@ -79,7 +79,20 @@ def draw_boundary_condition_ui(
def draw_boundary_condition_editable_ui(
layout: bpy.types.UILayout, props: Union[BIMStructuralProperties, BIMObjectStructuralProperties]
) -> None:
draw_attributes(props.boundary_condition_attributes, layout)
# Reimplement `draw_attributes` as we need to support multiple types for the same attribute.
for attribute in props.boundary_condition_attributes:
if attribute.data_type == "string":
row = layout.row(align=True)
bonsai.bim.helper.draw_attribute(attribute, row, enable_search=True)
else:
row = layout.row(align=True)
row.prop(attribute, "enum_value", text=attribute["name"])
if attribute.enum_value == "IfcBoolean":
row.prop(attribute, "bool_value", text="")
else:
row.prop(attribute, "float_value", text="")
if attribute.is_optional:
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
def draw_boundary_condition_read_only_ui(layout: bpy.types.UILayout, boundary_condition: dict[str, Any]) -> None:
@@ -605,6 +618,7 @@ class BIM_PT_boundary_conditions(Panel):
BoundaryConditionsData.load()
self.props = tool.Structural.get_structural_props()
assert self.layout
row = self.layout.row(align=True)
row.label(
@@ -636,9 +650,8 @@ class BIM_PT_boundary_conditions(Panel):
"active_boundary_condition_index",
)
if self.props.active_boundary_condition_id:
draw_boundary_condition_editable_ui(self.layout, self.props)
# bonsai.bim.helper.draw_attributes(self.props.boundary_condition_attributes, self.layout)
if self.props.active_boundary_condition_id:
draw_boundary_condition_editable_ui(self.layout, self.props)
class BIM_UL_boundary_conditions(UIList):
+68
View File
@@ -20,6 +20,8 @@ from __future__ import annotations
import bpy
import ifcopenshell
import ifcopenshell.api.context
import ifcopenshell.api.structural
import ifcopenshell.util.attribute
import ifcopenshell.util.representation
import json
import bonsai.bim.helper
@@ -29,6 +31,7 @@ from typing import Union, Any, TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.structural.prop import BIMStructuralProperties, BIMObjectStructuralProperties
from ifcopenshell.api.structural.edit_structural_boundary_condition import AttributeDict
class Structural(bonsai.core.tool.Structural):
@@ -214,3 +217,68 @@ class Structural(bonsai.core.tool.Structural):
return None
return undefined_representation
@classmethod
def import_boundary_condition_attributes(
cls,
boundary_condition: ifcopenshell.entity_instance,
props: Union[BIMStructuralProperties, BIMObjectStructuralProperties],
) -> None:
props_attrs = props.boundary_condition_attributes
props_attrs.clear()
schema = tool.Ifc.schema()
# Don't use `import_attributes`,
# because we need to support 2 types of values for the same props.
ifc_class = boundary_condition.is_a()
entity = schema.declaration_by_name(ifc_class).as_entity()
assert entity
for attribute in entity.all_attributes():
attribute_name = attribute.name()
value = getattr(boundary_condition, attribute_name)
new = props_attrs.add()
new.name = attribute_name
new.ifc_class = ifc_class
new.is_null = value is None
new.is_optional = attribute.optional()
# Select attribute values are typically wrapped.
if isinstance(value, ifcopenshell.entity_instance):
value = value.wrappedValue
if attribute_name == "Name":
new.string_value = "" if new.is_null else value
new.data_type = "string"
else:
enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)]
new.enum_items = json.dumps(enum_items)
if isinstance(value, bool):
new.bool_value = False if new.is_null else value
new.data_type = "boolean"
new.enum_value = "IfcBoolean"
else:
print(value)
new.float_value = 0 if new.is_null else value
new.data_type = "float"
new.enum_value = next(i for i in enum_items if i != "IfcBoolean")
@classmethod
def export_and_apply_boundary_condition_attributes(
cls,
boundary_condition: ifcopenshell.entity_instance,
props: Union[BIMStructuralProperties, BIMObjectStructuralProperties],
) -> None:
attributes: dict[str, AttributeDict] = {}
for attribute in props.boundary_condition_attributes:
if attribute.is_null:
attributes[attribute.name] = {"value": None, "type": "null"}
elif attribute.data_type == "string":
attributes[attribute.name] = {"value": attribute.string_value, "type": "string"}
elif attribute.enum_value == "IfcBoolean":
attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value}
else:
attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value}
ifc_file = tool.Ifc.get()
ifcopenshell.api.structural.edit_structural_boundary_condition(
ifc_file, condition=boundary_condition, attributes=attributes
)
@@ -16,11 +16,28 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any
from typing import Any, TypedDict, Literal, Union
AttributeDict = TypedDict(
"AttributeDict",
{
"type": Union[
Literal[
"string",
"null",
],
str, # IFC Class.
],
"value": Any,
},
)
def edit_structural_boundary_condition(
file: ifcopenshell.file, condition: ifcopenshell.entity_instance, attributes: dict[str, Any]
file: ifcopenshell.file,
condition: ifcopenshell.entity_instance,
attributes: dict[str, AttributeDict],
) -> None:
"""Edits the attributes of an IfcBoundaryCondition
@@ -29,6 +46,7 @@ def edit_structural_boundary_condition(
:param condition: The IfcBoundaryCondition entity you want to edit
:param attributes: a dictionary of attribute names and values.
Each value is represented by a dictionary.
:return: None
"""
for name, data in attributes.items():