Update for 2.8
[blender-addons.git] / mesh_extra_tools / mesh_extrude_and_reshape.py
blob8716bfb1b7f7a9b8f673c320ca21057117fb58c3
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 3
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 # ##### END GPL LICENSE BLOCK #####
18 # Contact for more information about the Addon:
19 # Email: germano.costa@ig.com.br
20 # Twitter: wii_mano @mano_wii
22 bl_info = {
23 "name": "Extrude and Reshape",
24 "author": "Germano Cavalcante",
25 "version": (0, 8, 1),
26 "blender": (2, 76, 5),
27 "location": "View3D > TOOLS > Tools > Mesh Tools > Add: > Extrude Menu (Alt + E)",
28 "description": "Extrude face and merge edge intersections "
29 "between the mesh and the new edges",
30 "wiki_url": "http://blenderartists.org/forum/"
31 "showthread.php?376618-Addon-Push-Pull-Face",
32 "category": "Mesh"}
34 import bpy
35 import bmesh
36 from mathutils.geometry import intersect_line_line
37 from bpy.types import Operator
40 class BVHco():
41 i = 0
42 c1x = 0.0
43 c1y = 0.0
44 c1z = 0.0
45 c2x = 0.0
46 c2y = 0.0
47 c2z = 0.0
50 def edges_BVH_overlap(bm, edges, epsilon=0.0001):
51 bco = set()
52 for e in edges:
53 bvh = BVHco()
54 bvh.i = e.index
55 b1 = e.verts[0]
56 b2 = e.verts[1]
57 co1 = b1.co.x
58 co2 = b2.co.x
59 if co1 <= co2:
60 bvh.c1x = co1 - epsilon
61 bvh.c2x = co2 + epsilon
62 else:
63 bvh.c1x = co2 - epsilon
64 bvh.c2x = co1 + epsilon
65 co1 = b1.co.y
66 co2 = b2.co.y
67 if co1 <= co2:
68 bvh.c1y = co1 - epsilon
69 bvh.c2y = co2 + epsilon
70 else:
71 bvh.c1y = co2 - epsilon
72 bvh.c2y = co1 + epsilon
73 co1 = b1.co.z
74 co2 = b2.co.z
75 if co1 <= co2:
76 bvh.c1z = co1 - epsilon
77 bvh.c2z = co2 + epsilon
78 else:
79 bvh.c1z = co2 - epsilon
80 bvh.c2z = co1 + epsilon
81 bco.add(bvh)
82 del edges
83 overlap = {}
84 oget = overlap.get
85 for e1 in bm.edges:
86 by = bz = True
87 a1 = e1.verts[0]
88 a2 = e1.verts[1]
89 c1x = a1.co.x
90 c2x = a2.co.x
91 if c1x > c2x:
92 tm = c1x
93 c1x = c2x
94 c2x = tm
95 for bvh in bco:
96 if c1x <= bvh.c2x and c2x >= bvh.c1x:
97 if by:
98 by = False
99 c1y = a1.co.y
100 c2y = a2.co.y
101 if c1y > c2y:
102 tm = c1y
103 c1y = c2y
104 c2y = tm
105 if c1y <= bvh.c2y and c2y >= bvh.c1y:
106 if bz:
107 bz = False
108 c1z = a1.co.z
109 c2z = a2.co.z
110 if c1z > c2z:
111 tm = c1z
112 c1z = c2z
113 c2z = tm
114 if c1z <= bvh.c2z and c2z >= bvh.c1z:
115 e2 = bm.edges[bvh.i]
116 if e1 != e2:
117 overlap[e1] = oget(e1, set()).union({e2})
118 return overlap
121 def intersect_edges_edges(overlap, precision=4):
122 epsilon = .1**precision
123 fpre_min = -epsilon
124 fpre_max = 1 + epsilon
125 splits = {}
126 sp_get = splits.get
127 new_edges1 = set()
128 new_edges2 = set()
129 targetmap = {}
130 for edg1 in overlap:
131 # print("***", ed1.index, "***")
132 for edg2 in overlap[edg1]:
133 a1 = edg1.verts[0]
134 a2 = edg1.verts[1]
135 b1 = edg2.verts[0]
136 b2 = edg2.verts[1]
138 # test if are linked
139 if a1 in {b1, b2} or a2 in {b1, b2}:
140 # print('linked')
141 continue
143 aco1, aco2 = a1.co, a2.co
144 bco1, bco2 = b1.co, b2.co
145 tp = intersect_line_line(aco1, aco2, bco1, bco2)
146 if tp:
147 p1, p2 = tp
148 if (p1 - p2).to_tuple(precision) == (0, 0, 0):
149 v = aco2 - aco1
150 f = p1 - aco1
151 x, y, z = abs(v.x), abs(v.y), abs(v.z)
152 max1 = 0 if x >= y and x >= z else\
153 1 if y >= x and y >= z else 2
154 fac1 = f[max1] / v[max1]
156 v = bco2 - bco1
157 f = p2 - bco1
158 x, y, z = abs(v.x), abs(v.y), abs(v.z)
159 max2 = 0 if x >= y and x >= z else\
160 1 if y >= x and y >= z else 2
161 fac2 = f[max2] / v[max2]
163 if fpre_min <= fac1 <= fpre_max:
164 # print(edg1.index, 'can intersect', edg2.index)
165 ed1 = edg1
167 elif edg1 in splits:
168 for ed1 in splits[edg1]:
169 a1 = ed1.verts[0]
170 a2 = ed1.verts[1]
172 vco1 = a1.co
173 vco2 = a2.co
175 v = vco2 - vco1
176 f = p1 - vco1
177 fac1 = f[max1] / v[max1]
178 if fpre_min <= fac1 <= fpre_max:
179 # print(e.index, 'can intersect', edg2.index)
180 break
181 else:
182 # print(edg1.index, 'really does not intersect', edg2.index)
183 continue
184 else:
185 # print(edg1.index, 'not intersect', edg2.index)
186 continue
188 if fpre_min <= fac2 <= fpre_max:
189 # print(ed1.index, 'actually intersect', edg2.index)
190 ed2 = edg2
192 elif edg2 in splits:
193 for ed2 in splits[edg2]:
194 b1 = ed2.verts[0]
195 b2 = ed2.verts[1]
197 vco1 = b1.co
198 vco2 = b2.co
200 v = vco2 - vco1
201 f = p2 - vco1
202 fac2 = f[max2] / v[max2]
203 if fpre_min <= fac2 <= fpre_max:
204 # print(ed1.index, 'actually intersect', e.index)
205 break
206 else:
207 # print(ed1.index, 'really does not intersect', ed2.index)
208 continue
209 else:
210 # print(ed1.index, 'not intersect', edg2.index)
211 continue
213 new_edges1.add(ed1)
214 new_edges2.add(ed2)
216 if abs(fac1) <= epsilon:
217 nv1 = a1
218 elif fac1 + epsilon >= 1:
219 nv1 = a2
220 else:
221 ne1, nv1 = bmesh.utils.edge_split(ed1, a1, fac1)
222 new_edges1.add(ne1)
223 splits[edg1] = sp_get(edg1, set()).union({ne1})
225 if abs(fac2) <= epsilon:
226 nv2 = b1
227 elif fac2 + epsilon >= 1:
228 nv2 = b2
229 else:
230 ne2, nv2 = bmesh.utils.edge_split(ed2, b1, fac2)
231 new_edges2.add(ne2)
232 splits[edg2] = sp_get(edg2, set()).union({ne2})
234 if nv1 != nv2: # necessary?
235 targetmap[nv1] = nv2
237 return new_edges1, new_edges2, targetmap
240 class Extrude_and_Reshape(Operator):
241 bl_idname = "mesh.extrude_reshape"
242 bl_label = "Extrude and Reshape"
243 bl_description = "Push and pull face entities to sculpt 3d models"
244 bl_options = {'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}
246 @classmethod
247 def poll(cls, context):
248 return context.mode is not 'EDIT_MESH'
250 def modal(self, context, event):
251 if self.confirm:
252 sface = self.bm.faces.active
253 if not sface:
254 for face in self.bm.faces:
255 if face.select is True:
256 sface = face
257 break
258 else:
259 return {'FINISHED'}
260 # edges to intersect
261 edges = set()
262 [[edges.add(ed) for ed in v.link_edges] for v in sface.verts]
264 overlap = edges_BVH_overlap(self.bm, edges, epsilon=0.0001)
265 overlap = {k: v for k, v in overlap.items() if k not in edges} # remove repetition
267 print([e.index for e in edges])
268 for a, b in overlap.items():
269 print(a.index, [e.index for e in b])
271 new_edges1, new_edges2, targetmap = intersect_edges_edges(overlap)
272 pos_weld = set()
273 for e in new_edges1:
274 v1, v2 = e.verts
275 if v1 in targetmap and v2 in targetmap:
276 pos_weld.add((targetmap[v1], targetmap[v2]))
277 if targetmap:
278 bmesh.ops.weld_verts(self.bm, targetmap=targetmap)
280 print([e.is_valid for e in new_edges1])
281 print([e.is_valid for e in new_edges2])
282 sp_faces1 = set()
284 for e in pos_weld:
285 v1, v2 = e
286 lf1 = set(v1.link_faces)
287 lf2 = set(v2.link_faces)
288 rlfe = lf1.intersection(lf2)
289 for f in rlfe:
290 try:
291 nf = bmesh.utils.face_split(f, v1, v2)
292 # sp_faces1.update({f, nf[0]})
293 except:
294 pass
296 # sp_faces2 = set()
297 for e in new_edges2:
298 lfe = set(e.link_faces)
299 v1, v2 = e.verts
300 lf1 = set(v1.link_faces)
301 lf2 = set(v2.link_faces)
302 rlfe = lf1.intersection(lf2)
303 for f in rlfe.difference(lfe):
304 nf = bmesh.utils.face_split(f, v1, v2)
305 # sp_faces2.update({f, nf[0]})
307 bmesh.update_edit_mesh(self.mesh, tessface=True, destructive=True)
308 return {'FINISHED'}
309 if self.cancel:
310 return {'FINISHED'}
311 self.cancel = event.type in {'ESC', 'NDOF_BUTTON_ESC'}
312 self.confirm = event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'}
313 return {'PASS_THROUGH'}
315 def execute(self, context):
316 self.mesh = context.object.data
317 self.bm = bmesh.from_edit_mesh(self.mesh)
318 try:
319 selection = self.bm.select_history[-1]
320 except:
321 for face in self.bm.faces:
322 if face.select is True:
323 selection = face
324 break
325 else:
326 return {'FINISHED'}
327 if not isinstance(selection, bmesh.types.BMFace):
328 bpy.ops.mesh.extrude_region_move('INVOKE_DEFAULT')
329 return {'FINISHED'}
330 else:
331 face = selection
332 # face.select = False
333 bpy.ops.mesh.select_all(action='DESELECT')
334 geom = []
335 for edge in face.edges:
336 if abs(edge.calc_face_angle(0) - 1.5707963267948966) < 0.01: # self.angle_tolerance:
337 geom.append(edge)
339 ret_dict = bmesh.ops.extrude_discrete_faces(self.bm, faces=[face])
341 for face in ret_dict['faces']:
342 self.bm.faces.active = face
343 face.select = True
344 sface = face
345 dfaces = bmesh.ops.dissolve_edges(
346 self.bm, edges=geom, use_verts=True, use_face_split=False
348 bmesh.update_edit_mesh(self.mesh, tessface=True, destructive=True)
349 bpy.ops.transform.translate(
350 'INVOKE_DEFAULT', constraint_axis=(False, False, True),
351 constraint_orientation='NORMAL', release_confirm=True
354 context.window_manager.modal_handler_add(self)
356 self.cancel = False
357 self.confirm = False
358 return {'RUNNING_MODAL'}
361 def operator_draw(self, context):
362 layout = self.layout
363 col = layout.column(align=True)
364 col.operator("mesh.extrude_reshape", text="Extrude and Reshape")
367 def register():
368 bpy.utils.register_class(Extrude_and_Reshape)
369 bpy.types.VIEW3D_MT_edit_mesh_extrude.append(operator_draw)
372 def unregister():
373 bpy.types.VIEW3D_MT_edit_mesh_extrude.remove(operator_draw)
374 bpy.utils.unregister_class(Extrude_and_Reshape)
377 if __name__ == "__main__":
378 register()