This commit is contained in:
Andrej730
2024-03-26 12:36:13 +05:00
parent b15e33e5e9
commit 06d823b9d8
7 changed files with 173 additions and 120 deletions
@@ -25,8 +25,16 @@ from shapely.geometry import Polygon
from shapely.ops import unary_union
import blenderbim.tool as tool
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.element
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
import blenderbim.bim
from typing import Literal, Union, Optional
AxisType = Literal["x", "y", "z"]
VectorTuple = tuple[float, float, float]
QuanityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"]
class QtoCalculator:
@@ -45,7 +53,7 @@ class QtoCalculator:
else:
self.mapping_dict[key][item] = None
def calculate_quantity(self, qto_name, quantity_name, obj):
def calculate_quantity(self, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
"""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):
@@ -54,11 +62,13 @@ class QtoCalculator:
args = ""
string += args
string += ")"
value = eval(string)
value: float = 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):
def guess_quantity(
self, prop_name: str, alternative_prop_names: list[str], obj: bpy.types.Object
) -> Union[float, None]:
"""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]
@@ -89,7 +99,7 @@ class QtoCalculator:
if value is None:
return
unit_type_keywords = {
unit_type_keywords: dict[str, QuanityTypes] = {
"length": "Q_LENGTH",
"width": "Q_LENGTH",
"height": "Q_LENGTH",
@@ -102,10 +112,10 @@ class QtoCalculator:
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):
def get_units(self, 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(self, o):
def get_linear_length(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the length of the longest edge of the object bounding box
:param blender-object o: Blender Object
@@ -116,7 +126,7 @@ class QtoCalculator:
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return max(x, y, z)
def get_length(self, o, vg_index=None, main_axis: str = ""):
def get_length(self, o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "") -> float:
if vg_index is None:
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
@@ -141,23 +151,23 @@ class QtoCalculator:
length += self.get_edge_distance(o, e)
return length
def get_stair_length(self, obj):
def get_stair_length(self, obj: bpy.types.Object) -> float:
length = self.get_length(obj)
height = self.get_height(obj)
stair_length = math.sqrt(pow(length, 2) + pow(height, 2))
return stair_length
def get_net_stair_area(self, obj):
def get_net_stair_area(self, obj: bpy.types.Object) -> float:
OBB_obj = self.get_OBB_object(obj)
OBB_net_footprint_area = self.get_net_footprint_area(OBB_obj)
return OBB_net_footprint_area
def get_gross_stair_area(self, obj):
def get_gross_stair_area(self, obj: bpy.types.Object) -> float:
OBB_obj = self.get_OBB_object(obj)
OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj)
return OBB_gross_footprint_area
def get_parametric_axis(self, obj):
def get_parametric_axis(self, obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]:
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj))
if relating_type:
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
@@ -172,7 +182,7 @@ class QtoCalculator:
return None
return None
def get_covering_gross_area(self, obj):
def get_covering_gross_area(self, obj: bpy.types.Object) -> float:
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_gross_footprint_area(obj)
@@ -181,7 +191,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_gross_footprint_area(obj)
def get_covering_net_area(self, obj):
def get_covering_net_area(self, obj: bpy.types.Object) -> float:
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_net_footprint_area(obj)
@@ -190,7 +200,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_net_footprint_area(obj)
def get_covering_width(self, obj):
def get_covering_width(self, obj: bpy.types.Object) -> float:
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_height(obj)
@@ -199,7 +209,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_height(obj)
def get_width(self, o):
def get_width(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the width of the object bounding box
:param blender-object o: blender object
@@ -209,7 +219,7 @@ class QtoCalculator:
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
return min(x, y)
def get_height(self, o):
def get_height(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the height of the object bounding box
:param blender-object o: blender object
@@ -217,32 +227,32 @@ class QtoCalculator:
"""
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
def get_opening_height(self, obj):
def get_opening_height(self, obj: bpy.types.Object) -> float:
if self.is_opening_horizontal(obj):
return self.get_width(obj)
else:
return self.get_height(obj)
def get_opening_depth(self, obj):
def get_opening_depth(self, obj: bpy.types.Object) -> float:
if self.is_opening_horizontal(obj):
return self.get_height(obj)
else:
return self.get_width(obj)
def get_opening_mapping_area(self, obj):
def get_opening_mapping_area(self, obj: bpy.types.Object) -> float:
if self.is_opening_horizontal(obj):
return self.get_net_footprint_area(obj)
else:
return self.get_net_side_area(obj)
def get_finish_ceiling_height(self, obj):
def get_finish_ceiling_height(self, obj: bpy.types.Object) -> float:
space_height = self.get_height(obj)
floor_height = self.get_finish_floor_height(obj)
ceiling_height = self.get_ceiling_height(obj)
finish_ceiling_height = space_height - floor_height - ceiling_height
return finish_ceiling_height
def get_finish_floor_height(self, obj):
def get_finish_floor_height(self, obj: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
finish_floor_height = 0
@@ -258,7 +268,7 @@ class QtoCalculator:
return finish_floor_height
def get_ceiling_height(self, obj):
def get_ceiling_height(self, obj: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
finish_ceiling_height = 0
@@ -274,7 +284,7 @@ class QtoCalculator:
return finish_ceiling_height
def get_net_perimeter(self, o):
def get_net_perimeter(self, o: bpy.types.Object) -> float:
parsed_edges = []
shared_edges = []
perimeter = 0
@@ -289,7 +299,7 @@ class QtoCalculator:
perimeter -= self.get_edge_key_distance(o, edge_key)
return perimeter
def get_gross_perimeter(self, o):
def get_gross_perimeter(self, o: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(o)
mesh = self.get_gross_element_mesh(element)
gross_obj = bpy.data.objects.new("GrossObj", mesh)
@@ -297,15 +307,15 @@ class QtoCalculator:
self.delete_obj(gross_obj)
return gross_perimeter
def get_space_net_perimeter(self, obj):
def get_space_net_perimeter(self, obj: bpy.types.Object) -> float:
pass
def get_rectangular_perimeter(self, obj):
def get_rectangular_perimeter(self, obj: bpy.types.Object) -> float:
length = self.get_length(obj, main_axis="x")
height = self.get_height(obj)
return (length + height) * 2
def get_lowest_polygons(self, o):
def get_lowest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
lowest_polygons = []
lowest_z = None
for polygon in o.data.polygons:
@@ -321,7 +331,7 @@ class QtoCalculator:
lowest_z = z
return lowest_polygons
def get_highest_polygons(self, o):
def get_highest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
highest_polygons = []
highest_z = None
for polygon in o.data.polygons:
@@ -337,13 +347,13 @@ class QtoCalculator:
highest_z = z
return highest_polygons
def get_edge_key_distance(self, obj, edge_key):
def get_edge_key_distance(self, 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(self, obj, edge):
def get_edge_distance(self, 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(self, obj):
def get_net_floor_area(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_footprint_area(obj)
@@ -359,7 +369,7 @@ class QtoCalculator:
return total_net_floor_area
def get_gross_ceiling_area(self, obj):
def get_gross_ceiling_area(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_top_area(obj)
@@ -375,7 +385,7 @@ class QtoCalculator:
return total_gross_ceiling_area
def get_net_ceiling_area(self, obj):
def get_net_ceiling_area(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_net_top_area(obj)
@@ -395,7 +405,7 @@ class QtoCalculator:
return total_net_ceiling_area
def get_space_net_volume(self, obj):
def get_space_net_volume(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_volume(obj)
@@ -410,7 +420,7 @@ class QtoCalculator:
return total_space_net_volume
def get_net_footprint_area(self, o):
def get_net_footprint_area(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the area of the footprint of the object, excluding any holes
:param blender-object o: blender object
@@ -421,7 +431,7 @@ class QtoCalculator:
area += polygon.area
return area
def get_gross_footprint_area(self, o):
def get_gross_footprint_area(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the area of the footprint of the object, without related opening and excluding any holes
:param blender-object o: blender object
@@ -437,7 +447,7 @@ class QtoCalculator:
self.delete_mesh(mesh)
return gross_footprint_area
def get_net_roofprint_area(self, o):
def get_net_roofprint_area(self, 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
@@ -449,7 +459,7 @@ class QtoCalculator:
area += polygon.area
return area
def get_side_area(self, o):
def get_side_area(self, 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.
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
@@ -457,8 +467,7 @@ class QtoCalculator:
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return max(x * z, y * z)
def get_cross_section_area(self, obj):
element = tool.Ifc.get_entity(obj)
def get_cross_section_area(self, obj: bpy.types.Object) -> float:
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
item = representation.Items[0]
while True:
@@ -474,7 +483,7 @@ class QtoCalculator:
return area
# TODO handle other types of sections, and then fall back to mesh parsing
def get_gross_surface_area(self, o, vg_index=None):
def get_gross_surface_area(self, o: bpy.types.Object, vg_index: Optional[int] = None) -> float:
if vg_index is None:
if not self.has_openings(o):
return self.get_net_surface_area(o)
@@ -492,29 +501,29 @@ class QtoCalculator:
area += polygon.area
return area
def get_net_surface_area(self, obj):
def get_net_surface_area(self, obj: bpy.types.Object) -> float:
return self.get_mesh_area(obj.data)
def get_mesh_area(self, mesh):
def get_mesh_area(self, mesh: bpy.types.Mesh) -> float:
area = 0
for polygon in mesh.polygons:
area += polygon.area
return area
def is_polygon_in_vg(self, polygon, vertices_in_vg):
def is_polygon_in_vg(self, 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(self, o):
def get_net_volume(self, o: bpy.types.Object) -> float:
o_mesh = bmesh.new()
o_mesh.from_mesh(o.data)
volume = o_mesh.calc_volume()
o_mesh.free()
return volume
def get_gross_volume(self, o):
def get_gross_volume(self, o: bpy.types.Object) -> float:
if not self.has_openings(o):
return self.get_net_volume(o)
@@ -529,16 +538,18 @@ class QtoCalculator:
return gross_volume
def has_openings(self, obj):
def has_openings(
self, 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(self, obj):
def get_obj_decompositions(self, 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(self, obj):
def get_gross_weight(self, obj: bpy.types.Object) -> Union[float, None]:
obj_mass_density = self.get_obj_mass_density(obj)
if not obj_mass_density:
return
@@ -546,7 +557,7 @@ class QtoCalculator:
gross_weight = obj_mass_density * gross_volume
return gross_weight
def get_net_weight(self, obj):
def get_net_weight(self, obj: bpy.types.Object) -> Union[float, None]:
obj_mass_density = self.get_obj_mass_density(obj)
if not obj_mass_density:
return
@@ -554,7 +565,7 @@ class QtoCalculator:
net_weight = obj_mass_density * net_volume
return net_weight
def get_obj_mass_density(self, obj):
def get_obj_mass_density(self, obj: bpy.types.Object) -> Union[float, None]:
entity = tool.Ifc.get_entity(obj)
material = ifcopenshell.util.element.get_material(entity)
if material is None:
@@ -629,7 +640,7 @@ class QtoCalculator:
# volume += v1.dot(v2.cross(v3)) / 6.0
# return volume
def get_opening_type(self, opening, obj):
def get_opening_type(self, opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]:
"""_summary_: Returns the opening type - OPENING / RECESS
:param blender-object opening: blender opening object
@@ -649,14 +660,22 @@ class QtoCalculator:
return "OPENING" if ray_intersections % 2 == 0 else "RECESS"
def get_opening_area(
self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False
):
self,
obj: bpy.types.Object,
angle_z1: int = 45,
angle_z2: int = 135,
min_area: int = 0,
ignore_recesses: bool = False,
) -> float:
"""_summary_: Returns the lateral area of the openings in the object.
:param obj: blender object
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector lower than this value will be ignored, defaults to 45
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector greater than this value will be ignored,defaults to 135
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored, defaults to 0
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area.
Openings with a normal_vector lower than this value will be ignored, defaults to 45
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area.
Openings with a normal_vector greater than this value will be ignored,defaults to 135
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored,
defaults to 0
:param bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False
:return float: Opening Area
"""
@@ -702,14 +721,14 @@ class QtoCalculator:
def get_lateral_area(
self,
obj,
obj: bpy.types.Object,
subtract_openings: bool = True,
exclude_end_areas: bool = False,
exclude_side_areas: bool = False,
angle_z1: int = 45,
angle_z2: int = 135,
main_axis: str = "",
):
) -> float:
"""_summary_
:param blender-object obj: blender object, bpy.types.Object
@@ -760,7 +779,7 @@ class QtoCalculator:
area += polygon.area
return area + total_opening_area
def get_gross_side_area(self, obj):
def get_gross_side_area(self, obj: bpy.types.Object) -> float:
if not self.has_openings(obj):
return self.get_net_side_area(obj)
@@ -768,15 +787,15 @@ class QtoCalculator:
return gross_side_area
def get_net_side_area(self, obj):
def get_net_side_area(self, obj: bpy.types.Object) -> float:
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):
def get_outer_surface_area(self, obj: bpy.types.Object) -> float:
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):
def get_end_area(self, obj: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(obj)
gross_mesh = self.get_gross_element_mesh(element)
gross_obj = bpy.data.objects.new("MyObject", gross_mesh)
@@ -790,7 +809,7 @@ class QtoCalculator:
return end_area
def get_gross_top_area(self, obj, angle: int = 45):
def get_gross_top_area(self, obj: bpy.types.Object, angle: int = 45) -> float:
"""_summary_: Returns the gross top area of the object.
:param blender-object obj: blender object
@@ -827,12 +846,14 @@ class QtoCalculator:
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(self, obj, angle: int = 45, ignore_internal: bool = True):
def get_net_top_area(self, obj: bpy.types.Object, angle: int = 45, ignore_internal: bool = True) -> float:
"""_summary_: Returns the net top area of the object.
:param blender-object obj: blender object
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), defaults to True
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area.
Values lower than this will be ignored, defaults to 45
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills),
defaults to True
:return float: Net Top Area
"""
z_axis = (0, 0, 1)
@@ -852,11 +873,11 @@ class QtoCalculator:
return area
def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True):
def get_projected_area(self, obj, projection_axis: AxisType = "z", is_gross: bool = True) -> float:
"""_summary_: Returns the projected area of the object.
:param blender-object obj: blender object
:param str projection_axis: Axis to project the area onto. Can be "X", "Y" or "Z"
:param str projection_axis: Axis to project the area onto. Can be "x", "y" or "z"
:param bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings
:return float: Projected Area
"""
@@ -891,7 +912,7 @@ class QtoCalculator:
return projected_polygon.area + void_area
return projected_polygon.area
def get_OBB_object(self, obj):
def get_OBB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
:param blender-object obj: Blender Object
@@ -932,7 +953,7 @@ class QtoCalculator:
return new_OBB_object
def get_AABB_object(self, obj):
def get_AABB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
:param blender-object obj: Blender Object
@@ -988,12 +1009,12 @@ class QtoCalculator:
def get_bisected_obj(
self,
obj,
plane_co_pos,
plane_no_pos,
plane_co_neg,
plane_no_neg,
):
obj: bpy.types.Object,
plane_co_pos: VectorTuple,
plane_no_pos: VectorTuple,
plane_co_neg: VectorTuple,
plane_no_neg: VectorTuple,
) -> bpy.types.Object:
"""_summary_: Returns the object bisected by two planes.
:param blender-object obj: Blender Object
@@ -1031,11 +1052,12 @@ class QtoCalculator:
return bis_obj
def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]):
def get_total_contact_area(self, obj: bpy.types.Object, class_filter: list[str] = ["IfcElement"]) -> float:
"""_summary_: Returns the total contact area of the object with other objects.
:param blender-object obj: Blender Object
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:param list [] class_filter: A list of classes used to filter the objects
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:return float: Total contact area of the object with other objects.
"""
total_contact_area = 0
@@ -1046,11 +1068,12 @@ class QtoCalculator:
return total_contact_area
def get_touching_objects(self, obj, class_filter):
def get_touching_objects(self, obj: bpy.types.Object, class_filter: list[str]) -> list[bpy.types.Object]:
"""_summary_: Returns a list of objects that are touching the object.
:param blender-object obj: Blender Object
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:param list [] class_filter: A list of classes used to filter the objects
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:return list: List of touching objects
"""
# rotate the object ever so slightly, otherwise bvhtree.overlap won't work properly. https://blender.stackexchange.com/a/275244/130742
@@ -1094,7 +1117,7 @@ class QtoCalculator:
return touching_objects
def get_contact_area(self, object1, object2):
def get_contact_area(self, object1: bpy.types.Object, object2: bpy.types.Object) -> float:
"""_summary_: Returns the contact area between two objects.
:param blender-object obj: Blender Object
@@ -1109,7 +1132,13 @@ class QtoCalculator:
total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2)
return total_area
def get_intersection_between_polygons(self, object1, poly1, object2, poly2):
def get_intersection_between_polygons(
self,
object1: bpy.types.Object,
poly1: bpy.types.MeshPolygon,
object2: bpy.types.Object,
poly2: bpy.types.MeshPolygon,
) -> float:
"""_summary_: Returns the intersection between two polygons.
:param blender-object object1: Blender Object
@@ -1152,7 +1181,9 @@ class QtoCalculator:
# TopologicalError - Generated Geometry might be invalid
return 0
def create_shapely_polygon(self, obj, polygon, trans_matrix):
def create_shapely_polygon(
self, obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix
) -> Polygon:
"""_summary_: Create a shapely polygon
:param blender-object obj: Blender Object
@@ -1171,12 +1202,14 @@ class QtoCalculator:
polygon_tuples.append((x, y))
return Polygon(polygon_tuples)
def get_gross_element_mesh(self, element):
def get_gross_element_mesh(self, element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
settings = ifcopenshell.geom.settings()
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
return self.create_mesh_from_shape(element, settings)
def create_mesh_from_shape(self, element, settings=None):
def create_mesh_from_shape(
self, element: ifcopenshell.entity_instance, settings: Optional[ifcopenshell.geom.settings] = None
) -> bpy.types.Mesh:
if settings is None:
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, element)
@@ -1203,12 +1236,12 @@ class QtoCalculator:
mesh.update()
return mesh
def get_bmesh_from_mesh(self, mesh):
def get_bmesh_from_mesh(self, mesh: bpy.types.Mesh) -> bmesh.types.BMesh:
bm = bmesh.new()
bm.from_mesh(mesh)
return bm
def get_object_main_axis(self, o):
def get_object_main_axis(self, o: bpy.types.Object) -> AxisType:
"""_summary_: Returns the main object axis. Useful for profile-defined objects.
:param blender-object o: Blender Object
@@ -1227,18 +1260,18 @@ class QtoCalculator:
else:
return "x"
def is_opening_horizontal(self, o):
def is_opening_horizontal(self, 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
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return z < x and z < y
def delete_mesh(self, mesh):
def delete_mesh(self, mesh: bpy.types.Mesh) -> None:
mesh.user_clear()
bpy.data.meshes.remove(mesh)
def delete_obj(self, obj):
def delete_obj(self, obj: bpy.types.Object) -> None:
bpy.data.objects.remove(obj, do_unlink=True)
+1 -1
View File
@@ -79,7 +79,7 @@ class Ifc(blenderbim.core.tool.Ifc):
return IfcStore.get_schema()
@classmethod
def get_entity(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance:
def get_entity(cls, obj: IFC_CONNECTED_TYPE) -> ifcopenshell.entity_instance:
ifc = IfcStore.get_file()
props = getattr(obj, "BIMObjectProperties", None)
if ifc and props and props.ifc_definition_id:
+33 -18
View File
@@ -23,14 +23,17 @@ import blenderbim.tool as tool
import ifcopenshell
from mathutils import Vector
from ifcopenshell import util
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
import ifcopenshell.util.unit
import ifcopenshell.util.element
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator, QuanityTypes
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
import blenderbim.bim.schema
from typing import Optional, Union, Literal
class Qto(blenderbim.core.tool.Qto):
@classmethod
def get_radius_of_selected_vertices(cls, obj):
def get_radius_of_selected_vertices(cls, obj: bpy.types.Object) -> float:
selected_verts = [v.co for v in obj.data.vertices if v.select]
total = Vector()
for v in selected_verts:
@@ -39,16 +42,16 @@ class Qto(blenderbim.core.tool.Qto):
return max([(v - circle_center).length for v in selected_verts])
@classmethod
def set_qto_result(cls, result):
def set_qto_result(cls, result: float) -> None:
bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
@classmethod
def add_object_base_qto(cls, obj):
def add_object_base_qto(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
product = tool.Ifc.get_entity(obj)
return cls.add_product_base_qto(product)
@classmethod
def add_product_base_qto(cls, product):
def add_product_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
base_quantity_name = cls.get_applicable_base_quantity_name(product)
if base_quantity_name:
return tool.Ifc.run(
@@ -58,7 +61,7 @@ class Qto(blenderbim.core.tool.Qto):
)
@classmethod
def get_applicable_quantity_names(cls, qto_name):
def get_applicable_quantity_names(cls, qto_name: str) -> list[str]:
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(qto_name)
return (
[property.Name for property in pset_template.HasPropertyTemplates]
@@ -67,7 +70,9 @@ class Qto(blenderbim.core.tool.Qto):
)
@classmethod
def get_applicable_base_quantity_name(cls, product=None):
def get_applicable_base_quantity_name(
cls, product: Optional[ifcopenshell.entity_instance] = None
) -> Union[str, None]:
if not product:
return
applicable_qto_names = blenderbim.bim.schema.ifc.psetqto.get_applicable_names(
@@ -76,19 +81,23 @@ class Qto(blenderbim.core.tool.Qto):
return next((qto_name for qto_name in applicable_qto_names if "Qto_" in qto_name and "Base" in qto_name), None)
@classmethod
def get_new_calculated_quantity(cls, qto_name, quantity_name, obj):
def get_new_calculated_quantity(cls, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
return QtoCalculator().calculate_quantity(qto_name, quantity_name, obj)
@classmethod
def get_new_guessed_quantity(cls, obj, quantity_name, alternative_prop_names):
def get_new_guessed_quantity(
cls, obj: bpy.types.Object, quantity_name: str, alternative_prop_names: list[str]
) -> Union[float, None]:
return QtoCalculator().guess_quantity(quantity_name, alternative_prop_names, obj)
@classmethod
def get_rounded_value(cls, new_quantity):
def get_rounded_value(cls, new_quantity: float) -> float:
return round(new_quantity, 3)
@classmethod
def get_calculated_object_quantities(cls, calculator, qto_name, obj):
def get_calculated_object_quantities(
cls, calculator: QtoCalculator, qto_name: str, obj: bpy.types.Object
) -> dict[str, float]:
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 []
@@ -97,15 +106,19 @@ class Qto(blenderbim.core.tool.Qto):
}
@classmethod
def has_calculator(cls, qto_name, quantity_name):
def has_calculator(cls, qto_name: str, quantity_name: str) -> bool:
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):
def convert_to_project_units(
cls,
value: float,
qto_name: Optional[str] = None,
quantity_name: Optional[str] = None,
quantity_type: Optional[QuanityTypes] = None,
) -> Union[float, 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 = {
@@ -135,7 +148,9 @@ class Qto(blenderbim.core.tool.Qto):
return value
@classmethod
def get_guessed_quantities(cls, obj, pset_qto_properties):
def get_guessed_quantities(
cls, obj: bpy.types.Object, pset_qto_properties: list[ifcopenshell.entity_instance]
) -> dict[str, float]:
calculated_quantities = {}
for pset_qto_property in pset_qto_properties:
quantity_name = pset_qto_property.get_info()["Name"]
@@ -153,7 +168,7 @@ class Qto(blenderbim.core.tool.Qto):
return calculated_quantities
@classmethod
def get_base_qto(cls, product):
def get_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
if not hasattr(product, "IsDefinedBy"):
return
for rel in product.IsDefinedBy or []:
@@ -166,7 +181,7 @@ class Qto(blenderbim.core.tool.Qto):
return rel.RelatingPropertyDefinition
@classmethod
def get_related_cost_item_quantities(cls, product):
def get_related_cost_item_quantities(cls, product: ifcopenshell.entity_instance) -> list[dict]:
"""_summary_: Returns the related cost item and related quantities of the product
:param ifc-instance product: ifc instance
+2
View File
@@ -21,6 +21,8 @@ import ifcopenshell
import ifcopenshell.util.representation
import blenderbim.core.tool
import blenderbim.core.geometry
import blenderbim.core.material
import blenderbim.core.style
import blenderbim.tool as tool
from mathutils import Vector
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
+1 -1
View File
@@ -96,7 +96,7 @@ class Style(blenderbim.core.tool.Style):
return obj.name
@classmethod
def get_style(cls, obj):
def get_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
if obj.BIMMaterialProperties.ifc_style_id:
try:
return tool.Ifc.get().by_id(obj.BIMMaterialProperties.ifc_style_id)
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import re
import pathlib
import ifcopenshell
@@ -25,10 +26,10 @@ from ifcopenshell.entity_instance import entity_instance
from functools import lru_cache
from typing import List, Generator, Optional
templates = {}
templates: dict[str, PsetQto] = {}
def get_template(schema):
def get_template(schema: str) -> PsetQto:
global templates
if schema not in templates:
templates[schema] = PsetQto(schema)
@@ -36,11 +37,13 @@ def get_template(schema):
class PsetQto:
# fmt: off
templates_path = {
"IFC2X3": "Pset_IFC2X3.ifc",
"IFC4": "Pset_IFC4_ADD2.ifc",
"IFC4X3": "Pset_IFC4X3.ifc"
}
# fmt: on
def __init__(self, schema: str, templates=None) -> None:
self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema)
@@ -18,7 +18,7 @@
from fractions import Fraction
from math import pi
from typing import Tuple, Iterable, Any, Union, Literal
from typing import Tuple, Iterable, Any, Union, Literal, Optional
import ifcopenshell
import ifcopenshell.api
@@ -525,7 +525,7 @@ def convert_unit(value, from_unit, to_unit):
)
def convert(value, from_prefix, from_unit, to_prefix, to_unit):
def convert(value: float, from_prefix: Optional[str], from_unit: str, to_prefix: Optional[str], to_unit: str) -> float:
"""Converts between length, area, and volume units
In this case, you manually specify the names and (optionally) prefixes to
@@ -534,12 +534,12 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit):
:param value: The numeric value you want to convert
:type value: float
:param from_prefix: A prefix from IfcSIPrefix. Can be None.
:type from_prefix: str,optional
:param from_prefix: A prefix from IfcSIPrefix. Can be None
:type from_prefix: str, optional
:param from_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
:type from_unit: str
:param to_prefix: A prefix from IfcSIPrefix. Can be None.
:type to_prefix: str,optional
:param to_prefix: A prefix from IfcSIPrefix. Can be None
:type to_prefix: str, optional
:param to_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
:type to_unit: str
:return: The converted value.
@@ -566,7 +566,7 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit):
return value
def calculate_unit_scale(ifc_file, unit_type="LENGTHUNIT"):
def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUNIT") -> float:
"""Returns a unit scale factor to convert to and from IFC project units and SI units.
Example: