diff --git a/src/bonsai/bonsai/bim/module/unit/__init__.py b/src/bonsai/bonsai/bim/module/unit/__init__.py index a6e65f605e..f6caa4c53f 100644 --- a/src/bonsai/bonsai/bim/module/unit/__init__.py +++ b/src/bonsai/bonsai/bim/module/unit/__init__.py @@ -23,6 +23,8 @@ from . import operator, prop, ui classes = ( operator.AddContextDependentUnit, operator.AddConversionBasedUnit, + operator.AddDerivedUnit, + operator.AddDerivedUnitElement, operator.AddMonetaryUnit, operator.AddSIUnit, operator.AssignSceneUnits, @@ -32,9 +34,11 @@ classes = ( operator.EditUnit, operator.EnableEditingUnit, operator.LoadUnits, + operator.RemoveDerivedUnitElement, operator.RemoveUnit, operator.UnassignUnit, prop.Unit, + prop.DerivedUnitElement, prop.BIMUnitProperties, ui.BIM_PT_units, ui.BIM_UL_units, diff --git a/src/bonsai/bonsai/bim/module/unit/data.py b/src/bonsai/bonsai/bim/module/unit/data.py index 6d83bb4e16..b8f28e9de8 100644 --- a/src/bonsai/bonsai/bim/module/unit/data.py +++ b/src/bonsai/bonsai/bim/module/unit/data.py @@ -41,6 +41,8 @@ class UnitsData: "unit_classes": cls.unit_classes(), "named_unit_types": cls.named_unit_types(), "conversion_unit_types": cls.conversion_unit_types(), + "derived_unit_types": cls.derived_unit_types(), + "named_units": cls.named_units(), "total_units": cls.get_total_units(), } cls.is_loaded = True @@ -88,6 +90,21 @@ class UnitsData: def conversion_unit_types(cls): return [(u, u, "") for u in ifcopenshell.util.unit.si_conversions.keys()] + @classmethod + def derived_unit_types(cls): + assert (entity := tool.Ifc.schema().declaration_by_name("IfcDerivedUnit").as_entity()) + values = ifcopenshell.util.attribute.get_enum_items(entity.all_attributes()[1]) + # USERDEFINED is skipped as it additionally requires a UserDefinedType. + return [(c, c, "") for c in sorted(values) if c != "USERDEFINED"] + + @classmethod + def named_units(cls): + results = [] + for unit in sorted(tool.Ifc.get().by_type("IfcNamedUnit"), key=lambda u: u.id()): + name = ifcopenshell.util.unit.get_full_unit_name(unit) + results.append((str(unit.id()), f"{name} ({unit.UnitType})", "")) + return results + @classmethod def get_total_units(cls): ifc = tool.Ifc.get() diff --git a/src/bonsai/bonsai/bim/module/unit/operator.py b/src/bonsai/bonsai/bim/module/unit/operator.py index f0cee9a6be..2f84655d0e 100644 --- a/src/bonsai/bonsai/bim/module/unit/operator.py +++ b/src/bonsai/bonsai/bim/module/unit/operator.py @@ -132,6 +132,56 @@ class AddContextDependentUnit(bpy.types.Operator, tool.Ifc.Operator): core.add_context_dependent_unit(tool.Ifc, tool.Unit, unit_type=self.unit_type, name=self.name) +class AddDerivedUnitElement(bpy.types.Operator): + bl_idname = "bim.add_derived_unit_element" + bl_label = "Add Derived Unit Element" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Add a new named unit and exponent to the derived unit definition" + + def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: + props = tool.Unit.get_unit_props() + props.derived_unit_elements.add() + return {"FINISHED"} + + +class RemoveDerivedUnitElement(bpy.types.Operator): + bl_idname = "bim.remove_derived_unit_element" + bl_label = "Remove Derived Unit Element" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Remove this named unit and exponent from the derived unit definition" + index: bpy.props.IntProperty() + + def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: + props = tool.Unit.get_unit_props() + props.derived_unit_elements.remove(self.index) + return {"FINISHED"} + + +class AddDerivedUnit(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_derived_unit" + bl_label = "Add Derived Unit" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Add a derived unit built from the defined named units and exponents" + + def _execute(self, context): + props = tool.Unit.get_unit_props() + elements = {} + for element in props.derived_unit_elements: + if not element.unit: + self.report({"ERROR"}, "No named unit selected. Add a named unit (e.g. an SI unit) first.") + return {"CANCELLED"} + unit = tool.Ifc.get().by_id(int(element.unit)) + if unit in elements: + self.report({"ERROR"}, f"Unit '{unit.Name}' is used in more than one element.") + return {"CANCELLED"} + elements[unit] = element.exponent + if not elements: + self.report({"ERROR"}, "A derived unit requires at least one element. Click 'Add Element' first.") + return {"CANCELLED"} + core.add_derived_unit(tool.Ifc, tool.Unit, unit_type=props.derived_unit_types, elements=elements) + props.derived_unit_elements.clear() + + class EnableEditingUnit(bpy.types.Operator): bl_idname = "bim.enable_editing_unit" bl_label = "Enable Editing Unit" diff --git a/src/bonsai/bonsai/bim/module/unit/prop.py b/src/bonsai/bonsai/bim/module/unit/prop.py index 60639ac3a1..9d22533420 100644 --- a/src/bonsai/bonsai/bim/module/unit/prop.py +++ b/src/bonsai/bonsai/bim/module/unit/prop.py @@ -50,6 +50,27 @@ def get_named_unit_types(self: "BIMUnitProperties", context: bpy.types.Context) return UnitsData.data["named_unit_types"] +def get_derived_unit_types(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: + if not UnitsData.is_loaded: + UnitsData.load() + return UnitsData.data["derived_unit_types"] + + +def get_named_units(self: "DerivedUnitElement", context: bpy.types.Context) -> list[tuple[str, str, str]]: + if not UnitsData.is_loaded: + UnitsData.load() + return UnitsData.data["named_units"] + + +class DerivedUnitElement(PropertyGroup): + unit: EnumProperty(items=get_named_units, name="Unit") + exponent: IntProperty(name="Exponent", default=1) + + if TYPE_CHECKING: + unit: str + exponent: int + + class Unit(PropertyGroup): unit_type: StringProperty(name="Unit Type") is_assigned: BoolProperty(name="Is Assigned") @@ -71,6 +92,8 @@ class BIMUnitProperties(PropertyGroup): unit_classes: EnumProperty(items=get_unit_classes, name="Unit Classes") conversion_unit_types: EnumProperty(items=get_conversion_unit_types, name="Conversion Unit Types") named_unit_types: EnumProperty(items=get_named_unit_types, name="Named Unit Types") + derived_unit_types: EnumProperty(items=get_derived_unit_types, name="Derived Unit Types") + derived_unit_elements: CollectionProperty(name="Derived Unit Elements", type=DerivedUnitElement) unit_attributes: CollectionProperty(name="Unit Attributes", type=Attribute) if TYPE_CHECKING: @@ -81,4 +104,6 @@ class BIMUnitProperties(PropertyGroup): unit_classes: str conversion_unit_types: str named_unit_types: str + derived_unit_types: str + derived_unit_elements: bpy.types.bpy_prop_collection_idprop[DerivedUnitElement] unit_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] diff --git a/src/bonsai/bonsai/bim/module/unit/ui.py b/src/bonsai/bonsai/bim/module/unit/ui.py index 848425cd4e..b64e694459 100644 --- a/src/bonsai/bonsai/bim/module/unit/ui.py +++ b/src/bonsai/bonsai/bim/module/unit/ui.py @@ -85,7 +85,16 @@ class BIM_PT_units(Panel): op = row.operator("bim.add_conversion_based_unit", text="", icon="ADD") op.name = self.props.conversion_unit_types elif self.props.unit_classes == "IfcDerivedUnit": - pass # TODO + prop_with_search(row, self.props, "derived_unit_types", text="") + row.operator("bim.add_derived_unit", text="", icon="ADD") + box = self.layout.box() + for i, element in enumerate(self.props.derived_unit_elements): + row = box.row(align=True) + prop_with_search(row, element, "unit", text="") + row.prop(element, "exponent", text="") + row.operator("bim.remove_derived_unit_element", text="", icon="X").index = i + row = box.row() + row.operator("bim.add_derived_unit_element", text="Add Element", icon="ADD") elif self.props.unit_classes == "IfcSIUnit": prop_with_search(row, self.props, "named_unit_types", text="") op = row.operator("bim.add_si_unit", text="", icon="ADD") diff --git a/src/bonsai/bonsai/core/unit.py b/src/bonsai/bonsai/core/unit.py index c0ecc46fcb..24bdd3bc87 100644 --- a/src/bonsai/bonsai/core/unit.py +++ b/src/bonsai/bonsai/core/unit.py @@ -91,6 +91,17 @@ def add_conversion_based_unit(ifc: type[tool.Ifc], unit: type[tool.Unit], name: return result +def add_derived_unit( + ifc: type[tool.Ifc], + unit: type[tool.Unit], + unit_type: str, + elements: dict[ifcopenshell.entity_instance, int], +) -> ifcopenshell.entity_instance: + result = ifc.run("unit.add_derived_unit", unit_type=unit_type, userdefinedtype=None, attributes=elements) + unit.import_units() + return result + + def enable_editing_unit(unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None: unit_tool.set_active_unit(unit) unit_tool.import_unit_attributes(unit) diff --git a/src/bonsai/test/core/test_unit.py b/src/bonsai/test/core/test_unit.py index 1c328fe84c..79ff70f1e7 100644 --- a/src/bonsai/test/core/test_unit.py +++ b/src/bonsai/test/core/test_unit.py @@ -174,6 +174,15 @@ class TestAddConversionBasedUnit: assert subject.add_conversion_based_unit(ifc, unit, name="name") == "unit" +class TestAddDerivedUnit: + def test_run(self, ifc, unit): + ifc.run( + "unit.add_derived_unit", unit_type="unit_type", userdefinedtype=None, attributes="elements" + ).should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_derived_unit(ifc, unit, unit_type="unit_type", elements="elements") == "unit" + + class TestEnableEditingUnit: def test_run(self, unit): unit.set_active_unit("unit").should_be_called()