mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Merge pull request #3032 from IfcOpenShell/quantities_project_units
Converting quantities to project units to fix #3025
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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}"
|
||||
# )
|
||||
|
||||
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user