diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py
index 8896cd3f15..b3d55ba5e0 100644
--- a/src/blenderbim/blenderbim/bim/module/pset/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py
@@ -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:
diff --git a/src/blenderbim/blenderbim/bim/module/qto/__init__.py b/src/blenderbim/blenderbim/bim/module/qto/__init__.py
index 9924927fa0..dada98ae88 100644
--- a/src/blenderbim/blenderbim/bim/module/qto/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/qto/__init__.py
@@ -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,
)
diff --git a/src/blenderbim/blenderbim/bim/module/qto/calculator.py b/src/blenderbim/blenderbim/bim/module/qto/calculator.py
index 874bf37e72..bb9456cc50 100644
--- a/src/blenderbim/blenderbim/bim/module/qto/calculator.py
+++ b/src/blenderbim/blenderbim/bim/module/qto/calculator.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/qto/data.py b/src/blenderbim/blenderbim/bim/module/qto/data.py
index 7d5fcc6a66..1be10de8fd 100644
--- a/src/blenderbim/blenderbim/bim/module/qto/data.py
+++ b/src/blenderbim/blenderbim/bim/module/qto/data.py
@@ -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
-
-
-
-
-
-
-
-
-
-
diff --git a/src/blenderbim/blenderbim/bim/module/qto/operator.py b/src/blenderbim/blenderbim/bim/module/qto/operator.py
index 7a12a45123..9be81dfafc 100644
--- a/src/blenderbim/blenderbim/bim/module/qto/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/qto/operator.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/qto/prop.py b/src/blenderbim/blenderbim/bim/module/qto/prop.py
index 8829c6178a..3cc1e9d446 100644
--- a/src/blenderbim/blenderbim/bim/module/qto/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/qto/prop.py
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see .
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")
diff --git a/src/blenderbim/blenderbim/bim/module/qto/ui.py b/src/blenderbim/blenderbim/bim/module/qto/ui.py
index c7a79d1522..c7f4863463 100644
--- a/src/blenderbim/blenderbim/bim/module/qto/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/qto/ui.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/core/qto.py b/src/blenderbim/blenderbim/core/qto.py
index a1495f3d68..1b31bd69ad 100644
--- a/src/blenderbim/blenderbim/core/qto.py
+++ b/src/blenderbim/blenderbim/core/qto.py
@@ -17,35 +17,14 @@
# along with BlenderBIM Add-on. If not, see .
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,
- )
diff --git a/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json b/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json
index 3211ee6724..29b9045161 100644
--- a/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json
+++ b/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json
@@ -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
diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py
index 5957abbdc8..a4ddbd617e 100644
--- a/src/ifc5d/ifc5d/qto.py
+++ b/src/ifc5d/ifc5d/qto.py
@@ -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}