From 487a2014f5b2977a35153e1b6d426754492f676d Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Mon, 1 Sep 2025 21:29:05 +0200 Subject: [PATCH 01/10] proposal to add massunit and timeunit in project creation wizard --- .../bonsai/bim/module/project/operator.py | 8 +++ src/bonsai/bonsai/bim/module/project/ui.py | 4 ++ src/bonsai/bonsai/bim/prop.py | 23 +++++++ src/bonsai/bonsai/core/unit.py | 19 +++++- src/bonsai/bonsai/tool/unit.py | 26 +++++++- .../ifcopenshell/api/unit/assign_unit.py | 66 ++++++++++++++++++- 6 files changed, 142 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 0650cda238..34d6cdb91d 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -98,6 +98,8 @@ class NewProject(bpy.types.Operator): bpy.context.scene.unit_settings.length_unit = "METERS" bim_props.area_unit = "SQUARE_METRE" bim_props.volume_unit = "CUBIC_METRE" + bim_props.mass_unit = "KILOGRAM" + bim_props.time_unit = "SECOND" pprops.template_file = "0" elif self.preset == "metric_mm": pprops.export_schema = "IFC4" @@ -105,6 +107,8 @@ class NewProject(bpy.types.Operator): bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" bim_props.area_unit = "SQUARE_METRE" bim_props.volume_unit = "CUBIC_METRE" + bim_props.mass_unit = "KILOGRAM" + bim_props.time_unit = "SECOND" pprops.template_file = "0" elif self.preset == "imperial_ft": pprops.export_schema = "IFC4" @@ -112,6 +116,8 @@ class NewProject(bpy.types.Operator): bpy.context.scene.unit_settings.length_unit = "FEET" bim_props.area_unit = "square foot" bim_props.volume_unit = "cubic foot" + bim_props.mass_unit = "POUND" + bim_props.time_unit = "SECOND" pprops.template_file = "0" elif self.preset == "demo": pprops.export_schema = "IFC4" @@ -119,6 +125,8 @@ class NewProject(bpy.types.Operator): bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" bim_props.area_unit = "SQUARE_METRE" bim_props.volume_unit = "CUBIC_METRE" + bim_props.mass_unit = "KILOGRAM" + bim_props.time_unit = "SECOND" pprops.template_file = "IFC4 Demo Template.ifc" if self.preset != "wizard": diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 6674a1a51a..731a9d0688 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -356,6 +356,10 @@ class BIM_PT_new_project_wizard(Panel): row.prop(props, "area_unit", text="Area Unit") row = self.layout.row() row.prop(props, "volume_unit", text="Volume Unit") + row = self.layout.row() + row.prop(props, "mass_unit", text="Mass Unit") + row = self.layout.row() + row.prop(props, "time_unit", text="Time Unit") prop_with_search(self.layout, pprops, "template_file", text="Template") row = self.layout.row() diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 7a4464c986..eeea68eab9 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -586,7 +586,28 @@ class BIMProperties(PropertyGroup): ], name="IFC Volume Unit", ) + mass_unit: EnumProperty( + items=[ + ("KILOGRAM", "Kilogram", "Kilograms"), + ("GRAM", "Gram", "Grams"), + ("POUND", "Pound (Mass)", "Pounds"), + ("OUNCE", "Ounce", "Ounces"), + ("TON", "Ton", "Metric Tons"), + ], + name="Mass Unit", + default="KILOGRAM", + ) + time_unit: EnumProperty( + items=[ + ("SECOND", "Second", "Seconds"), + ("MINUTE", "Minutes", "Minutes"), + ("HOUR", "Hour", "Hours"), + ("DAY", "Day", "Days"), + ], + name="Time Unit", + default="HOUR", + ) if TYPE_CHECKING: is_dirty: bool schema_dir: str @@ -599,6 +620,8 @@ class BIMProperties(PropertyGroup): section_line_decorator_width: float area_unit: str volume_unit: str + mass_unit: str + time_unit: str class IfcParameter(PropertyGroup): diff --git a/src/bonsai/bonsai/core/unit.py b/src/bonsai/bonsai/core/unit.py index a075c9c5e5..8517343b37 100644 --- a/src/bonsai/bonsai/core/unit.py +++ b/src/bonsai/bonsai/core/unit.py @@ -34,14 +34,29 @@ def assign_scene_units(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> None: areaunit = ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=prefix) prefix = unit.get_scene_unit_si_prefix("VOLUMEUNIT") volumeunit = ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=prefix) + + prefix = unit.get_scene_unit_si_prefix("MASSUNIT") + if prefix == "CONVERSION": + mass_unit_name = unit.get_scene_unit_name("MASSUNIT") + massunit = ifc.run("unit.add_conversion_based_unit", name=mass_unit_name.lower()) + else: + massunit = ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix=prefix) + + prefix = unit.get_scene_unit_si_prefix("TIMEUNIT") + if prefix == "CONVERSION": + time_unit_name = unit.get_scene_unit_name("TIMEUNIT") + timeunit = ifc.run("unit.add_conversion_based_unit", name=time_unit_name.lower()) + else: + timeunit = ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=prefix) else: lengthunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("LENGTHUNIT")) areaunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("AREAUNIT")) volumeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("VOLUMEUNIT")) + massunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("MASSUNIT")) + timeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("TIMEUNIT")) planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree") - - ifc.run("unit.assign_unit", units=[lengthunit, areaunit, volumeunit, planeangleunit]) + ifc.run("unit.assign_unit", units=[lengthunit, areaunit, volumeunit, planeangleunit, massunit, timeunit]) def assign_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None: diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index d01d51f3cb..b18cb6ff09 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -266,7 +266,7 @@ def parse_distance_string(input_string: str, use_project_unit: bool = True) -> t class Unit(bonsai.core.tool.Unit): - UNIT_TYPE = Literal["LENGTHUNIT", "AREAUNIT", "VOLUMEUNIT"] + UNIT_TYPE = Literal["LENGTHUNIT", "AREAUNIT", "VOLUMEUNIT", "MASSUNIT", "TIMEUNIT"] @staticmethod def format_distance(meters: float, use_imperial: bool = None, **kwargs) -> str: @@ -343,6 +343,10 @@ class Unit(bonsai.core.tool.Unit): return bim_props.area_unit elif unit_type == "VOLUMEUNIT": return bim_props.volume_unit + elif unit_type == "MASSUNIT": + return bim_props.mass_unit + elif unit_type == "TIMEUNIT": + return bim_props.time_unit else: assert_never(unit_type) @@ -359,6 +363,26 @@ class Unit(bonsai.core.tool.Unit): unit = bim_props.area_unit elif unit_type == "VOLUMEUNIT": unit = bim_props.volume_unit + elif unit_type == "MASSUNIT": + unit = bim_props.mass_unit + if unit == "KILOGRAM": + return "KILO" + elif unit == "GRAM": + return None + elif unit == "TON": + return "MEGA" + elif unit in ["POUND", "OUNCE"]: + return "CONVERSION" + else: + return None + + elif unit_type == "TIMEUNIT": + unit = bim_props.time_unit + if unit == "SECOND": + return None + else: + return "CONVERSION" + else: assert_never(unit_type) if "/" in unit: diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py index d57acbd435..58abfd4ea4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py @@ -27,6 +27,8 @@ def assign_unit( length: Optional[dict] = None, area: Optional[dict] = None, volume: Optional[dict] = None, + mass: Optional[dict] = None, + time: Optional[dict] = None, ) -> ifcopenshell.entity_instance: """Assign default project units @@ -51,9 +53,11 @@ def assign_unit( # You need a project before you can assign units. ifcopenshell.api.root.create_entity(model, ifc_class="IfcProject") - # Millimeters and square meters + # Millimeters, square meters, kilograms, and seconds length = ifcopenshell.api.unit.add_si_unit(model, unit_type="LENGTHUNIT", prefix="MILLI") area = ifcopenshell.api.unit.add_si_unit(model, unit_type="AREAUNIT") + mass = ifcopenshell.api.unit.add_si_unit(model, unit_type="MASSUNIT", prefix="KILO") + time = ifcopenshell.api.unit.add_si_unit(model, unit_type="TIMEUNIT") # Make it our default units, if we are doing a metric building ifcopenshell.api.unit.assign_unit(model, units=[length, area]) @@ -71,6 +75,8 @@ def assign_unit( usecase.settings["length"] = length or {"is_metric": True, "raw": "MILLIMETERS"} usecase.settings["area"] = area or {"is_metric": True, "raw": "METERS"} usecase.settings["volume"] = volume or {"is_metric": True, "raw": "METERS"} + usecase.settings["mass"] = mass or {"is_metric": True, "raw": "KILOGRAM"} + usecase.settings["time"] = time or {"is_metric": True, "raw": "SECOND"} return usecase.execute() @@ -116,7 +122,45 @@ class Usecase: units.add(unit) unit_assignment.Units = list(units) + def create_time_conversion_unit(self, name: str, factor: float) -> ifcopenshell.entity_instance: + """Create a conversion-based time unit""" + dimensional_exponents = self.file.createIfcDimensionalExponents(0, 0, 1, 0, 0, 0, 0) + si_unit = self.file.createIfcSIUnit(None, "TIMEUNIT", None, "SECOND") + value_component = self.file.create_entity("IfcReal", **{"wrappedValue": factor}) + conversion_factor = self.file.createIfcMeasureWithUnit(value_component, si_unit) + return self.file.createIfcConversionBasedUnit(dimensional_exponents, "TIMEUNIT", name, conversion_factor) + + def create_mass_conversion_unit(self, name: str, factor: float) -> ifcopenshell.entity_instance: + """Create a conversion-based mass unit""" + dimensional_exponents = self.file.createIfcDimensionalExponents(0, 1, 0, 0, 0, 0, 0) # Mass dimension + si_unit = self.file.createIfcSIUnit(None, "MASSUNIT", "KILO", "GRAM") + value_component = self.file.create_entity("IfcReal", **{"wrappedValue": factor}) + conversion_factor = self.file.createIfcMeasureWithUnit(value_component, si_unit) + return self.file.createIfcConversionBasedUnit(dimensional_exponents, "MASSUNIT", name, conversion_factor) + def create_metric_unit(self, unit_type: str, data: dict) -> ifcopenshell.entity_instance: + if unit_type == "mass": + if data["raw"] == "KILOGRAM": + return self.file.createIfcSIUnit(None, "MASSUNIT", "KILO", "GRAM") + elif data["raw"] == "GRAM": + return self.file.createIfcSIUnit(None, "MASSUNIT", None, "GRAM") + elif data["raw"] == "TON": + return self.file.createIfcSIUnit(None, "MASSUNIT", "MEGA", "GRAM") + else: + return self.file.createIfcSIUnit(None, "MASSUNIT", "KILO", "GRAM") + + elif unit_type == "time": + if data["raw"] == "SECOND": + return self.file.createIfcSIUnit(None, "TIMEUNIT", None, "SECOND") + elif data["raw"] == "MINUTE": + return self.create_time_conversion_unit("minute", 60.0) + elif data["raw"] == "HOUR": + return self.create_time_conversion_unit("hour", 3600.0) + elif data["raw"] == "DAY": + return self.create_time_conversion_unit("day", 86400.0) + else: + return self.file.createIfcSIUnit(None, "TIMEUNIT", None, "SECOND") + type_prefix = "" if unit_type == "area": type_prefix = "SQUARE_" @@ -139,6 +183,26 @@ class Usecase: elif unit_type == "volume": dimensional_exponents = self.file.createIfcDimensionalExponents(3, 0, 0, 0, 0, 0, 0) name_prefix = "cubic" + elif unit_type == "mass": + if data["raw"] == "POUND": + return self.create_mass_conversion_unit("pound", 0.45359237) + elif data["raw"] == "OUNCE": + return self.create_mass_conversion_unit("ounce", 0.0283495) + else: + return self.create_mass_conversion_unit("pound", 0.45359237) + + elif unit_type == "time": + if data["raw"] == "SECOND": + return self.file.createIfcSIUnit(None, "TIMEUNIT", None, "SECOND") + elif data["raw"] == "MINUTE": + return self.create_time_conversion_unit("minute", 60.0) + elif data["raw"] == "HOUR": + return self.create_time_conversion_unit("hour", 3600.0) + elif data["raw"] == "DAY": + return self.create_time_conversion_unit("day", 86400.0) + else: + return self.file.createIfcSIUnit(None, "TIMEUNIT", None, "SECOND") + si_unit = self.file.createIfcSIUnit( None, "{}UNIT".format(unit_type.upper()), From 146301b0c01e9196c00fd88b4bf616300cb7d871 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Mon, 1 Sep 2025 22:32:52 +0200 Subject: [PATCH 02/10] use kilogram as default for SI and Seconds for Imperial --- src/bonsai/bonsai/bim/prop.py | 2 +- src/bonsai/bonsai/core/unit.py | 9 +++++++-- src/bonsai/bonsai/tool/unit.py | 8 +++----- .../ifcopenshell/api/unit/add_conversion_based_unit.py | 6 +++++- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index eeea68eab9..93730b475b 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -590,7 +590,7 @@ class BIMProperties(PropertyGroup): items=[ ("KILOGRAM", "Kilogram", "Kilograms"), ("GRAM", "Gram", "Grams"), - ("POUND", "Pound (Mass)", "Pounds"), + ("POUND", "Pound", "Pounds"), ("OUNCE", "Ounce", "Ounces"), ("TON", "Ton", "Metric Tons"), ], diff --git a/src/bonsai/bonsai/core/unit.py b/src/bonsai/bonsai/core/unit.py index 8517343b37..2e8e627cb7 100644 --- a/src/bonsai/bonsai/core/unit.py +++ b/src/bonsai/bonsai/core/unit.py @@ -52,8 +52,13 @@ def assign_scene_units(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> None: lengthunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("LENGTHUNIT")) areaunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("AREAUNIT")) volumeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("VOLUMEUNIT")) - massunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("MASSUNIT")) - timeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("TIMEUNIT")) + massunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("MASSUNIT").lower()) + + time_unit_name = unit.get_scene_unit_name("TIMEUNIT") + if time_unit_name == "SECOND": + timeunit = ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None) + else: + timeunit = ifc.run("unit.add_conversion_based_unit", name=time_unit_name.lower()) planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree") ifc.run("unit.assign_unit", units=[lengthunit, areaunit, volumeunit, planeangleunit, massunit, timeunit]) diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index b18cb6ff09..293136fb8c 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -365,24 +365,22 @@ class Unit(bonsai.core.tool.Unit): unit = bim_props.volume_unit elif unit_type == "MASSUNIT": unit = bim_props.mass_unit - if unit == "KILOGRAM": - return "KILO" - elif unit == "GRAM": + if unit == "GRAM": return None + elif unit == "KILOGRAM": + return "KILO" elif unit == "TON": return "MEGA" elif unit in ["POUND", "OUNCE"]: return "CONVERSION" else: return None - elif unit_type == "TIMEUNIT": unit = bim_props.time_unit if unit == "SECOND": return None else: return "CONVERSION" - else: assert_never(unit_type) if "/" in unit: diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py index bb1e920257..51aa200810 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py @@ -61,7 +61,11 @@ def add_conversion_based_unit( dimensions = ifcopenshell.util.unit.named_dimensions[unit_type] exponents = file.createIfcDimensionalExponents(*dimensions) si_name = ifcopenshell.util.unit.si_type_names[unit_type] - si_unit = file.createIfcSIUnit(UnitType=unit_type, Name=si_name) + + if unit_type == "MASSUNIT": + si_unit = file.createIfcSIUnit(UnitType=unit_type, Name=si_name, Prefix="KILO") + else: + si_unit = file.createIfcSIUnit(UnitType=unit_type, Name=si_name) conversion_real = ifcopenshell.util.unit.si_conversions.get(name, 1) value_component = file.create_entity("IfcReal", **{"wrappedValue": conversion_real}) From a4e76ed6eeebfd339e6dcc42354390a1b4ce8013 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Fri, 5 Sep 2025 20:22:56 +0200 Subject: [PATCH 03/10] Updated with core developer feedback: fixed for Tonne and added tests --- src/bonsai/bonsai/bim/prop.py | 4 +- src/bonsai/bonsai/tool/unit.py | 6 +- src/bonsai/test/core/test_unit.py | 155 +++++++++++- src/bonsai/test/tool/test_unit.py | 143 +++++++++++ .../api/unit/add_conversion_based_unit.py | 2 +- .../ifcopenshell/util/unit.py | 3 + .../test/util/test_unit.py | 230 ++++++++++++++++++ 7 files changed, 535 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 93730b475b..7b24d2a1d1 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -592,7 +592,7 @@ class BIMProperties(PropertyGroup): ("GRAM", "Gram", "Grams"), ("POUND", "Pound", "Pounds"), ("OUNCE", "Ounce", "Ounces"), - ("TON", "Ton", "Metric Tons"), + ("TONNE", "Tonne", "Metric Tons"), ], name="Mass Unit", default="KILOGRAM", @@ -601,7 +601,7 @@ class BIMProperties(PropertyGroup): time_unit: EnumProperty( items=[ ("SECOND", "Second", "Seconds"), - ("MINUTE", "Minutes", "Minutes"), + ("MINUTE", "Minute", "Minutes"), ("HOUR", "Hour", "Hours"), ("DAY", "Day", "Days"), ], diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index 293136fb8c..a3c9569d62 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -344,9 +344,9 @@ class Unit(bonsai.core.tool.Unit): elif unit_type == "VOLUMEUNIT": return bim_props.volume_unit elif unit_type == "MASSUNIT": - return bim_props.mass_unit + return bim_props.mass_unit.lower() elif unit_type == "TIMEUNIT": - return bim_props.time_unit + return bim_props.time_unit.lower() else: assert_never(unit_type) @@ -369,7 +369,7 @@ class Unit(bonsai.core.tool.Unit): return None elif unit == "KILOGRAM": return "KILO" - elif unit == "TON": + elif unit == "TONNE": return "MEGA" elif unit in ["POUND", "OUNCE"]: return "CONVERSION" diff --git a/src/bonsai/test/core/test_unit.py b/src/bonsai/test/core/test_unit.py index d79b6b83fb..d758b7829c 100644 --- a/src/bonsai/test/core/test_unit.py +++ b/src/bonsai/test/core/test_unit.py @@ -26,6 +26,9 @@ class TestAssignSceneUnits: unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix") + unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("KILO") + unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return(None) + ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return( "lengthunit" ) @@ -38,7 +41,13 @@ class TestAssignSceneUnits: ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") - ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called() + ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="KILO").should_be_called().will_return("massunit") + + ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + ).should_be_called() subject.assign_scene_units(ifc, unit) def test_creating_and_assigning_imperial_units(self, ifc, unit): @@ -54,7 +63,15 @@ class TestAssignSceneUnits: ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") - ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called() + unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("massname") + ifc.run("unit.add_conversion_based_unit", name="massname").should_be_called().will_return("massunit") + + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("timename") + ifc.run("unit.add_conversion_based_unit", name="timename").should_be_called().will_return("timeunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + ).should_be_called() subject.assign_scene_units(ifc, unit) @@ -162,3 +179,137 @@ class TestEditUnit: unit.import_units().should_be_called() unit.clear_active_unit().should_be_called() subject.edit_unit(ifc, unit, unit="unit") + + +class TestAssignSceneUnitsWithMassAndTime: + def test_creating_and_assigning_metric_units_with_mass_and_time(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(True) + unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix") + unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix") + unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix") + unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("KILO") + unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return("CONVERSION") + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("minute") + + ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return( + "lengthunit" + ) + ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="prefix").should_be_called().will_return("areaunit") + ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="prefix").should_be_called().will_return( + "volumeunit" + ) + ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="KILO").should_be_called().will_return("massunit") + ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("timeunit") + ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + ).should_be_called() + subject.assign_scene_units(ifc, unit) + + def test_creating_and_assigning_imperial_units_with_mass_and_time(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(False) + unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot") + unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot") + unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot") + unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("pound") + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("SECOND") + + ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit") + ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit") + ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit") + ifc.run("unit.add_conversion_based_unit", name="pound").should_be_called().will_return("massunit") + ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit") + ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + ).should_be_called() + subject.assign_scene_units(ifc, unit) + + def test_creating_and_assigning_conversion_based_mass_units(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(False) + unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot") + unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot") + unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot") + unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("tonne") + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("minute") + + ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit") + ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit") + ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit") + ifc.run("unit.add_conversion_based_unit", name="tonne").should_be_called().will_return("massunit") + ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("timeunit") + ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + ).should_be_called() + subject.assign_scene_units(ifc, unit) + + +class TestAddTimeUnit: + def test_adding_si_time_unit(self, ifc, unit): + ifc.run("unit.add_si_unit", unit_type="TIMEUNIT").should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_si_unit(ifc, unit, unit_type="TIMEUNIT") == "unit" + + def test_adding_conversion_based_time_unit_minute(self, ifc, unit): + ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_conversion_based_unit(ifc, unit, name="minute") == "unit" + + def test_adding_conversion_based_time_unit_hour(self, ifc, unit): + ifc.run("unit.add_conversion_based_unit", name="hour").should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_conversion_based_unit(ifc, unit, name="hour") == "unit" + + def test_adding_conversion_based_time_unit_day(self, ifc, unit): + ifc.run("unit.add_conversion_based_unit", name="day").should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_conversion_based_unit(ifc, unit, name="day") == "unit" + + +class TestAssignMassAndTimeUnits: + def test_assigning_mass_unit(self, ifc, unit): + ifc.run("unit.assign_unit", units=["massunit"]).should_be_called() + unit.import_units().should_be_called() + subject.assign_unit(ifc, unit, unit="massunit") + + def test_assigning_time_unit(self, ifc, unit): + ifc.run("unit.assign_unit", units=["timeunit"]).should_be_called() + unit.import_units().should_be_called() + subject.assign_unit(ifc, unit, unit="timeunit") + + def test_assigning_multiple_mass_and_time_units(self, ifc, unit): + ifc.run("unit.assign_unit", units=["massunit"]).should_be_called() + unit.import_units().should_be_called() + ifc.run("unit.assign_unit", units=["timeunit"]).should_be_called() + unit.import_units().should_be_called() + + subject.assign_unit(ifc, unit, unit="massunit") + subject.assign_unit(ifc, unit, unit="timeunit") + + +class TestUnassignMassAndTimeUnits: + def test_unassigning_mass_unit(self, ifc, unit): + ifc.run("unit.unassign_unit", units=["massunit"]).should_be_called() + unit.import_units().should_be_called() + subject.unassign_unit(ifc, unit, unit="massunit") + + def test_unassigning_time_unit(self, ifc, unit): + ifc.run("unit.unassign_unit", units=["timeunit"]).should_be_called() + unit.import_units().should_be_called() + subject.unassign_unit(ifc, unit, unit="timeunit") + + +class TestRemoveMassAndTimeUnits: + def test_removing_mass_unit(self, ifc, unit): + ifc.run("unit.remove_unit", unit="massunit").should_be_called() + unit.import_units().should_be_called() + subject.remove_unit(ifc, unit, unit="massunit") + + def test_removing_time_unit(self, ifc, unit): + ifc.run("unit.remove_unit", unit="timeunit").should_be_called() + unit.import_units().should_be_called() + subject.remove_unit(ifc, unit, unit="timeunit") diff --git a/src/bonsai/test/tool/test_unit.py b/src/bonsai/test/tool/test_unit.py index 212c72a559..190a8fa302 100644 --- a/src/bonsai/test/tool/test_unit.py +++ b/src/bonsai/test/tool/test_unit.py @@ -135,6 +135,35 @@ class TestGetSceneUnitName(NewFile): bpy.context.scene.unit_settings.system = "NONE" assert subject.get_scene_unit_name("LENGTHUNIT") == "foot" + def test_getting_mass_unit_names(self): + """Test getting mass unit names for different systems""" + assert bpy.context.scene + props = tool.Blender.get_bim_props() + props.mass_unit = "GRAM" + assert subject.get_scene_unit_name("MASSUNIT") == "gram" + props.mass_unit = "KILOGRAM" + assert subject.get_scene_unit_name("MASSUNIT") == "kilogram" + props.mass_unit = "POUND" + assert subject.get_scene_unit_name("MASSUNIT") == "pound" + props.mass_unit = "OUNCE" + assert subject.get_scene_unit_name("MASSUNIT") == "ounce" + props.mass_unit = "TONNE" + assert subject.get_scene_unit_name("MASSUNIT") == "tonne" + + def test_getting_time_unit_names(self): + """Test getting time unit names for different systems""" + assert bpy.context.scene + props = tool.Blender.get_bim_props() + + props.time_unit = "SECOND" + assert subject.get_scene_unit_name("TIMEUNIT") == "second" + props.time_unit = "MINUTE" + assert subject.get_scene_unit_name("TIMEUNIT") == "minute" + props.time_unit = "HOUR" + assert subject.get_scene_unit_name("TIMEUNIT") == "hour" + props.time_unit = "DAY" + assert subject.get_scene_unit_name("TIMEUNIT") == "day" + class TestGetSceneUnitSIPrefix: def test_run(self): @@ -162,6 +191,30 @@ class TestGetSceneUnitSIPrefix: props.volume_unit = "MILLI/CUBIC_METRE" assert subject.get_scene_unit_si_prefix("VOLUMEUNIT") == "MILLI" + def test_mass_and_time_unit_prefixes(self): + assert bpy.context.scene + props = tool.Blender.get_bim_props() + + props.mass_unit = "KILOGRAM" + assert subject.get_scene_unit_si_prefix("MASSUNIT") == "KILO" + props.mass_unit = "GRAM" + assert subject.get_scene_unit_si_prefix("MASSUNIT") is None + props.mass_unit = "POUND" + assert subject.get_scene_unit_si_prefix("MASSUNIT") == "CONVERSION" + props.mass_unit = "OUNCE" + assert subject.get_scene_unit_si_prefix("MASSUNIT") == "CONVERSION" + props.mass_unit = "TONNE" + assert subject.get_scene_unit_si_prefix("MASSUNIT") == "MEGA" + + props.time_unit = "SECOND" + assert subject.get_scene_unit_si_prefix("TIMEUNIT") is None + props.time_unit = "MINUTE" + assert subject.get_scene_unit_si_prefix("TIMEUNIT") == "CONVERSION" + props.time_unit = "HOUR" + assert subject.get_scene_unit_si_prefix("TIMEUNIT") == "CONVERSION" + props.time_unit = "DAY" + assert subject.get_scene_unit_si_prefix("TIMEUNIT") == "CONVERSION" + class TestImportUnitAttributes(NewFile): def test_importing_derived_units(self): @@ -298,6 +351,96 @@ class TestImportUnits(NewFile): assert props.units[5].unit_type == unit6.UnitType assert props.units[5].ifc_class == unit6.is_a() + def test_importing_mass_and_time_units(self): + """Test importing mass and time conversion based units""" + ifc = ifcopenshell.api.project.create_file() + tool.Ifc.set(ifc) + + tonne_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="tonne") + pound_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="pound") + ounce_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="ounce") + + minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="minute") + hour_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="hour") + day_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="day") + + kg_unit = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="MASSUNIT", prefix="KILO") + gram_unit = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="MASSUNIT") + second_unit = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="TIMEUNIT") + + ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject") + ifcopenshell.api.unit.assign_unit(ifc, units=[tonne_unit, minute_unit, kg_unit]) + + subject.import_units() + props = tool.Unit.get_unit_props() + + assert len(props.units) == 15 + + unit_ids = [u.ifc_definition_id for u in props.units] + assert tonne_unit.id() in unit_ids + assert pound_unit.id() in unit_ids + assert ounce_unit.id() in unit_ids + assert minute_unit.id() in unit_ids + assert hour_unit.id() in unit_ids + assert day_unit.id() in unit_ids + assert kg_unit.id() in unit_ids + assert gram_unit.id() in unit_ids + assert second_unit.id() in unit_ids + + tonne_prop = next(u for u in props.units if u.ifc_definition_id == tonne_unit.id()) + assert tonne_prop.name == "tonne" + assert tonne_prop.unit_type == "MASSUNIT" + assert tonne_prop.is_assigned is True + assert tonne_prop.ifc_class == "IfcConversionBasedUnit" + + pound_prop = next(u for u in props.units if u.ifc_definition_id == pound_unit.id()) + assert pound_prop.name == "pound" + assert pound_prop.unit_type == "MASSUNIT" + assert pound_prop.is_assigned is False + assert pound_prop.ifc_class == "IfcConversionBasedUnit" + + ounce_prop = next(u for u in props.units if u.ifc_definition_id == ounce_unit.id()) + assert ounce_prop.name == "ounce" + assert ounce_prop.unit_type == "MASSUNIT" + assert ounce_prop.is_assigned is False + assert ounce_prop.ifc_class == "IfcConversionBasedUnit" + + kg_prop = next(u for u in props.units if u.ifc_definition_id == kg_unit.id()) + assert kg_prop.name == "KILOGRAM" + assert kg_prop.unit_type == "MASSUNIT" + assert kg_prop.is_assigned is True + assert kg_prop.ifc_class == "IfcSIUnit" + + gram_prop = next(u for u in props.units if u.ifc_definition_id == gram_unit.id()) + assert gram_prop.name == "GRAM" + assert gram_prop.unit_type == "MASSUNIT" + assert gram_prop.is_assigned is False + assert gram_prop.ifc_class == "IfcSIUnit" + + minute_prop = next(u for u in props.units if u.ifc_definition_id == minute_unit.id()) + assert minute_prop.name == "minute" + assert minute_prop.unit_type == "TIMEUNIT" + assert minute_prop.is_assigned is True + assert minute_prop.ifc_class == "IfcConversionBasedUnit" + + hour_prop = next(u for u in props.units if u.ifc_definition_id == hour_unit.id()) + assert hour_prop.name == "hour" + assert hour_prop.unit_type == "TIMEUNIT" + assert hour_prop.is_assigned is False + assert hour_prop.ifc_class == "IfcConversionBasedUnit" + + day_prop = next(u for u in props.units if u.ifc_definition_id == day_unit.id()) + assert day_prop.name == "day" + assert day_prop.unit_type == "TIMEUNIT" + assert day_prop.is_assigned is False + assert day_prop.ifc_class == "IfcConversionBasedUnit" + + second_prop = next(u for u in props.units if u.ifc_definition_id == second_unit.id()) + assert second_prop.name == "SECOND" + assert second_prop.unit_type == "TIMEUNIT" + assert second_prop.is_assigned is False + assert second_prop.ifc_class == "IfcSIUnit" + class TestIsSceneUnitMetric(NewFile): def test_run(self): diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py index 51aa200810..6a4f17f8fd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py @@ -32,7 +32,7 @@ def add_conversion_based_unit( function. You can choose from one of: inch, foot, yard, mile, square inch, square foot, square yard, acre, square mile, cubic inch, cubic foot, cubic yard, litre, fluid ounce UK, fluid ounce US, pint UK, pint - US, gallon UK, gallon US, degree, ounce, pound, ton UK, ton US, lbf, + US, gallon UK, gallon US, degree, ounce, pound, ton UK, ton US, tonne, lbf, kip, psi, ksi, minute, hour, day, btu, and fahrenheit. :param name: A converted name chosen from the list above. diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index cc767fe3a7..86254cb826 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -209,6 +209,7 @@ si_conversions = { "pound": 0.454, "ton UK": 1016.0469088, "ton US": 907.18474, + "tonne": 1000.0, "lbf": 4.4482216153, "kip": 4448.2216153, "psi": 6894.7572932, @@ -253,6 +254,7 @@ imperial_types = { "pound": "MASSUNIT", "ton UK": "MASSUNIT", "ton US": "MASSUNIT", + "tonne": "MASSUNIT", "lbf": "FORCEUNIT", "kip": "FORCEUNIT", "psi": "PRESSUREUNIT", @@ -323,6 +325,7 @@ unit_symbols = { "pound": "lb", "ton UK": "ton", "ton US": "ton", + "tonne": "t", "lbf": "lbf", "kip": "kip", "psi": "psi", diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index 6e28a28e70..ff7b7eede5 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -391,3 +391,233 @@ class TestConvertFileLengthUnitsIFC4(test.bootstrap.IFC4, TestConvertFileLengthU class TestConvertFileLengthUnitsIFC4X3(test.bootstrap.IFC4X3, TestConvertFileLengthUnits): pass + + +class TestAddConversionBasedUnitMassAndTime(test.bootstrap.IFC4): + def test_adding_mass_units_creates_proper_massunit(self): + mass_units = [ + ("tonne", 1000.0), + ("pound", 0.454), + ("ounce", 0.02835), + ("ton UK", 1016.0469088), + ("ton US", 907.18474), + ] + + for name, expected_conversion in mass_units: + unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name=name) + + assert unit.is_a("IfcConversionBasedUnit") + + assert unit.UnitType == "MASSUNIT" + + assert unit.Name == name + + actual_conversion = unit.ConversionFactor.ValueComponent.wrappedValue + assert actual_conversion == expected_conversion + + target_unit = unit.ConversionFactor.UnitComponent + assert target_unit.is_a("IfcSIUnit") + assert target_unit.UnitType == "MASSUNIT" + assert target_unit.Name == "GRAM" + assert target_unit.Prefix == "KILO" + + def test_adding_time_units_creates_proper_timeunit(self): + time_units = [ + ("minute", 60), + ("hour", 3600), + ("day", 86400), + ] + + for name, expected_conversion in time_units: + unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name=name) + + assert unit.is_a("IfcConversionBasedUnit") + + assert unit.UnitType == "TIMEUNIT" + + assert unit.Name == name + + actual_conversion = unit.ConversionFactor.ValueComponent.wrappedValue + assert actual_conversion == expected_conversion + + target_unit = unit.ConversionFactor.UnitComponent + assert target_unit.is_a("IfcSIUnit") + assert target_unit.UnitType == "TIMEUNIT" + assert target_unit.Name == "SECOND" + assert target_unit.Prefix is None + + def test_mass_unit_integration_with_project(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + + kg_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="MASSUNIT", prefix="KILO") + tonne_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="tonne") + pound_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="pound") + + ifcopenshell.api.unit.assign_unit(self.file, units=[kg_unit]) + project_mass_unit = subject.get_project_unit(self.file, "MASSUNIT") + assert project_mass_unit == kg_unit + + unit_assignment = subject.get_unit_assignment(self.file) + assert unit_assignment + assigned_units = list(unit_assignment.Units or []) + assert kg_unit in assigned_units + + ifcopenshell.api.unit.assign_unit(self.file, units=[tonne_unit]) + project_mass_unit = subject.get_project_unit(self.file, "MASSUNIT") + assert project_mass_unit == tonne_unit + + unit_assignment = subject.get_unit_assignment(self.file) + assigned_units = list(unit_assignment.Units or []) + assert tonne_unit in assigned_units + + length_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[pound_unit, length_unit]) + + unit_assignment = subject.get_unit_assignment(self.file) + assigned_units = list(unit_assignment.Units or []) + assert pound_unit in assigned_units + assert length_unit in assigned_units + + def test_time_unit_integration_with_project(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + + second_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="TIMEUNIT") + minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="minute") + hour_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="hour") + + ifcopenshell.api.unit.assign_unit(self.file, units=[second_unit]) + project_time_unit = subject.get_project_unit(self.file, "TIMEUNIT") + assert project_time_unit == second_unit + + unit_assignment = subject.get_unit_assignment(self.file) + assert unit_assignment + assigned_units = list(unit_assignment.Units or []) + assert second_unit in assigned_units + + ifcopenshell.api.unit.assign_unit(self.file, units=[minute_unit]) + project_time_unit = subject.get_project_unit(self.file, "TIMEUNIT") + assert project_time_unit == minute_unit + + unit_assignment = subject.get_unit_assignment(self.file) + assigned_units = list(unit_assignment.Units or []) + assert minute_unit in assigned_units + + length_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[hour_unit, length_unit]) + + unit_assignment = subject.get_unit_assignment(self.file) + assigned_units = list(unit_assignment.Units or []) + assert hour_unit in assigned_units + assert length_unit in assigned_units + + def test_multiple_unit_types_can_coexist(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + + kg_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="MASSUNIT", prefix="KILO") + minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="minute") + meter_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") + + ifcopenshell.api.unit.assign_unit(self.file, units=[kg_unit, minute_unit, meter_unit]) + + unit_assignment = subject.get_unit_assignment(self.file) + assigned_units = list(unit_assignment.Units or []) + + assert kg_unit in assigned_units + assert minute_unit in assigned_units + assert meter_unit in assigned_units + + assert subject.get_project_unit(self.file, "MASSUNIT") == kg_unit + assert subject.get_project_unit(self.file, "TIMEUNIT") == minute_unit + assert subject.get_project_unit(self.file, "LENGTHUNIT") == meter_unit + + def test_unknown_mass_or_time_units_fall_back_to_userdefined(self): + unknown_mass_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="unknown_mass_unit") + assert unknown_mass_unit.UnitType == "USERDEFINED" + + unknown_time_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="unknown_time_unit") + assert unknown_time_unit.UnitType == "USERDEFINED" + + def test_mass_units_individually(self): + tonne_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="tonne") + assert tonne_unit.UnitType == "MASSUNIT" + assert tonne_unit.ConversionFactor.ValueComponent.wrappedValue == 1000.0 + + pound_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="pound") + assert pound_unit.UnitType == "MASSUNIT" + assert pound_unit.ConversionFactor.ValueComponent.wrappedValue == 0.454 + + ounce_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="ounce") + assert ounce_unit.UnitType == "MASSUNIT" + assert ounce_unit.ConversionFactor.ValueComponent.wrappedValue == 0.02835 + + def test_time_units_individually(self): + minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="minute") + assert minute_unit.UnitType == "TIMEUNIT" + assert minute_unit.ConversionFactor.ValueComponent.wrappedValue == 60 + + hour_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="hour") + assert hour_unit.UnitType == "TIMEUNIT" + assert hour_unit.ConversionFactor.ValueComponent.wrappedValue == 3600 + + day_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="day") + assert day_unit.UnitType == "TIMEUNIT" + assert day_unit.ConversionFactor.ValueComponent.wrappedValue == 86400 + + +class TestMassAndTimeUnitDictionaries(test.bootstrap.IFC4): + def test_mass_units_in_imperial_types(self): + expected_mass_units = ["ounce", "pound", "ton UK", "ton US", "tonne"] + + def test_mass_units_in_imperial_types(self): + expected_mass_units = ["ounce", "pound", "ton UK", "ton US", "tonne"] + + for unit_name in expected_mass_units: + assert unit_name in subject.imperial_types + assert subject.imperial_types[unit_name] == "MASSUNIT" + + def test_time_units_in_imperial_types(self): + expected_time_units = ["minute", "hour", "day"] + + for unit_name in expected_time_units: + assert unit_name in subject.imperial_types + assert subject.imperial_types[unit_name] == "TIMEUNIT" + + def test_mass_units_have_conversion_factors(self): + expected_mass_conversions = { + "ounce": 0.02835, + "pound": 0.454, + "ton UK": 1016.0469088, + "ton US": 907.18474, + "tonne": 1000.0, + } + + for unit_name, expected_factor in expected_mass_conversions.items(): + assert unit_name in subject.si_conversions + assert subject.si_conversions[unit_name] == expected_factor + + def test_time_units_have_conversion_factors(self): + expected_time_conversions = { + "minute": 60, + "hour": 3600, + "day": 86400, + } + + for unit_name, expected_factor in expected_time_conversions.items(): + assert unit_name in subject.si_conversions + assert subject.si_conversions[unit_name] == expected_factor + + def test_mass_and_time_units_have_symbols(self): + expected_symbols = { + "ounce": "oz", + "pound": "lb", + "ton UK": "ton", + "ton US": "ton", + "tonne": "t", + "minute": "min", + "hour": "hr", + "day": "day", + } + + for unit_name, expected_symbol in expected_symbols.items(): + assert unit_name in subject.unit_symbols + assert subject.unit_symbols[unit_name] == expected_symbol From 169ca806c06608da134f0c0f64c6ca334750c7c2 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 10 Sep 2025 23:07:47 +0200 Subject: [PATCH 04/10] Updated with core developer feedback --- src/bonsai/test/core/test_unit.py | 209 ++++------------ .../ifcopenshell/api/unit/assign_unit.py | 74 +----- .../unit/test_add_conversion_based_unit.py | 108 ++++++++ .../test/util/test_unit.py | 230 ------------------ 4 files changed, 167 insertions(+), 454 deletions(-) diff --git a/src/bonsai/test/core/test_unit.py b/src/bonsai/test/core/test_unit.py index d758b7829c..00ffe1dd5e 100644 --- a/src/bonsai/test/core/test_unit.py +++ b/src/bonsai/test/core/test_unit.py @@ -32,45 +32,78 @@ class TestAssignSceneUnits: ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return( "lengthunit" ) - ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="prefix").should_be_called().will_return("areaunit") - ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="prefix").should_be_called().will_return( "volumeunit" ) - + ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="KILO").should_be_called().will_return("massunit") + ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit") ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") - ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="KILO").should_be_called().will_return("massunit") - - ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit") - ifc.run( - "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit", "planeangleunit"] ).should_be_called() subject.assign_scene_units(ifc, unit) def test_creating_and_assigning_imperial_units(self, ifc, unit): unit.is_scene_unit_metric().should_be_called().will_return(False) - unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("lengthname") - ifc.run("unit.add_conversion_based_unit", name="lengthname").should_be_called().will_return("lengthunit") - - unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("areaname") - ifc.run("unit.add_conversion_based_unit", name="areaname").should_be_called().will_return("areaunit") - - unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("volumename") - ifc.run("unit.add_conversion_based_unit", name="volumename").should_be_called().will_return("volumeunit") + unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot") + unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot") + unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot") + unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("pound") + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("SECOND") + ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit") + ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit") + ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit") + ifc.run("unit.add_conversion_based_unit", name="pound").should_be_called().will_return("massunit") + ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit") ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") - unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("massname") - ifc.run("unit.add_conversion_based_unit", name="massname").should_be_called().will_return("massunit") + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit", "planeangleunit"] + ).should_be_called() + subject.assign_scene_units(ifc, unit) - unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("timename") - ifc.run("unit.add_conversion_based_unit", name="timename").should_be_called().will_return("timeunit") + def test_creating_metric_units_with_conversion_based_mass_and_time(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(True) + unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("MILLI") + unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return(None) + unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return(None) + unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("CONVERSION") + unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("tonne") + unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return("CONVERSION") + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("minute") + + ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="MILLI").should_be_called().will_return("lengthunit") + ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=None).should_be_called().will_return("areaunit") + ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=None).should_be_called().will_return("volumeunit") + ifc.run("unit.add_conversion_based_unit", name="tonne").should_be_called().will_return("massunit") + ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("timeunit") + ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") ifc.run( - "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit", "planeangleunit"] + ).should_be_called() + subject.assign_scene_units(ifc, unit) + + def test_creating_imperial_units_with_conversion_based_units(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(False) + unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("inch") + unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square inch") + unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic inch") + unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("ounce") + unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("hour") + + ifc.run("unit.add_conversion_based_unit", name="inch").should_be_called().will_return("lengthunit") + ifc.run("unit.add_conversion_based_unit", name="square inch").should_be_called().will_return("areaunit") + ifc.run("unit.add_conversion_based_unit", name="cubic inch").should_be_called().will_return("volumeunit") + ifc.run("unit.add_conversion_based_unit", name="ounce").should_be_called().will_return("massunit") + ifc.run("unit.add_conversion_based_unit", name="hour").should_be_called().will_return("timeunit") + ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") + + ifc.run( + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit", "planeangleunit"] ).should_be_called() subject.assign_scene_units(ifc, unit) @@ -179,137 +212,3 @@ class TestEditUnit: unit.import_units().should_be_called() unit.clear_active_unit().should_be_called() subject.edit_unit(ifc, unit, unit="unit") - - -class TestAssignSceneUnitsWithMassAndTime: - def test_creating_and_assigning_metric_units_with_mass_and_time(self, ifc, unit): - unit.is_scene_unit_metric().should_be_called().will_return(True) - unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix") - unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix") - unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix") - unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("KILO") - unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return("CONVERSION") - unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("minute") - - ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return( - "lengthunit" - ) - ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="prefix").should_be_called().will_return("areaunit") - ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="prefix").should_be_called().will_return( - "volumeunit" - ) - ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="KILO").should_be_called().will_return("massunit") - ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("timeunit") - ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") - - ifc.run( - "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] - ).should_be_called() - subject.assign_scene_units(ifc, unit) - - def test_creating_and_assigning_imperial_units_with_mass_and_time(self, ifc, unit): - unit.is_scene_unit_metric().should_be_called().will_return(False) - unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot") - unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot") - unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot") - unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("pound") - unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("SECOND") - - ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit") - ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit") - ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit") - ifc.run("unit.add_conversion_based_unit", name="pound").should_be_called().will_return("massunit") - ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit") - ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") - - ifc.run( - "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] - ).should_be_called() - subject.assign_scene_units(ifc, unit) - - def test_creating_and_assigning_conversion_based_mass_units(self, ifc, unit): - unit.is_scene_unit_metric().should_be_called().will_return(False) - unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot") - unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot") - unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot") - unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("tonne") - unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("minute") - - ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit") - ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit") - ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit") - ifc.run("unit.add_conversion_based_unit", name="tonne").should_be_called().will_return("massunit") - ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("timeunit") - ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") - - ifc.run( - "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] - ).should_be_called() - subject.assign_scene_units(ifc, unit) - - -class TestAddTimeUnit: - def test_adding_si_time_unit(self, ifc, unit): - ifc.run("unit.add_si_unit", unit_type="TIMEUNIT").should_be_called().will_return("unit") - unit.import_units().should_be_called() - assert subject.add_si_unit(ifc, unit, unit_type="TIMEUNIT") == "unit" - - def test_adding_conversion_based_time_unit_minute(self, ifc, unit): - ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("unit") - unit.import_units().should_be_called() - assert subject.add_conversion_based_unit(ifc, unit, name="minute") == "unit" - - def test_adding_conversion_based_time_unit_hour(self, ifc, unit): - ifc.run("unit.add_conversion_based_unit", name="hour").should_be_called().will_return("unit") - unit.import_units().should_be_called() - assert subject.add_conversion_based_unit(ifc, unit, name="hour") == "unit" - - def test_adding_conversion_based_time_unit_day(self, ifc, unit): - ifc.run("unit.add_conversion_based_unit", name="day").should_be_called().will_return("unit") - unit.import_units().should_be_called() - assert subject.add_conversion_based_unit(ifc, unit, name="day") == "unit" - - -class TestAssignMassAndTimeUnits: - def test_assigning_mass_unit(self, ifc, unit): - ifc.run("unit.assign_unit", units=["massunit"]).should_be_called() - unit.import_units().should_be_called() - subject.assign_unit(ifc, unit, unit="massunit") - - def test_assigning_time_unit(self, ifc, unit): - ifc.run("unit.assign_unit", units=["timeunit"]).should_be_called() - unit.import_units().should_be_called() - subject.assign_unit(ifc, unit, unit="timeunit") - - def test_assigning_multiple_mass_and_time_units(self, ifc, unit): - ifc.run("unit.assign_unit", units=["massunit"]).should_be_called() - unit.import_units().should_be_called() - ifc.run("unit.assign_unit", units=["timeunit"]).should_be_called() - unit.import_units().should_be_called() - - subject.assign_unit(ifc, unit, unit="massunit") - subject.assign_unit(ifc, unit, unit="timeunit") - - -class TestUnassignMassAndTimeUnits: - def test_unassigning_mass_unit(self, ifc, unit): - ifc.run("unit.unassign_unit", units=["massunit"]).should_be_called() - unit.import_units().should_be_called() - subject.unassign_unit(ifc, unit, unit="massunit") - - def test_unassigning_time_unit(self, ifc, unit): - ifc.run("unit.unassign_unit", units=["timeunit"]).should_be_called() - unit.import_units().should_be_called() - subject.unassign_unit(ifc, unit, unit="timeunit") - - -class TestRemoveMassAndTimeUnits: - def test_removing_mass_unit(self, ifc, unit): - ifc.run("unit.remove_unit", unit="massunit").should_be_called() - unit.import_units().should_be_called() - subject.remove_unit(ifc, unit, unit="massunit") - - def test_removing_time_unit(self, ifc, unit): - ifc.run("unit.remove_unit", unit="timeunit").should_be_called() - unit.import_units().should_be_called() - subject.remove_unit(ifc, unit, unit="timeunit") diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py index 58abfd4ea4..f9563410cd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py @@ -27,8 +27,6 @@ def assign_unit( length: Optional[dict] = None, area: Optional[dict] = None, volume: Optional[dict] = None, - mass: Optional[dict] = None, - time: Optional[dict] = None, ) -> ifcopenshell.entity_instance: """Assign default project units @@ -53,19 +51,16 @@ def assign_unit( # You need a project before you can assign units. ifcopenshell.api.root.create_entity(model, ifc_class="IfcProject") - # Millimeters, square meters, kilograms, and seconds + # Create units explicitly length = ifcopenshell.api.unit.add_si_unit(model, unit_type="LENGTHUNIT", prefix="MILLI") area = ifcopenshell.api.unit.add_si_unit(model, unit_type="AREAUNIT") mass = ifcopenshell.api.unit.add_si_unit(model, unit_type="MASSUNIT", prefix="KILO") - time = ifcopenshell.api.unit.add_si_unit(model, unit_type="TIMEUNIT") + time = ifcopenshell.api.unit.add_conversion_based_unit(model, name="minute") - # Make it our default units, if we are doing a metric building - ifcopenshell.api.unit.assign_unit(model, units=[length, area]) + # Assign all units to the project + ifcopenshell.api.unit.assign_unit(model, units=[length, area, mass, time]) - # Alternatively, you may specify without any arguments to - # automatically create millimeters, square meters, and cubic meters - # as a convenience for testing purposes. Sorry imperial folks, we - # prioritise metric here. + # Alternatively, for basic metric units (length, area, volume only) ifcopenshell.api.unit.assign_unit(model) """ usecase = Usecase() @@ -75,8 +70,6 @@ def assign_unit( usecase.settings["length"] = length or {"is_metric": True, "raw": "MILLIMETERS"} usecase.settings["area"] = area or {"is_metric": True, "raw": "METERS"} usecase.settings["volume"] = volume or {"is_metric": True, "raw": "METERS"} - usecase.settings["mass"] = mass or {"is_metric": True, "raw": "KILOGRAM"} - usecase.settings["time"] = time or {"is_metric": True, "raw": "SECOND"} return usecase.execute() @@ -122,45 +115,7 @@ class Usecase: units.add(unit) unit_assignment.Units = list(units) - def create_time_conversion_unit(self, name: str, factor: float) -> ifcopenshell.entity_instance: - """Create a conversion-based time unit""" - dimensional_exponents = self.file.createIfcDimensionalExponents(0, 0, 1, 0, 0, 0, 0) - si_unit = self.file.createIfcSIUnit(None, "TIMEUNIT", None, "SECOND") - value_component = self.file.create_entity("IfcReal", **{"wrappedValue": factor}) - conversion_factor = self.file.createIfcMeasureWithUnit(value_component, si_unit) - return self.file.createIfcConversionBasedUnit(dimensional_exponents, "TIMEUNIT", name, conversion_factor) - - def create_mass_conversion_unit(self, name: str, factor: float) -> ifcopenshell.entity_instance: - """Create a conversion-based mass unit""" - dimensional_exponents = self.file.createIfcDimensionalExponents(0, 1, 0, 0, 0, 0, 0) # Mass dimension - si_unit = self.file.createIfcSIUnit(None, "MASSUNIT", "KILO", "GRAM") - value_component = self.file.create_entity("IfcReal", **{"wrappedValue": factor}) - conversion_factor = self.file.createIfcMeasureWithUnit(value_component, si_unit) - return self.file.createIfcConversionBasedUnit(dimensional_exponents, "MASSUNIT", name, conversion_factor) - def create_metric_unit(self, unit_type: str, data: dict) -> ifcopenshell.entity_instance: - if unit_type == "mass": - if data["raw"] == "KILOGRAM": - return self.file.createIfcSIUnit(None, "MASSUNIT", "KILO", "GRAM") - elif data["raw"] == "GRAM": - return self.file.createIfcSIUnit(None, "MASSUNIT", None, "GRAM") - elif data["raw"] == "TON": - return self.file.createIfcSIUnit(None, "MASSUNIT", "MEGA", "GRAM") - else: - return self.file.createIfcSIUnit(None, "MASSUNIT", "KILO", "GRAM") - - elif unit_type == "time": - if data["raw"] == "SECOND": - return self.file.createIfcSIUnit(None, "TIMEUNIT", None, "SECOND") - elif data["raw"] == "MINUTE": - return self.create_time_conversion_unit("minute", 60.0) - elif data["raw"] == "HOUR": - return self.create_time_conversion_unit("hour", 3600.0) - elif data["raw"] == "DAY": - return self.create_time_conversion_unit("day", 86400.0) - else: - return self.file.createIfcSIUnit(None, "TIMEUNIT", None, "SECOND") - type_prefix = "" if unit_type == "area": type_prefix = "SQUARE_" @@ -183,25 +138,6 @@ class Usecase: elif unit_type == "volume": dimensional_exponents = self.file.createIfcDimensionalExponents(3, 0, 0, 0, 0, 0, 0) name_prefix = "cubic" - elif unit_type == "mass": - if data["raw"] == "POUND": - return self.create_mass_conversion_unit("pound", 0.45359237) - elif data["raw"] == "OUNCE": - return self.create_mass_conversion_unit("ounce", 0.0283495) - else: - return self.create_mass_conversion_unit("pound", 0.45359237) - - elif unit_type == "time": - if data["raw"] == "SECOND": - return self.file.createIfcSIUnit(None, "TIMEUNIT", None, "SECOND") - elif data["raw"] == "MINUTE": - return self.create_time_conversion_unit("minute", 60.0) - elif data["raw"] == "HOUR": - return self.create_time_conversion_unit("hour", 3600.0) - elif data["raw"] == "DAY": - return self.create_time_conversion_unit("day", 86400.0) - else: - return self.file.createIfcSIUnit(None, "TIMEUNIT", None, "SECOND") si_unit = self.file.createIfcSIUnit( None, diff --git a/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py b/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py index 03a8a7edfd..87ed50ec8f 100644 --- a/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py +++ b/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py @@ -18,6 +18,7 @@ import test.bootstrap import ifcopenshell.api.unit +import ifcopenshell.util.unit as subject class TestAddConversionBasedUnitIFC2X3(test.bootstrap.IFC2X3): @@ -61,3 +62,110 @@ class TestAddConversionBasedUnitIFC4(test.bootstrap.IFC4, TestAddConversionBased assert si_unit.Prefix is None assert si_unit.Name == "KELVIN" assert unit.ConversionOffset == -459.67 + + def test_adding_mass_units_creates_proper_massunit(self): + mass_units = [ + ("tonne", 1000.0), + ("pound", 0.454), + ("ounce", 0.02835), + ("ton UK", 1016.0469088), + ("ton US", 907.18474), + ] + + for name, expected_conversion in mass_units: + unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name=name) + + assert unit.is_a("IfcConversionBasedUnit") + assert unit.UnitType == "MASSUNIT" + assert unit.Name == name + + actual_conversion = unit.ConversionFactor.ValueComponent.wrappedValue + assert actual_conversion == expected_conversion + + target_unit = unit.ConversionFactor.UnitComponent + assert target_unit.is_a("IfcSIUnit") + assert target_unit.UnitType == "MASSUNIT" + assert target_unit.Name == "GRAM" + assert target_unit.Prefix == "KILO" + + def test_adding_time_units_creates_proper_timeunit(self): + time_units = [ + ("minute", 60), + ("hour", 3600), + ("day", 86400), + ] + + for name, expected_conversion in time_units: + unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name=name) + + assert unit.is_a("IfcConversionBasedUnit") + assert unit.UnitType == "TIMEUNIT" + assert unit.Name == name + + actual_conversion = unit.ConversionFactor.ValueComponent.wrappedValue + assert actual_conversion == expected_conversion + + target_unit = unit.ConversionFactor.UnitComponent + assert target_unit.is_a("IfcSIUnit") + assert target_unit.UnitType == "TIMEUNIT" + assert target_unit.Name == "SECOND" + assert target_unit.Prefix is None + + def test_unknown_units_fall_back_to_userdefined(self): + unknown_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="unknown_unit") + assert unknown_unit.UnitType == "USERDEFINED" + assert unknown_unit.Name == "unknown_unit" + + def test_mass_units_in_imperial_types(self): + expected_mass_units = ["ounce", "pound", "ton UK", "ton US", "tonne"] + + for unit_name in expected_mass_units: + assert unit_name in subject.imperial_types + assert subject.imperial_types[unit_name] == "MASSUNIT" + + def test_time_units_in_imperial_types(self): + expected_time_units = ["minute", "hour", "day"] + + for unit_name in expected_time_units: + assert unit_name in subject.imperial_types + assert subject.imperial_types[unit_name] == "TIMEUNIT" + + def test_mass_units_have_conversion_factors(self): + expected_mass_conversions = { + "ounce": 0.02835, + "pound": 0.454, + "ton UK": 1016.0469088, + "ton US": 907.18474, + "tonne": 1000.0, + } + + for unit_name, expected_factor in expected_mass_conversions.items(): + assert unit_name in subject.si_conversions + assert subject.si_conversions[unit_name] == expected_factor + + def test_time_units_have_conversion_factors(self): + expected_time_conversions = { + "minute": 60, + "hour": 3600, + "day": 86400, + } + + for unit_name, expected_factor in expected_time_conversions.items(): + assert unit_name in subject.si_conversions + assert subject.si_conversions[unit_name] == expected_factor + + def test_mass_and_time_units_have_symbols(self): + expected_symbols = { + "ounce": "oz", + "pound": "lb", + "ton UK": "ton", + "ton US": "ton", + "tonne": "t", + "minute": "min", + "hour": "hr", + "day": "day", + } + + for unit_name, expected_symbol in expected_symbols.items(): + assert unit_name in subject.unit_symbols + assert subject.unit_symbols[unit_name] == expected_symbol diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index ff7b7eede5..6e28a28e70 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -391,233 +391,3 @@ class TestConvertFileLengthUnitsIFC4(test.bootstrap.IFC4, TestConvertFileLengthU class TestConvertFileLengthUnitsIFC4X3(test.bootstrap.IFC4X3, TestConvertFileLengthUnits): pass - - -class TestAddConversionBasedUnitMassAndTime(test.bootstrap.IFC4): - def test_adding_mass_units_creates_proper_massunit(self): - mass_units = [ - ("tonne", 1000.0), - ("pound", 0.454), - ("ounce", 0.02835), - ("ton UK", 1016.0469088), - ("ton US", 907.18474), - ] - - for name, expected_conversion in mass_units: - unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name=name) - - assert unit.is_a("IfcConversionBasedUnit") - - assert unit.UnitType == "MASSUNIT" - - assert unit.Name == name - - actual_conversion = unit.ConversionFactor.ValueComponent.wrappedValue - assert actual_conversion == expected_conversion - - target_unit = unit.ConversionFactor.UnitComponent - assert target_unit.is_a("IfcSIUnit") - assert target_unit.UnitType == "MASSUNIT" - assert target_unit.Name == "GRAM" - assert target_unit.Prefix == "KILO" - - def test_adding_time_units_creates_proper_timeunit(self): - time_units = [ - ("minute", 60), - ("hour", 3600), - ("day", 86400), - ] - - for name, expected_conversion in time_units: - unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name=name) - - assert unit.is_a("IfcConversionBasedUnit") - - assert unit.UnitType == "TIMEUNIT" - - assert unit.Name == name - - actual_conversion = unit.ConversionFactor.ValueComponent.wrappedValue - assert actual_conversion == expected_conversion - - target_unit = unit.ConversionFactor.UnitComponent - assert target_unit.is_a("IfcSIUnit") - assert target_unit.UnitType == "TIMEUNIT" - assert target_unit.Name == "SECOND" - assert target_unit.Prefix is None - - def test_mass_unit_integration_with_project(self): - ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") - - kg_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="MASSUNIT", prefix="KILO") - tonne_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="tonne") - pound_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="pound") - - ifcopenshell.api.unit.assign_unit(self.file, units=[kg_unit]) - project_mass_unit = subject.get_project_unit(self.file, "MASSUNIT") - assert project_mass_unit == kg_unit - - unit_assignment = subject.get_unit_assignment(self.file) - assert unit_assignment - assigned_units = list(unit_assignment.Units or []) - assert kg_unit in assigned_units - - ifcopenshell.api.unit.assign_unit(self.file, units=[tonne_unit]) - project_mass_unit = subject.get_project_unit(self.file, "MASSUNIT") - assert project_mass_unit == tonne_unit - - unit_assignment = subject.get_unit_assignment(self.file) - assigned_units = list(unit_assignment.Units or []) - assert tonne_unit in assigned_units - - length_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") - ifcopenshell.api.unit.assign_unit(self.file, units=[pound_unit, length_unit]) - - unit_assignment = subject.get_unit_assignment(self.file) - assigned_units = list(unit_assignment.Units or []) - assert pound_unit in assigned_units - assert length_unit in assigned_units - - def test_time_unit_integration_with_project(self): - ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") - - second_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="TIMEUNIT") - minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="minute") - hour_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="hour") - - ifcopenshell.api.unit.assign_unit(self.file, units=[second_unit]) - project_time_unit = subject.get_project_unit(self.file, "TIMEUNIT") - assert project_time_unit == second_unit - - unit_assignment = subject.get_unit_assignment(self.file) - assert unit_assignment - assigned_units = list(unit_assignment.Units or []) - assert second_unit in assigned_units - - ifcopenshell.api.unit.assign_unit(self.file, units=[minute_unit]) - project_time_unit = subject.get_project_unit(self.file, "TIMEUNIT") - assert project_time_unit == minute_unit - - unit_assignment = subject.get_unit_assignment(self.file) - assigned_units = list(unit_assignment.Units or []) - assert minute_unit in assigned_units - - length_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") - ifcopenshell.api.unit.assign_unit(self.file, units=[hour_unit, length_unit]) - - unit_assignment = subject.get_unit_assignment(self.file) - assigned_units = list(unit_assignment.Units or []) - assert hour_unit in assigned_units - assert length_unit in assigned_units - - def test_multiple_unit_types_can_coexist(self): - ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") - - kg_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="MASSUNIT", prefix="KILO") - minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="minute") - meter_unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") - - ifcopenshell.api.unit.assign_unit(self.file, units=[kg_unit, minute_unit, meter_unit]) - - unit_assignment = subject.get_unit_assignment(self.file) - assigned_units = list(unit_assignment.Units or []) - - assert kg_unit in assigned_units - assert minute_unit in assigned_units - assert meter_unit in assigned_units - - assert subject.get_project_unit(self.file, "MASSUNIT") == kg_unit - assert subject.get_project_unit(self.file, "TIMEUNIT") == minute_unit - assert subject.get_project_unit(self.file, "LENGTHUNIT") == meter_unit - - def test_unknown_mass_or_time_units_fall_back_to_userdefined(self): - unknown_mass_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="unknown_mass_unit") - assert unknown_mass_unit.UnitType == "USERDEFINED" - - unknown_time_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="unknown_time_unit") - assert unknown_time_unit.UnitType == "USERDEFINED" - - def test_mass_units_individually(self): - tonne_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="tonne") - assert tonne_unit.UnitType == "MASSUNIT" - assert tonne_unit.ConversionFactor.ValueComponent.wrappedValue == 1000.0 - - pound_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="pound") - assert pound_unit.UnitType == "MASSUNIT" - assert pound_unit.ConversionFactor.ValueComponent.wrappedValue == 0.454 - - ounce_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="ounce") - assert ounce_unit.UnitType == "MASSUNIT" - assert ounce_unit.ConversionFactor.ValueComponent.wrappedValue == 0.02835 - - def test_time_units_individually(self): - minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="minute") - assert minute_unit.UnitType == "TIMEUNIT" - assert minute_unit.ConversionFactor.ValueComponent.wrappedValue == 60 - - hour_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="hour") - assert hour_unit.UnitType == "TIMEUNIT" - assert hour_unit.ConversionFactor.ValueComponent.wrappedValue == 3600 - - day_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="day") - assert day_unit.UnitType == "TIMEUNIT" - assert day_unit.ConversionFactor.ValueComponent.wrappedValue == 86400 - - -class TestMassAndTimeUnitDictionaries(test.bootstrap.IFC4): - def test_mass_units_in_imperial_types(self): - expected_mass_units = ["ounce", "pound", "ton UK", "ton US", "tonne"] - - def test_mass_units_in_imperial_types(self): - expected_mass_units = ["ounce", "pound", "ton UK", "ton US", "tonne"] - - for unit_name in expected_mass_units: - assert unit_name in subject.imperial_types - assert subject.imperial_types[unit_name] == "MASSUNIT" - - def test_time_units_in_imperial_types(self): - expected_time_units = ["minute", "hour", "day"] - - for unit_name in expected_time_units: - assert unit_name in subject.imperial_types - assert subject.imperial_types[unit_name] == "TIMEUNIT" - - def test_mass_units_have_conversion_factors(self): - expected_mass_conversions = { - "ounce": 0.02835, - "pound": 0.454, - "ton UK": 1016.0469088, - "ton US": 907.18474, - "tonne": 1000.0, - } - - for unit_name, expected_factor in expected_mass_conversions.items(): - assert unit_name in subject.si_conversions - assert subject.si_conversions[unit_name] == expected_factor - - def test_time_units_have_conversion_factors(self): - expected_time_conversions = { - "minute": 60, - "hour": 3600, - "day": 86400, - } - - for unit_name, expected_factor in expected_time_conversions.items(): - assert unit_name in subject.si_conversions - assert subject.si_conversions[unit_name] == expected_factor - - def test_mass_and_time_units_have_symbols(self): - expected_symbols = { - "ounce": "oz", - "pound": "lb", - "ton UK": "ton", - "ton US": "ton", - "tonne": "t", - "minute": "min", - "hour": "hr", - "day": "day", - } - - for unit_name, expected_symbol in expected_symbols.items(): - assert unit_name in subject.unit_symbols - assert subject.unit_symbols[unit_name] == expected_symbol From 93579b8f663ed597776e29203d3e10f0104d5c81 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Mon, 15 Sep 2025 23:38:26 +0200 Subject: [PATCH 05/10] Cleanup based on core developer's feedback --- src/bonsai/test/core/test_unit.py | 8 +- .../ifcopenshell/api/unit/assign_unit.py | 15 ++- .../unit/test_add_conversion_based_unit.py | 101 ++++-------------- 3 files changed, 36 insertions(+), 88 deletions(-) diff --git a/src/bonsai/test/core/test_unit.py b/src/bonsai/test/core/test_unit.py index 00ffe1dd5e..818ad72021 100644 --- a/src/bonsai/test/core/test_unit.py +++ b/src/bonsai/test/core/test_unit.py @@ -41,7 +41,7 @@ class TestAssignSceneUnits: ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") ifc.run( - "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit", "planeangleunit"] + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] ).should_be_called() subject.assign_scene_units(ifc, unit) @@ -61,7 +61,7 @@ class TestAssignSceneUnits: ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") ifc.run( - "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit", "planeangleunit"] + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] ).should_be_called() subject.assign_scene_units(ifc, unit) @@ -83,7 +83,7 @@ class TestAssignSceneUnits: ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") ifc.run( - "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit", "planeangleunit"] + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] ).should_be_called() subject.assign_scene_units(ifc, unit) @@ -103,7 +103,7 @@ class TestAssignSceneUnits: ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") ifc.run( - "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "massunit", "timeunit", "planeangleunit"] + "unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"] ).should_be_called() subject.assign_scene_units(ifc, unit) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py index f9563410cd..5861e4248a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py @@ -51,16 +51,21 @@ def assign_unit( # You need a project before you can assign units. ifcopenshell.api.root.create_entity(model, ifc_class="IfcProject") - # Create units explicitly + # Millimeters and square meters length = ifcopenshell.api.unit.add_si_unit(model, unit_type="LENGTHUNIT", prefix="MILLI") area = ifcopenshell.api.unit.add_si_unit(model, unit_type="AREAUNIT") - mass = ifcopenshell.api.unit.add_si_unit(model, unit_type="MASSUNIT", prefix="KILO") - time = ifcopenshell.api.unit.add_conversion_based_unit(model, name="minute") - # Assign all units to the project + # Optionally, add mass and time units + mass = ifcopenshell.api.unit.add_si_unit(model, unit_type="MASSUNIT", prefix="KILO") + time = ifcopenshell.api.unit.add_si_unit(model, unit_type="TIMEUNIT") + + # Make these the default units for the project ifcopenshell.api.unit.assign_unit(model, units=[length, area, mass, time]) - # Alternatively, for basic metric units (length, area, volume only) + # Alternatively, you may specify without any arguments to + # automatically create millimeters, square meters, and cubic meters + # as a convenience for testing purposes. Sorry imperial folks, we + # prioritise metric here. ifcopenshell.api.unit.assign_unit(model) """ usecase = Usecase() diff --git a/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py b/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py index 87ed50ec8f..93a5002396 100644 --- a/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py +++ b/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py @@ -18,8 +18,6 @@ import test.bootstrap import ifcopenshell.api.unit -import ifcopenshell.util.unit as subject - class TestAddConversionBasedUnitIFC2X3(test.bootstrap.IFC2X3): def test_run(self): @@ -41,28 +39,6 @@ class TestAddConversionBasedUnitIFC2X3(test.bootstrap.IFC2X3): assert si_unit.Prefix is None assert si_unit.Name == "METRE" - -class TestAddConversionBasedUnitIFC4(test.bootstrap.IFC4, TestAddConversionBasedUnitIFC2X3): - def test_adding_a_unit_with_offset(self): - unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="fahrenheit") - assert unit.is_a("IfcConversionBasedUnitWithOffset") - assert unit.Dimensions.LengthExponent == 0 - assert unit.Dimensions.MassExponent == 0 - assert unit.Dimensions.TimeExponent == 0 - assert unit.Dimensions.ElectricCurrentExponent == 0 - assert unit.Dimensions.ThermodynamicTemperatureExponent == 1 - assert unit.Dimensions.AmountOfSubstanceExponent == 0 - assert unit.Dimensions.LuminousIntensityExponent == 0 - assert unit.UnitType == "THERMODYNAMICTEMPERATUREUNIT" - assert unit.Name == "fahrenheit" - assert unit.ConversionFactor.ValueComponent.wrappedValue == 1.8 - si_unit = unit.ConversionFactor.UnitComponent - assert si_unit.is_a("IfcSIUnit") - assert si_unit.UnitType == "THERMODYNAMICTEMPERATUREUNIT" - assert si_unit.Prefix is None - assert si_unit.Name == "KELVIN" - assert unit.ConversionOffset == -459.67 - def test_adding_mass_units_creates_proper_massunit(self): mass_units = [ ("tonne", 1000.0), @@ -111,61 +87,28 @@ class TestAddConversionBasedUnitIFC4(test.bootstrap.IFC4, TestAddConversionBased assert target_unit.Name == "SECOND" assert target_unit.Prefix is None +class TestAddConversionBasedUnitIFC4(test.bootstrap.IFC4, TestAddConversionBasedUnitIFC2X3): + def test_adding_a_unit_with_offset(self): + unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="fahrenheit") + assert unit.is_a("IfcConversionBasedUnitWithOffset") + assert unit.Dimensions.LengthExponent == 0 + assert unit.Dimensions.MassExponent == 0 + assert unit.Dimensions.TimeExponent == 0 + assert unit.Dimensions.ElectricCurrentExponent == 0 + assert unit.Dimensions.ThermodynamicTemperatureExponent == 1 + assert unit.Dimensions.AmountOfSubstanceExponent == 0 + assert unit.Dimensions.LuminousIntensityExponent == 0 + assert unit.UnitType == "THERMODYNAMICTEMPERATUREUNIT" + assert unit.Name == "fahrenheit" + assert unit.ConversionFactor.ValueComponent.wrappedValue == 1.8 + si_unit = unit.ConversionFactor.UnitComponent + assert si_unit.is_a("IfcSIUnit") + assert si_unit.UnitType == "THERMODYNAMICTEMPERATUREUNIT" + assert si_unit.Prefix is None + assert si_unit.Name == "KELVIN" + assert unit.ConversionOffset == -459.67 + def test_unknown_units_fall_back_to_userdefined(self): unknown_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="unknown_unit") assert unknown_unit.UnitType == "USERDEFINED" - assert unknown_unit.Name == "unknown_unit" - - def test_mass_units_in_imperial_types(self): - expected_mass_units = ["ounce", "pound", "ton UK", "ton US", "tonne"] - - for unit_name in expected_mass_units: - assert unit_name in subject.imperial_types - assert subject.imperial_types[unit_name] == "MASSUNIT" - - def test_time_units_in_imperial_types(self): - expected_time_units = ["minute", "hour", "day"] - - for unit_name in expected_time_units: - assert unit_name in subject.imperial_types - assert subject.imperial_types[unit_name] == "TIMEUNIT" - - def test_mass_units_have_conversion_factors(self): - expected_mass_conversions = { - "ounce": 0.02835, - "pound": 0.454, - "ton UK": 1016.0469088, - "ton US": 907.18474, - "tonne": 1000.0, - } - - for unit_name, expected_factor in expected_mass_conversions.items(): - assert unit_name in subject.si_conversions - assert subject.si_conversions[unit_name] == expected_factor - - def test_time_units_have_conversion_factors(self): - expected_time_conversions = { - "minute": 60, - "hour": 3600, - "day": 86400, - } - - for unit_name, expected_factor in expected_time_conversions.items(): - assert unit_name in subject.si_conversions - assert subject.si_conversions[unit_name] == expected_factor - - def test_mass_and_time_units_have_symbols(self): - expected_symbols = { - "ounce": "oz", - "pound": "lb", - "ton UK": "ton", - "ton US": "ton", - "tonne": "t", - "minute": "min", - "hour": "hr", - "day": "day", - } - - for unit_name, expected_symbol in expected_symbols.items(): - assert unit_name in subject.unit_symbols - assert subject.unit_symbols[unit_name] == expected_symbol + assert unknown_unit.Name == "unknown_unit" \ No newline at end of file From b1c4822315c483086724e49780b26c05db3d9d8c Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Mon, 15 Sep 2025 23:42:34 +0200 Subject: [PATCH 06/10] black formatting --- .../test/api/unit/test_add_conversion_based_unit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py b/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py index 93a5002396..a29b9b2ad8 100644 --- a/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py +++ b/src/ifcopenshell-python/test/api/unit/test_add_conversion_based_unit.py @@ -19,6 +19,7 @@ import test.bootstrap import ifcopenshell.api.unit + class TestAddConversionBasedUnitIFC2X3(test.bootstrap.IFC2X3): def test_run(self): unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="foot") @@ -87,6 +88,7 @@ class TestAddConversionBasedUnitIFC2X3(test.bootstrap.IFC2X3): assert target_unit.Name == "SECOND" assert target_unit.Prefix is None + class TestAddConversionBasedUnitIFC4(test.bootstrap.IFC4, TestAddConversionBasedUnitIFC2X3): def test_adding_a_unit_with_offset(self): unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="fahrenheit") @@ -111,4 +113,4 @@ class TestAddConversionBasedUnitIFC4(test.bootstrap.IFC4, TestAddConversionBased def test_unknown_units_fall_back_to_userdefined(self): unknown_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="unknown_unit") assert unknown_unit.UnitType == "USERDEFINED" - assert unknown_unit.Name == "unknown_unit" \ No newline at end of file + assert unknown_unit.Name == "unknown_unit" From f902c7a50af601b4882f77539b755dda447ceab1 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Tue, 16 Sep 2025 18:24:20 +0200 Subject: [PATCH 07/10] ui change to add or not add mass and time units --- .../bonsai/bim/module/project/operator.py | 6 -- src/bonsai/bonsai/bim/module/project/ui.py | 15 +++-- src/bonsai/bonsai/bim/prop.py | 5 ++ src/bonsai/bonsai/core/tool.py | 2 +- src/bonsai/bonsai/core/unit.py | 57 ++++++++++--------- src/bonsai/bonsai/tool/unit.py | 6 ++ src/bonsai/test/core/test_unit.py | 31 ++++++++++ 7 files changed, 83 insertions(+), 39 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 34d6cdb91d..26850ce85a 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -98,8 +98,6 @@ class NewProject(bpy.types.Operator): bpy.context.scene.unit_settings.length_unit = "METERS" bim_props.area_unit = "SQUARE_METRE" bim_props.volume_unit = "CUBIC_METRE" - bim_props.mass_unit = "KILOGRAM" - bim_props.time_unit = "SECOND" pprops.template_file = "0" elif self.preset == "metric_mm": pprops.export_schema = "IFC4" @@ -107,8 +105,6 @@ class NewProject(bpy.types.Operator): bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" bim_props.area_unit = "SQUARE_METRE" bim_props.volume_unit = "CUBIC_METRE" - bim_props.mass_unit = "KILOGRAM" - bim_props.time_unit = "SECOND" pprops.template_file = "0" elif self.preset == "imperial_ft": pprops.export_schema = "IFC4" @@ -116,8 +112,6 @@ class NewProject(bpy.types.Operator): bpy.context.scene.unit_settings.length_unit = "FEET" bim_props.area_unit = "square foot" bim_props.volume_unit = "cubic foot" - bim_props.mass_unit = "POUND" - bim_props.time_unit = "SECOND" pprops.template_file = "0" elif self.preset == "demo": pprops.export_schema = "IFC4" diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 731a9d0688..ed6e65e674 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -357,15 +357,20 @@ class BIM_PT_new_project_wizard(Panel): row = self.layout.row() row.prop(props, "volume_unit", text="Volume Unit") row = self.layout.row() - row.prop(props, "mass_unit", text="Mass Unit") - row = self.layout.row() - row.prop(props, "time_unit", text="Time Unit") prop_with_search(self.layout, pprops, "template_file", text="Template") - + self.layout.use_property_split = False + row = self.layout.row() + label = "Add Mass and Time Units" if not props.add_mass_time_units else "Remove Mass and Time Units" + row.prop(props, "add_mass_time_units", toggle=True, text=label) + self.layout.use_property_split = True + if props.add_mass_time_units: + row = self.layout.row() + row.prop(props, "mass_unit", text="Mass Unit") + row = self.layout.row() + row.prop(props, "time_unit", text="Time Unit") row = self.layout.row() row.operator("bim.create_project") - class BIM_PT_project_library(Panel): bl_label = "Project Library" bl_idname = "BIM_PT_project_library" diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 7b24d2a1d1..c5a5bd9300 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -586,6 +586,11 @@ class BIMProperties(PropertyGroup): ], name="IFC Volume Unit", ) + add_mass_time_units: bpy.props.BoolProperty( + name="Add Mass and Time Units", + description="Enable to define mass and time units for the project", + default=False + ) mass_unit: EnumProperty( items=[ ("KILOGRAM", "Kilogram", "Kilograms"), diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 7fcfd25fde..d8e4dd2396 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -1123,7 +1123,7 @@ class Unit: def set_active_unit(cls, unit): pass def get_project_currency_unit(cls): pass def get_currency_name(cls): pass - + def add_mass_and_time_units(cls): pass @interface class Voider: diff --git a/src/bonsai/bonsai/core/unit.py b/src/bonsai/bonsai/core/unit.py index 2e8e627cb7..a4c52508f1 100644 --- a/src/bonsai/bonsai/core/unit.py +++ b/src/bonsai/bonsai/core/unit.py @@ -28,40 +28,43 @@ if TYPE_CHECKING: def assign_scene_units(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> None: if unit.is_scene_unit_metric(): - prefix = unit.get_scene_unit_si_prefix("LENGTHUNIT") - lengthunit = ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix=prefix) - prefix = unit.get_scene_unit_si_prefix("AREAUNIT") - areaunit = ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=prefix) - prefix = unit.get_scene_unit_si_prefix("VOLUMEUNIT") - volumeunit = ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=prefix) + lengthunit = ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix=unit.get_scene_unit_si_prefix("LENGTHUNIT")) + areaunit = ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=unit.get_scene_unit_si_prefix("AREAUNIT")) + volumeunit = ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=unit.get_scene_unit_si_prefix("VOLUMEUNIT")) + planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree") + units = [lengthunit, areaunit, volumeunit, planeangleunit] - prefix = unit.get_scene_unit_si_prefix("MASSUNIT") - if prefix == "CONVERSION": - mass_unit_name = unit.get_scene_unit_name("MASSUNIT") - massunit = ifc.run("unit.add_conversion_based_unit", name=mass_unit_name.lower()) - else: - massunit = ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix=prefix) + if unit.add_mass_and_time_units(): + prefix = unit.get_scene_unit_si_prefix("MASSUNIT") + if prefix == "CONVERSION": + massunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("MASSUNIT").lower()) + else: + massunit = ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix=prefix) + prefix = unit.get_scene_unit_si_prefix("TIMEUNIT") + if prefix == "CONVERSION": + timeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("TIMEUNIT").lower()) + else: + timeunit = ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=prefix) + units += [massunit, timeunit] - prefix = unit.get_scene_unit_si_prefix("TIMEUNIT") - if prefix == "CONVERSION": - time_unit_name = unit.get_scene_unit_name("TIMEUNIT") - timeunit = ifc.run("unit.add_conversion_based_unit", name=time_unit_name.lower()) - else: - timeunit = ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=prefix) else: lengthunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("LENGTHUNIT")) areaunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("AREAUNIT")) volumeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("VOLUMEUNIT")) - massunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("MASSUNIT").lower()) + planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree") + units = [lengthunit, areaunit, volumeunit, planeangleunit] - time_unit_name = unit.get_scene_unit_name("TIMEUNIT") - if time_unit_name == "SECOND": - timeunit = ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None) - else: - timeunit = ifc.run("unit.add_conversion_based_unit", name=time_unit_name.lower()) - - planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree") - ifc.run("unit.assign_unit", units=[lengthunit, areaunit, volumeunit, planeangleunit, massunit, timeunit]) + if unit.add_mass_and_time_units(): + massunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("MASSUNIT").lower()) + time_unit_name = unit.get_scene_unit_name("TIMEUNIT") + if time_unit_name == "SECOND": + timeunit = ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None) + else: + timeunit = ifc.run("unit.add_conversion_based_unit", name=time_unit_name.lower()) + units += [massunit, timeunit] + print("Add mass and time units:", unit.add_mass_and_time_units()) + print("Assigning units:", units) + ifc.run("unit.assign_unit", units=units) def assign_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None: diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index a3c9569d62..c982c43523 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -499,3 +499,9 @@ class Unit(bonsai.core.tool.Unit): elif ifc_class == "IfcMonetaryUnit": return "COPY_ID" return "MOD_MESHDEFORM" + + @classmethod + def add_mass_and_time_units(cls) -> bool: + """Return True if the user wants to add mass and time units, False otherwise.""" + bim_props = tool.Blender.get_bim_props() + return getattr(bim_props, "add_mass_time_units", False) diff --git a/src/bonsai/test/core/test_unit.py b/src/bonsai/test/core/test_unit.py index 818ad72021..2bbd783cc3 100644 --- a/src/bonsai/test/core/test_unit.py +++ b/src/bonsai/test/core/test_unit.py @@ -26,6 +26,7 @@ class TestAssignSceneUnits: unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix") + unit.add_mass_and_time_units().should_be_called().will_return(True) unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("KILO") unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return(None) @@ -45,11 +46,25 @@ class TestAssignSceneUnits: ).should_be_called() subject.assign_scene_units(ifc, unit) + def test_creating_and_assigning_metric_units_without_mass_and_time(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(True) + unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("CENTI") + unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("CENTI") + unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("CENTI") + unit.add_mass_and_time_units().should_be_called().will_return(False) + ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="CENTI").should_be_called().will_return("lengthunit") + ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="CENTI").should_be_called().will_return("areaunit") + ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="CENTI").should_be_called().will_return("volumeunit") + ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") + ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called() + subject.assign_scene_units(ifc, unit) + def test_creating_and_assigning_imperial_units(self, ifc, unit): unit.is_scene_unit_metric().should_be_called().will_return(False) unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot") unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot") unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot") + unit.add_mass_and_time_units().should_be_called().will_return(True) unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("pound") unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("SECOND") @@ -65,11 +80,26 @@ class TestAssignSceneUnits: ).should_be_called() subject.assign_scene_units(ifc, unit) + def test_creating_and_assigning_imperial_units_without_mass_and_time(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(False) + unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("yard") + unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square yard") + unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic yard") + unit.add_mass_and_time_units().should_be_called().will_return(False) + ifc.run("unit.add_conversion_based_unit", name="yard").should_be_called().will_return("lengthunit") + ifc.run("unit.add_conversion_based_unit", name="square yard").should_be_called().will_return("areaunit") + ifc.run("unit.add_conversion_based_unit", name="cubic yard").should_be_called().will_return("volumeunit") + ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") + ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called() + subject.assign_scene_units(ifc, unit) + + def test_creating_metric_units_with_conversion_based_mass_and_time(self, ifc, unit): unit.is_scene_unit_metric().should_be_called().will_return(True) unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("MILLI") unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return(None) unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return(None) + unit.add_mass_and_time_units().should_be_called().will_return(True) unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("CONVERSION") unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("tonne") unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return("CONVERSION") @@ -92,6 +122,7 @@ class TestAssignSceneUnits: unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("inch") unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square inch") unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic inch") + unit.add_mass_and_time_units().should_be_called().will_return(True) unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("ounce") unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("hour") From f222a8ba4f9fdb67af173f9c1940c8267fd8eddf Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Mon, 29 Sep 2025 09:13:18 +0200 Subject: [PATCH 08/10] Add mass and time units as a panel to project wizard UI --- .../bonsai/bim/module/project/__init__.py | 2 ++ src/bonsai/bonsai/bim/module/project/ui.py | 29 +++++++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index 1316eb0325..7e1bdb7ff0 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -114,6 +114,7 @@ def register(): kmi = km.keymap_items.new("bim.save_project", "S", "PRESS", ctrl=True) kmi.properties.should_save_as = False addon_keymaps.append((km, kmi)) + bpy.utils.register_class(ui.BIM_PT_mass_time_units) def unregister(): @@ -131,3 +132,4 @@ def unregister(): for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) addon_keymaps.clear() + bpy.utils.unregister_class(ui.BIM_PT_mass_time_units) diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index ed6e65e674..f1b087e34b 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -358,19 +358,30 @@ class BIM_PT_new_project_wizard(Panel): row.prop(props, "volume_unit", text="Volume Unit") row = self.layout.row() prop_with_search(self.layout, pprops, "template_file", text="Template") - self.layout.use_property_split = False - row = self.layout.row() - label = "Add Mass and Time Units" if not props.add_mass_time_units else "Remove Mass and Time Units" - row.prop(props, "add_mass_time_units", toggle=True, text=label) self.layout.use_property_split = True - if props.add_mass_time_units: - row = self.layout.row() - row.prop(props, "mass_unit", text="Mass Unit") - row = self.layout.row() - row.prop(props, "time_unit", text="Time Unit") row = self.layout.row() row.operator("bim.create_project") +class BIM_PT_mass_time_units(Panel): + bl_label = "Mass and Time Units" + bl_idname = "BIM_PT_mass_time_units" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_new_project_wizard" + + def draw(self, context): + layout = self.layout + props = tool.Blender.get_bim_props() + label = "Add Mass and Time Units" if not props.add_mass_time_units else "Remove Mass and Time Units" + layout.prop(props, "add_mass_time_units", toggle=True, text=label) + if props.add_mass_time_units: + row = layout.row() + row.prop(props, "mass_unit", text="Mass Unit") + row = layout.row() + row.prop(props, "time_unit", text="Time Unit") + class BIM_PT_project_library(Panel): bl_label = "Project Library" bl_idname = "BIM_PT_project_library" From a98cad161e1e773d2f6aebfce758a775ec427e2b Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Mon, 29 Sep 2025 09:31:00 +0200 Subject: [PATCH 09/10] Refactor mass and time units panel into collapsible section using .panel method in project wizard UI --- .../bonsai/bim/module/project/__init__.py | 2 -- src/bonsai/bonsai/bim/module/project/ui.py | 31 ++++++++----------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index 7e1bdb7ff0..1316eb0325 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -114,7 +114,6 @@ def register(): kmi = km.keymap_items.new("bim.save_project", "S", "PRESS", ctrl=True) kmi.properties.should_save_as = False addon_keymaps.append((km, kmi)) - bpy.utils.register_class(ui.BIM_PT_mass_time_units) def unregister(): @@ -132,4 +131,3 @@ def unregister(): for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) addon_keymaps.clear() - bpy.utils.unregister_class(ui.BIM_PT_mass_time_units) diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index f1b087e34b..b0dae84afe 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -358,29 +358,24 @@ class BIM_PT_new_project_wizard(Panel): row.prop(props, "volume_unit", text="Volume Unit") row = self.layout.row() prop_with_search(self.layout, pprops, "template_file", text="Template") + + header, body = self.layout.panel("Mass and Time Units", default_closed=True) + if header: + header.label(text="Mass and Time Units") + if body: + label = "Add Mass and Time Units" if not props.add_mass_time_units else "Remove Mass and Time Units" + body.prop(props, "add_mass_time_units", toggle=True, text=label) + if props.add_mass_time_units: + row = body.row() + row.prop(props, "mass_unit", text="Mass Unit") + row = body.row() + row.prop(props, "time_unit", text="Time Unit") + self.layout.use_property_split = True row = self.layout.row() row.operator("bim.create_project") -class BIM_PT_mass_time_units(Panel): - bl_label = "Mass and Time Units" - bl_idname = "BIM_PT_mass_time_units" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - bl_parent_id = "BIM_PT_new_project_wizard" - def draw(self, context): - layout = self.layout - props = tool.Blender.get_bim_props() - label = "Add Mass and Time Units" if not props.add_mass_time_units else "Remove Mass and Time Units" - layout.prop(props, "add_mass_time_units", toggle=True, text=label) - if props.add_mass_time_units: - row = layout.row() - row.prop(props, "mass_unit", text="Mass Unit") - row = layout.row() - row.prop(props, "time_unit", text="Time Unit") class BIM_PT_project_library(Panel): bl_label = "Project Library" From 5294fa1cbe0276a73ee28f9a3e303d5d5fb8cc53 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Mon, 15 Dec 2025 13:39:46 +0100 Subject: [PATCH 10/10] Add preference for mass and time units visibility in project wizard --- src/bonsai/bonsai/bim/module/project/ui.py | 24 +++++++++++----------- src/bonsai/bonsai/bim/prop.py | 2 +- src/bonsai/bonsai/bim/ui.py | 8 ++++++++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index b0dae84afe..6fe5b55604 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -359,24 +359,24 @@ class BIM_PT_new_project_wizard(Panel): row = self.layout.row() prop_with_search(self.layout, pprops, "template_file", text="Template") - header, body = self.layout.panel("Mass and Time Units", default_closed=True) - if header: - header.label(text="Mass and Time Units") - if body: - label = "Add Mass and Time Units" if not props.add_mass_time_units else "Remove Mass and Time Units" - body.prop(props, "add_mass_time_units", toggle=True, text=label) - if props.add_mass_time_units: - row = body.row() - row.prop(props, "mass_unit", text="Mass Unit") - row = body.row() - row.prop(props, "time_unit", text="Time Unit") + if tool.Blender.get_addon_preferences().mass_time_units_in_wizard: + header, body = self.layout.panel("Mass and Time Units", default_closed=True) + if header: + header.label(text="Mass and Time Units") + if body: + label = "Add Mass and Time Units" if not props.add_mass_time_units else "Remove Mass and Time Units" + body.prop(props, "add_mass_time_units", toggle=True, text=label) + if props.add_mass_time_units: + row = body.row() + row.prop(props, "mass_unit", text="Mass Unit") + row = body.row() + row.prop(props, "time_unit", text="Time Unit") self.layout.use_property_split = True row = self.layout.row() row.operator("bim.create_project") - class BIM_PT_project_library(Panel): bl_label = "Project Library" bl_idname = "BIM_PT_project_library" diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index c5a5bd9300..08137a133e 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -589,7 +589,7 @@ class BIMProperties(PropertyGroup): add_mass_time_units: bpy.props.BoolProperty( name="Add Mass and Time Units", description="Enable to define mass and time units for the project", - default=False + default=False, ) mass_unit: EnumProperty( items=[ diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index b7ed2180c1..d41546904c 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -684,6 +684,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): default=False, ) + mass_time_units_in_wizard: BoolProperty( + name="Mass and time units in project wizard", + description="Show mass and time units section in the new project wizard panel", + default=False, + ) + if TYPE_CHECKING: svg2pdf_command: str svg2dxf_command: str @@ -720,6 +726,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): doc: DocPreferences default_parameters: DefaultParameters container_hide_show_isolate: bool + mass_time_units_in_wizard: bool def draw(self, context: bpy.types.Context) -> None: layout = self.layout @@ -901,6 +908,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "container_hide_show_isolate") + layout.prop(self, "mass_time_units_in_wizard") # Scene panel groups