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()),