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.aggregate
import ifcopenshell.api.group import ifcopenshell.api.group
import ifcopenshell.api.structural import ifcopenshell.api.structural
import ifcopenshell.util.attribute
import bonsai.bim.helper import bonsai.bim.helper
import bonsai.core.structural as core import bonsai.core.structural as core
import bonsai.tool as tool import bonsai.tool as tool
from math import degrees from math import degrees
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from bonsai.bim.module.structural.decorator import LoadsDecorator from bonsai.bim.module.structural.decorator import LoadsDecorator
from typing import Literal from typing import Literal, Any, TYPE_CHECKING
class ShowLoads(bpy.types.Operator): class ShowLoads(bpy.types.Operator):
@@ -175,39 +174,17 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator):
bl_idname = "bim.enable_editing_structural_boundary_condition" bl_idname = "bim.enable_editing_structural_boundary_condition"
bl_label = "Enable Editing Structural Boundary Condition" bl_label = "Enable Editing Structural Boundary Condition"
bl_options = {"REGISTER", "UNDO"} 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): def execute(self, context):
obj = context.active_object obj = context.active_object
assert obj assert obj
props = tool.Structural.get_object_structural_props(obj) props = tool.Structural.get_object_structural_props(obj)
props.boundary_condition_attributes.clear()
condition = tool.Ifc.get().by_id(self.boundary_condition) condition = tool.Ifc.get().by_id(self.boundary_condition)
schema = tool.Ifc.schema() tool.Structural.import_boundary_condition_attributes(condition, props)
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"
props.active_boundary_condition = self.boundary_condition props.active_boundary_condition = self.boundary_condition
return {"FINISHED"} return {"FINISHED"}
@@ -216,7 +193,10 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_structural_boundary_condition" bl_idname = "bim.edit_structural_boundary_condition"
bl_label = "Edit Structural Boundary Condition" bl_label = "Edit Structural Boundary Condition"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
connection: bpy.props.IntProperty() connection: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
connection: int
def _execute(self, context): def _execute(self, context):
obj = context.active_object obj = context.active_object
@@ -227,18 +207,7 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
connection = file.by_id(self.connection) connection = file.by_id(self.connection)
condition = connection.AppliedCondition condition = connection.AppliedCondition
attributes = {} tool.Structural.export_and_apply_boundary_condition_attributes(condition, props)
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)
bpy.ops.bim.disable_editing_structural_boundary_condition() bpy.ops.bim.disable_editing_structural_boundary_condition()
return {"FINISHED"} return {"FINISHED"}
@@ -955,36 +924,15 @@ class EnableEditingBoundaryCondition(bpy.types.Operator):
bl_idname = "bim.enable_editing_boundary_condition" bl_idname = "bim.enable_editing_boundary_condition"
bl_label = "Enable Editing Boundary Condition" bl_label = "Enable Editing Boundary Condition"
bl_options = {"REGISTER", "UNDO"} 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): 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) boundary_condition = tool.Ifc.get().by_id(self.boundary_condition)
# bonsai.bim.helper.import_attributes(data["type"], props.boundary_condition_attributes, data) props = tool.Structural.get_structural_props()
schema = tool.Ifc.schema() tool.Structural.import_boundary_condition_attributes(boundary_condition, props)
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.active_boundary_condition_id = self.boundary_condition props.active_boundary_condition_id = self.boundary_condition
return {"FINISHED"} return {"FINISHED"}
@@ -1023,22 +971,8 @@ class EditBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
props = tool.Structural.get_structural_props() props = tool.Structural.get_structural_props()
self.file = tool.Ifc.get() ifc_file = tool.Ifc.get()
# attributes = bonsai.bim.helper.export_attributes(props.boundary_condition_attributes) condition = ifc_file.by_id(props.active_boundary_condition_id)
attributes = {} tool.Structural.export_and_apply_boundary_condition_attributes(condition, props)
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,
)
bpy.ops.bim.load_boundary_conditions() bpy.ops.bim.load_boundary_conditions()
return {"FINISHED"} return {"FINISHED"}
+17 -4
View File
@@ -79,7 +79,20 @@ def draw_boundary_condition_ui(
def draw_boundary_condition_editable_ui( def draw_boundary_condition_editable_ui(
layout: bpy.types.UILayout, props: Union[BIMStructuralProperties, BIMObjectStructuralProperties] layout: bpy.types.UILayout, props: Union[BIMStructuralProperties, BIMObjectStructuralProperties]
) -> None: ) -> 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: 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() BoundaryConditionsData.load()
self.props = tool.Structural.get_structural_props() self.props = tool.Structural.get_structural_props()
assert self.layout
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label( row.label(
@@ -636,9 +650,8 @@ class BIM_PT_boundary_conditions(Panel):
"active_boundary_condition_index", "active_boundary_condition_index",
) )
if self.props.active_boundary_condition_id: if self.props.active_boundary_condition_id:
draw_boundary_condition_editable_ui(self.layout, self.props) draw_boundary_condition_editable_ui(self.layout, self.props)
# bonsai.bim.helper.draw_attributes(self.props.boundary_condition_attributes, self.layout)
class BIM_UL_boundary_conditions(UIList): class BIM_UL_boundary_conditions(UIList):
+68
View File
@@ -20,6 +20,8 @@ from __future__ import annotations
import bpy import bpy
import ifcopenshell import ifcopenshell
import ifcopenshell.api.context import ifcopenshell.api.context
import ifcopenshell.api.structural
import ifcopenshell.util.attribute
import ifcopenshell.util.representation import ifcopenshell.util.representation
import json import json
import bonsai.bim.helper import bonsai.bim.helper
@@ -29,6 +31,7 @@ from typing import Union, Any, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from bonsai.bim.module.structural.prop import BIMStructuralProperties, BIMObjectStructuralProperties 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): class Structural(bonsai.core.tool.Structural):
@@ -214,3 +217,68 @@ class Structural(bonsai.core.tool.Structural):
return None return None
return undefined_representation 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 # You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell 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( 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: ) -> None:
"""Edits the attributes of an IfcBoundaryCondition """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 condition: The IfcBoundaryCondition entity you want to edit
:param attributes: a dictionary of attribute names and values. :param attributes: a dictionary of attribute names and values.
Each value is represented by a dictionary.
:return: None :return: None
""" """
for name, data in attributes.items(): for name, data in attributes.items():