diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 7a115d0ea9..df8fb90be6 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -27,6 +27,8 @@ import addon_utils import ifcopenshell import ifcopenshell.api import ifcopenshell.util.placement +import blenderbim.tool as tool +import blenderbim.core.style from blenderbim.bim.ifc import IfcStore @@ -105,7 +107,7 @@ class IfcExporter: continue try: if isinstance(obj, bpy.types.Material): - bpy.ops.bim.update_style_colours(material=obj.name) + blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=obj) else: bpy.ops.bim.update_representation(obj=obj.name) except ReferenceError: diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index c5c5946324..aa69a7b3dd 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -97,5 +97,5 @@ class AddAggregate(bpy.types.Operator): aggregate = bpy.data.objects.new("Assembly", None) aggregate_collection.objects.link(aggregate) bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class="IfcElementAssembly") - bpy.ops.bim.assign_object(related_object=obj.name, relating_object=aggregate.name) + core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 548370c0c9..e60cb0e519 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -25,6 +25,7 @@ import ifcopenshell.util.representation import logging import ifcopenshell.api import blenderbim.core.geometry as core +import blenderbim.core.style import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore from blenderbim.bim import import_ifc @@ -108,7 +109,7 @@ class AddRepresentation(bpy.types.Operator): return {"FINISHED"} [ - bpy.ops.bim.add_style(material=s.material.name) + blenderbim.core.style.add_style(tool.Ifc, tool.Style, obj=s.material) for s in obj.material_slots if s.material and not s.material.BIMMaterialProperties.ifc_style_id ] diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index f855edb95f..0a42679c93 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -69,7 +69,7 @@ class CreateProject(bpy.types.Operator): building_storey = bpy.data.objects.new(self.get_name("IfcBuildingStorey", "My Storey"), None) bpy.ops.bim.assign_class(obj=project.name, ifc_class="IfcProject") - bpy.ops.bim.assign_unit() + blenderbim.core.unit.assign_scene_units(tool.Ifc, tool.Unit) model = blenderbim.core.context.add_context( tool.Ifc, context_type="Model", context_identifier="", target_view="", parent=0 diff --git a/src/blenderbim/blenderbim/bim/module/spatial/data.py b/src/blenderbim/blenderbim/bim/module/spatial/data.py index 4eac3aefc0..a9db36f7dd 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/data.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/data.py @@ -64,4 +64,4 @@ class SpatialData: @classmethod def is_directly_contained(cls): - return bool(getattr(tool.Ifc.get_entity(bpy.context.active_object), "ContainedInStructure")) + return bool(getattr(tool.Ifc.get_entity(bpy.context.active_object), "ContainedInStructure", False)) diff --git a/src/blenderbim/blenderbim/bim/module/unit/__init__.py b/src/blenderbim/blenderbim/bim/module/unit/__init__.py index 4fd0c0c258..11f5ab1395 100644 --- a/src/blenderbim/blenderbim/bim/module/unit/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/unit/__init__.py @@ -20,6 +20,7 @@ import bpy from . import ui, prop, operator classes = ( + operator.AssignSceneUnits, operator.AssignUnit, operator.UnassignUnit, operator.LoadUnits, diff --git a/src/blenderbim/blenderbim/bim/module/unit/data.py b/src/blenderbim/blenderbim/bim/module/unit/data.py new file mode 100644 index 0000000000..1111ed0dfa --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/unit/data.py @@ -0,0 +1,38 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.tool as tool + + +def refresh(): + UnitsData.is_loaded = False + + +class UnitsData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = {"total_units": cls.get_total_units()} + cls.is_loaded = True + + @classmethod + def get_total_units(cls): + ifc = tool.Ifc.get() + return len(ifc.by_type("IfcDerivedUnit") + ifc.by_type("IfcNamedUnit") + ifc.by_type("IfcMonetaryUnit")) diff --git a/src/blenderbim/blenderbim/bim/module/unit/operator.py b/src/blenderbim/blenderbim/bim/module/unit/operator.py index aa0527f690..631c25c842 100644 --- a/src/blenderbim/blenderbim/bim/module/unit/operator.py +++ b/src/blenderbim/blenderbim/bim/module/unit/operator.py @@ -21,274 +21,131 @@ import json import ifcopenshell.api import ifcopenshell.util.unit import blenderbim.bim.helper +import blenderbim.bim.handler +import blenderbim.tool as tool +import blenderbim.core.unit as core from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.unit.data import Data -class AssignUnit(bpy.types.Operator): +class Operator: + def execute(self, context): + IfcStore.execute_ifc_operator(self, context) + blenderbim.bim.handler.refresh_ui_data + return {"FINISHED"} + + +class AssignSceneUnits(bpy.types.Operator, Operator): + bl_idname = "bim.assign_scene_units" + bl_label = "Assign Scene Units" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.assign_scene_units(tool.Ifc, tool.Unit) + + +class AssignUnit(bpy.types.Operator, Operator): bl_idname = "bim.assign_unit" bl_label = "Assign Unit" bl_options = {"REGISTER", "UNDO"} unit: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - self.file = IfcStore.get_file() - if self.unit: - ifcopenshell.api.run("unit.assign_unit", self.file, units=[self.file.by_id(self.unit)]) - else: - ifcopenshell.api.run("unit.assign_unit", self.file, **self.get_units(context)) - Data.load(self.file) - if self.unit: - bpy.ops.bim.load_units() - return {"FINISHED"} - - def get_units(self, context): - scene = context.scene - units = { - "length": { - "ifc": None, - "is_metric": scene.unit_settings.system != "IMPERIAL", - "raw": scene.unit_settings.length_unit, - }, - "area": { - "ifc": None, - "is_metric": scene.unit_settings.system != "IMPERIAL", - "raw": scene.unit_settings.length_unit, - }, - "volume": { - "ifc": None, - "is_metric": scene.unit_settings.system != "IMPERIAL", - "raw": scene.unit_settings.length_unit, - }, - } - for data in units.values(): - if data["raw"] == "ADAPTIVE": - if data["is_metric"]: - data["raw"] = "METERS" - else: - data["raw"] = "FEET" - return units + core.assign_unit(tool.Ifc, tool.Unit, unit=tool.Ifc.get().by_id(self.unit)) -class UnassignUnit(bpy.types.Operator): +class UnassignUnit(bpy.types.Operator, Operator): bl_idname = "bim.unassign_unit" bl_label = "Unassign Unit" bl_options = {"REGISTER", "UNDO"} unit: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - self.file = IfcStore.get_file() - ifcopenshell.api.run("unit.unassign_unit", IfcStore.get_file(), units=[self.file.by_id(self.unit)]) - Data.load(self.file) - bpy.ops.bim.load_units() - return {"FINISHED"} + core.unassign_unit(tool.Ifc, tool.Unit, unit=tool.Ifc.get().by_id(self.unit)) -class LoadUnits(bpy.types.Operator): +class LoadUnits(bpy.types.Operator, Operator): bl_idname = "bim.load_units" bl_label = "Load Units" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - props = context.scene.BIMUnitProperties - props.units.clear() - - for ifc_definition_id, unit in Data.units.items(): - name = unit.get("Name", "") - - if unit["type"] == "IfcMonetaryUnit": - name = unit["Currency"] - - if unit["type"] == "IfcSIUnit" and unit["Prefix"]: - if "_" in name: - name_components = name.split("_") - name = f"{name_components[0]} {unit['Prefix']}{name_components[1]}" - else: - name = f"{unit['Prefix']}{name}" - - icon = "MOD_MESHDEFORM" - if unit["type"] == "IfcSIUnit": - icon = "SNAP_GRID" - elif unit["type"] == "IfcMonetaryUnit": - icon = "COPY_ID" - - unit_type = unit.get("UserDefinedType", None) - if not unit_type: - unit_type = unit.get("UnitType", None) - if unit["type"] == "IfcMonetaryUnit": - unit_type = "CURRENCY" - - new = props.units.add() - new.ifc_definition_id = ifc_definition_id - new.name = name - new.unit_type = unit_type - new.is_assigned = ifc_definition_id in Data.unit_assignment - new.icon = icon - - props.is_editing = True - bpy.ops.bim.disable_editing_unit() - return {"FINISHED"} + def _execute(self, context): + core.load_units(tool.Unit) -class DisableUnitEditingUI(bpy.types.Operator): +class DisableUnitEditingUI(bpy.types.Operator, Operator): bl_idname = "bim.disable_unit_editing_ui" bl_label = "Disable Unit Editing UI" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - context.scene.BIMUnitProperties.is_editing = False - return {"FINISHED"} + def _execute(self, context): + core.disable_unit_editing_ui(tool.Unit) -class RemoveUnit(bpy.types.Operator): +class RemoveUnit(bpy.types.Operator, Operator): bl_idname = "bim.remove_unit" bl_label = "Remove Unit" bl_options = {"REGISTER", "UNDO"} unit: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - props = context.scene.BIMUnitProperties - self.file = IfcStore.get_file() - ifcopenshell.api.run("unit.remove_unit", self.file, **{"unit": self.file.by_id(self.unit)}) - Data.load(self.file) - bpy.ops.bim.load_units() - return {"FINISHED"} + core.remove_unit(tool.Ifc, tool.Unit, unit=tool.Ifc.get().by_id(self.unit)) -class AddMonetaryUnit(bpy.types.Operator): +class AddMonetaryUnit(bpy.types.Operator, Operator): bl_idname = "bim.add_monetary_unit" bl_label = "Add Monetary Unit" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - props = context.scene.BIMUnitProperties - self.file = IfcStore.get_file() - ifcopenshell.api.run("unit.add_monetary_unit", self.file) - Data.load(self.file) - bpy.ops.bim.load_units() - return {"FINISHED"} + core.add_monetary_unit(tool.Ifc, tool.Unit) -class AddSIUnit(bpy.types.Operator): +class AddSIUnit(bpy.types.Operator, Operator): bl_idname = "bim.add_si_unit" bl_label = "Add SI Unit" bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) + unit_type: bpy.props.StringProperty() def _execute(self, context): - props = context.scene.BIMUnitProperties - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "unit.add_si_unit", - self.file, - unit_type=props.named_unit_types, - name=ifcopenshell.util.unit.si_type_names[props.named_unit_types], - ) - Data.load(self.file) - bpy.ops.bim.load_units() - return {"FINISHED"} + core.add_si_unit(tool.Ifc, tool.Unit, unit_type=self.unit_type) -class AddContextDependentUnit(bpy.types.Operator): +class AddContextDependentUnit(bpy.types.Operator, Operator): bl_idname = "bim.add_context_dependent_unit" bl_label = "Add Context Dependent Unit" bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) + name: bpy.props.StringProperty() + unit_type: bpy.props.StringProperty() def _execute(self, context): - props = context.scene.BIMUnitProperties - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "unit.add_context_dependent_unit", self.file, unit_type=props.named_unit_types, name="THINGAMAJIG" - ) - Data.load(self.file) - bpy.ops.bim.load_units() - return {"FINISHED"} + core.add_context_dependent_unit(tool.Ifc, tool.Unit, unit_type=self.unit_type, name=self.name) -class EnableEditingUnit(bpy.types.Operator): +class EnableEditingUnit(bpy.types.Operator, Operator): bl_idname = "bim.enable_editing_unit" bl_label = "Enable Editing Unit" bl_options = {"REGISTER", "UNDO"} unit: bpy.props.IntProperty() - def execute(self, context): - props = context.scene.BIMUnitProperties - props.unit_attributes.clear() - data = Data.units[self.unit] - blenderbim.bim.helper.import_attributes( - data["type"], - props.unit_attributes, - data, - lambda name, prop, data: self.import_attributes(name, prop, data, context), - ) - props.active_unit_id = self.unit - return {"FINISHED"} - - def import_attributes(self, name, prop, data, context): - if name == "Dimensions" and data["type"] != "IfcSIUnit": - new = context.scene.BIMUnitProperties.unit_attributes.add() - new.name = name - new.is_null = data[name] is None - new.is_optional = False - new.data_type = "string" - new.string_value = json.dumps([e for e in IfcStore.get_file().by_id(data["id"]).Dimensions]) - return True + def _execute(self, context): + core.enable_editing_unit(tool.Unit, unit=tool.Ifc.get().by_id(self.unit)) -class DisableEditingUnit(bpy.types.Operator): +class DisableEditingUnit(bpy.types.Operator, Operator): bl_idname = "bim.disable_editing_unit" bl_label = "Disable Editing Unit" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - context.scene.BIMUnitProperties.active_unit_id = 0 - return {"FINISHED"} + def _execute(self, context): + core.disable_editing_unit(tool.Unit) -class EditUnit(bpy.types.Operator): +class EditUnit(bpy.types.Operator, Operator): bl_idname = "bim.edit_unit" bl_label = "Edit Unit" bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) + unit: bpy.props.IntProperty() def _execute(self, context): - props = context.scene.BIMUnitProperties - attributes = blenderbim.bim.helper.export_attributes(props.unit_attributes, self.export_attributes) - self.file = IfcStore.get_file() - unit = self.file.by_id(props.active_unit_id) - if unit.is_a("IfcMonetaryUnit"): - ifcopenshell.api.run("unit.edit_monetary_unit", self.file, **{"unit": unit, "attributes": attributes}) - elif unit.is_a("IfcDerivedUnit"): - ifcopenshell.api.run("unit.edit_derived_unit", self.file, **{"unit": unit, "attributes": attributes}) - elif unit.is_a("IfcNamedUnit"): - ifcopenshell.api.run("unit.edit_named_unit", self.file, **{"unit": unit, "attributes": attributes}) - Data.load(IfcStore.get_file()) - bpy.ops.bim.load_units() - return {"FINISHED"} - - def export_attributes(self, attributes, prop): - if prop.name == "Dimensions": - try: - attributes[prop.name] = json.loads(prop.get_value()) - except: - attributes[prop.name] = (0, 0, 0, 0, 0, 0, 0) - return True + core.edit_unit(tool.Ifc, tool.Unit, unit=tool.Ifc.get().by_id(self.unit)) diff --git a/src/blenderbim/blenderbim/bim/module/unit/prop.py b/src/blenderbim/blenderbim/bim/module/unit/prop.py index 9dcddcbd1a..46fa36c9ca 100644 --- a/src/blenderbim/blenderbim/bim/module/unit/prop.py +++ b/src/blenderbim/blenderbim/bim/module/unit/prop.py @@ -69,7 +69,7 @@ class Unit(PropertyGroup): name: StringProperty(name="Name") unit_type: StringProperty(name="Unit Type") is_assigned: BoolProperty(name="Is Assigned") - icon: StringProperty(name="Icon") + ifc_class: StringProperty(name="IFC Class") ifc_definition_id: IntProperty(name="IFC Definition ID") diff --git a/src/blenderbim/blenderbim/bim/module/unit/ui.py b/src/blenderbim/blenderbim/bim/module/unit/ui.py index 790e960fe9..2c896f1ac0 100644 --- a/src/blenderbim/blenderbim/bim/module/unit/ui.py +++ b/src/blenderbim/blenderbim/bim/module/unit/ui.py @@ -19,7 +19,7 @@ import blenderbim.bim.helper from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore -from ifcopenshell.api.unit.data import Data +from blenderbim.bim.module.unit.data import UnitsData class BIM_PT_units(Panel): @@ -36,13 +36,13 @@ class BIM_PT_units(Panel): return file def draw(self, context): - self.file = IfcStore.get_file() - if not Data.is_loaded: - Data.load(self.file) + if not UnitsData.is_loaded: + UnitsData.load() + self.props = context.scene.BIMUnitProperties row = self.layout.row(align=True) - row.label(text="{} Units Found".format(len(Data.units)), icon="SNAP_GRID") + row.label(text="{} Units Found".format(UnitsData.data["total_units"]), icon="SNAP_GRID") if self.props.is_editing: row.operator("bim.disable_unit_editing_ui", text="", icon="CANCEL") else: @@ -60,10 +60,13 @@ class BIM_PT_units(Panel): pass # TODO elif self.props.unit_classes == "IfcSIUnit": row.prop(self.props, "named_unit_types", text="") - row.operator("bim.add_si_unit", text="", icon="ADD") + op = row.operator("bim.add_si_unit", text="", icon="ADD") + op.unit_type = self.props.named_unit_types elif self.props.unit_classes == "IfcContextDependentUnit": row.prop(self.props, "named_unit_types", text="") - row.operator("bim.add_context_dependent_unit", text="", icon="ADD") + op = row.operator("bim.add_context_dependent_unit", text="", icon="ADD") + op.name = "THINGAMAJIG" + op.unit_type = self.props.named_unit_types self.layout.template_list( "BIM_UL_units", @@ -85,8 +88,14 @@ class BIM_UL_units(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): props = context.scene.BIMUnitProperties if item: + icon = "MOD_MESHDEFORM" + if item.ifc_class == "IfcSIUnit": + icon = "SNAP_GRID" + elif item.ifc_class == "IfcMonetaryUnit": + icon = "COPY_ID" + row = layout.row(align=True) - row.label(text=item.unit_type or "No Type", icon=item.icon) + row.label(text=item.unit_type or "No Type", icon=icon) row.label(text=item.name or "Unnamed") if item.is_assigned: diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 3ee27e9484..7a623024a5 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -130,6 +130,22 @@ class Surveyor: def get_absolute_matrix(cls, obj): pass +@interface +class Unit: + def clear_active_unit(cls): pass + def disable_editing_units(cls): pass + def enable_editing_units(cls): pass + def export_unit_attributes(cls): pass + def get_scene_unit_name(cls, unit_type): pass + def get_scene_unit_si_prefix(cls): pass + def get_si_name_from_unit_type(cls, unit_type): pass + def get_unit_class(cls, unit): pass + def import_unit_attributes(cls, unit): pass + def import_units(cls): pass + def is_scene_unit_metric(cls): pass + def set_active_unit(cls, unit): pass + + @interface class Voider: def can_void(cls, opening, element): pass diff --git a/src/blenderbim/blenderbim/core/unit.py b/src/blenderbim/blenderbim/core/unit.py new file mode 100644 index 0000000000..4c38e370ca --- /dev/null +++ b/src/blenderbim/blenderbim/core/unit.py @@ -0,0 +1,99 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + + +def assign_scene_units(ifc, unit): + length_name = unit.get_scene_unit_name("length") + area_name = unit.get_scene_unit_name("area") + volume_name = unit.get_scene_unit_name("volume") + + if unit.is_scene_unit_metric(): + prefix = unit.get_scene_unit_si_prefix() + lengthunit = ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", name=length_name, prefix=prefix) + areaunit = ifc.run("unit.add_si_unit", unit_type="AREAUNIT", name=area_name, prefix=prefix) + volumeunit = ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", name=volume_name, prefix=prefix) + else: + lengthunit = ifc.run("unit.add_conversion_based_unit", name=length_name) + areaunit = ifc.run("unit.add_conversion_based_unit", name=area_name) + volumeunit = ifc.run("unit.add_conversion_based_unit", name=volume_name) + + ifc.run("unit.assign_unit", units=[lengthunit, areaunit, volumeunit]) + + +def assign_unit(ifc, unit_tool, unit=None): + ifc.run("unit.assign_unit", units=[unit]) + unit_tool.import_units() + + +def unassign_unit(ifc, unit_tool, unit=None): + ifc.run("unit.unassign_unit", units=[unit]) + unit_tool.import_units() + + +def load_units(unit): + unit.import_units() + unit.enable_editing_units() + + +def disable_unit_editing_ui(unit): + unit.disable_editing_units() + + +def remove_unit(ifc, unit_tool, unit=None): + ifc.run("unit.remove_unit", unit=unit) + unit_tool.import_units() + + +def add_monetary_unit(ifc, unit): + result = ifc.run("unit.add_monetary_unit") + unit.import_units() + return result + + +def add_si_unit(ifc, unit, unit_type=None): + result = ifc.run("unit.add_si_unit", unit_type=unit_type, name=unit.get_si_name_from_unit_type(unit_type)) + unit.import_units() + return result + + +def add_context_dependent_unit(ifc, unit, unit_type=None, name=None): + result = ifc.run("unit.add_context_dependent_unit", unit_type=unit_type, name=name) + unit.import_units() + return result + + +def enable_editing_unit(unit_tool, unit=None): + unit_tool.set_active_unit(unit) + unit_tool.import_unit_attributes(unit) + + +def disable_editing_unit(unit): + unit.clear_active_unit() + + +def edit_unit(ifc, unit_tool, unit=None): + attributes = unit_tool.export_unit_attributes() + unit_class = unit_tool.get_unit_class(unit) + if unit_class == "IfcMonetaryUnit": + ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes) + elif unit_class == "IfcDerivedUnit": + ifc.run("unit.edit_derived_unit", unit=unit, attributes=attributes) + elif unit_class == "IfcNamedUnit": + ifc.run("unit.edit_named_unit", unit=unit, attributes=attributes) + unit_tool.import_units() + unit_tool.clear_active_unit() diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index 16719d31a2..04cd36f721 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -25,3 +25,4 @@ from blenderbim.tool.ifc import Ifc from blenderbim.tool.owner import Owner from blenderbim.tool.style import Style from blenderbim.tool.surveyor import Surveyor +from blenderbim.tool.unit import Unit diff --git a/src/blenderbim/blenderbim/tool/unit.py b/src/blenderbim/blenderbim/tool/unit.py new file mode 100644 index 0000000000..e377592028 --- /dev/null +++ b/src/blenderbim/blenderbim/tool/unit.py @@ -0,0 +1,157 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import json +import ifcopenshell +import blenderbim.core.tool +import blenderbim.tool as tool + + +class Unit(blenderbim.core.tool.Unit): + @classmethod + def clear_active_unit(cls): + bpy.context.scene.BIMUnitProperties.active_unit_id = 0 + + @classmethod + def disable_editing_units(cls): + bpy.context.scene.BIMUnitProperties.is_editing = False + + @classmethod + def enable_editing_units(cls): + bpy.context.scene.BIMUnitProperties.is_editing = True + + @classmethod + def export_unit_attributes(cls): + def callback(attributes, prop): + if prop.name == "Dimensions": + try: + attributes[prop.name] = json.loads(prop.get_value()) + except: + attributes[prop.name] = (0, 0, 0, 0, 0, 0, 0) + return True + + props = bpy.context.scene.BIMUnitProperties + return blenderbim.bim.helper.export_attributes(props.unit_attributes, callback=callback) + + @classmethod + def get_scene_unit_name(cls, unit_type): + props = bpy.context.scene.unit_settings + is_metric = props.system == "METRIC" or props.system == "NONE" + + if is_metric: + name = "METRE" + elif props.length_unit == "MILES": + name = "mile" + elif props.length_unit == "FEET" or props.length_unit == "ADAPTIVE": + name = "foot" + elif props.length_unit == "INCHES": + name = "inch" + elif props.length_unit == "THOU": + name = "thou" + + if unit_type == "length": + return name + elif unit_type == "area": + if is_metric: + return f"SQUARE_{name}" + return f"square {name}" + elif unit_type == "volume": + if is_metric: + return f"CUBIC_{name}" + return f"cubic {name}" + + @classmethod + def get_scene_unit_si_prefix(cls): + props = bpy.context.scene.unit_settings + if props.length_unit == "ADAPTIVE" or props.length_unit == "METERS": + return + return props.length_unit.replace("METERS", "") + + @classmethod + def get_si_name_from_unit_type(cls, unit_type): + return ifcopenshell.util.unit.si_type_names.get(unit_type, None) + + @classmethod + def get_unit_class(cls, unit): + return unit.is_a() + + @classmethod + def import_unit_attributes(cls, unit): + def callback(name, prop, data): + if name == "Dimensions" and data["type"] != "IfcSIUnit": + new = bpy.context.scene.BIMUnitProperties.unit_attributes.add() + new.name = name + new.is_null = data[name] is None + new.is_optional = False + new.data_type = "string" + new.string_value = json.dumps([e for e in tool.Ifc.get().by_id(data["id"]).Dimensions]) + return True + + props = bpy.context.scene.BIMUnitProperties + props.unit_attributes.clear() + blenderbim.bim.helper.import_attributes2(unit, props.unit_attributes, callback=callback) + + @classmethod + def import_units(cls): + props = bpy.context.scene.BIMUnitProperties + props.units.clear() + + units = [] + for unit_class in ["IfcDerivedUnit", "IfcMonetaryUnit", "IfcNamedUnit"]: + units += tool.Ifc.get().by_type(unit_class) + + assigned_units = tool.Ifc.get().by_type("IfcUnitAssignment") + if assigned_units: + assigned_units = assigned_units[0].Units + + for unit in units: + name = "" + if unit.is_a("IfcMonetaryUnit"): + name = unit.Currency + elif not unit.is_a("IfcDerivedUnit"): + name = unit.Name or "" + + if unit.is_a("IfcSIUnit") and unit.Prefix: + if "_" in name: + name_components = name.split("_") + name = f"{name_components[0]} {unit.Prefix}{name_components[1]}" + else: + name = f"{unit.Prefix}{name}" + + if unit.is_a("IfcMonetaryUnit"): + unit_type = "CURRENCY" + else: + unit_type = getattr(unit, "UserDefinedType", None) + if not unit_type: + unit_type = getattr(unit, "UnitType", None) + + new = props.units.add() + new.ifc_definition_id = unit.id() + new.name = name + new.unit_type = unit_type + new.is_assigned = unit in assigned_units + new.ifc_class = unit.is_a() + + @classmethod + def is_scene_unit_metric(cls): + return bpy.context.scene.unit_settings.system in ["METRIC", "NONE"] + + @classmethod + def set_active_unit(cls, unit): + bpy.context.scene.BIMUnitProperties.active_unit_id = unit.id() diff --git a/src/blenderbim/pytest.ini b/src/blenderbim/pytest.ini index 9abbd60225..61129246d0 100644 --- a/src/blenderbim/pytest.ini +++ b/src/blenderbim/pytest.ini @@ -4,6 +4,8 @@ markers = context geometry owner - void pset_template spatial + style + unit + void diff --git a/src/blenderbim/test/bim/feature/unit.feature b/src/blenderbim/test/bim/feature/unit.feature new file mode 100644 index 0000000000..ec6cd50019 --- /dev/null +++ b/src/blenderbim/test/bim/feature/unit.feature @@ -0,0 +1,71 @@ +@unit +Feature: Unit + +Scenario: Assign scene units + Given an empty IFC project + When I press "bim.assign_scene_units" + Then nothing happens + +Scenario: Assign unit + Given an empty IFC project + And the variable "unit" is "{ifc}.by_type('IfcSIUnit')[0].id()" + When I press "bim.assign_unit(unit={unit})" + Then nothing happens + +Scenario: Unassign unit + Given an empty IFC project + And the variable "unit" is "{ifc}.by_type('IfcSIUnit')[0].id()" + When I press "bim.unassign_unit(unit={unit})" + Then nothing happens + +Scenario: Load units + Given an empty IFC project + When I press "bim.load_units" + Then nothing happens + +Scenario: Disable unit editing UI + Given an empty IFC project + And I press "bim.load_units" + When I press "bim.disable_unit_editing_ui" + Then nothing happens + +Scenario: Remove unit + Given an empty IFC project + And the variable "unit" is "{ifc}.by_type('IfcSIUnit')[0].id()" + When I press "bim.remove_unit(unit={unit})" + Then nothing happens + +Scenario: Add monetary unit + Given an empty IFC project + When I press "bim.add_monetary_unit" + Then nothing happens + +Scenario: Add SI unit + Given an empty IFC project + When I press "bim.add_si_unit(unit_type='LENGTHUNIT')" + Then nothing happens + +Scenario: Add context dependent unit + Given an empty IFC project + When I press "bim.add_context_dependent_unit(name='THINGAMAJIGS', unit_type='LENGTHUNIT')" + Then nothing happens + +Scenario: Enable editing unit + Given an empty IFC project + And the variable "unit" is "{ifc}.by_type('IfcSIUnit')[0].id()" + When I press "bim.enable_editing_unit(unit={unit})" + Then nothing happens + +Scenario: Disable editing unit + Given an empty IFC project + And the variable "unit" is "{ifc}.by_type('IfcSIUnit')[0].id()" + And I press "bim.enable_editing_unit(unit={unit})" + When I press "bim.disable_editing_unit" + Then nothing happens + +Scenario: Edit unit + Given an empty IFC project + And the variable "unit" is "{ifc}.by_type('IfcSIUnit')[0].id()" + And I press "bim.enable_editing_unit(unit={unit})" + When I press "bim.edit_unit(unit={unit})" + Then nothing happens diff --git a/src/blenderbim/test/core/bootstrap.py b/src/blenderbim/test/core/bootstrap.py index f4e54b2fee..2921c6d8dd 100644 --- a/src/blenderbim/test/core/bootstrap.py +++ b/src/blenderbim/test/core/bootstrap.py @@ -98,6 +98,13 @@ def container(): prophet.verify() +@pytest.fixture +def unit(): + prophet = Prophecy(blenderbim.core.tool.Unit) + yield prophet + prophet.verify() + + class Prophecy: def __init__(self, cls): self.subject = cls diff --git a/src/blenderbim/test/core/test_unit.py b/src/blenderbim/test/core/test_unit.py new file mode 100644 index 0000000000..9cc837b7e8 --- /dev/null +++ b/src/blenderbim/test/core/test_unit.py @@ -0,0 +1,154 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.core.unit as subject +from test.core.bootstrap import ifc, unit + + +class TestAssignSceneUnits: + def test_creating_and_assigning_metric_units(self, ifc, unit): + unit.is_scene_unit_metric().should_be_called().will_return(True) + unit.get_scene_unit_name("length").should_be_called().will_return("name") + unit.get_scene_unit_si_prefix().should_be_called().will_return("prefix") + ifc.run( + "unit.add_si_unit", unit_type="LENGTHUNIT", name="name", prefix="prefix" + ).should_be_called().will_return("lengthunit") + + unit.get_scene_unit_name("area").should_be_called().will_return("name") + ifc.run("unit.add_si_unit", unit_type="AREAUNIT", name="name", prefix="prefix").should_be_called().will_return( + "areaunit" + ) + + unit.get_scene_unit_name("volume").should_be_called().will_return("name") + ifc.run( + "unit.add_si_unit", unit_type="VOLUMEUNIT", name="name", prefix="prefix" + ).should_be_called().will_return("volumeunit") + + ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit"]).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("length").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("area").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("volume").should_be_called().will_return("volumename") + ifc.run("unit.add_conversion_based_unit", name="volumename").should_be_called().will_return("volumeunit") + + ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit"]).should_be_called() + subject.assign_scene_units(ifc, unit) + + +class TestAssignUnit: + def test_run(self, ifc, unit): + ifc.run("unit.assign_unit", units=["unit"]).should_be_called() + unit.import_units().should_be_called() + subject.assign_unit(ifc, unit, unit="unit") + + +class TestUnassignUnit: + def test_run(self, ifc, unit): + ifc.run("unit.unassign_unit", units=["unit"]).should_be_called() + unit.import_units().should_be_called() + subject.unassign_unit(ifc, unit, unit="unit") + + +class TestLoadUnits: + def test_run(self, unit): + unit.import_units().should_be_called() + unit.enable_editing_units().should_be_called() + subject.load_units(unit) + + +class TestDisableUnitEditingUI: + def test_run(self, unit): + unit.disable_editing_units().should_be_called() + subject.disable_unit_editing_ui(unit) + + +class TestRemoveUnit: + def test_run(self, ifc, unit): + ifc.run("unit.remove_unit", unit="unit").should_be_called() + unit.import_units().should_be_called() + subject.remove_unit(ifc, unit, unit="unit") + + +class TestAddMonetaryUnit: + def test_run(self, ifc, unit): + ifc.run("unit.add_monetary_unit").should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_monetary_unit(ifc, unit) == "unit" + + +class TestAddSIUnit: + def test_run(self, ifc, unit): + unit.get_si_name_from_unit_type("unit_type").should_be_called().will_return("name") + ifc.run("unit.add_si_unit", unit_type="unit_type", name="name").should_be_called().will_return("unit") + unit.import_units().should_be_called() + assert subject.add_si_unit(ifc, unit, unit_type="unit_type") == "unit" + + +class TestAddContextDependentUnit: + def test_run(self, ifc, unit): + ifc.run("unit.add_context_dependent_unit", unit_type="unit_type", name="name").should_be_called().will_return( + "unit" + ) + unit.import_units().should_be_called() + assert subject.add_context_dependent_unit(ifc, unit, unit_type="unit_type", name="name") == "unit" + + +class TestEnableEditingUnit: + def test_run(self, unit): + unit.set_active_unit("unit").should_be_called() + unit.import_unit_attributes("unit").should_be_called() + subject.enable_editing_unit(unit, unit="unit") + + +class TestDisableEditingUnit: + def test_run(self, unit): + unit.clear_active_unit().should_be_called() + subject.disable_editing_unit(unit) + + +class TestEditUnit: + def test_editing_monetary_units(self, ifc, unit): + unit.export_unit_attributes().should_be_called().will_return("attributes") + unit.get_unit_class("unit").should_be_called().will_return("IfcMonetaryUnit") + ifc.run("unit.edit_monetary_unit", unit="unit", attributes="attributes").should_be_called() + unit.import_units().should_be_called() + unit.clear_active_unit().should_be_called() + subject.edit_unit(ifc, unit, unit="unit") + + def test_editing_derived_units(self, ifc, unit): + unit.export_unit_attributes().should_be_called().will_return("attributes") + unit.get_unit_class("unit").should_be_called().will_return("IfcDerivedUnit") + ifc.run("unit.edit_derived_unit", unit="unit", attributes="attributes").should_be_called() + unit.import_units().should_be_called() + unit.clear_active_unit().should_be_called() + subject.edit_unit(ifc, unit, unit="unit") + + def test_editing_named_units(self, ifc, unit): + unit.export_unit_attributes().should_be_called().will_return("attributes") + unit.get_unit_class("unit").should_be_called().will_return("IfcNamedUnit") + ifc.run("unit.edit_named_unit", unit="unit", attributes="attributes").should_be_called() + unit.import_units().should_be_called() + unit.clear_active_unit().should_be_called() + subject.edit_unit(ifc, unit, unit="unit") diff --git a/src/blenderbim/test/tool/test_unit.py b/src/blenderbim/test/tool/test_unit.py new file mode 100644 index 0000000000..41250a9d77 --- /dev/null +++ b/src/blenderbim/test/tool/test_unit.py @@ -0,0 +1,315 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell +import blenderbim.core.tool +import blenderbim.tool as tool +from test.bim.bootstrap import NewFile +from blenderbim.tool.unit import Unit as subject + + +class TestImplementsTool(NewFile): + def test_run(self): + assert isinstance(subject(), blenderbim.core.tool.Unit) + + +class TestClearActiveUnit(NewFile): + def test_run(self): + bpy.context.scene.BIMUnitProperties.active_unit_id = 1 + subject.clear_active_unit() + assert bpy.context.scene.BIMUnitProperties.active_unit_id == 0 + + +class TestDisableEditingUnits(NewFile): + def test_run(self): + bpy.context.scene.BIMUnitProperties.is_editing = True + subject.disable_editing_units() + assert bpy.context.scene.BIMUnitProperties.is_editing == False + + +class TestEnableEditingUnits(NewFile): + def test_run(self): + bpy.context.scene.BIMUnitProperties.is_editing = False + subject.enable_editing_units() + assert bpy.context.scene.BIMUnitProperties.is_editing == True + + +class TestExportUnitAttributes(NewFile): + def test_exporting_derived_units(self): + TestImportUnitAttributes().test_importing_derived_units() + assert subject.export_unit_attributes() == { + "UnitType": "ANGULARVELOCITYUNIT", + "UserDefinedType": "UserDefinedType", + } + + def test_exporting_monetary_units(self): + TestImportUnitAttributes().test_importing_monetary_units() + assert subject.export_unit_attributes() == {"Currency": "Currency"} + + def test_exporting_monetary_units_ifc2x3(self): + TestImportUnitAttributes().test_importing_monetary_units_ifc2x3() + assert subject.export_unit_attributes() == {"Currency": "USD"} + + def test_exporting_context_dependent_units(self): + TestImportUnitAttributes().test_importing_context_dependent_units() + assert subject.export_unit_attributes() == { + "UnitType": "ABSORBEDDOSEUNIT", + "Name": "Name", + "Dimensions": [1, 2, 3, 4, 5, 6, 7], + } + + def test_exporting_conversion_based_units(self): + TestImportUnitAttributes().test_importing_conversion_based_units() + assert subject.export_unit_attributes() == { + "UnitType": "ABSORBEDDOSEUNIT", + "Name": "Name", + "Dimensions": [1, 2, 3, 4, 5, 6, 7], + } + + def test_exporting_conversion_based_with_offset_units(self): + TestImportUnitAttributes().test_importing_conversion_based_with_offset_units() + assert subject.export_unit_attributes() == { + "UnitType": "ABSORBEDDOSEUNIT", + "Name": "Name", + "Dimensions": [1, 2, 3, 4, 5, 6, 7], + "ConversionOffset": 1, + } + + def test_exporting_si_units(self): + TestImportUnitAttributes().test_importing_si_units() + assert subject.export_unit_attributes() == {"UnitType": "ABSORBEDDOSEUNIT", "Prefix": "EXA", "Name": "AMPERE"} + + +class TestGetSceneUnitName(NewFile): + def test_getting_a_metric_name(self): + bpy.context.scene.unit_settings.system = "METRIC" + assert subject.get_scene_unit_name("length") == "METRE" + assert subject.get_scene_unit_name("area") == "SQUARE_METRE" + assert subject.get_scene_unit_name("volume") == "CUBIC_METRE" + + def test_getting_an_imperial_name(self): + bpy.context.scene.unit_settings.system = "IMPERIAL" + bpy.context.scene.unit_settings.length_unit = "MILES" + assert subject.get_scene_unit_name("length") == "mile" + assert subject.get_scene_unit_name("area") == "square mile" + assert subject.get_scene_unit_name("volume") == "cubic mile" + bpy.context.scene.unit_settings.length_unit = "FEET" + assert subject.get_scene_unit_name("length") == "foot" + assert subject.get_scene_unit_name("area") == "square foot" + assert subject.get_scene_unit_name("volume") == "cubic foot" + bpy.context.scene.unit_settings.length_unit = "INCHES" + assert subject.get_scene_unit_name("length") == "inch" + assert subject.get_scene_unit_name("area") == "square inch" + assert subject.get_scene_unit_name("volume") == "cubic inch" + bpy.context.scene.unit_settings.length_unit = "THOU" + assert subject.get_scene_unit_name("length") == "thou" + assert subject.get_scene_unit_name("area") == "square thou" + assert subject.get_scene_unit_name("volume") == "cubic thou" + bpy.context.scene.unit_settings.length_unit = "ADAPTIVE" + assert subject.get_scene_unit_name("length") == "foot" + assert subject.get_scene_unit_name("area") == "square foot" + assert subject.get_scene_unit_name("volume") == "cubic foot" + + def test_getting_a_name_with_no_unit_system(self): + bpy.context.scene.unit_settings.system = "NONE" + assert subject.get_scene_unit_name("length") == "METRE" + + +class TestGetSceneUnitSIPrefix: + def test_run(self): + bpy.context.scene.unit_settings.system = "METRIC" + bpy.context.scene.unit_settings.length_unit = "METERS" + assert subject.get_scene_unit_si_prefix() is None + bpy.context.scene.unit_settings.length_unit = "MICROMETERS" + assert subject.get_scene_unit_si_prefix() == "MICRO" + bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" + assert subject.get_scene_unit_si_prefix() == "MILLI" + bpy.context.scene.unit_settings.length_unit = "CENTIMETERS" + assert subject.get_scene_unit_si_prefix() == "CENTI" + bpy.context.scene.unit_settings.length_unit = "KILOMETERS" + assert subject.get_scene_unit_si_prefix() == "KILO" + bpy.context.scene.unit_settings.length_unit = "ADAPTIVE" + assert subject.get_scene_unit_si_prefix() is None + + +class TestGetSINameFromUnitType: + def test_run(self): + for key, value in ifcopenshell.util.unit.si_type_names.items(): + assert subject.get_si_name_from_unit_type(key) == value + + +class TestGetUnitClass: + def test_run(self): + ifc = ifcopenshell.file() + assert subject.get_unit_class(ifc.createIfcSIUnit()) == "IfcSIUnit" + assert subject.get_unit_class(ifc.createIfcNamedUnit()) == "IfcNamedUnit" + + +class TestImportUnitAttributes(NewFile): + def test_importing_derived_units(self): + ifc = ifcopenshell.file() + unit = ifc.createIfcDerivedUnit() + unit.UnitType = "ANGULARVELOCITYUNIT" + unit.UserDefinedType = "UserDefinedType" + subject.import_unit_attributes(unit) + props = bpy.context.scene.BIMUnitProperties + assert props.unit_attributes.get("UnitType").enum_value == "ANGULARVELOCITYUNIT" + assert props.unit_attributes.get("UserDefinedType").string_value == "UserDefinedType" + + def test_importing_monetary_units(self): + ifc = ifcopenshell.file() + unit = ifc.createIfcMonetaryUnit() + unit.Currency = "Currency" + subject.import_unit_attributes(unit) + props = bpy.context.scene.BIMUnitProperties + assert props.unit_attributes.get("Currency").string_value == "Currency" + + def test_importing_monetary_units_ifc2x3(self): + ifc = ifcopenshell.file(schema="IFC2X3") + unit = ifc.createIfcMonetaryUnit() + unit.Currency = "USD" + subject.import_unit_attributes(unit) + props = bpy.context.scene.BIMUnitProperties + assert props.unit_attributes.get("Currency").enum_value == "USD" + + def test_importing_context_dependent_units(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + unit = ifc.createIfcContextDependentUnit() + unit.UnitType = "ABSORBEDDOSEUNIT" + unit.Name = "Name" + unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7) + subject.import_unit_attributes(unit) + props = bpy.context.scene.BIMUnitProperties + assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes.get("Name").string_value == "Name" + assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]" + + def test_importing_conversion_based_units(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + unit = ifc.createIfcConversionBasedUnit() + unit.UnitType = "ABSORBEDDOSEUNIT" + unit.Name = "Name" + unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7) + subject.import_unit_attributes(unit) + props = bpy.context.scene.BIMUnitProperties + assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes.get("Name").string_value == "Name" + assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]" + + def test_importing_conversion_based_with_offset_units(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + unit = ifc.createIfcConversionBasedUnitWithOffset() + unit.UnitType = "ABSORBEDDOSEUNIT" + unit.Name = "Name" + unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7) + unit.ConversionOffset = 1 + subject.import_unit_attributes(unit) + props = bpy.context.scene.BIMUnitProperties + assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes.get("Name").string_value == "Name" + assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]" + assert props.unit_attributes.get("ConversionOffset").float_value == 1 + + def test_importing_si_units(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + unit = ifc.createIfcSIUnit() + unit.UnitType = "ABSORBEDDOSEUNIT" + unit.Prefix = "EXA" + unit.Name = "AMPERE" + subject.import_unit_attributes(unit) + props = bpy.context.scene.BIMUnitProperties + assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes.get("Prefix").enum_value == "EXA" + assert props.unit_attributes.get("Name").enum_value == "AMPERE" + assert props.unit_attributes.get("Dimensions") is None + + +class TestImportUnits(NewFile): + def test_importing_multiple_units(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + unit1 = ifc.createIfcDerivedUnit(UnitType="ANGULARVELOCITYUNIT") + unit2 = ifc.createIfcMonetaryUnit(Currency="Currency") + unit3 = ifc.createIfcContextDependentUnit(Name="Name", UnitType="ABSORBEDDOSEUNIT") + unit4 = ifc.createIfcConversionBasedUnit(Name="Name", UnitType="ABSORBEDDOSEUNIT") + unit5 = ifc.createIfcSIUnit(Name="AMPERE", Prefix="MILLI", UnitType="ABSORBEDDOSEUNIT") + unit6 = ifc.createIfcSIUnit(Name="CUBIC_METRE", Prefix="CENTI", UnitType="ABSORBEDDOSEUNIT") + ifc.createIfcUnitAssignment(Units=[unit2]) + subject.import_units() + props = bpy.context.scene.BIMUnitProperties + assert len(props.units) == 6 + + assert props.units[0].ifc_definition_id == unit1.id() + assert props.units[0].name == "" + assert props.units[0].is_assigned is False + assert props.units[0].unit_type == unit1.UnitType + assert props.units[0].ifc_class == unit1.is_a() + + assert props.units[1].ifc_definition_id == unit2.id() + assert props.units[1].name == "Currency" + assert props.units[1].is_assigned is True + assert props.units[1].unit_type == "CURRENCY" + assert props.units[1].ifc_class == unit2.is_a() + + assert props.units[2].ifc_definition_id == unit3.id() + assert props.units[2].name == "Name" + assert props.units[2].is_assigned is False + assert props.units[2].unit_type == unit3.UnitType + assert props.units[2].ifc_class == unit3.is_a() + + assert props.units[3].ifc_definition_id == unit4.id() + assert props.units[3].name == "Name" + assert props.units[3].is_assigned is False + assert props.units[3].unit_type == unit4.UnitType + assert props.units[3].ifc_class == unit4.is_a() + + assert props.units[4].ifc_definition_id == unit5.id() + assert props.units[4].name == "MILLIAMPERE" + assert props.units[4].is_assigned is False + assert props.units[4].unit_type == unit5.UnitType + assert props.units[4].ifc_class == unit5.is_a() + + assert props.units[5].ifc_definition_id == unit6.id() + assert props.units[5].name == "CUBIC CENTIMETRE" + assert props.units[5].is_assigned is False + assert props.units[5].unit_type == unit6.UnitType + assert props.units[5].ifc_class == unit6.is_a() + + +class TestIsSceneUnitMetric(NewFile): + def test_run(self): + props = bpy.context.scene.unit_settings + props.system = "METRIC" + assert subject.is_scene_unit_metric() is True + props.system = "IMPERIAL" + assert subject.is_scene_unit_metric() is False + props.system = "NONE" + assert subject.is_scene_unit_metric() is True + + +class TestSetActiveUnit(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + unit = ifc.createIfcSIUnit() + subject.set_active_unit(unit) + assert bpy.context.scene.BIMUnitProperties.active_unit_id == unit.id() 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 new file mode 100644 index 0000000000..915a882caf --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py @@ -0,0 +1,37 @@ +import ifcopenshell +import ifcopenshell.util.unit + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"unit_type": "LENGTHUNIT", "name": "METRE", "prefix": None, "conversion_offset": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + if self.settings["unit_type"] == "LENGTHUNIT": + dimensional_exponents = self.file.createIfcDimensionalExponents(1, 0, 0, 0, 0, 0, 0) + si_unit = self.file.createIfcSIUnit(UnitType=self.settings["unit_type"], Name="METRE") + elif self.settings["unit_type"] == "AREAUNIT": + dimensional_exponents = self.file.createIfcDimensionalExponents(2, 0, 0, 0, 0, 0, 0) + si_unit = self.file.createIfcSIUnit(UnitType=self.settings["unit_type"], Name="SQUARE_METRE") + elif self.settings["unit_type"] == "VOLUMEUNIT": + dimensional_exponents = self.file.createIfcDimensionalExponents(3, 0, 0, 0, 0, 0, 0) + si_unit = self.file.createIfcSIUnit(UnitType=self.settings["unit_type"], Name="CUBIC_METRE") + + conversion_real = ifcopenshell.util.unit.si_conversions.get(self.settings["name"], 1) + value_component = self.file.create_entity("IfcReal", **{"wrappedValue": conversion_real}) + conversion_factor = self.file.createIfcMeasureWithUnit(value_component, si_unit) + + if self.settings["conversion_offset"]: + return self.file.createIfcConversionBasedUnit( + dimensional_exponents, + self.settings["unit_type"], + self.settings["name"], + conversion_factor, + self.settings["conversion_offset"], + ) + return self.file.createIfcConversionBasedUnit( + dimensional_exponents, self.settings["unit_type"], self.settings["name"], conversion_factor + )