Cleanup: simplify file name incrementing logic
[blender-addons.git] / mesh_tools / mesh_extrude_and_reshape.py
blobee26cdcde9ee2083e24d9e434b3ca6489e69a302
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, 80, 0),
27 "location": "View3D > UI > 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 "doc_url": "http://blenderartists.org/forum/"
31 "showthread.php?376618-Addon-Push-Pull-Face",
32 "category": "Mesh",
35 import bpy
36 import bmesh
37 from mathutils.geometry import intersect_line_line
38 from bpy.types import Operator
41 class BVHco():
42 i = 0
43 c1x = 0.0
44 c1y = 0.0
45 c1z = 0.0
46 c2x = 0.0
47 c2y = 0.0
48 c2z = 0.0
51 def edges_BVH_overlap(bm, edges, epsilon=0.0001):
52 bco = set()
53 for e in edges:
54 bvh = BVHco()
55 bvh.i = e.index
56 b1 = e.verts[0]
57 b2 = e.verts[1]
58 co1 = b1.co.x
59 co2 = b2.co.x
60 if co1 <= co2:
61 bvh.c1x = co1 - epsilon
62 bvh.c2x = co2 + epsilon
63 else:
64 bvh.c1x = co2 - epsilon
65 bvh.c2x = co1 + epsilon
66 co1 = b1.co.y
67 co2 = b2.co.y
68 if co1 <= co2:
69 bvh.c1y = co1 - epsilon
70 bvh.c2y = co2 + epsilon
71 else:
72 bvh.c1y = co2 - epsilon
73 bvh.c2y = co1 + epsilon
74 co1 = b1.co.z
75 co2 = b2.co.z
76 if co1 <= co2:
77 bvh.c1z = co1 - epsilon
78 bvh.c2z = co2 + epsilon
79 else:
80 bvh.c1z = co2 - epsilon
81 bvh.c2z = co1 + epsilon
82 bco.add(bvh)
83 del edges
84 overlap = {}
85 oget = overlap.get
86 for e1 in bm.edges:
87 by = bz = True
88 a1 = e1.verts[0]
89 a2 = e1.verts[1]
90 c1x = a1.co.x
91 c2x = a2.co.x
92 if c1x > c2x:
93 tm = c1x
94 c1x = c2x
95 c2x = tm
96 for bvh in bco:
97 if c1x <= bvh.c2x and c2x >= bvh.c1x:
98 if by:
99 by = False
100 c1y = a1.co.y
101 c2y = a2.co.y
102 if c1y > c2y:
103 tm = c1y
104 c1y = c2y
105 c2y = tm
106 if c1y <= bvh.c2y and c2y >= bvh.c1y:
107 if bz:
108 bz = False
109 c1z = a1.co.z
110 c2z = a2.co.z
111 if c1z > c2z:
112 tm = c1z
113 c1z = c2z
114 c2z = tm
115 if c1z <= bvh.c2z and c2z >= bvh.c1z:
116 e2 = bm.edges[bvh.i]
117 if e1 != e2:
118 overlap[e1] = oget(e1, set()).union({e2})
119 return overlap
122 def intersect_edges_edges(overlap, precision=4):
123 epsilon = .1**precision
124 fpre_min = -epsilon
125 fpre_max = 1 + epsilon
126 splits = {}
127 sp_get = splits.get
128 new_edges1 = set()
129 new_edges2 = set()
130 targetmap = {}
131 for edg1 in overlap:
132 # print("***", ed1.index, "***")
133 for edg2 in overlap[edg1]:
134 a1 = edg1.verts[0]
135 a2 = edg1.verts[1]
136 b1 = edg2.verts[0]
137 b2 = edg2.verts[1]
139 # test if are linked
140 if a1 in {b1, b2} or a2 in {b1, b2}:
141 # print('linked')
142 continue
144 aco1, aco2 = a1.co, a2.co
145 bco1, bco2 = b1.co, b2.co
146 tp = intersect_line_line(aco1, aco2, bco1, bco2)
147 if tp:
148 p1, p2 = tp
149 if (p1 - p2).to_tuple(precision) == (0, 0, 0):
150 v = aco2 - aco1
151 f = p1 - aco1
152 x, y, z = abs(v.x), abs(v.y), abs(v.z)
153 max1 = 0 if x >= y and x >= z else\
154 1 if y >= x and y >= z else 2
155 fac1 = f[max1] / v[max1]
157 v = bco2 - bco1
158 f = p2 - bco1
159 x, y, z = abs(v.x), abs(v.y), abs(v.z)
160 max2 = 0 if x >= y and x >= z else\
161 1 if y >= x and y >= z else 2
162 fac2 = f[max2] / v[max2]
164 if fpre_min <= fac1 <= fpre_max:
165 # print(edg1.index, 'can intersect', edg2.index)
166 ed1 = edg1
168 elif edg1 in splits:
169 for ed1 in splits[edg1]:
170 a1 = ed1.verts[0]
171 a2 = ed1.verts[1]
173 vco1 = a1.co
174 vco2 = a2.co
176 v = vco2 - vco1
177 f = p1 - vco1
178 fac1 = f[max1] / v[max1]
179 if fpre_min <= fac1 <= fpre_max:
180 # print(e.index, 'can intersect', edg2.index)
181 break
182 else:
183 # print(edg1.index, 'really does not intersect', edg2.index)
184 continue
185 else:
186 # print(edg1.index, 'not intersect', edg2.index)
187 continue
189 if fpre_min <= fac2 <= fpre_max:
190 # print(ed1.index, 'actually intersect', edg2.index)
191 ed2 = edg2
193 elif edg2 in splits:
194 for ed2 in splits[edg2]:
195 b1 = ed2.verts[0]
196 b2 = ed2.verts[1]
198 vco1 = b1.co
199 vco2 = b2.co
201 v = vco2 - vco1
202 f = p2 - vco1
203 fac2 = f[max2] / v[max2]
204 if fpre_min <= fac2 <= fpre_max:
205 # print(ed1.index, 'actually intersect', e.index)
206 break
207 else:
208 # print(ed1.index, 'really does not intersect', ed2.index)
209 continue
210 else:
211 # print(ed1.index, 'not intersect', edg2.index)
212 continue
214 new_edges1.add(ed1)
215 new_edges2.add(ed2)
217 if abs(fac1) <= epsilon:
218 nv1 = a1
219 elif fac1 + epsilon >= 1:
220 nv1 = a2
221 else:
222 ne1, nv1 = bmesh.utils.edge_split(ed1, a1, fac1)
223 new_edges1.add(ne1)
224 splits[edg1] = sp_get(edg1, set()).union({ne1})
226 if abs(fac2) <= epsilon:
227 nv2 = b1
228 elif fac2 + epsilon >= 1:
229 nv2 = b2
230 else:
231 ne2, nv2 = bmesh.utils.edge_split(ed2, b1, fac2)
232 new_edges2.add(ne2)
233 splits[edg2] = sp_get(edg2, set()).union({ne2})
235 if nv1 != nv2: # necessary?
236 targetmap[nv1] = nv2
238 return new_edges1, new_edges2, targetmap
241 class ER_OT_Extrude_and_Reshape(Operator):
242 bl_idname = "mesh.extrude_reshape"
243 bl_label = "Extrude and Reshape"
244 bl_description = "Push and pull face entities to sculpt 3d models"
245 bl_options = {'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}
247 @classmethod
248 def poll(cls, context):
249 if context.mode=='EDIT_MESH':
250 return True
252 def modal(self, context, event):
253 if self.confirm:
254 sface = self.bm.faces.active
255 if not sface:
256 for face in self.bm.faces:
257 if face.select is True:
258 sface = face
259 break
260 else:
261 return {'FINISHED'}
262 # edges to intersect
263 edges = set()
264 [[edges.add(ed) for ed in v.link_edges] for v in sface.verts]
266 overlap = edges_BVH_overlap(self.bm, edges, epsilon=0.0001)
267 overlap = {k: v for k, v in overlap.items() if k not in edges} # remove repetition
269 print([e.index for e in edges])
270 for a, b in overlap.items():
271 print(a.index, [e.index for e in b])
273 new_edges1, new_edges2, targetmap = intersect_edges_edges(overlap)
274 pos_weld = set()
275 for e in new_edges1:
276 v1, v2 = e.verts
277 if v1 in targetmap and v2 in targetmap:
278 pos_weld.add((targetmap[v1], targetmap[v2]))
279 if targetmap:
280 bmesh.ops.weld_verts(self.bm, targetmap=targetmap)
282 print([e.is_valid for e in new_edges1])
283 print([e.is_valid for e in new_edges2])
284 sp_faces1 = set()
286 for e in pos_weld:
287 v1, v2 = e
288 lf1 = set(v1.link_faces)
289 lf2 = set(v2.link_faces)
290 rlfe = lf1.intersection(lf2)
291 for f in rlfe:
292 try:
293 nf = bmesh.utils.face_split(f, v1, v2)
294 # sp_faces1.update({f, nf[0]})
295 except:
296 pass
298 # sp_faces2 = set()
299 for e in new_edges2:
300 lfe = set(e.link_faces)
301 v1, v2 = e.verts
302 lf1 = set(v1.link_faces)
303 lf2 = set(v2.link_faces)
304 rlfe = lf1.intersection(lf2)
305 for f in rlfe.difference(lfe):
306 nf = bmesh.utils.face_split(f, v1, v2)
307 # sp_faces2.update({f, nf[0]})
309 bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
310 return {'FINISHED'}
311 if self.cancel:
312 return {'FINISHED'}
313 self.cancel = event.type in {'ESC', 'NDOF_BUTTON_ESC'}
314 self.confirm = event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'}
315 return {'PASS_THROUGH'}
317 def execute(self, context):
318 self.mesh = context.object.data
319 self.bm = bmesh.from_edit_mesh(self.mesh)
320 try:
321 selection = self.bm.select_history[-1]
322 except:
323 for face in self.bm.faces:
324 if face.select is True:
325 selection = face
326 break
327 else:
328 return {'FINISHED'}
329 if not isinstance(selection, bmesh.types.BMFace):
330 bpy.ops.mesh.extrude_region_move('INVOKE_DEFAULT')
331 return {'FINISHED'}
332 else:
333 face = selection
334 # face.select = False
335 bpy.ops.mesh.select_all(action='DESELECT')
336 geom = []
337 for edge in face.edges:
338 if abs(edge.calc_face_angle(0) - 1.5707963267948966) < 0.01: # self.angle_tolerance:
339 geom.append(edge)
341 ret_dict = bmesh.ops.extrude_discrete_faces(self.bm, faces=[face])
343 for face in ret_dict['faces']:
344 self.bm.faces.active = face
345 face.select = True
346 sface = face
347 dfaces = bmesh.ops.dissolve_edges(
348 self.bm, edges=geom, use_verts=True, use_face_split=False
350 bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
351 bpy.ops.transform.translate(
352 'INVOKE_DEFAULT', constraint_axis=(False, False, True),
353 orient_type='NORMAL', release_confirm=True
356 context.window_manager.modal_handler_add(self)
358 self.cancel = False
359 self.confirm = False
360 return {'RUNNING_MODAL'}
363 def operator_draw(self, context):
364 layout = self.layout
365 col = layout.column(align=True)
366 col.operator("mesh.extrude_reshape")
369 def register():
370 bpy.utils.register_class(ER_OT_Extrude_and_Reshape)
373 def unregister():
374 bpy.utils.unregister_class(ER_OT_Extrude_and_Reshape)
377 if __name__ == "__main__":
378 register()