diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py index 1afaf3ad09..2fa01640eb 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py @@ -381,32 +381,21 @@ class CalculateQuantity(bpy.types.Operator): self.qto_calculator = QtoCalculator() obj = context.active_object prop = obj.PsetProperties.properties.get(self.prop) - prop.metadata.float_value = self.calculate_quantity(obj, context) + quantity = self.calculate_quantity(obj, context) + + if quantity is None: + self.report({"ERROR"}, "Could not calculate quantity") + return {"CANCELLED"} + + prop.metadata.float_value = quantity return {"FINISHED"} def calculate_quantity(self, obj, context): quantity = self.qto_calculator.calculate_quantity(obj.PsetProperties.active_pset_name, self.prop, obj) - prefix, name = self.get_blender_prefix_name(context) - quantity = ifcopenshell.util.unit.convert(quantity, None, "METRE", prefix, name) + if quantity is None: + return return round(quantity, 3) - def get_prefix_name(self, value): - if "/" in value: - return value.split("/") - return None, value - - def get_blender_prefix_name(self, context): - unit_settings = context.scene.unit_settings - if unit_settings.system == "IMPERIAL": - if unit_settings.length_unit == "INCHES": - return None, "inch" - elif unit_settings.length_unit == "FEET": - return None, "foot" - elif unit_settings.system == "METRIC": - if unit_settings.length_unit == "METERS": - return None, "METRE" - return unit_settings.length_unit[0 : -len("METERS")], "METRE" - class GuessQuantity(bpy.types.Operator): bl_idname = "bim.guess_quantity" @@ -423,35 +412,7 @@ class GuessQuantity(bpy.types.Operator): def guess_quantity(self, obj, context): quantity = self.qto_calculator.guess_quantity(self.prop, [p.name for p in obj.PsetProperties.properties], obj) - if "area" in self.prop.lower(): - if context.scene.BIMProperties.area_unit: - prefix, name = self.get_prefix_name(context.scene.BIMProperties.area_unit) - quantity = ifcopenshell.util.unit.convert(quantity, None, "SQUARE_METRE", prefix, name) - elif "volume" in self.prop.lower(): - if context.scene.BIMProperties.volume_unit: - prefix, name = self.get_prefix_name(context.scene.BIMProperties.volume_unit) - quantity = ifcopenshell.util.unit.convert(quantity, None, "CUBIC_METRE", prefix, name) - else: - prefix, name = self.get_blender_prefix_name(context) - quantity = ifcopenshell.util.unit.convert(quantity, None, "METRE", prefix, name) - return round(quantity, 3) - - def get_prefix_name(self, value): - if "/" in value: - return value.split("/") - return None, value - - def get_blender_prefix_name(self, context): - unit_settings = context.scene.unit_settings - if unit_settings.system == "IMPERIAL": - if unit_settings.length_unit == "INCHES": - return None, "inch" - elif unit_settings.length_unit == "FEET": - return None, "foot" - elif unit_settings.system == "METRIC": - if unit_settings.length_unit == "METERS": - return None, "METRE" - return unit_settings.length_unit[0 : -len("METERS")], "METRE" + return round(quantity, 3) if quantity is not None else None class CopyPropertyToSelection(bpy.types.Operator, Operator): diff --git a/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py b/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py index a624d8c934..2452708fbe 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py @@ -26,10 +26,10 @@ from shapely.ops import unary_union import blenderbim.tool as tool import ifcopenshell from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper +import blenderbim.bim class QtoCalculator: - def __init__(self): self.mapping_dict = {} for key in mapper.keys(): @@ -46,6 +46,7 @@ class QtoCalculator: self.mapping_dict[key][item] = None def calculate_quantity(self, qto_name, quantity_name, obj): + """calculates the value of the quantity in the project units""" string = "self.mapping_dict[qto_name][quantity_name](obj" if isinstance(mapper[qto_name][quantity_name], dict): args = mapper[qto_name][quantity_name]["args"] @@ -53,33 +54,53 @@ class QtoCalculator: args = "" string += args string += ")" - return eval(string) + value = eval(string) + + return tool.Qto.convert_to_project_units(value, qto_name, quantity_name) or value def guess_quantity(self, prop_name, alternative_prop_names, obj): + """guess the value of the quantity by name, returns the value in the project units""" prop_name = prop_name.lower() alternative_prop_names = [p.lower() for p in alternative_prop_names] + value = None if "length" in prop_name and "width" not in alternative_prop_names and "height" not in alternative_prop_names: - return self.get_linear_length(obj) + value = self.get_linear_length(obj) elif "length" in prop_name: - return self.get_length(obj) + value = self.get_length(obj) elif "width" in prop_name and "length" not in alternative_prop_names: - return self.get_length(obj) + value = self.get_length(obj) elif "width" in prop_name: - return self.get_width(obj) + value = self.get_width(obj) elif "height" in prop_name or "depth" in prop_name: - return self.get_height(obj) + value = self.get_height(obj) elif "perimeter" in prop_name: - return self.get_net_perimeter(obj) + value = self.get_net_perimeter(obj) elif "area" in prop_name and ("footprint" in prop_name or "section" in prop_name or "floor" in prop_name): - return self.get_net_footprint_area(obj) + value = self.get_net_footprint_area(obj) elif "area" in prop_name and "side" in prop_name: - return self.get_side_area(obj) + value = self.get_side_area(obj) elif "area" in prop_name: - return self.get_gross_surface_area(obj) + value = self.get_gross_surface_area(obj) elif "volume" in prop_name and "gross" in prop_name: - return self.get_gross_volume(obj) - elif "volume" in prop_name : - return self.get_net_volume(obj) + value = self.get_gross_volume(obj) + elif "volume" in prop_name: + value = self.get_net_volume(obj) + + if value is None: + return + + unit_type_keywords = { + "length": "Q_LENGTH", + "width": "Q_LENGTH", + "height": "Q_LENGTH", + "depth": "Q_LENGTH", + "perimeter": "Q_LENGTH", + "area": "Q_AREA", + "volume": "Q_VOLUME", + } + + unit_type = next(unit_type_keywords[k] for k in unit_type_keywords if k in prop_name) + return tool.Qto.convert_to_project_units(value, quantity_type=unit_type) or value def get_units(self, o, vg_index): return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]]) @@ -100,7 +121,7 @@ class QtoCalculator: x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length - if self.get_object_main_axis(o) == "x" or main_axis=="x": + if self.get_object_main_axis(o) == "x" or main_axis == "x": return max(x, y) if self.get_object_main_axis(o) == "z": return max(z, x) @@ -226,7 +247,10 @@ class QtoCalculator: decompositions = ifcopenshell.util.element.get_decomposition(element) finish_floor_height = 0 for decomposition in decompositions: - if decomposition.get_info()['PredefinedType'] == 'FLOORING' and decomposition.get_info()['type'] == 'IfcCovering' : + if ( + decomposition.get_info()["PredefinedType"] == "FLOORING" + and decomposition.get_info()["type"] == "IfcCovering" + ): floor_obj = tool.Ifc.get_object(decomposition) new_finish_floor_height = self.get_height(floor_obj) if new_finish_floor_height > finish_floor_height: @@ -239,7 +263,10 @@ class QtoCalculator: decompositions = ifcopenshell.util.element.get_decomposition(element) finish_ceiling_height = 0 for decomposition in decompositions: - if decomposition.get_info()['PredefinedType'] == 'CEILING' and decomposition.get_info()['type'] == 'IfcCovering' : + if ( + decomposition.get_info()["PredefinedType"] == "CEILING" + and decomposition.get_info()["type"] == "IfcCovering" + ): ceiling_obj = tool.Ifc.get_object(decomposition) new_finish_ceiling_height = self.get_height(ceiling_obj) if new_finish_ceiling_height > finish_ceiling_height: @@ -247,8 +274,6 @@ class QtoCalculator: return finish_ceiling_height - - def get_net_perimeter(self, o): parsed_edges = [] shared_edges = [] @@ -276,9 +301,9 @@ class QtoCalculator: pass def get_rectangular_perimeter(self, obj): - length = self.get_length(obj, main_axis='x') + length = self.get_length(obj, main_axis="x") height = self.get_height(obj) - return (length+height)*2 + return (length + height) * 2 def get_lowest_polygons(self, o): lowest_polygons = [] @@ -297,7 +322,6 @@ class QtoCalculator: return lowest_polygons def get_highest_polygons(self, o): - highest_polygons = [] highest_z = None for polygon in o.data.polygons: @@ -327,8 +351,8 @@ class QtoCalculator: total_net_floor_area = self.get_net_footprint_area(obj) for decomposition in decompositions: - decomposition_type = decomposition.get_info()['type'] - if decomposition_type == 'IfcColumn' or decomposition_type == 'IfcColumn': + decomposition_type = decomposition.get_info()["type"] + if decomposition_type == "IfcColumn" or decomposition_type == "IfcColumn": decomposition_obj = tool.Ifc.get_object(decomposition) net_footprint_obj_area = self.get_net_footprint_area(decomposition_obj) total_net_floor_area -= net_footprint_obj_area @@ -343,9 +367,9 @@ class QtoCalculator: total_gross_ceiling_area = 0 for decomposition in decompositions: - decomposition_type = decomposition.get_info()['type'] - decomposition_predefined_type = decomposition.get_info()['PredefinedType'] - if decomposition_type == 'IfcCovering' and decomposition_predefined_type == 'CEILING': + decomposition_type = decomposition.get_info()["type"] + decomposition_predefined_type = decomposition.get_info()["PredefinedType"] + if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING": decomposition_obj = tool.Ifc.get_object(decomposition) total_gross_ceiling_area += self.get_gross_footprint_area(decomposition_obj) @@ -359,13 +383,13 @@ class QtoCalculator: total_net_ceiling_area = 0 for decomposition in decompositions: - decomposition_type = decomposition.get_info()['type'] - decomposition_predefined_type = decomposition.get_info()['PredefinedType'] - if decomposition_type == 'IfcCovering' and decomposition_predefined_type == 'CEILING': + decomposition_type = decomposition.get_info()["type"] + decomposition_predefined_type = decomposition.get_info()["PredefinedType"] + if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING": decomposition_obj = tool.Ifc.get_object(decomposition) total_net_ceiling_area += self.get_net_footprint_area(decomposition_obj) - if decomposition_type == 'IfcWall' or decomposition_type == 'IfcColumn': + if decomposition_type == "IfcWall" or decomposition_type == "IfcColumn": decomposition_obj = tool.Ifc.get_object(decomposition) total_net_ceiling_area -= self.get_net_roofprint_area(decomposition_obj) @@ -379,8 +403,8 @@ class QtoCalculator: total_space_net_volume = self.get_gross_volume(obj) for decomposition in decompositions: - decomposition_type = decomposition.get_info()['type'] - if decomposition_type == 'IfcWall' or decomposition_type == 'IfcColumn': + decomposition_type = decomposition.get_info()["type"] + if decomposition_type == "IfcWall" or decomposition_type == "IfcColumn": decomposition_obj = tool.Ifc.get_object(decomposition) total_space_net_volume -= self.get_net_volume(decomposition_obj) @@ -541,33 +565,37 @@ class QtoCalculator: or material.is_a("IfcMaterialProfileSet") or material.is_a("IfcMaterialConstituentSet") ): - return + return - if material.is_a('IfcMaterial'): + if material.is_a("IfcMaterial"): material_mass_density = ifcopenshell.util.element.get_pset(material, "Pset_MaterialCommon", "MassDensity") return material_mass_density - if material.is_a('IfcMaterialLayerSetUsage'): + if material.is_a("IfcMaterialLayerSetUsage"): material_layers = material.ForLayerSet.MaterialLayers densities = [] thicknesses = [] obj_mass_density = 0 for material_layer in material_layers: - material_mass_density = ifcopenshell.util.element.get_pset(material_layer.Material, "Pset_MaterialCommon", "MassDensity") + material_mass_density = ifcopenshell.util.element.get_pset( + material_layer.Material, "Pset_MaterialCommon", "MassDensity" + ) if material_mass_density is None: return densities.append(material_mass_density) thickness = material_layer.LayerThickness thicknesses.append(thickness) - obj_mass_density = obj_mass_density + (material_mass_density* thickness) + obj_mass_density = obj_mass_density + (material_mass_density * thickness) total_thickness = sum(thicknesses) - obj_mass_density = obj_mass_density/total_thickness + obj_mass_density = obj_mass_density / total_thickness return obj_mass_density - if material.is_a('IfcMaterialProfileSetUsage'): + if material.is_a("IfcMaterialProfileSetUsage"): material_profiles = material.ForProfileSet.MaterialProfiles if len(material_profiles) == 1: - material_mass_density = ifcopenshell.util.element.get_pset(material_profiles[0].Material, "Pset_MaterialCommon", "MassDensity") + material_mass_density = ifcopenshell.util.element.get_pset( + material_profiles[0].Material, "Pset_MaterialCommon", "MassDensity" + ) return material_mass_density else: return @@ -639,8 +667,8 @@ class QtoCalculator: for opening in openings: opening_id = opening.RelatedOpeningElement.GlobalId ifc_opening_element = ifc.by_guid(opening_id) - #bl_opening_obj = tool.Ifc.get_object(ifc_opening_element) - #mesh = bpy.data.meshes.new('myMesh') + # bl_opening_obj = tool.Ifc.get_object(ifc_opening_element) + # mesh = bpy.data.meshes.new('myMesh') mesh = self.get_gross_element_mesh(ifc_opening_element) bl_opening_obj = bpy.data.objects.new("MyObject", mesh) @@ -656,8 +684,12 @@ class QtoCalculator: bl_OBB_opening_object = self.get_OBB_object(bl_opening_obj) opening_area = self.get_lateral_area( - #self.get_OBB_object(bl_opening_obj), angle_z1=angle_z1, angle_z2=angle_z2, exclude_end_areas=True - bl_OBB_opening_object, angle_z1=angle_z1, angle_z2=angle_z2, exclude_end_areas=True, main_axis = 'x', + # self.get_OBB_object(bl_opening_obj), angle_z1=angle_z1, angle_z2=angle_z2, exclude_end_areas=True + bl_OBB_opening_object, + angle_z1=angle_z1, + angle_z2=angle_z2, + exclude_end_areas=True, + main_axis="x", ) if opening_area >= min_area: total_opening_area += opening_area @@ -732,16 +764,16 @@ class QtoCalculator: if not self.has_openings(obj): return self.get_net_side_area(obj) - gross_side_area = self.get_lateral_area(obj, exclude_end_areas = True, subtract_openings = False, main_axis = 'x') / 2 + gross_side_area = self.get_lateral_area(obj, exclude_end_areas=True, subtract_openings=False, main_axis="x") / 2 return gross_side_area def get_net_side_area(self, obj): - net_side_area = self.get_lateral_area(obj, exclude_end_areas = True, main_axis = 'x') / 2 + net_side_area = self.get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2 return net_side_area def get_outer_surface_area(self, obj): - outer_surface_area = self.get_lateral_area(obj, exclude_end_areas = True, angle_z1 = 0, angle_z2 = 360) + outer_surface_area = self.get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360) return outer_surface_area def get_end_area(self, obj): @@ -751,7 +783,7 @@ class QtoCalculator: gross_obj.matrix_world = obj.matrix_world - end_area = self.get_lateral_area(gross_obj, exclude_side_areas = True) / 2 + end_area = self.get_lateral_area(gross_obj, exclude_side_areas=True) / 2 self.delete_obj(gross_obj) self.delete_mesh(gross_mesh) @@ -774,7 +806,7 @@ class QtoCalculator: ifc = tool.Ifc.get() ifc_element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id) -# if len(openings := ifc_element.HasOpenings) != 0: + # if len(openings := ifc_element.HasOpenings) != 0: if len(openings := self.has_openings(obj)) != 0: for opening in openings: if opening.RelatedOpeningElement.PredefinedType == "OPENING": @@ -813,8 +845,8 @@ class QtoCalculator: if angle_to_z_axis < angle: # offset the raycast, otherwise the raycast will always collide with the object. - offset = polygon.center+Vector((0,0,0.01)) - if ignore_internal and obj.ray_cast(offset, (0,0,1))[0]: + offset = polygon.center + Vector((0, 0, 0.01)) + if ignore_internal and obj.ray_cast(offset, (0, 0, 1))[0]: continue area += polygon.area @@ -942,7 +974,7 @@ class QtoCalculator: new_AABB_object = bpy.data.objects.new(f"OBB_{ifc_id}", aabb_mesh) new_AABB_object.matrix_world = obj.matrix_world - # create new collection for QtoCalculator + # create new collection for QtoCalculator collection = bpy.data.collections.get("QtoCalculator", bpy.data.collections.new("QtoCalculator")) if not bpy.context.scene.collection.children.get(collection.name): bpy.context.scene.collection.children.link(collection) @@ -1098,12 +1130,7 @@ class QtoCalculator: return 0 # touching polygons should be coplanar: - plane_intersection = mathutils.geometry.intersect_plane_plane( - center1, - normal1, - center2, - normal2 - ) + plane_intersection = mathutils.geometry.intersect_plane_plane(center1, normal1, center2, normal2) # sometimes coplanar planes will interesect far off into the distance. This is a crude way of filtering out those intersections. if plane_intersection[0] is None or (plane_intersection[0] - center1).magnitude > 20: @@ -1212,15 +1239,16 @@ class QtoCalculator: bpy.data.meshes.remove(mesh) def delete_obj(self, obj): - bpy.data.objects.remove(obj, do_unlink = True) + bpy.data.objects.remove(obj, do_unlink=True) + # # Following code is here temporarily to test newly created functions: -#qto = QtoCalculator() -#o = bpy.context.active_object -#sel = bpy.context.selected_objects +# qto = QtoCalculator() +# o = bpy.context.active_object +# sel = bpy.context.selected_objects # -#nl = '\n' +# nl = '\n' # print( # f"get_linear_length: {qto.get_linear_length(o)}{nl}{nl}" # f"get_width: {qto.get_width(o)}{nl}{nl}" @@ -1245,5 +1273,3 @@ class QtoCalculator: # f"get_touching_objects(o, ['IfcElement']): {qto.get_touching_objects(o, ['IfcElement'])}{nl}{nl}" # #f"get_contact_area: {qto.get_contact_area(o)}{nl}{nl}" # ) - - diff --git a/src/blenderbim/blenderbim/tool/qto.py b/src/blenderbim/blenderbim/tool/qto.py index d3b79cf3ce..60ec399fec 100644 --- a/src/blenderbim/blenderbim/tool/qto.py +++ b/src/blenderbim/blenderbim/tool/qto.py @@ -90,13 +90,48 @@ class Qto(blenderbim.core.tool.Qto): return { quantity_name: cls.get_rounded_value(calculator.calculate_quantity(qto_name, quantity_name, obj)) for quantity_name in cls.get_applicable_quantity_names(qto_name) or [] - if cls.has_calculator(qto_name, quantity_name) and calculator.calculate_quantity(qto_name, quantity_name, obj) is not None + if cls.has_calculator(qto_name, quantity_name) + and calculator.calculate_quantity(qto_name, quantity_name, obj) is not None } @classmethod def has_calculator(cls, qto_name, quantity_name): return bool(mapper.get(qto_name, {}).get(quantity_name, None)) + @classmethod + def convert_to_project_units(cls, value, qto_name=None, quantity_name=None, quantity_type=None): + """You can either specify `quantity_type` or provide `qto_name/quantity_name` + to let method figure the `quantity_type` from the templates + + `quantity_type` values are `Q_LENGTH`, `Q_AREA`, `Q_VOLUME` + """ + ifc_file = tool.Ifc.get() + quantity_to_unit_types = { + "Q_LENGTH": ("LENGTHUNIT", "METRE"), + "Q_AREA": ("AREAUNIT", "SQUARE_METRE"), + "Q_VOLUME": ("VOLUMEUNIT", "CUBIC_METRE"), + } + if not quantity_type: + qt = blenderbim.bim.schema.ifc.psetqto.get_by_name(qto_name) + quantity_type = next(q.TemplateType for q in qt.HasPropertyTemplates if q.Name == quantity_name) + + unit_type = quantity_to_unit_types.get(quantity_type, None) + if not unit_type: + return + + unit_type, base_unit = unit_type + project_unit = ifcopenshell.util.unit.get_project_unit(ifc_file, unit_type) + if not project_unit: + return + value = ifcopenshell.util.unit.convert( + value, + from_prefix=None, + from_unit=base_unit, + to_prefix=getattr(project_unit, "Prefix", None), + to_unit=project_unit.Name, + ) + return value + @classmethod def get_guessed_quantities(cls, obj, pset_qto_properties): calculated_quantities = {} diff --git a/src/blenderbim/test/tool/test_qto.py b/src/blenderbim/test/tool/test_qto.py index e5fe515d3e..1e13207c0c 100644 --- a/src/blenderbim/test/tool/test_qto.py +++ b/src/blenderbim/test/tool/test_qto.py @@ -29,24 +29,32 @@ class TestImplementsTool(test.bim.bootstrap.NewFile): def test_run(self): assert isinstance(subject(), blenderbim.core.tool.Qto) + class TestGetRadiusOfSelectedVertices(test.bim.bootstrap.NewFile): def test_run(self): bpy.ops.mesh.primitive_circle_add() assert round(subject.get_radius_of_selected_vertices(bpy.data.objects.get("Circle")), 3) == 1 + class TestSetQtoResult(test.bim.bootstrap.NewFile): def test_run(self): subject.set_qto_result(123.4567) assert bpy.context.scene.BIMQtoProperties.qto_result == "123.457" + class TestGetApplicableQuantityNames(test.bim.bootstrap.NewFile): def test_run(self): ifc = ifcopenshell.file() tool.Ifc.set(ifc) schema = ifc.schema - properties_templates = ifcopenshell.util.pset.PsetQto(schema).get_by_name('Qto_WallBaseQuantities').get_info()['HasPropertyTemplates'] + properties_templates = ( + ifcopenshell.util.pset.PsetQto(schema) + .get_by_name("Qto_WallBaseQuantities") + .get_info()["HasPropertyTemplates"] + ) applicable_quantity_names = [a.Name for a in properties_templates] - assert subject.get_applicable_quantity_names('Qto_WallBaseQuantities') == applicable_quantity_names + assert subject.get_applicable_quantity_names("Qto_WallBaseQuantities") == applicable_quantity_names + class TestGetApplicableBaseQuantityName(test.bim.bootstrap.NewFile): def test_run(self): @@ -58,34 +66,57 @@ class TestGetApplicableBaseQuantityName(test.bim.bootstrap.NewFile): def test_no_base_quantity(self): ifc = ifcopenshell.file() tool.Ifc.set(ifc) - ifcopenshell.api.run("root.create_entity", ifc, ifc_class = "IfcProject") - product = ifc.by_type('IfcProject')[0] + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + product = ifc.by_type("IfcProject")[0] assert subject.get_applicable_base_quantity_name(product) == None + class TestGetRoundedValue(test.bim.bootstrap.NewFile): def test_run(self): quantity = 1.2345 assert subject.get_rounded_value(quantity) == 1.234 + class TestGetCalculatedObjectQuantities(test.bim.bootstrap.NewFile): - def test_run(self): - ifc = ifcopenshell.file() - tool.Ifc.set(ifc) - project = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject", name="My Project") - ifcopenshell.api.run("unit.assign_unit", ifc) - context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model") - bpy.ops.mesh.primitive_cube_add(location=(0.0,0.0,0.0), size = 2) + def setup_file(self): + self.ifc = ifcopenshell.file() + tool.Ifc.set(self.ifc) + project = ifcopenshell.api.run("root.create_entity", self.ifc, ifc_class="IfcProject", name="My Project") + + def setup_units(self, units): + ifcopenshell.api.run("unit.assign_unit", self.ifc, **units) + + def calculate_quantities(self, obj): + context = ifcopenshell.api.run("context.add_context", self.ifc, context_type="Model") + bpy.ops.mesh.primitive_cube_add(location=(0.0, 0.0, 0.0), size=2) obj = bpy.context.active_object - element = blenderbim.core.root.assign_class(tool.Ifc, - tool.Collector, - tool.Root, - obj=obj, - ifc_class="IfcWall", - predefined_type = "ELEMENTEDWALL", - context = context) + element = blenderbim.core.root.assign_class( + tool.Ifc, + tool.Collector, + tool.Root, + obj=obj, + ifc_class="IfcWall", + predefined_type="ELEMENTEDWALL", + context=context, + ) calculator = QtoCalculator() - base_qto = ifcopenshell.api.run("pset.add_qto", ifc, product=element, name="Qto_WallBaseQuantities") - quantities = subject.get_calculated_object_quantities(calculator = calculator, qto_name = "Qto_WallBaseQuantities", obj = obj) + base_qto = ifcopenshell.api.run("pset.add_qto", self.ifc, product=element, name="Qto_WallBaseQuantities") + quantities = subject.get_calculated_object_quantities( + calculator=calculator, qto_name="Qto_WallBaseQuantities", obj=obj + ) + return quantities + + def test_meters_project_unit(self): + self.setup_file() + self.setup_units( + { + "length": {"is_metric": True, "raw": "METERS"}, + "area": {"is_metric": True, "raw": "SQUARE_METERS"}, + "volume": {"is_metric": True, "raw": "CUBIC_METERS"}, + } + ) + quantities = self.calculate_quantities(bpy.context.active_object) + assert quantities["Length"] == 2 assert quantities["Width"] == 2 assert quantities["Height"] == 2 @@ -96,23 +127,69 @@ class TestGetCalculatedObjectQuantities(test.bim.bootstrap.NewFile): assert quantities["GrossVolume"] == 8 assert quantities["NetVolume"] == 8 + def test_prefix_project_unit(self): + self.setup_file() + self.setup_units( + { + "length": {"is_metric": True, "raw": "MILLIMETERS"}, + "area": {"is_metric": True, "raw": "SQUARE_MILLIMETERS"}, + "volume": {"is_metric": True, "raw": "CUBIC_MILLIMETERS"}, + } + ) + quantities = self.calculate_quantities(bpy.context.active_object) + + assert quantities["Length"] == 2e3 + assert quantities["Width"] == 2e3 + assert quantities["Height"] == 2e3 + assert quantities["GrossFootprintArea"] == 4e6 + assert quantities["NetFootprintArea"] == 4e6 + assert quantities["GrossSideArea"] == 4e6 + assert quantities["NetSideArea"] == 4e6 + assert quantities["GrossVolume"] == 8e9 + assert quantities["NetVolume"] == 8e9 + + def test_imperial_project_unit(self): + self.setup_file() + self.setup_units( + { + "length": {"is_metric": False, "raw": "FEET"}, + "area": {"is_metric": False, "raw": "FEET"}, + "volume": {"is_metric": False, "raw": "FEET"}, + } + ) + quantities = self.calculate_quantities(bpy.context.active_object) + + assert quantities["Length"] == 6.562 + assert quantities["Width"] == 6.562 + assert quantities["Height"] == 6.562 + assert quantities["GrossFootprintArea"] == 43.056 + assert quantities["NetFootprintArea"] == 43.056 + assert quantities["GrossSideArea"] == 43.056 + assert quantities["NetSideArea"] == 43.056 + assert quantities["GrossVolume"] == 282.517 + assert quantities["NetVolume"] == 282.517 + + class TestAddObjectBaseQto(test.bim.bootstrap.NewFile): def test_run(self): ifc = ifcopenshell.file() tool.Ifc.set(ifc) project = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject", name="My Project") context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model") - bpy.ops.mesh.primitive_cube_add(location=(0.0,0.0,0.0), size = 2) + bpy.ops.mesh.primitive_cube_add(location=(0.0, 0.0, 0.0), size=2) obj = bpy.context.active_object - element = blenderbim.core.root.assign_class(tool.Ifc, - tool.Collector, - tool.Root, - obj=obj, - ifc_class="IfcWall", - predefined_type = "ELEMENTEDWALL", - context = context) + element = blenderbim.core.root.assign_class( + tool.Ifc, + tool.Collector, + tool.Root, + obj=obj, + ifc_class="IfcWall", + predefined_type="ELEMENTEDWALL", + context=context, + ) assert subject.add_object_base_qto(obj).Name == "Qto_WallBaseQuantities" + class TestAddProductBaseQto(test.bim.bootstrap.NewFile): def test_run(self): ifc = ifcopenshell.file() @@ -121,6 +198,7 @@ class TestAddProductBaseQto(test.bim.bootstrap.NewFile): base_qto = subject.add_product_base_qto(wall) assert base_qto.Name == "Qto_WallBaseQuantities" + class TestGetBaseQto(test.bim.bootstrap.NewFile): def test_run(self): ifc = ifcopenshell.file() @@ -130,7 +208,7 @@ class TestGetBaseQto(test.bim.bootstrap.NewFile): tool.Ifc.link(wall, wall_obj) product = tool.Ifc.get_entity(wall_obj) pset_qto = ifcopenshell.api.run("pset.add_qto", ifc, product=wall, name="Qto_Basefoo") - assert subject.get_base_qto(product).id() == pset_qto.get_info()['id'] + assert subject.get_base_qto(product).id() == pset_qto.get_info()["id"] assert subject.get_base_qto(product).Name == pset_qto.Name def test_isempty(self): @@ -142,6 +220,7 @@ class TestGetBaseQto(test.bim.bootstrap.NewFile): product = tool.Ifc.get_entity(wall_obj) assert not subject.get_base_qto(product) == True + class TestGetRelatedCostItemQuantities(test.bim.bootstrap.NewFile): def test_run(self): ifc = ifcopenshell.file() @@ -153,8 +232,10 @@ class TestGetRelatedCostItemQuantities(test.bim.bootstrap.NewFile): ifcopenshell.api.run("cost.edit_cost_item", ifc, cost_item=item, attributes={"Name": "Foo"}) qto = ifcopenshell.api.run("pset.add_qto", ifc, product=wall, name="Qto_WallBaseQuantities") ifcopenshell.api.run("pset.edit_qto", ifc, qto=qto, properties={"NetVolume": 42.0}) - ifcopenshell.api.run("cost.assign_cost_item_quantity", ifc, cost_item=item, products=[wall], prop_name="NetVolume") - assert subject.get_related_cost_item_quantities(wall)[0]['cost_item_name'] == "Foo" - assert subject.get_related_cost_item_quantities(wall)[0]['quantity_name'] == "NetVolume" - assert subject.get_related_cost_item_quantities(wall)[0]['quantity_value'] == 42 - assert subject.get_related_cost_item_quantities(wall)[0]['quantity_type'] == "IfcQuantityVolume" + ifcopenshell.api.run( + "cost.assign_cost_item_quantity", ifc, cost_item=item, products=[wall], prop_name="NetVolume" + ) + assert subject.get_related_cost_item_quantities(wall)[0]["cost_item_name"] == "Foo" + assert subject.get_related_cost_item_quantities(wall)[0]["quantity_name"] == "NetVolume" + assert subject.get_related_cost_item_quantities(wall)[0]["quantity_value"] == 42 + assert subject.get_related_cost_item_quantities(wall)[0]["quantity_type"] == "IfcQuantityVolume"