Merge branch 'blender-v4.0-release'
[blender-addons.git] / mesh_tools / mesh_extrude_and_reshape.py
blob4b50b396848371d4e1de1c319b4537f4e9545982
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # Contact for more information about the Addon:
6 # Email: germano.costa@ig.com.br
7 # Twitter: wii_mano @mano_wii
9 bl_info = {
10 "name": "Extrude and Reshape",
11 "author": "Germano Cavalcante",
12 "version": (0, 8, 1),
13 "blender": (2, 80, 0),
14 "location": "View3D > UI > Tools > Mesh Tools > Add: > Extrude Menu (Alt + E)",
15 "description": "Extrude face and merge edge intersections "
16 "between the mesh and the new edges",
17 "doc_url": "http://blenderartists.org/forum/"
18 "showthread.php?376618-Addon-Push-Pull-Face",
19 "category": "Mesh",
22 import bpy
23 import bmesh
24 from mathutils.geometry import intersect_line_line
25 from bpy.types import Operator
28 class BVHco():
29 i = 0
30 c1x = 0.0
31 c1y = 0.0
32 c1z = 0.0
33 c2x = 0.0
34 c2y = 0.0
35 c2z = 0.0
38 def edges_BVH_overlap(bm, edges, epsilon=0.0001):
39 bco = set()
40 for e in edges:
41 bvh = BVHco()
42 bvh.i = e.index
43 b1 = e.verts[0]
44 b2 = e.verts[1]
45 co1 = b1.co.x
46 co2 = b2.co.x
47 if co1 <= co2:
48 bvh.c1x = co1 - epsilon
49 bvh.c2x = co2 + epsilon
50 else:
51 bvh.c1x = co2 - epsilon
52 bvh.c2x = co1 + epsilon
53 co1 = b1.co.y
54 co2 = b2.co.y
55 if co1 <= co2:
56 bvh.c1y = co1 - epsilon
57 bvh.c2y = co2 + epsilon
58 else:
59 bvh.c1y = co2 - epsilon
60 bvh.c2y = co1 + epsilon
61 co1 = b1.co.z
62 co2 = b2.co.z
63 if co1 <= co2:
64 bvh.c1z = co1 - epsilon
65 bvh.c2z = co2 + epsilon
66 else:
67 bvh.c1z = co2 - epsilon
68 bvh.c2z = co1 + epsilon
69 bco.add(bvh)
70 del edges
71 overlap = {}
72 oget = overlap.get
73 for e1 in bm.edges:
74 by = bz = True
75 a1 = e1.verts[0]
76 a2 = e1.verts[1]
77 c1x = a1.co.x
78 c2x = a2.co.x
79 if c1x > c2x:
80 tm = c1x
81 c1x = c2x
82 c2x = tm
83 for bvh in bco:
84 if c1x <= bvh.c2x and c2x >= bvh.c1x:
85 if by:
86 by = False
87 c1y = a1.co.y
88 c2y = a2.co.y
89 if c1y > c2y:
90 tm = c1y
91 c1y = c2y
92 c2y = tm
93 if c1y <= bvh.c2y and c2y >= bvh.c1y:
94 if bz:
95 bz = False
96 c1z = a1.co.z
97 c2z = a2.co.z
98 if c1z > c2z:
99 tm = c1z
100 c1z = c2z
101 c2z = tm
102 if c1z <= bvh.c2z and c2z >= bvh.c1z:
103 e2 = bm.edges[bvh.i]
104 if e1 != e2:
105 overlap[e1] = oget(e1, set()).union({e2})
106 return overlap
109 def intersect_edges_edges(overlap, precision=4):
110 epsilon = .1**precision
111 fpre_min = -epsilon
112 fpre_max = 1 + epsilon
113 splits = {}
114 sp_get = splits.get
115 new_edges1 = set()
116 new_edges2 = set()
117 targetmap = {}
118 for edg1 in overlap:
119 # print("***", ed1.index, "***")
120 for edg2 in overlap[edg1]:
121 a1 = edg1.verts[0]
122 a2 = edg1.verts[1]
123 b1 = edg2.verts[0]
124 b2 = edg2.verts[1]
126 # test if are linked
127 if a1 in {b1, b2} or a2 in {b1, b2}:
128 # print('linked')
129 continue
131 aco1, aco2 = a1.co, a2.co
132 bco1, bco2 = b1.co, b2.co
133 tp = intersect_line_line(aco1, aco2, bco1, bco2)
134 if tp:
135 p1, p2 = tp
136 if (p1 - p2).to_tuple(precision) == (0, 0, 0):
137 v = aco2 - aco1
138 f = p1 - aco1
139 x, y, z = abs(v.x), abs(v.y), abs(v.z)
140 max1 = 0 if x >= y and x >= z else\
141 1 if y >= x and y >= z else 2
142 fac1 = f[max1] / v[max1]
144 v = bco2 - bco1
145 f = p2 - bco1
146 x, y, z = abs(v.x), abs(v.y), abs(v.z)
147 max2 = 0 if x >= y and x >= z else\
148 1 if y >= x and y >= z else 2
149 fac2 = f[max2] / v[max2]
151 if fpre_min <= fac1 <= fpre_max:
152 # print(edg1.index, 'can intersect', edg2.index)
153 ed1 = edg1
155 elif edg1 in splits:
156 for ed1 in splits[edg1]:
157 a1 = ed1.verts[0]
158 a2 = ed1.verts[1]
160 vco1 = a1.co
161 vco2 = a2.co
163 v = vco2 - vco1
164 f = p1 - vco1
165 fac1 = f[max1] / v[max1]
166 if fpre_min <= fac1 <= fpre_max:
167 # print(e.index, 'can intersect', edg2.index)
168 break
169 else:
170 # print(edg1.index, 'really does not intersect', edg2.index)
171 continue
172 else:
173 # print(edg1.index, 'not intersect', edg2.index)
174 continue
176 if fpre_min <= fac2 <= fpre_max:
177 # print(ed1.index, 'actually intersect', edg2.index)
178 ed2 = edg2
180 elif edg2 in splits:
181 for ed2 in splits[edg2]:
182 b1 = ed2.verts[0]
183 b2 = ed2.verts[1]
185 vco1 = b1.co
186 vco2 = b2.co
188 v = vco2 - vco1
189 f = p2 - vco1
190 fac2 = f[max2] / v[max2]
191 if fpre_min <= fac2 <= fpre_max:
192 # print(ed1.index, 'actually intersect', e.index)
193 break
194 else:
195 # print(ed1.index, 'really does not intersect', ed2.index)
196 continue
197 else:
198 # print(ed1.index, 'not intersect', edg2.index)
199 continue
201 new_edges1.add(ed1)
202 new_edges2.add(ed2)
204 if abs(fac1) <= epsilon:
205 nv1 = a1
206 elif fac1 + epsilon >= 1:
207 nv1 = a2
208 else:
209 ne1, nv1 = bmesh.utils.edge_split(ed1, a1, fac1)
210 new_edges1.add(ne1)
211 splits[edg1] = sp_get(edg1, set()).union({ne1})
213 if abs(fac2) <= epsilon:
214 nv2 = b1
215 elif fac2 + epsilon >= 1:
216 nv2 = b2
217 else:
218 ne2, nv2 = bmesh.utils.edge_split(ed2, b1, fac2)
219 new_edges2.add(ne2)
220 splits[edg2] = sp_get(edg2, set()).union({ne2})
222 if nv1 != nv2: # necessary?
223 targetmap[nv1] = nv2
225 return new_edges1, new_edges2, targetmap
228 class ER_OT_Extrude_and_Reshape(Operator):
229 bl_idname = "mesh.extrude_reshape"
230 bl_label = "Extrude and Reshape"
231 bl_description = "Push and pull face entities to sculpt 3d models"
232 bl_options = {'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}
234 @classmethod
235 def poll(cls, context):
236 if context.mode=='EDIT_MESH':
237 return True
239 def modal(self, context, event):
240 if self.confirm:
241 sface = self.bm.faces.active
242 if not sface:
243 for face in self.bm.faces:
244 if face.select is True:
245 sface = face
246 break
247 else:
248 return {'FINISHED'}
249 # edges to intersect
250 edges = set()
251 [[edges.add(ed) for ed in v.link_edges] for v in sface.verts]
253 overlap = edges_BVH_overlap(self.bm, edges, epsilon=0.0001)
254 overlap = {k: v for k, v in overlap.items() if k not in edges} # remove repetition
256 print([e.index for e in edges])
257 for a, b in overlap.items():
258 print(a.index, [e.index for e in b])
260 new_edges1, new_edges2, targetmap = intersect_edges_edges(overlap)
261 pos_weld = set()
262 for e in new_edges1:
263 v1, v2 = e.verts
264 if v1 in targetmap and v2 in targetmap:
265 pos_weld.add((targetmap[v1], targetmap[v2]))
266 if targetmap:
267 bmesh.ops.weld_verts(self.bm, targetmap=targetmap)
269 print([e.is_valid for e in new_edges1])
270 print([e.is_valid for e in new_edges2])
271 sp_faces1 = set()
273 for e in pos_weld:
274 v1, v2 = e
275 lf1 = set(v1.link_faces)
276 lf2 = set(v2.link_faces)
277 rlfe = lf1.intersection(lf2)
278 for f in rlfe:
279 try:
280 nf = bmesh.utils.face_split(f, v1, v2)
281 # sp_faces1.update({f, nf[0]})
282 except:
283 pass
285 # sp_faces2 = set()
286 for e in new_edges2:
287 lfe = set(e.link_faces)
288 v1, v2 = e.verts
289 lf1 = set(v1.link_faces)
290 lf2 = set(v2.link_faces)
291 rlfe = lf1.intersection(lf2)
292 for f in rlfe.difference(lfe):
293 nf = bmesh.utils.face_split(f, v1, v2)
294 # sp_faces2.update({f, nf[0]})
296 bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
297 return {'FINISHED'}
298 if self.cancel:
299 return {'FINISHED'}
300 self.cancel = event.type in {'ESC', 'NDOF_BUTTON_ESC'}
301 self.confirm = event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'}
302 return {'PASS_THROUGH'}
304 def execute(self, context):
305 self.mesh = context.object.data
306 self.bm = bmesh.from_edit_mesh(self.mesh)
307 try:
308 selection = self.bm.select_history[-1]
309 except:
310 for face in self.bm.faces:
311 if face.select is True:
312 selection = face
313 break
314 else:
315 return {'FINISHED'}
316 if not isinstance(selection, bmesh.types.BMFace):
317 bpy.ops.mesh.extrude_region_move('INVOKE_DEFAULT')
318 return {'FINISHED'}
319 else:
320 face = selection
321 # face.select = False
322 bpy.ops.mesh.select_all(action='DESELECT')
323 geom = []
324 for edge in face.edges:
325 if abs(edge.calc_face_angle(0) - 1.5707963267948966) < 0.01: # self.angle_tolerance:
326 geom.append(edge)
328 ret_dict = bmesh.ops.extrude_discrete_faces(self.bm, faces=[face])
330 for face in ret_dict['faces']:
331 self.bm.faces.active = face
332 face.select = True
333 sface = face
334 dfaces = bmesh.ops.dissolve_edges(
335 self.bm, edges=geom, use_verts=True, use_face_split=False
337 bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
338 bpy.ops.transform.translate(
339 'INVOKE_DEFAULT', constraint_axis=(False, False, True),
340 orient_type='NORMAL', release_confirm=True
343 context.window_manager.modal_handler_add(self)
345 self.cancel = False
346 self.confirm = False
347 return {'RUNNING_MODAL'}
350 def operator_draw(self, context):
351 layout = self.layout
352 col = layout.column(align=True)
353 col.operator("mesh.extrude_reshape")
356 def register():
357 bpy.utils.register_class(ER_OT_Extrude_and_Reshape)
360 def unregister():
361 bpy.utils.unregister_class(ER_OT_Extrude_and_Reshape)
364 if __name__ == "__main__":
365 register()