mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
Add additional quantity calculations to QtoCalculator (#2405)
This commit is contained in:
@@ -316,6 +316,13 @@ endif
|
||||
cp -r dist/working/PyP6Xer-1.13.0/xerparser dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by QTOCalculator
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/b5/9a/625d4fc91ef85873801a16700840786117df4c016162a4532c998a7fe6bc/Shapely-1.8.4.tar.gz
|
||||
cd dist/working && tar -xzvf shapely*
|
||||
cp -r dist/working/Shapely-1.8.4/shapely dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by xerparser and IFC4D
|
||||
# TODO: remove this dependency. It's only used to show a progress bar.
|
||||
mkdir dist/working
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, Vukas Pajic <vulepajic@gmail.om>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -16,7 +16,14 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mathutils import Vector
|
||||
import bpy, bmesh
|
||||
import mathutils
|
||||
from mathutils import Vector, Matrix
|
||||
from mathutils.bvhtree import BVHTree
|
||||
import math
|
||||
from shapely.geometry import Polygon
|
||||
from shapely.ops import unary_union
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class QtoCalculator:
|
||||
@@ -40,7 +47,7 @@ class QtoCalculator:
|
||||
elif "area" in prop_name and "side" in prop_name:
|
||||
return self.get_side_area(obj)
|
||||
elif "area" in prop_name:
|
||||
return self.get_area(obj)
|
||||
return self.get_total_surface_area(obj)
|
||||
elif "volume" in prop_name:
|
||||
return self.get_volume(obj)
|
||||
|
||||
@@ -48,6 +55,11 @@ class QtoCalculator:
|
||||
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):
|
||||
"""_summary_: Returns the length of the longest edge of the object bounding box
|
||||
|
||||
:param blender-object o: Blender Object
|
||||
:return float: 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
|
||||
@@ -72,11 +84,21 @@ class QtoCalculator:
|
||||
return length
|
||||
|
||||
def get_width(self, o):
|
||||
"""_summary_: Returns the width of the object bounding box
|
||||
|
||||
:param blender-object o: blender object
|
||||
:return float: width
|
||||
"""
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
return min(x, y)
|
||||
|
||||
def get_height(self, o):
|
||||
"""_summary_: Returns the height of the object bounding box
|
||||
|
||||
:param blender-object o: blender object
|
||||
:return float: height
|
||||
"""
|
||||
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
|
||||
def get_perimeter(self, o):
|
||||
@@ -110,18 +132,52 @@ class QtoCalculator:
|
||||
lowest_z = z
|
||||
return lowest_polygons
|
||||
|
||||
def get_highest_polygons(self, o):
|
||||
|
||||
highest_polygons = []
|
||||
highest_z = None
|
||||
for polygon in o.data.polygons:
|
||||
z = round(polygon.center[2], 3)
|
||||
if highest_z is None:
|
||||
highest_z = z
|
||||
if z > highest_z:
|
||||
continue
|
||||
elif z == highest_z:
|
||||
highest_polygons.append(polygon)
|
||||
elif z < highest_z:
|
||||
highest_polygons = [polygon]
|
||||
highest_z = z
|
||||
return highest_polygons
|
||||
|
||||
def get_edge_key_distance(self, obj, edge_key):
|
||||
return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length
|
||||
|
||||
def get_edge_distance(self, obj, edge):
|
||||
return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length
|
||||
|
||||
def get_footprint_area(self, o):
|
||||
def get_net_footprint_area(self, o):
|
||||
"""_summary_: Returns the area of the footprint of the object, excluding any holes
|
||||
|
||||
:param blender-object o: blender object
|
||||
:return float: footprint area
|
||||
"""
|
||||
area = 0
|
||||
for polygon in self.get_lowest_polygons(o):
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_net_roofprint_area(self, o):
|
||||
# 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
|
||||
|
||||
:param blender-object o: Blender Object
|
||||
:return float: Area
|
||||
"""
|
||||
area = 0
|
||||
for polygon in self.get_highest_polygons(o):
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_side_area(self, o):
|
||||
# There are a few dumb options for this, but this seems the dumbest
|
||||
# until I get more practical experience on what works best.
|
||||
@@ -130,7 +186,7 @@ class QtoCalculator:
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
return max(x * z, y * z)
|
||||
|
||||
def get_area(self, o, vg_index=None):
|
||||
def get_total_surface_area(self, o, vg_index=None):
|
||||
if vg_index is None:
|
||||
area = 0
|
||||
for polygon in o.data.polygons:
|
||||
@@ -149,29 +205,551 @@ class QtoCalculator:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_volume(self, o, vg_index=None):
|
||||
volume = 0
|
||||
ob_mat = o.matrix_world
|
||||
me = o.data
|
||||
me.calc_loop_triangles()
|
||||
for tf in me.loop_triangles:
|
||||
tfv = tf.vertices
|
||||
if len(tf.vertices) == 3:
|
||||
tf_tris = ((me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]),)
|
||||
else:
|
||||
tf_tris = (
|
||||
(me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]),
|
||||
(
|
||||
me.vertices[tfv[2]],
|
||||
me.vertices[tfv[3]],
|
||||
me.vertices[tfv[0]],
|
||||
),
|
||||
def get_volume(self, o):
|
||||
o_mesh = bmesh.new()
|
||||
o_mesh.from_mesh(o.data)
|
||||
return o_mesh.calc_volume()
|
||||
|
||||
# The following is @Moult's older code. Keeping it here just in case the bmesh function is buggy. -vulevukusej
|
||||
|
||||
# def get_volume(self, o, vg_index=None):
|
||||
# volume = 0
|
||||
# ob_mat = o.matrix_world
|
||||
# me = o.data
|
||||
# me.calc_loop_triangles()
|
||||
# for tf in me.loop_triangles:
|
||||
# tfv = tf.vertices
|
||||
# if len(tf.vertices) == 3:
|
||||
# tf_tris = ((me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]),)
|
||||
# else:
|
||||
# tf_tris = (
|
||||
# (me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]),
|
||||
# (
|
||||
# me.vertices[tfv[2]],
|
||||
# me.vertices[tfv[3]],
|
||||
# me.vertices[tfv[0]],
|
||||
# ),
|
||||
# )
|
||||
|
||||
# for tf_iter in tf_tris:
|
||||
# v1 = ob_mat @ tf_iter[0].co
|
||||
# v2 = ob_mat @ tf_iter[1].co
|
||||
# v3 = ob_mat @ tf_iter[2].co
|
||||
|
||||
# volume += v1.dot(v2.cross(v3)) / 6.0
|
||||
# return volume
|
||||
|
||||
def get_opening_type(self, opening, obj):
|
||||
"""_summary_: Returns the opening type - OPENING / RECESS
|
||||
|
||||
:param blender-object opening: blender opening object
|
||||
:param blender-object obj: blender object
|
||||
:return string: "OPENING" or "RECESS"
|
||||
"""
|
||||
polygons = opening.data.polygons
|
||||
ray_intersections = 0
|
||||
|
||||
for polygon in polygons:
|
||||
normal_vector = (polygon.normal.x, polygon.normal.y, polygon.normal.z)
|
||||
polygon_centre = (polygon.center.x, polygon.center.y, polygon.center.z)
|
||||
if obj.ray_cast(polygon_centre, normal_vector)[0]:
|
||||
ray_intersections += 1
|
||||
|
||||
# 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(
|
||||
self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False
|
||||
):
|
||||
"""_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 bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False
|
||||
:return float: Opening Area
|
||||
"""
|
||||
total_opening_area = 0
|
||||
ifc = tool.Ifc.get()
|
||||
ifc_element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if len(openings := ifc_element.HasOpenings) != 0:
|
||||
for opening in openings:
|
||||
opening_id = opening.RelatedOpeningElement.GlobalId
|
||||
ifc_opening_element = ifc.by_guid(opening_id)
|
||||
bl_opening_obj = tool.Ifc.get_object(ifc_opening_element)
|
||||
|
||||
opening_type = (
|
||||
ifc_opening_element.PredefinedType
|
||||
if ifc_opening_element.PredefinedType is not None
|
||||
else self.get_opening_type(bl_opening_obj, obj)
|
||||
)
|
||||
|
||||
for tf_iter in tf_tris:
|
||||
v1 = ob_mat @ tf_iter[0].co
|
||||
v2 = ob_mat @ tf_iter[1].co
|
||||
v3 = ob_mat @ tf_iter[2].co
|
||||
if ignore_recesses and opening_type == "RECESS":
|
||||
continue
|
||||
opening_area = self.get_lateral_area(
|
||||
self.get_OBB_object(bl_opening_obj), angle_z1=angle_z1, angle_z2=angle_z2, exclude_end_areas=True
|
||||
)
|
||||
if opening_area >= min_area:
|
||||
total_opening_area += opening_area
|
||||
|
||||
return total_opening_area
|
||||
|
||||
def get_lateral_area(
|
||||
self,
|
||||
obj,
|
||||
subtract_openings: bool = True,
|
||||
exclude_end_areas: bool = False,
|
||||
exclude_side_areas: bool = False,
|
||||
angle_z1: int = 45,
|
||||
angle_z2: int = 135,
|
||||
):
|
||||
"""_summary_
|
||||
|
||||
:param blender-object obj: blender object, bpy.types.Object
|
||||
:param bool subtract_openings: Toggle whether opening-areas should be subtracted, defaults to True
|
||||
:param bool exclude_end_areas: , defaults to False
|
||||
:param bool exclude_side_areas: , defaults to False
|
||||
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the 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 area. Openings with a normal_vector greater than this value will be ignored, defaults to 135
|
||||
:return float: Lateral Area
|
||||
"""
|
||||
x_axis = [1, 0, 0]
|
||||
y_axis = [0, 1, 0]
|
||||
z_axis = [0, 0, 1]
|
||||
area = 0
|
||||
total_opening_area = (
|
||||
0 if subtract_openings else self.get_opening_area(obj, angle_z1=angle_z1, angle_z2=angle_z2)
|
||||
)
|
||||
polygons = obj.data.polygons
|
||||
|
||||
for polygon in polygons:
|
||||
angle_to_z_axis = math.degrees(polygon.normal.rotation_difference(Vector(z_axis)).angle)
|
||||
if angle_to_z_axis < angle_z1 or angle_to_z_axis > angle_z2:
|
||||
continue
|
||||
if exclude_end_areas:
|
||||
angle_to_x_axis = math.degrees(polygon.normal.rotation_difference(Vector(x_axis)).angle)
|
||||
if angle_to_x_axis < 45 or angle_to_x_axis > 135:
|
||||
continue
|
||||
if exclude_side_areas:
|
||||
angle_to_y_axis = math.degrees(polygon.normal.rotation_difference(Vector(y_axis)).angle)
|
||||
if angle_to_y_axis < 45 or angle_to_y_axis > 135:
|
||||
continue
|
||||
area += polygon.area
|
||||
return area + total_opening_area
|
||||
|
||||
def get_gross_top_area(self, obj, angle: int = 45):
|
||||
"""_summary_: Returns the gross 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
|
||||
:return float: Gross Top Area
|
||||
"""
|
||||
|
||||
z_axis = (0, 0, 1)
|
||||
area = 0
|
||||
opening_area = 0
|
||||
polygons = obj.data.polygons
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
ifc_element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
|
||||
if len(openings := ifc_element.HasOpenings) != 0:
|
||||
for opening in openings:
|
||||
if opening.RelatedOpeningElement.PredefinedType == "OPENING":
|
||||
opening_id = opening.RelatedOpeningElement.GlobalId
|
||||
|
||||
entity = ifc.by_guid(opening_id)
|
||||
open_obj = tool.Ifc.get_object(entity)
|
||||
opening_area += self.get_net_top_area(open_obj, angle=angle)
|
||||
else:
|
||||
continue
|
||||
|
||||
for polygon in polygons:
|
||||
normal_vector = (polygon.normal.x, polygon.normal.y, polygon.normal.z)
|
||||
angle_to_z_axis = math.degrees(polygon.normal.rotation_difference(Vector(z_axis)).angle)
|
||||
|
||||
if angle_to_z_axis < angle:
|
||||
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(self, obj, angle: int = 45, ignore_internal: bool = True):
|
||||
"""_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
|
||||
:return float: Net Top Area
|
||||
"""
|
||||
z_axis = (0, 0, 1)
|
||||
area = 0
|
||||
polygons = obj.data.polygons
|
||||
|
||||
for polygon in polygons:
|
||||
normal_vector = (polygon.normal.x, polygon.normal.y, polygon.normal.z)
|
||||
angle_to_z_axis = math.degrees(polygon.normal.rotation_difference(Vector(z_axis)).angle)
|
||||
|
||||
if angle_to_z_axis < angle:
|
||||
# offset the raycast, otherwise the raycast will always collide with the object.
|
||||
offset = polygon.center+Vector((0,0,0.01))
|
||||
if ignore_internal and obj.ray_cast(offset, (0,0,1))[0]:
|
||||
continue
|
||||
area += polygon.area
|
||||
|
||||
return area
|
||||
|
||||
def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True):
|
||||
"""_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 bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings
|
||||
:return float: Projected Area
|
||||
"""
|
||||
|
||||
odata = obj.data
|
||||
polygons = obj.data.polygons
|
||||
shapely_polygons = []
|
||||
|
||||
axes = {"x": ["y", "z"], "y": ["x", "z"], "z": ["x", "y"]}[projection_axis]
|
||||
|
||||
for polygon in polygons:
|
||||
if getattr(polygon.normal, projection_axis) == 0:
|
||||
continue
|
||||
polygon_tuples = []
|
||||
|
||||
for loop_index in polygon.loop_indices:
|
||||
loop = odata.loops[loop_index]
|
||||
a = getattr(odata.vertices[loop.vertex_index].co, axes[0])
|
||||
b = getattr(odata.vertices[loop.vertex_index].co, axes[1])
|
||||
polygon_tuples.append((a, b))
|
||||
|
||||
pgon = Polygon(polygon_tuples)
|
||||
shapely_polygons.append(pgon)
|
||||
|
||||
projected_polygon = unary_union(shapely_polygons)
|
||||
if is_gross:
|
||||
void_area = 0
|
||||
voids = projected_polygon.interiors
|
||||
for void in voids:
|
||||
void_polygon = Polygon(void)
|
||||
void_area += void_polygon.area
|
||||
return projected_polygon.area + void_area
|
||||
return projected_polygon.area
|
||||
|
||||
def get_OBB_object(self, obj):
|
||||
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:return blender-object: OBB of the Object
|
||||
"""
|
||||
ifc_id = obj.BIMObjectProperties.ifc_definition_id
|
||||
bbox = obj.bound_box
|
||||
# matrix transformation to go from obj coordinates to world coordinates:
|
||||
obb = [Vector(v) for v in bbox]
|
||||
obb_mesh = bpy.data.meshes.new(f"OBB_{ifc_id}")
|
||||
|
||||
# list of faces, with each tuple referring to an vertex-index in obb
|
||||
faces = [
|
||||
(0, 1, 2, 3),
|
||||
(7, 6, 5, 4),
|
||||
(5, 6, 2, 1),
|
||||
(0, 3, 7, 4),
|
||||
(0, 4, 5, 1),
|
||||
(2, 6, 7, 3),
|
||||
]
|
||||
|
||||
obb_mesh.from_pydata(vertices=obb, edges=[], faces=faces)
|
||||
# obb_mesh.transform(obj.matrix_world)
|
||||
|
||||
# create a new object from the mesh
|
||||
new_OBB_object = bpy.data.objects.new(f"OBB_{ifc_id}", obb_mesh)
|
||||
new_OBB_object.matrix_world = obj.matrix_world
|
||||
|
||||
# create new collection for QtoCalculator
|
||||
collection = bpy.data.collections.get("QtoCalculator", bpy.data.collections.new("QtoCalculator"))
|
||||
if not bpy.context.scene.collection.children.get(collection.name):
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
|
||||
# add object to scene collection and then hide them.
|
||||
collection.objects.get(new_OBB_object.name, collection.objects.link(new_OBB_object))
|
||||
new_OBB_object.hide_set(True)
|
||||
|
||||
return new_OBB_object
|
||||
|
||||
def get_AABB_object(self, obj):
|
||||
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:return blender-object: AABB of the Object
|
||||
"""
|
||||
ifc_id = obj.BIMObjectProperties.ifc_definition_id
|
||||
aabb_mesh = bpy.data.meshes.new(f"OBB_{ifc_id}")
|
||||
|
||||
x = [v.co.x for v in obj.data.vertices]
|
||||
y = [v.co.y for v in obj.data.vertices]
|
||||
z = [v.co.z for v in obj.data.vertices]
|
||||
|
||||
min_x, max_x, min_y, max_y, min_z, max_z = min(x), max(x), min(y), max(y), min(z), max(z)
|
||||
|
||||
vertices = [
|
||||
(min_x, min_y, min_z),
|
||||
(min_x, min_y, max_z),
|
||||
(min_x, max_y, max_z),
|
||||
(min_x, max_y, min_z),
|
||||
(max_x, min_y, min_z),
|
||||
(max_x, min_y, max_z),
|
||||
(max_x, max_y, max_z),
|
||||
(max_x, max_y, min_z),
|
||||
]
|
||||
|
||||
faces = [
|
||||
(0, 1, 2, 3),
|
||||
(7, 6, 5, 4),
|
||||
(5, 6, 2, 1),
|
||||
(0, 3, 7, 4),
|
||||
(0, 4, 5, 1),
|
||||
(2, 6, 7, 3),
|
||||
]
|
||||
|
||||
aabb_mesh.from_pydata(vertices=vertices, edges=[], faces=faces)
|
||||
aabb_mesh.update()
|
||||
|
||||
# create a new object from the mesh
|
||||
new_AABB_object = bpy.data.objects.new(f"OBB_{ifc_id}", aabb_mesh)
|
||||
new_AABB_object.matrix_world = obj.matrix_world
|
||||
|
||||
# create new collection for QtoCalculator
|
||||
collection = bpy.data.collections.get("QtoCalculator", bpy.data.collections.new("QtoCalculator"))
|
||||
if not bpy.context.scene.collection.children.get(collection.name):
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
|
||||
# add object to scene collection and then hide them.
|
||||
collection.objects.link(new_AABB_object)
|
||||
new_AABB_object.hide_set(True)
|
||||
|
||||
return new_AABB_object
|
||||
|
||||
def get_bisected_obj(
|
||||
self,
|
||||
obj,
|
||||
plane_co_pos,
|
||||
plane_no_pos,
|
||||
plane_co_neg,
|
||||
plane_no_neg,
|
||||
):
|
||||
"""_summary_: Returns the object bisected by two planes.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param tuple(x,y,z) plane_co_pos: Point on upper bisection plane. Example: (0,0,0)
|
||||
:param tuple(x,y,z) plane_no_pos: Tuple describing the normal vector of the upper bisection plane. Example: (0,0,1)
|
||||
:param tuple(x,y,z) plane_co_neg: Point on lower bisection plane. Example: (0,0,0)
|
||||
:param tuple(x,y,z) plane_no_neg: Tuple describing the normal vector of the lower bisection plane. Example: (0,0,-1)
|
||||
:return _type_: _description_
|
||||
"""
|
||||
ifc_id = obj.BIMObjectProperties.ifc_definition_id
|
||||
|
||||
bis_obj = obj.copy()
|
||||
bis_obj.data = obj.data.copy()
|
||||
bis_obj.name = f"Bisected_{ifc_id}"
|
||||
|
||||
collection = bpy.data.collections.get("QtoCalculator", bpy.data.collections.new("QtoCalculator"))
|
||||
if not bpy.context.scene.collection.children.get(collection.name):
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
|
||||
collection.objects.link(bis_obj)
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
bpy.context.view_layer.objects.active = bis_obj
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.mesh.select_all(action="SELECT")
|
||||
|
||||
bpy.ops.mesh.bisect(plane_co=plane_co_pos, plane_no=plane_no_pos, use_fill=True, clear_outer=True)
|
||||
|
||||
bpy.ops.mesh.select_all(action="SELECT")
|
||||
bpy.ops.mesh.bisect(plane_co=plane_co_neg, plane_no=plane_no_neg, use_fill=True, clear_outer=True)
|
||||
bpy.ops.object.editmode_toggle()
|
||||
bis_obj.hide_set(True)
|
||||
|
||||
return bis_obj
|
||||
|
||||
def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]):
|
||||
"""_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"]
|
||||
:return float: Total contact area of the object with other objects.
|
||||
"""
|
||||
total_contact_area = 0
|
||||
touching_objects = self.get_touching_objects(obj, class_filter)
|
||||
|
||||
for o in touching_objects:
|
||||
total_contact_area += self.get_contact_area(obj, o)
|
||||
|
||||
return total_contact_area
|
||||
|
||||
def get_touching_objects(self, obj, class_filter):
|
||||
"""_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"]
|
||||
: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
|
||||
# I still prefer using bhvtree over ifcclash simply because of the considerable speed improvement @vulevukusej
|
||||
obj.rotation_euler[0] += math.radians(0.001)
|
||||
obj.rotation_euler[1] += math.radians(0.001)
|
||||
bpy.context.evaluated_depsgraph_get().update()
|
||||
|
||||
obj_mesh = bmesh.new()
|
||||
obj_mesh.from_mesh(obj.data)
|
||||
obj_mesh.transform(obj.matrix_world)
|
||||
obj_tree = BVHTree.FromBMesh(obj_mesh)
|
||||
|
||||
touching_objects = []
|
||||
filtered_objects = []
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
for f in class_filter:
|
||||
filtered_objects += ifc.by_type(f)
|
||||
|
||||
for o in filtered_objects:
|
||||
blender_o = tool.Ifc.get_object(o)
|
||||
if blender_o == obj:
|
||||
continue
|
||||
o_mesh = bmesh.new()
|
||||
try:
|
||||
o_mesh.from_mesh(blender_o.data)
|
||||
except:
|
||||
# i'm too tired to debug this properly. Not sure what causes this error. @vulevukusej
|
||||
continue
|
||||
o_mesh.transform(blender_o.matrix_world)
|
||||
o_tree = BVHTree.FromBMesh(o_mesh)
|
||||
|
||||
if len(obj_tree.overlap(o_tree)) > 0:
|
||||
touching_objects.append(blender_o)
|
||||
|
||||
# return the objects to their original states
|
||||
blender_o.rotation_euler[0] -= math.radians(0.001)
|
||||
blender_o.rotation_euler[1] -= math.radians(0.001)
|
||||
bpy.context.evaluated_depsgraph_get().update()
|
||||
|
||||
return touching_objects
|
||||
|
||||
def get_contact_area(self, object1, object2):
|
||||
"""_summary_: Returns the contact area between two objects.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param blender-object obj: Blender Object
|
||||
:return float: contact area between the two objects.
|
||||
"""
|
||||
# list of tuples, each tuple containing the index of the polygon in object1 and object2 that are touching
|
||||
total_area = 0
|
||||
|
||||
for poly1 in object1.data.polygons:
|
||||
for poly2 in object2.data.polygons:
|
||||
total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2)
|
||||
return total_area
|
||||
|
||||
def get_intersection_between_polygons(self, object1, poly1, object2, poly2):
|
||||
"""_summary_: Returns the intersection between two polygons.
|
||||
|
||||
:param blender-object object1: Blender Object
|
||||
:param blender-polygon poly1: Blender Polygon
|
||||
:param blender-object object1: Blender Object
|
||||
:param blender-polygon poly1: Blender Polygon
|
||||
:return float: intersection area of the two polygons.
|
||||
"""
|
||||
# get normal vectors according to world axis
|
||||
normal1 = object1.rotation_euler.to_matrix() @ poly1.normal
|
||||
center1 = object1.matrix_world @ poly1.center
|
||||
normal2 = object2.rotation_euler.to_matrix() @ poly2.normal
|
||||
center2 = object2.matrix_world @ poly2.center
|
||||
|
||||
angle_between_normals = normal1.rotation_difference(normal2).angle
|
||||
|
||||
if math.degrees(angle_between_normals) < 178:
|
||||
return 0
|
||||
|
||||
# touching polygons should be coplanar:
|
||||
plane_intersection = mathutils.geometry.intersect_plane_plane(
|
||||
center1,
|
||||
normal1,
|
||||
center2,
|
||||
normal2
|
||||
)
|
||||
|
||||
# sometimes coplanar planes will interesect far off into the distance. This is a crude way of filtering out those intersections.
|
||||
if plane_intersection[0] is None or (plane_intersection[0] - center1).magnitude > 20:
|
||||
return 0
|
||||
|
||||
# calculate rotation between face and vertical Z-axis. This makes it easier to calculate intersection area later
|
||||
rotation_to_z = normal1.rotation_difference(Vector((0, 0, 1)))
|
||||
center_of_rotation = center1
|
||||
|
||||
# rotation around face.center in world space / https://blender.stackexchange.com/a/12324/130742
|
||||
trans_matrix = Matrix.Translation(center_of_rotation) @ rotation_to_z.to_matrix().to_4x4()
|
||||
|
||||
pgon1 = self.create_shapely_polygon(object1, poly1, trans_matrix)
|
||||
pgon2 = self.create_shapely_polygon(object2, poly2, trans_matrix)
|
||||
|
||||
try:
|
||||
return pgon1.intersection(pgon2).area
|
||||
except:
|
||||
# TopologicalError - Generated Geometry might be invalid
|
||||
return 0
|
||||
|
||||
def create_shapely_polygon(self, obj, polygon, trans_matrix):
|
||||
"""_summary_: Create a shapely polygon
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param blender-polygon polygon: Blender Polygon
|
||||
:param matrix trans_matrix: Matrix that rotates the polygon to face upwards
|
||||
:return Shapely Polygon: Shapely Polygon
|
||||
"""
|
||||
polygon_tuples = []
|
||||
odata = obj.data
|
||||
for loop_index in polygon.loop_indices:
|
||||
loop = odata.loops[loop_index]
|
||||
coords = obj.matrix_world @ odata.vertices[loop.vertex_index].co
|
||||
rotated_coords = trans_matrix @ coords
|
||||
x = rotated_coords.x
|
||||
y = rotated_coords.y
|
||||
polygon_tuples.append((x, y))
|
||||
return Polygon(polygon_tuples)
|
||||
|
||||
# # Following code is here temporarily to test newly created functions:
|
||||
|
||||
qto = QtoCalculator()
|
||||
o = bpy.context.active_object
|
||||
sel = bpy.context.selected_objects
|
||||
|
||||
nl = '\n'
|
||||
print(
|
||||
f"get_linear_length: {qto.get_linear_length(o)}{nl}{nl}"
|
||||
f"get_width: {qto.get_width(o)}{nl}{nl}"
|
||||
f"get_height: {qto.get_height(o)}{nl}{nl}"
|
||||
f"get_perimeter: {qto.get_perimeter(o)}{nl}{nl}"
|
||||
f"get_lowest_polygons: {qto.get_lowest_polygons(o)}{nl}{nl}"
|
||||
f"get_highest_polygons: {qto.get_highest_polygons(o)}{nl}{nl}"
|
||||
f"get_net_footprint_area: {qto.get_net_footprint_area(o)}{nl}{nl}"
|
||||
f"get_net_roofprint_area: {qto.get_net_roofprint_area(o)}{nl}{nl}"
|
||||
f"get_side_area: {qto.get_side_area(o)}{nl}{nl}"
|
||||
f"get_total_surface_area: {qto.get_total_surface_area(o)}{nl}{nl}"
|
||||
f"get_volume: {qto.get_volume(o)}{nl}{nl}"
|
||||
f"get_opening_area(o, angle_z1=45, angle_z2=135, min_area=0, ignore_recesses=False): {qto.get_opening_area(o, angle_z1=45, angle_z2=135, min_area=0, ignore_recesses=False)}{nl}{nl}"
|
||||
f"get_lateral_area(o, subtract_openings=True, exclude_end_areas=False, exclude_side_areas=False, angle_z1=45, angle_z2=135): {qto.get_lateral_area(o, subtract_openings=True, exclude_end_areas=False, exclude_side_areas=False, angle_z1=45, angle_z2=135)}{nl}{nl}"
|
||||
f"get_gross_top_area: {qto.get_gross_top_area(o, angle=45)}{nl}{nl}"
|
||||
f"get_net_top_area(o, angle=45, ignore_internal=True): {qto.get_net_top_area(o, angle=45, ignore_internal=True)}{nl}{nl}"
|
||||
f"get_projected_area(o, projection_axis='z', is_gross=True): {qto.get_projected_area(o, projection_axis='z', is_gross=True)}{nl}{nl}"
|
||||
f"get_OBB_object: {qto.get_OBB_object(o)}{nl}{nl}"
|
||||
f"get_AABB_object: {qto.get_AABB_object(o)}{nl}{nl}"
|
||||
f"get_bisected_obj(o, plane_co_pos=(0,0,1), plane_no_pos=(0,0,1), plane_co_neg=(0,0,1), plane_no_neg=(0,0,1)): {qto.get_bisected_obj(o, plane_co_pos=(0,0,1), plane_no_pos=(0,0,1), plane_co_neg=(0,0,1), plane_no_neg=(0,0,1))}{nl}{nl}"
|
||||
f"get_total_contact_area(o, class_filter=['IfcWall', 'IfcSlab']): {qto.get_total_contact_area(o, class_filter=['IfcWall', 'IfcSlab'])}{nl}{nl}"
|
||||
f"get_touching_objects(o, ['IfcElement']): {qto.get_touching_objects(o, ['IfcElement'])}{nl}{nl}"
|
||||
#f"get_contact_area: {qto.get_contact_area(o)}{nl}{nl}"
|
||||
)
|
||||
|
||||
|
||||
volume += v1.dot(v2.cross(v3)) / 6.0
|
||||
return volume
|
||||
|
||||
@@ -24,6 +24,7 @@ import blenderbim.core.qto as core
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.qto import helper
|
||||
from ifcopenshell.api.pset.data import Data as PsetData
|
||||
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
|
||||
|
||||
|
||||
class CalculateCircleRadius(bpy.types.Operator):
|
||||
@@ -107,6 +108,26 @@ class ExecuteQtoMethod(bpy.types.Operator):
|
||||
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"}
|
||||
|
||||
|
||||
@@ -47,6 +47,16 @@ class BIMQtoProperties(PropertyGroup):
|
||||
"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",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user