Reimplement manual qty calculator using autodetected calculator functions and redo qto UI

This commit is contained in:
Dion Moult
2024-05-29 15:14:11 +10:00
parent 1bda47f76e
commit 8c3d10a2bd
10 changed files with 282 additions and 238 deletions
@@ -27,10 +27,8 @@ import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.pset as core
import blenderbim.core.qto as QtoCore
import blenderbim.bim.module.pset.data
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
class Operator:
@@ -20,16 +20,17 @@ import bpy
from . import ui, prop, operator
classes = (
operator.AssignBaseQto,
operator.CalculateCircleRadius,
operator.CalculateEdgeLengths,
operator.CalculateFaceAreas,
operator.CalculateObjectVolumes,
operator.ExecuteQtoMethod,
operator.CalculateSingleQuantity,
operator.PerformQuantityTakeOff,
operator.QuantifyObjects,
prop.BIMQtoProperties,
ui.BIM_PT_qto_utilities,
ui.BIM_PT_qto,
ui.BIM_PT_qto_manual,
ui.BIM_PT_qto_simple,
ui.BIM_PT_qto_cost,
)
@@ -38,17 +38,19 @@ VectorTuple = tuple[float, float, float]
def get_units(o: bpy.types.Object, vg_index: int) -> int:
return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]])
def get_linear_length(o: bpy.types.Object) -> float:
"""_summary_: Returns the length of the longest edge of the object bounding box
:param blender-object o: Blender Object
:return float: Length
def get_linear_length(o: bpy.types.Object) -> float:
"""Returns the length of the longest edge of the object bounding box
:param o: Blender Object
:return: Length
"""
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
return max(x, y, z)
def get_length(o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "x") -> float:
if vg_index is None:
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
@@ -74,22 +76,26 @@ def get_length(o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: s
length += get_edge_distance(o, e)
return length
def get_stair_length(obj: bpy.types.Object) -> float:
length = get_length(obj)
height = get_height(obj)
stair_length = math.sqrt(pow(length, 2) + pow(height, 2))
return stair_length
def get_net_stair_area(obj: bpy.types.Object) -> float:
OBB_obj = get_OBB_object(obj)
OBB_net_footprint_area = get_net_footprint_area(OBB_obj)
return OBB_net_footprint_area
def get_gross_stair_area(obj: bpy.types.Object) -> float:
OBB_obj = get_OBB_object(obj)
OBB_gross_footprint_area = get_gross_footprint_area(OBB_obj)
return OBB_gross_footprint_area
def get_parametric_axis(obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]:
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj))
if relating_type:
@@ -105,6 +111,7 @@ def get_parametric_axis(obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None
return None
return None
def get_covering_gross_area(obj: bpy.types.Object) -> float:
parametrix_axis = get_parametric_axis(obj)
if not parametrix_axis:
@@ -114,6 +121,7 @@ def get_covering_gross_area(obj: bpy.types.Object) -> float:
elif parametrix_axis == "AXIS3":
return get_gross_footprint_area(obj)
def get_covering_net_area(obj: bpy.types.Object) -> float:
parametrix_axis = get_parametric_axis(obj)
if not parametrix_axis:
@@ -123,6 +131,7 @@ def get_covering_net_area(obj: bpy.types.Object) -> float:
elif parametrix_axis == "AXIS3":
return get_net_footprint_area(obj)
def get_covering_width(obj: bpy.types.Object) -> float:
parametrix_axis = get_parametric_axis(obj)
if not parametrix_axis:
@@ -132,6 +141,7 @@ def get_covering_width(obj: bpy.types.Object) -> float:
elif parametrix_axis == "AXIS3":
return get_height(obj)
def get_width(o: bpy.types.Object) -> float:
"""_summary_: Returns the width of the object bounding box
@@ -142,6 +152,7 @@ def get_width(o: bpy.types.Object) -> float:
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
return min(x, y)
def get_height(o: bpy.types.Object) -> float:
"""_summary_: Returns the height of the object bounding box
@@ -150,38 +161,45 @@ def get_height(o: bpy.types.Object) -> float:
"""
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
def get_opening_height(obj: bpy.types.Object) -> float:
if is_opening_horizontal(obj):
return get_width(obj)
else:
return get_height(obj)
def get_opening_depth(obj: bpy.types.Object) -> float:
if is_opening_horizontal(obj):
return get_height(obj)
else:
return get_width(obj)
def get_opening_mapping_area(obj: bpy.types.Object) -> float:
if is_opening_horizontal(obj):
return get_net_footprint_area(obj)
else:
return get_net_side_area(obj)
def get_finish_ceiling_height(obj: bpy.types.Object) -> float:
floor_height = get_finish_floor_height(obj)
ceiling_height = get_ceiling_height(obj)
finish_ceiling_height = ceiling_height - floor_height
return finish_ceiling_height
def get_max_global_z(obj: bpy.types.Object) -> float:
z_values = [(obj.matrix_world @ Vector(co))[2] for co in obj.bound_box]
return max(z_values)
def get_min_global_z(obj: bpy.types.Object) -> float:
z_values = [(obj.matrix_world @ Vector(co))[2] for co in obj.bound_box]
return min(z_values)
def get_finish_floor_height(obj: bpy.types.Object) -> float:
space_min_z_value = get_min_global_z(obj)
@@ -200,6 +218,7 @@ def get_finish_floor_height(obj: bpy.types.Object) -> float:
return flooring_max_z_value - space_min_z_value
def get_ceiling_height(obj: bpy.types.Object) -> float:
space_min_z_value = get_min_global_z(obj)
space_max_z_value = get_max_global_z(obj)
@@ -219,6 +238,7 @@ def get_ceiling_height(obj: bpy.types.Object) -> float:
return ceiling_min_z_value - space_min_z_value
def get_net_perimeter(o: bpy.types.Object) -> float:
parsed_edges = []
shared_edges = []
@@ -234,6 +254,7 @@ def get_net_perimeter(o: bpy.types.Object) -> float:
perimeter -= get_edge_key_distance(o, edge_key)
return perimeter
def get_gross_perimeter(o: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(o)
mesh = get_gross_element_mesh(element)
@@ -242,14 +263,17 @@ def get_gross_perimeter(o: bpy.types.Object) -> float:
delete_obj(gross_obj)
return gross_perimeter
def get_space_net_perimeter(obj: bpy.types.Object) -> float:
pass
def get_rectangular_perimeter(obj: bpy.types.Object) -> float:
length = get_length(obj, main_axis="x")
height = get_height(obj)
return (length + height) * 2
def get_lowest_polygons(o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
lowest_polygons = []
lowest_z = None
@@ -266,6 +290,7 @@ def get_lowest_polygons(o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
lowest_z = z
return lowest_polygons
def get_highest_polygons(o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
highest_polygons = []
highest_z = None
@@ -282,12 +307,15 @@ def get_highest_polygons(o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
highest_z = z
return highest_polygons
def get_edge_key_distance(obj: bpy.types.Object, edge_key: tuple[int, int]) -> float:
return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length
def get_edge_distance(obj: bpy.types.Object, edge: bpy.types.MeshEdge) -> float:
return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length
def get_net_floor_area(obj: bpy.types.Object) -> float:
decompositions = get_obj_decompositions(obj)
if not decompositions:
@@ -304,6 +332,7 @@ def get_net_floor_area(obj: bpy.types.Object) -> float:
return total_net_floor_area
def get_gross_ceiling_area(obj: bpy.types.Object) -> float:
decompositions = get_obj_decompositions(obj)
if not decompositions:
@@ -320,6 +349,7 @@ def get_gross_ceiling_area(obj: bpy.types.Object) -> float:
return total_gross_ceiling_area
def get_net_ceiling_area(obj: bpy.types.Object) -> float:
decompositions = get_obj_decompositions(obj)
if not decompositions:
@@ -340,6 +370,7 @@ def get_net_ceiling_area(obj: bpy.types.Object) -> float:
return total_net_ceiling_area
def get_space_net_volume(obj: bpy.types.Object) -> float:
decompositions = get_obj_decompositions(obj)
if not decompositions:
@@ -355,6 +386,7 @@ def get_space_net_volume(obj: bpy.types.Object) -> float:
return total_space_net_volume
def get_net_footprint_area(o: bpy.types.Object) -> float:
"""_summary_: Returns the area of the footprint of the object, excluding any holes
@@ -366,6 +398,7 @@ def get_net_footprint_area(o: bpy.types.Object) -> float:
area += polygon.area
return area
def get_gross_footprint_area(o: bpy.types.Object) -> float:
"""_summary_: Returns the area of the footprint of the object, without related opening and excluding any holes
@@ -382,6 +415,7 @@ def get_gross_footprint_area(o: bpy.types.Object) -> float:
delete_mesh(mesh)
return gross_footprint_area
def get_net_roofprint_area(o: bpy.types.Object) -> float:
# Is roofprint the right word? Couldn't think of anything better - vulevukusej
"""_summary_: Returns the area of the net roofprint of the object, excluding any holes
@@ -394,6 +428,7 @@ def get_net_roofprint_area(o: bpy.types.Object) -> float:
area += polygon.area
return area
def get_side_area(o: bpy.types.Object) -> float:
# There are a few dumb options for this, but this seems the dumbest
# until I get more practical experience on what works best.
@@ -402,6 +437,7 @@ def get_side_area(o: bpy.types.Object) -> float:
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return max(x * z, y * z)
def get_cross_section_area(obj: bpy.types.Object) -> float:
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
item = representation.Items[0]
@@ -418,6 +454,7 @@ def get_cross_section_area(obj: bpy.types.Object) -> float:
return area
# TODO handle other types of sections, and then fall back to mesh parsing
def get_gross_surface_area(o: bpy.types.Object, vg_index: Optional[int] = None) -> float:
if vg_index is None:
if not has_openings(o):
@@ -436,21 +473,25 @@ def get_gross_surface_area(o: bpy.types.Object, vg_index: Optional[int] = None)
area += polygon.area
return area
def get_net_surface_area(obj: bpy.types.Object) -> float:
return get_mesh_area(obj.data)
def get_mesh_area(mesh: bpy.types.Mesh) -> float:
area = 0
for polygon in mesh.polygons:
area += polygon.area
return area
def is_polygon_in_vg(polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.types.MeshVertex]) -> bool:
for v in polygon.vertices:
if v not in vertices_in_vg:
return False
return True
def get_net_volume(o: bpy.types.Object) -> float:
o_mesh = bmesh.new()
o_mesh.from_mesh(o.data)
@@ -458,6 +499,7 @@ def get_net_volume(o: bpy.types.Object) -> float:
o_mesh.free()
return volume
def get_gross_volume(o: bpy.types.Object) -> float:
if not has_openings(o):
return get_net_volume(o)
@@ -473,17 +515,18 @@ def get_gross_volume(o: bpy.types.Object) -> float:
return gross_volume
def has_openings(
obj: bpy.types.Object
) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
def has_openings(obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
element = tool.Ifc.get_entity(obj)
return element and getattr(element, "HasOpenings", [])
def get_obj_decompositions(obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]:
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
return decompositions
def get_gross_weight(obj: bpy.types.Object) -> Union[float, None]:
obj_mass_density = get_obj_mass_density(obj)
if not obj_mass_density:
@@ -492,6 +535,7 @@ def get_gross_weight(obj: bpy.types.Object) -> Union[float, None]:
gross_weight = obj_mass_density * gross_volume
return gross_weight
def get_net_weight(obj: bpy.types.Object) -> Union[float, None]:
obj_mass_density = get_obj_mass_density(obj)
if not obj_mass_density:
@@ -500,6 +544,7 @@ def get_net_weight(obj: bpy.types.Object) -> Union[float, None]:
net_weight = obj_mass_density * net_volume
return net_weight
def get_obj_mass_density(obj: bpy.types.Object) -> Union[float, None]:
entity = tool.Ifc.get_entity(obj)
material = ifcopenshell.util.element.get_material(entity)
@@ -546,6 +591,7 @@ def get_obj_mass_density(obj: bpy.types.Object) -> Union[float, None]:
else:
return
def get_opening_type(opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]:
"""_summary_: Returns the opening type - OPENING / RECESS
@@ -565,8 +611,8 @@ def get_opening_type(opening: bpy.types.Object, obj: bpy.types.Object) -> Litera
# If an odd number of face-normal vectors intersect with the object, then the void is a recess, otherwise it's an opening
return "OPENING" if ray_intersections % 2 == 0 else "RECESS"
def get_opening_area(
obj: bpy.types.Object,
angle_z1: int = 45,
angle_z2: int = 135,
@@ -625,8 +671,8 @@ def get_opening_area(
return total_opening_area
def get_lateral_area(
obj: bpy.types.Object,
subtract_openings: bool = True,
exclude_end_areas: bool = False,
@@ -665,9 +711,7 @@ def get_lateral_area(
top_axis = x_axis
area = 0
total_opening_area = (
0 if subtract_openings else get_opening_area(obj, angle_z1=angle_z1, angle_z2=angle_z2)
)
total_opening_area = 0 if subtract_openings else get_opening_area(obj, angle_z1=angle_z1, angle_z2=angle_z2)
polygons = obj.data.polygons
for polygon in polygons:
@@ -685,6 +729,7 @@ def get_lateral_area(
area += polygon.area
return area + total_opening_area
def get_gross_side_area(obj: bpy.types.Object) -> float:
if not has_openings(obj):
return get_net_side_area(obj)
@@ -693,14 +738,17 @@ def get_gross_side_area(obj: bpy.types.Object) -> float:
return gross_side_area
def get_net_side_area(obj: bpy.types.Object) -> float:
net_side_area = get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2
return net_side_area
def get_outer_surface_area(obj: bpy.types.Object) -> float:
outer_surface_area = get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360)
return outer_surface_area
def get_end_area(obj: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(obj)
gross_mesh = get_gross_element_mesh(element)
@@ -715,6 +763,7 @@ def get_end_area(obj: bpy.types.Object) -> float:
return end_area
def get_gross_top_area(obj: bpy.types.Object, angle: int = 45) -> float:
"""_summary_: Returns the gross top area of the object.
@@ -748,6 +797,7 @@ def get_gross_top_area(obj: bpy.types.Object, angle: int = 45) -> float:
area += polygon.area
return area + opening_area
# curently net top area is larger then projected area, because its taking into account internal polygons, or window sills
def get_net_top_area(obj: bpy.types.Object, angle: int = 45, ignore_internal: bool = True) -> float:
"""_summary_: Returns the net top area of the object.
@@ -775,6 +825,7 @@ def get_net_top_area(obj: bpy.types.Object, angle: int = 45, ignore_internal: bo
return area
def get_projected_area(obj, projection_axis: AxisType = "z", is_gross: bool = True) -> float:
"""_summary_: Returns the projected area of the object.
@@ -814,6 +865,7 @@ def get_projected_area(obj, projection_axis: AxisType = "z", is_gross: bool = Tr
return projected_polygon.area + void_area
return projected_polygon.area
def get_OBB_object(obj: bpy.types.Object) -> bpy.types.Object:
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
@@ -855,6 +907,7 @@ def get_OBB_object(obj: bpy.types.Object) -> bpy.types.Object:
return new_OBB_object
def get_AABB_object(obj: bpy.types.Object) -> bpy.types.Object:
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
@@ -909,8 +962,8 @@ def get_AABB_object(obj: bpy.types.Object) -> bpy.types.Object:
return new_AABB_object
def get_bisected_obj(
obj: bpy.types.Object,
plane_co_pos: VectorTuple,
plane_no_pos: VectorTuple,
@@ -954,6 +1007,7 @@ def get_bisected_obj(
return bis_obj
def get_total_contact_area(obj: bpy.types.Object, class_filter: list[str] = ["IfcElement"]) -> float:
"""_summary_: Returns the total contact area of the object with other objects.
@@ -970,6 +1024,7 @@ def get_total_contact_area(obj: bpy.types.Object, class_filter: list[str] = ["If
return total_contact_area
def get_touching_objects(obj: bpy.types.Object, class_filter: list[str]) -> list[bpy.types.Object]:
"""_summary_: Returns a list of objects that are touching the object.
@@ -1019,6 +1074,7 @@ def get_touching_objects(obj: bpy.types.Object, class_filter: list[str]) -> list
return touching_objects
def get_contact_area(object1: bpy.types.Object, object2: bpy.types.Object) -> float:
"""_summary_: Returns the contact area between two objects.
@@ -1034,8 +1090,8 @@ def get_contact_area(object1: bpy.types.Object, object2: bpy.types.Object) -> fl
total_area += get_intersection_between_polygons(object1, poly1, object2, poly2)
return total_area
def get_intersection_between_polygons(
object1: bpy.types.Object,
poly1: bpy.types.MeshPolygon,
object2: bpy.types.Object,
@@ -1083,9 +1139,8 @@ def get_intersection_between_polygons(
# TopologicalError - Generated Geometry might be invalid
return 0
def create_shapely_polygon(
obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix
) -> Polygon:
def create_shapely_polygon(obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix) -> Polygon:
"""_summary_: Create a shapely polygon
:param blender-object obj: Blender Object
@@ -1104,11 +1159,13 @@ def create_shapely_polygon(
polygon_tuples.append((x, y))
return Polygon(polygon_tuples)
def get_gross_element_mesh(element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
settings = ifcopenshell.geom.settings()
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
return create_mesh_from_shape(element, settings)
def create_mesh_from_shape(
element: ifcopenshell.entity_instance, settings: Optional[ifcopenshell.geom.settings] = None
) -> bpy.types.Mesh:
@@ -1138,11 +1195,13 @@ def create_mesh_from_shape(
mesh.update()
return mesh
def get_bmesh_from_mesh(mesh: bpy.types.Mesh) -> bmesh.types.BMesh:
bm = bmesh.new()
bm.from_mesh(mesh)
return bm
def get_object_main_axis(o: bpy.types.Object) -> AxisType:
"""_summary_: Returns the main object axis. Useful for profile-defined objects.
@@ -1162,6 +1221,7 @@ def get_object_main_axis(o: bpy.types.Object) -> AxisType:
else:
return "x"
def is_opening_horizontal(o: bpy.types.Object) -> bool:
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
@@ -1169,10 +1229,12 @@ def is_opening_horizontal(o: bpy.types.Object) -> bool:
return z < x and z < y
def delete_mesh(mesh: bpy.types.Mesh) -> None:
mesh.user_clear()
bpy.data.meshes.remove(mesh)
def delete_obj(obj: bpy.types.Object) -> None:
bpy.data.objects.remove(obj, do_unlink=True)
@@ -19,9 +19,11 @@
import bpy
import blenderbim.tool as tool
def refresh():
QtoData.is_loaded = False
class QtoData:
data = {}
is_loaded = False
@@ -29,9 +31,9 @@ class QtoData:
@classmethod
def load(cls):
cls.data = {
"has_cost_item" : cls.has_cost_item(),
"relating_cost_items" : cls.relating_cost_items(),
}
"has_cost_item": cls.has_cost_item(),
"relating_cost_items": cls.relating_cost_items(),
}
cls.is_loaded = True
@@ -53,23 +55,13 @@ class QtoData:
for relating_cost_item in relating_cost_items:
results.append(
{
'cost_item_id' : relating_cost_item['cost_item_id'],
'cost_item_name' : relating_cost_item['cost_item_name'],
'quantity_id' : relating_cost_item['quantity_id'],
'quantity_name' : relating_cost_item['quantity_name'],
'quantity_value' : relating_cost_item['quantity_value'],
'quantity_type' : relating_cost_item['quantity_type'],
"cost_item_id": relating_cost_item["cost_item_id"],
"cost_item_name": relating_cost_item["cost_item_name"],
"quantity_id": relating_cost_item["quantity_id"],
"quantity_name": relating_cost_item["quantity_name"],
"quantity_value": relating_cost_item["quantity_value"],
"quantity_type": relating_cost_item["quantity_type"],
}
)
return results
@@ -23,7 +23,6 @@ import blenderbim.tool as tool
import blenderbim.core.qto as core
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.qto import helper
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
class CalculateCircleRadius(bpy.types.Operator):
@@ -85,104 +84,37 @@ class CalculateObjectVolumes(bpy.types.Operator):
return {"FINISHED"}
class ExecuteQtoMethod(bpy.types.Operator):
bl_idname = "bim.execute_qto_method"
bl_label = "Execute Qto Method"
class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.calculate_single_quantity"
bl_label = "Calculate Single Quantity"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def execute(self, context):
selected_mesh_objects = [o for o in context.selected_objects if o.type == "MESH"]
props = context.scene.BIMQtoProperties
result = 0
if props.qto_methods == "HEIGHT":
for obj in selected_mesh_objects:
result += helper.calculate_height(obj)
elif props.qto_methods == "VOLUME":
result = helper.calculate_volumes(selected_mesh_objects, context)
elif props.qto_methods == "FORMWORK":
result = helper.calculate_formwork_area(selected_mesh_objects, context)
elif props.qto_methods == "SIDE_FORMWORK":
result = helper.calculate_side_formwork_area(selected_mesh_objects, context)
elif props.qto_methods == "NetFootprintArea":
result = QtoCalculator().get_net_footprint_area(selected_mesh_objects[0])
elif props.qto_methods == "NetRoofprintArea":
result = QtoCalculator().get_net_roofprint_area(selected_mesh_objects[0])
elif props.qto_methods == "LateralArea":
result = QtoCalculator().get_lateral_area(selected_mesh_objects[0])
elif props.qto_methods == "TotalSurfaceArea":
result = QtoCalculator().get_total_surface_area(selected_mesh_objects[0])
elif props.qto_methods == "OpeningArea":
result = QtoCalculator().get_opening_area(selected_mesh_objects[0])
elif props.qto_methods == "GrossTopArea":
result = QtoCalculator().get_gross_top_area(selected_mesh_objects[0])
elif props.qto_methods == "NetTopArea":
result = QtoCalculator().get_net_top_area(selected_mesh_objects[0])
elif props.qto_methods == "ProjectedArea":
result = QtoCalculator().get_projected_area(selected_mesh_objects[0])
elif props.qto_methods == "TotalContactArea":
result = QtoCalculator().get_total_contact_area(selected_mesh_objects[0])
elif props.qto_methods == "ContactArea":
result = QtoCalculator().get_contact_area(selected_mesh_objects[0], selected_mesh_objects[1])
props.qto_result = str(round(result, 3))
return {"FINISHED"}
class QuantifyObjects(bpy.types.Operator):
bl_idname = "bim.quantify_objects"
bl_label = "Quantify Objects"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return IfcStore.get_file() and context.selected_objects
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
props = context.scene.BIMQtoProperties
self.file = IfcStore.get_file()
for obj in (o for o in context.selected_objects if o.type == "MESH"):
if not obj.BIMObjectProperties.ifc_definition_id:
continue
result = 0
if props.qto_methods == "HEIGHT":
result = helper.calculate_height(obj)
elif props.qto_methods == "VOLUME":
result = helper.calculate_volumes([obj], context)
elif props.qto_methods == "FORMWORK":
result = helper.calculate_formwork_area([obj], context)
elif props.qto_methods == "SIDE_FORMWORK":
result = helper.calculate_side_formwork_area([obj], context)
if not result:
continue
result = round(result, 3)
qto = ifcopenshell.api.run(
"pset.add_qto",
self.file,
product=self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
name=props.qto_name,
)
ifcopenshell.api.run("pset.edit_qto", self.file, qto=qto, properties={props.prop_name: result})
return {"FINISHED"}
class AssignBaseQto(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_objects_base_qto"
bl_label = "Assign IFC Object Quantity Set"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Assign IFC quantity set to selected object"
bl_description = "Calculate a single quantity using a function on the selected objects"
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.selected_objects
def _execute(self, context):
core.assign_objects_base_qto(tool.Ifc, tool.Qto, selected_objects=context.selected_objects)
import ifc5d.qto
props = context.scene.BIMQtoProperties
elements = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element:
elements.add(element)
rules = {
"calculators": {
props.calculator: {
"IfcProduct": {props.qto_name: {props.prop_name: props.calculator_function}},
}
}
}
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
return {"FINISHED"}
@@ -199,13 +131,14 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
import ifc5d.qto
props = context.scene.BIMQtoProperties
elements = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element:
elements.add(element)
rules = ifc5d.qto.get_rules("IFC4QtoBaseQuantities")
rules = ifc5d.qto.rules[props.qto_rule]
ifc_file = tool.Ifc.get()
results = ifc5d.qto.quantify(ifc_file, elements, rules)
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifc5d.qto
from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -31,34 +32,32 @@ from bpy.props import (
)
def get_qto_rule(self, context):
results = []
for rule_id, rule in ifc5d.qto.rules.items():
results.append((rule_id, rule["name"], rule["description"]))
return results
def get_calculator(self, context):
results = []
for name, calculator in ifc5d.qto.calculators.items():
results.append((name, name, calculator.__doc__))
return results
def get_calculator_function(self, context):
calculator = ifc5d.qto.calculators[self.calculator]
results = []
for function in calculator.get_functions():
results.append((function.id, function.name, function.description))
return results
class BIMQtoProperties(PropertyGroup):
qto_rule: EnumProperty(items=get_qto_rule, name="Qto Rule")
calculator: EnumProperty(items=get_calculator, name="Calculator")
calculator_function: EnumProperty(items=get_calculator_function, name="Calculator Function")
qto_result: StringProperty(default="", name="Qto Result")
qto_methods: EnumProperty(
items=[
("HEIGHT", "Height", "Calculate the Z height of an object"),
("VOLUME", "Volume", "Calculate the volume of an object"),
(
"FORMWORK",
"Formwork",
"Calculate the exposed formwork for all bottoms and sides (e.g. for beams and slabs) of one or more objects",
),
(
"SIDE_FORMWORK",
"Side Formwork",
"Calculate the exposed formwork for all sides only (e.g. for columns) of one or more objects",
),
("NetFootprintArea", "Net footprint area", "Calculate the net footprint area"),
("NetRoofprintArea", "Net roofprint area", "Calculate the net roofprint area"),
("LateralArea", "Lateral area", "Calculate the lateral area"),
("TotalSurfaceArea", "Total surface area", "Calculate the total surface area"),
("OpeningArea", "Opening area", "Calculate the opening area"),
("GrossTopArea", "Gross top area", "Calculate the gross top area"),
("NetTopArea", "Net top area", "Calculate the net top area"),
("ProjectedArea", "Projected area", "Calculate the projected area"),
("TotalContactArea", "Total contact area", "Get the total contact area"),
("ContactArea", "Contact area between two objects", "Get the contact area")
],
name="Qto Methods",
)
qto_name: StringProperty(name="Qto Name")
prop_name: StringProperty(name="Prop Name")
qto_name: StringProperty(name="Qto Name", default="My_Qto")
prop_name: StringProperty(name="Prop Name", default="MyDimension")
+87 -38
View File
@@ -20,8 +20,8 @@ import bpy
from blenderbim.bim.module.qto.data import QtoData
class BIM_PT_qto_utilities(bpy.types.Panel):
bl_idname = "BIM_PT_qto_utilities"
class BIM_PT_qto(bpy.types.Panel):
bl_idname = "BIM_PT_qto"
bl_label = "Quantity Take-off"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
@@ -31,9 +31,56 @@ class BIM_PT_qto_utilities(bpy.types.Panel):
bl_options = {"HIDE_HEADER"}
def draw(self, context):
if not QtoData.is_loaded:
QtoData.load()
layout = self.layout
props = context.scene.BIMQtoProperties
row = layout.row()
if context.selected_objects:
row.label(text=f"Quantifying {len(context.selected_objects)} Selected Objects", icon="MOD_EDGESPLIT")
else:
row.label(text="Quantifying All Objects", icon="MOD_EDGESPLIT")
row = layout.row()
row.prop(props, "qto_rule", text="")
row = layout.row()
row.operator("bim.perform_quantity_take_off")
class BIM_PT_qto_manual(bpy.types.Panel):
bl_idname = "BIM_PT_qto_manual"
bl_label = "Manual Quantification"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
def draw(self, context):
layout = self.layout
props = context.scene.BIMQtoProperties
row = layout.row()
row.prop(props, "calculator")
row = layout.row()
row.prop(props, "calculator_function", text="Function")
row = layout.row(align=True)
row.prop(props, "qto_name", text="")
row.prop(props, "prop_name", text="")
row = layout.row()
row.operator("bim.calculate_single_quantity")
class BIM_PT_qto_simple(bpy.types.Panel):
bl_idname = "BIM_PT_qto_simple"
bl_label = "Simple Quantity Calculator"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
def draw(self, context):
layout = self.layout
props = context.scene.BIMQtoProperties
@@ -49,42 +96,44 @@ class BIM_PT_qto_utilities(bpy.types.Panel):
row = layout.row(align=True)
row.operator("bim.calculate_object_volumes")
row = layout.row(align=True)
row.prop(props, "qto_methods", text="")
row.operator("bim.execute_qto_method", icon="PROPERTIES", text="")
row = layout.row(align=True)
row.prop(props, "qto_name", text="")
row.prop(props, "prop_name", text="")
row.operator("bim.quantify_objects", icon="COPYDOWN", text="")
class BIM_PT_qto_cost(bpy.types.Panel):
bl_idname = "BIM_PT_qto_cost"
bl_label = "Parametric Cost Relationships"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_qto"
row = layout.row(align=True)
row.operator("bim.assign_objects_base_qto")
def draw(self, context):
if not QtoData.is_loaded:
QtoData.load()
row = layout.row(align=True)
row.operator("bim.calculate_all_quantities", icon="MOD_EDGESPLIT")
if not context.selected_objects:
row = self.layout.row()
row.label(text="No Selected Object")
return
if context.selected_objects:
row = layout.row(align=True)
row.label(text=f"Relating Cost Item:")
if QtoData.data['has_cost_item']:
for relating_cost_item in QtoData.data['relating_cost_items']:
row.label(text=f"\n")
row = layout.row(align=True)
row.label(text=f"Cost item name:")
row.label(text=f"{relating_cost_item['cost_item_name']}")
row = layout.row(align=True)
row.label(text=f"Quantity name:")
row.label(text=f"{relating_cost_item['quantity_name']}")
row = layout.row(align=True)
row.label(text=f"Quantity value:")
row.label(text=f"{relating_cost_item['quantity_value']}")
row = layout.row(align=True)
row.label(text=f"Quantity type:")
row.label(text=f"{relating_cost_item['quantity_type']}")
row = layout.row(align=True)
else:
row = layout.row(align=True)
row.label(text = f"No cost item related")
if not QtoData.data["has_cost_item"]:
row = self.layout.row()
row.label(text="No Related Cost Item")
return
row = self.layout.row(align=True)
row.label(text="Relating Cost Item:")
for relating_cost_item in QtoData.data["relating_cost_items"]:
row.label(text="\n")
row = self.layout.row(align=True)
row.label(text="Cost item name:")
row.label(text=f"{relating_cost_item['cost_item_name']}")
row = self.layout.row(align=True)
row.label(text="Quantity name:")
row.label(text=f"{relating_cost_item['quantity_name']}")
row = self.layout.row(align=True)
row.label(text="Quantity value:")
row.label(text=f"{relating_cost_item['quantity_value']}")
row = self.layout.row(align=True)
row.label(text="Quantity type:")
row.label(text=f"{relating_cost_item['quantity_type']}")
row = self.layout.row(align=True)
+1 -22
View File
@@ -17,35 +17,14 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
def calculate_circle_radius(qto: tool.Qto, obj: bpy.types.Object) -> float:
result = qto.get_radius_of_selected_vertices(obj)
qto.set_qto_result(result)
return result
def assign_objects_base_qto(ifc: tool.Ifc, qto: tool.Qto, selected_objects: list[bpy.types.Object]) -> None:
for obj in selected_objects:
assign_object_base_qto(ifc, qto, obj)
def assign_object_base_qto(ifc: tool.Ifc, qto: tool.Qto, obj: bpy.types.Object) -> None:
product = ifc.get_entity(obj)
if not product:
return
base_quantity_name = qto.get_applicable_base_quantity_name(product)
if not base_quantity_name:
return
ifc.run(
"pset.add_qto",
product=product,
name=base_quantity_name,
)
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "IFC4 Base Quantities - IfcOpenShell",
"description": "This ruleset quantifies every single possible standardised base quantity in IFC4 using only IfcOpenShell as a geometry processor.",
"calculators": {
"IOSTriangulation": {
"IfcOpenShell": {
"IfcActuator": {
"Qto_ActuatorBaseQuantities": {
"GrossWeight": null
+44 -13
View File
@@ -23,16 +23,20 @@ import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.util.selector
import multiprocessing
from typing import Optional
from collections import namedtuple
from typing import Iterable
def get_rules(name: str):
cwd = os.path.dirname(os.path.realpath(__file__))
Function = namedtuple("Function", ["id", "name", "description"])
rules = {}
cwd = os.path.dirname(os.path.realpath(__file__))
for name in ("IFC4QtoBaseQuantities", "IFC4QtoBaseQuantitiesBlender"):
with open(os.path.join(cwd, name + ".json"), "r") as f:
return json.load(f)
rules[name] = json.load(f)
def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict):
def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict) -> dict:
results = {}
for calculator, queries in rules["calculators"].items():
calculator = calculators[calculator]
@@ -43,7 +47,7 @@ def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_inst
return results
def edit_qtos(ifc_file, results):
def edit_qtos(ifc_file, results) -> None:
for element, qtos in results.items():
for name, quantities in qtos.items():
qto = ifcopenshell.util.element.get_pset(element, name, should_inherit=False)
@@ -54,14 +58,17 @@ def edit_qtos(ifc_file, results):
ifcopenshell.api.pset.edit_qto(ifc_file, qto=qto, properties=quantities)
class IOSTriangulation:
class IfcOpenShell:
"""Calculates Model body context geometry using the default IfcOpenShell
iterator on triangulation elements."""
@staticmethod
def calculate(
ifc_file: ifcopenshell.file,
elements: set[ifcopenshell.entity_instance],
qtos: dict,
results: dict,
):
) -> None:
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.shape
@@ -89,10 +96,10 @@ class IOSTriangulation:
tasks = []
if gross_qtos:
tasks.append((IOSTriangulation.create_iterator(ifc_file, gross_settings, elements), gross_qtos))
tasks.append((IfcOpenShell.create_iterator(ifc_file, gross_settings, list(elements)), gross_qtos))
if net_qtos:
tasks.append((IOSTriangulation.create_iterator(ifc_file, net_settings, elements), net_qtos))
tasks.append((IfcOpenShell.create_iterator(ifc_file, net_settings, list(elements)), net_qtos))
for iterator, qtos in tasks:
if iterator.initialize():
@@ -108,13 +115,26 @@ class IOSTriangulation:
break
@staticmethod
def create_iterator(ifc_file, settings, elements):
def create_iterator(
ifc_file: ifcopenshell.file, settings: ifcopenshell.geom.settings, elements: list[ifcopenshell.entity_instance]
) -> ifcopenshell.geom.iterator:
return ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements)
@staticmethod
def get_functions() -> list[Function]:
return [
Function("get_volume", "Volume", "Calculates the volume of a manifold shape"),
Function("get_x", "X Length", "Calculates the length along the local X axis"),
]
class Blender:
"""Calculates geometry based on currently loaded Blender objects."""
@staticmethod
def calculate(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], qtos: dict, results: dict):
def calculate(
ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], qtos: dict, results: dict
) -> None:
import blenderbim.tool as tool
import blenderbim.bim.module.qto.calculator as calculator
@@ -132,5 +152,16 @@ class Blender:
formula_function = formula_functions[formula] = getattr(calculator, formula)
results[element][name][quantity] = formula_function(obj)
@staticmethod
def get_functions() -> list[Function]:
return [
Function(
"get_linear_length",
"Maximum Bounding Length",
"Calculates the length of the maximum local bounding box",
),
Function("get_length", "Length", "Calculates the length assumed as the maximum of the local X or Y axis"),
]
calculators = {"Blender": Blender, "IOSTriangulation": IOSTriangulation}
calculators = {"Blender": Blender, "IfcOpenShell": IfcOpenShell}