File headers: use SPDX license identifiers
[blender-addons.git] / object_print3d_utils / mesh_helpers.py
blob6ce5da5072352fe74d444078e8c4c8cd899eedd2
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8-80 compliant>
5 # Generic helper functions, to be used by any modules.
8 import bmesh
11 def bmesh_copy_from_object(obj, transform=True, triangulate=True, apply_modifiers=False):
12 """Returns a transformed, triangulated copy of the mesh"""
14 assert obj.type == 'MESH'
16 if apply_modifiers and obj.modifiers:
17 import bpy
18 depsgraph = bpy.context.evaluated_depsgraph_get()
19 obj_eval = obj.evaluated_get(depsgraph)
20 me = obj_eval.to_mesh()
21 bm = bmesh.new()
22 bm.from_mesh(me)
23 obj_eval.to_mesh_clear()
24 else:
25 me = obj.data
26 if obj.mode == 'EDIT':
27 bm_orig = bmesh.from_edit_mesh(me)
28 bm = bm_orig.copy()
29 else:
30 bm = bmesh.new()
31 bm.from_mesh(me)
33 # TODO. remove all customdata layers.
34 # would save ram
36 if transform:
37 bm.transform(obj.matrix_world)
39 if triangulate:
40 bmesh.ops.triangulate(bm, faces=bm.faces)
42 return bm
45 def bmesh_from_object(obj):
46 """Object/Edit Mode get mesh, use bmesh_to_object() to write back."""
47 me = obj.data
49 if obj.mode == 'EDIT':
50 bm = bmesh.from_edit_mesh(me)
51 else:
52 bm = bmesh.new()
53 bm.from_mesh(me)
55 return bm
58 def bmesh_to_object(obj, bm):
59 """Object/Edit Mode update the object."""
60 me = obj.data
62 if obj.mode == 'EDIT':
63 bmesh.update_edit_mesh(me, loop_triangles=True)
64 else:
65 bm.to_mesh(me)
66 me.update()
69 def bmesh_calc_area(bm):
70 """Calculate the surface area."""
71 return sum(f.calc_area() for f in bm.faces)
74 def bmesh_check_self_intersect_object(obj):
75 """Check if any faces self intersect returns an array of edge index values."""
76 import array
77 import mathutils
79 if not obj.data.polygons:
80 return array.array('i', ())
82 bm = bmesh_copy_from_object(obj, transform=False, triangulate=False)
83 tree = mathutils.bvhtree.BVHTree.FromBMesh(bm, epsilon=0.00001)
84 overlap = tree.overlap(tree)
85 faces_error = {i for i_pair in overlap for i in i_pair}
87 return array.array('i', faces_error)
90 def bmesh_face_points_random(f, num_points=1, margin=0.05):
91 import random
92 from random import uniform
94 # for pradictable results
95 random.seed(f.index)
97 uniform_args = 0.0 + margin, 1.0 - margin
98 vecs = [v.co for v in f.verts]
100 for _ in range(num_points):
101 u1 = uniform(*uniform_args)
102 u2 = uniform(*uniform_args)
103 u_tot = u1 + u2
105 if u_tot > 1.0:
106 u1 = 1.0 - u1
107 u2 = 1.0 - u2
109 side1 = vecs[1] - vecs[0]
110 side2 = vecs[2] - vecs[0]
112 yield vecs[0] + u1 * side1 + u2 * side2
115 def bmesh_check_thick_object(obj, thickness):
116 import array
117 import bpy
119 # Triangulate
120 bm = bmesh_copy_from_object(obj, transform=True, triangulate=False)
122 # map original faces to their index.
123 face_index_map_org = {f: i for i, f in enumerate(bm.faces)}
124 ret = bmesh.ops.triangulate(bm, faces=bm.faces)
125 face_map = ret["face_map"]
126 del ret
127 # old edge -> new mapping
129 # Convert new/old map to index dict.
131 # Create a real mesh (lame!)
132 context = bpy.context
133 layer = context.view_layer
134 scene_collection = context.layer_collection.collection
136 me_tmp = bpy.data.meshes.new(name="~temp~")
137 bm.to_mesh(me_tmp)
138 obj_tmp = bpy.data.objects.new(name=me_tmp.name, object_data=me_tmp)
139 scene_collection.objects.link(obj_tmp)
141 layer.update()
142 ray_cast = obj_tmp.ray_cast
144 EPS_BIAS = 0.0001
146 faces_error = set()
147 bm_faces_new = bm.faces[:]
149 for f in bm_faces_new:
150 no = f.normal
151 no_sta = no * EPS_BIAS
152 no_end = no * thickness
153 for p in bmesh_face_points_random(f, num_points=6):
154 # Cast the ray backwards
155 p_a = p - no_sta
156 p_b = p - no_end
157 p_dir = p_b - p_a
159 ok, _co, no, index = ray_cast(p_a, p_dir, distance=p_dir.length)
161 if ok:
162 # Add the face we hit
163 for f_iter in (f, bm_faces_new[index]):
164 # if the face wasn't triangulated, just use existing
165 f_org = face_map.get(f_iter, f_iter)
166 f_org_index = face_index_map_org[f_org]
167 faces_error.add(f_org_index)
169 bm.free()
171 scene_collection.objects.unlink(obj_tmp)
172 bpy.data.objects.remove(obj_tmp)
173 bpy.data.meshes.remove(me_tmp)
175 layer.update()
177 return array.array('i', faces_error)
180 def face_is_distorted(ele, angle_distort):
181 no = ele.normal
182 angle_fn = no.angle
184 for loop in ele.loops:
185 loopno = loop.calc_normal()
187 if loopno.dot(no) < 0.0:
188 loopno.negate()
190 if angle_fn(loopno, 1000.0) > angle_distort:
191 return True
193 return False