mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Bonsai: implement Add Derived Unit in the units UI (#2554)
The IfcDerivedUnit branch of the Add Unit panel was a literal
`pass # TODO`, so derived units (velocity, thermal conductance, etc.)
could not be created from the UI even though
ifcopenshell.api.unit.add_derived_unit already supports them.
Add the missing flow, mirroring the module's existing add-unit patterns:
- UnitsData caches derived_unit_types (IfcDerivedUnitEnum from the schema,
USERDEFINED excluded since it additionally requires a UserDefinedType)
and named_units (the project's existing IfcNamedUnits, labelled with
their full unit name and unit type).
- A DerivedUnitElement property group (unit enum + integer exponent) and a
derived_unit_elements collection on BIMUnitProperties.
- Operators to add/remove element rows and AddDerivedUnit, which validates
(at least one element per the schema's SET [1:?], no unselected unit, no
duplicate units), builds the {named unit: exponent} mapping, and calls
core.add_derived_unit -> unit.add_derived_unit; rows are cleared on
success.
- The panel draws the type dropdown, per-row unit/exponent/remove controls,
and an Add Element button.
Verified live in headless Blender with the real operators: metre^1 x
second^-1 with LINEARVELOCITYUNIT creates
IfcDerivedUnit((IfcDerivedUnitElement(metre,1), IfcDerivedUnitElement(
second,-1)), .LINEARVELOCITYUNIT.), assignable via bim.assign_unit; the
empty-rows and duplicate-unit error paths report cleanly; existing SI and
monetary unit flows are unaffected. Core test_unit.py: 19 passed
(includes a new TestAddDerivedUnit).
Maintainer context: Moult invited an implementation on the issue
("Indeed I couldn't think of a nice UI for it, go for it!").
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user