Merge branch 'blender-v3.6-release'
[blender-addons.git] / mesh_snap_utilities_line / op_line.py
blobeacc551be20a947c98660b1fadb975dfec77ad88
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 import bpy
4 import bmesh
6 from mathutils import Vector
7 from mathutils.geometry import intersect_point_line
9 from .common_utilities import snap_utilities
10 from .common_classes import (
11 CharMap,
12 Constrain,
13 SnapNavigation,
14 SnapUtilities,
18 if not __package__:
19 __package__ = "mesh_snap_utilities_line"
22 def get_closest_edge(bm, point, dist):
23 r_edge = None
24 for edge in bm.edges:
25 v1 = edge.verts[0].co
26 v2 = edge.verts[1].co
27 # Test the BVH (AABB) first
28 for i in range(3):
29 if v1[i] <= v2[i]:
30 isect = v1[i] - dist <= point[i] <= v2[i] + dist
31 else:
32 isect = v2[i] - dist <= point[i] <= v1[i] + dist
34 if not isect:
35 break
36 else:
37 ret = intersect_point_line(point, v1, v2)
39 if ret[1] < 0.0:
40 tmp = v1
41 elif ret[1] > 1.0:
42 tmp = v2
43 else:
44 tmp = ret[0]
46 new_dist = (point - tmp).length
47 if new_dist <= dist:
48 dist = new_dist
49 r_edge = edge
51 return r_edge
54 def get_loose_linked_edges(vert):
55 linked = [e for e in vert.link_edges if e.is_wire]
56 for e in linked:
57 linked += [le for v in e.verts if v.is_wire for le in v.link_edges if le not in linked]
58 return linked
61 def make_line(self, bm_geom, location):
62 obj = self.main_snap_obj.data[0]
63 bm = self.main_bm
64 split_faces = set()
66 update_edit_mesh = False
68 if bm_geom is None:
69 vert = bm.verts.new(location)
70 self.list_verts.append(vert)
71 update_edit_mesh = True
73 elif isinstance(bm_geom, bmesh.types.BMVert):
74 if (bm_geom.co - location).length_squared < .001:
75 if self.list_verts == [] or self.list_verts[-1] != bm_geom:
76 self.list_verts.append(bm_geom)
77 else:
78 vert = bm.verts.new(location)
79 self.list_verts.append(vert)
80 update_edit_mesh = True
82 elif isinstance(bm_geom, bmesh.types.BMEdge):
83 self.list_edges.append(bm_geom)
84 ret = intersect_point_line(
85 location, bm_geom.verts[0].co, bm_geom.verts[1].co)
87 if (ret[0] - location).length_squared < .001:
88 if ret[1] == 0.0:
89 vert = bm_geom.verts[0]
90 elif ret[1] == 1.0:
91 vert = bm_geom.verts[1]
92 else:
93 edge, vert = bmesh.utils.edge_split(
94 bm_geom, bm_geom.verts[0], ret[1])
95 update_edit_mesh = True
97 if self.list_verts == [] or self.list_verts[-1] != vert:
98 self.list_verts.append(vert)
99 self.geom = vert # hack to highlight in the drawing
100 # self.list_edges.append(edge)
102 else: # constrain point is near
103 vert = bm.verts.new(location)
104 self.list_verts.append(vert)
105 update_edit_mesh = True
107 elif isinstance(bm_geom, bmesh.types.BMFace):
108 split_faces.add(bm_geom)
109 vert = bm.verts.new(location)
110 self.list_verts.append(vert)
111 update_edit_mesh = True
113 # draw, split and create face
114 if len(self.list_verts) >= 2:
115 v1, v2 = self.list_verts[-2:]
116 edge = bm.edges.get([v1, v2])
117 if edge:
118 self.list_edges.append(edge)
119 else:
120 if not v2.link_edges:
121 edge = bm.edges.new([v1, v2])
122 self.list_edges.append(edge)
123 else: # split face
124 v1_link_faces = v1.link_faces
125 v2_link_faces = v2.link_faces
126 if v1_link_faces and v2_link_faces:
127 split_faces.update(
128 set(v1_link_faces).intersection(v2_link_faces))
130 else:
131 if v1_link_faces:
132 faces = v1_link_faces
133 co2 = v2.co.copy()
134 else:
135 faces = v2_link_faces
136 co2 = v1.co.copy()
138 for face in faces:
139 if bmesh.geometry.intersect_face_point(face, co2):
140 co = co2 - face.calc_center_median()
141 if co.dot(face.normal) < 0.001:
142 split_faces.add(face)
144 if split_faces:
145 edge = bm.edges.new([v1, v2])
146 self.list_edges.append(edge)
147 ed_list = get_loose_linked_edges(v2)
148 for face in split_faces:
149 facesp = bmesh.utils.face_split_edgenet(face, ed_list)
150 del split_faces
151 else:
152 if self.intersect:
153 facesp = bmesh.ops.connect_vert_pair(
154 bm, verts=[v1, v2], verts_exclude=bm.verts)
155 # print(facesp)
156 if not self.intersect or not facesp['edges']:
157 edge = bm.edges.new([v1, v2])
158 self.list_edges.append(edge)
159 else:
160 for edge in facesp['edges']:
161 self.list_edges.append(edge)
162 update_edit_mesh = True
164 # create face
165 if self.create_face:
166 ed_list = set(self.list_edges)
167 for edge in v2.link_edges:
168 if edge not in ed_list and edge.other_vert(v2) in self.list_verts:
169 ed_list.add(edge)
170 break
172 ed_list.update(get_loose_linked_edges(v2))
173 ed_list = list(ed_list)
175 # WORKAROUND: `edgenet_fill` only works with loose edges or boundary
176 # edges, so remove the other edges and create temporary elements to
177 # replace them.
178 targetmap = {}
179 ed_new = []
180 for edge in ed_list:
181 if not edge.is_wire and not edge.is_boundary:
182 v1, v2 = edge.verts
183 tmp_vert = bm.verts.new(v2.co)
184 e1 = bm.edges.new([v1, tmp_vert])
185 e2 = bm.edges.new([tmp_vert, v2])
186 ed_list.remove(edge)
187 ed_new.append(e1)
188 ed_new.append(e2)
189 targetmap[tmp_vert] = v2
191 bmesh.ops.edgenet_fill(bm, edges=ed_list + ed_new)
192 if targetmap:
193 bmesh.ops.weld_verts(bm, targetmap=targetmap)
195 update_edit_mesh = True
196 # print('face created')
198 if update_edit_mesh:
199 obj.data.update_gpu_tag()
200 obj.data.update_tag()
201 obj.update_from_editmode()
202 obj.update_tag()
203 bmesh.update_edit_mesh(obj.data)
204 self.sctx.tag_update_drawn_snap_object(self.main_snap_obj)
205 # bm.verts.index_update()
207 bpy.ops.ed.undo_push(message="Undo draw line*")
209 return [obj.matrix_world @ v.co for v in self.list_verts]
212 class SnapUtilitiesLine(SnapUtilities, bpy.types.Operator):
213 """Make Lines. Connect them to split faces"""
214 bl_idname = "mesh.snap_utilities_line"
215 bl_label = "Line Tool"
216 bl_options = {'REGISTER'}
218 wait_for_input: bpy.props.BoolProperty(name="Wait for Input", default=True)
220 def _exit(self, context):
221 # avoids unpredictable crashes
222 del self.main_snap_obj
223 del self.main_bm
224 del self.list_edges
225 del self.list_verts
226 del self.list_verts_co
228 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
229 context.area.header_text_set(None)
230 self.snap_context_free()
232 # Restore initial state
233 context.tool_settings.mesh_select_mode = self.select_mode
234 context.space_data.overlay.show_face_center = self.show_face_center
236 def _init_snap_line_context(self, context):
237 self.prevloc = Vector()
238 self.list_verts = []
239 self.list_edges = []
240 self.list_verts_co = []
241 self.bool_update = True
242 self.vector_constrain = ()
243 self.len = 0
245 if not (self.bm and self.obj):
246 self.obj = context.edit_object
247 self.bm = bmesh.from_edit_mesh(self.obj.data)
249 self.main_snap_obj = self.snap_obj = self.sctx._get_snap_obj_by_obj(
250 self.obj)
251 self.main_bm = self.bm
253 def _shift_contrain_callback(self):
254 if isinstance(self.geom, bmesh.types.BMEdge):
255 mat = self.main_snap_obj.mat
256 verts_co = [mat @ v.co for v in self.geom.verts]
257 return verts_co[1] - verts_co[0]
259 def modal(self, context, event):
260 if self.navigation_ops.run(context, event, self.prevloc if self.vector_constrain else self.location):
261 return {'RUNNING_MODAL'}
263 context.area.tag_redraw()
265 if event.ctrl and event.type == 'Z' and event.value == 'PRESS':
266 bpy.ops.ed.undo()
267 if not self.wait_for_input:
268 self._exit(context)
269 return {'FINISHED'}
270 else:
271 del self.bm
272 del self.main_bm
273 self.charmap.clear()
275 bpy.ops.object.mode_set(mode='EDIT') # just to be sure
276 bpy.ops.mesh.select_all(action='DESELECT')
277 context.tool_settings.mesh_select_mode = (True, False, True)
278 context.space_data.overlay.show_face_center = True
280 self.snap_context_update(context)
281 self._init_snap_line_context(context)
282 self.sctx.update_all()
284 return {'RUNNING_MODAL'}
286 is_making_lines = bool(self.list_verts_co)
288 if (event.type == 'MOUSEMOVE' or self.bool_update) and self.charmap.length_entered_value == 0.0:
289 mval = Vector((event.mouse_region_x, event.mouse_region_y))
291 if self.rv3d.view_matrix != self.rotMat:
292 self.rotMat = self.rv3d.view_matrix.copy()
293 self.bool_update = True
294 snap_utilities.cache.clear()
295 else:
296 self.bool_update = False
298 self.snap_obj, self.prevloc, self.location, self.type, self.bm, self.geom, self.len = snap_utilities(
299 self.sctx,
300 self.main_snap_obj,
301 mval,
302 constrain=self.vector_constrain,
303 previous_vert=(
304 self.list_verts[-1] if self.list_verts else None),
305 increment=self.incremental)
307 self.snap_to_grid()
309 if is_making_lines and self.preferences.auto_constrain:
310 loc = self.list_verts_co[-1]
311 vec, type = self.constrain.update(
312 self.sctx.region, self.sctx.rv3d, mval, loc)
313 self.vector_constrain = [loc, loc + vec, type]
315 if event.value == 'PRESS':
316 if is_making_lines and self.charmap.modal_(context, event):
317 self.bool_update = self.charmap.length_entered_value == 0.0
319 if not self.bool_update:
320 text_value = self.charmap.length_entered_value
321 vector = (self.location -
322 self.list_verts_co[-1]).normalized()
323 self.location = self.list_verts_co[-1] + \
324 (vector * text_value)
325 del vector
327 elif self.constrain.modal(event, self._shift_contrain_callback):
328 self.bool_update = True
329 if self.constrain.last_vec:
330 if self.list_verts_co:
331 loc = self.list_verts_co[-1]
332 else:
333 loc = self.location
335 self.vector_constrain = (
336 loc, loc + self.constrain.last_vec, self.constrain.last_type)
337 else:
338 self.vector_constrain = None
340 elif event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'}:
341 if event.type == 'LEFTMOUSE' or self.charmap.length_entered_value != 0.0:
342 if not is_making_lines and self.bm:
343 self.main_snap_obj = self.snap_obj
344 self.main_bm = self.bm
346 mat_inv = self.main_snap_obj.mat.inverted_safe()
347 point = mat_inv @ self.location
348 geom2 = self.geom
349 if geom2:
350 geom2.select = False
352 if self.vector_constrain:
353 geom2 = get_closest_edge(self.main_bm, point, .001)
355 self.list_verts_co = make_line(self, geom2, point)
357 self.vector_constrain = None
358 self.charmap.clear()
359 else:
360 self._exit(context)
361 return {'FINISHED'}
363 elif event.type == 'F8':
364 self.vector_constrain = None
365 self.constrain.toggle()
367 elif event.type in {'RIGHTMOUSE', 'ESC'}:
368 if not self.wait_for_input or not is_making_lines or event.type == 'ESC':
369 if self.geom:
370 self.geom.select = True
371 self._exit(context)
372 return {'FINISHED'}
373 else:
374 snap_utilities.cache.clear()
375 self.vector_constrain = None
376 self.list_edges = []
377 self.list_verts = []
378 self.list_verts_co = []
379 self.charmap.clear()
381 a = ""
382 if is_making_lines:
383 a = 'length: ' + self.charmap.get_converted_length_str(self.len)
385 context.area.header_text_set(
386 text="hit: %.3f %.3f %.3f %s" % (*self.location, a))
388 if True or is_making_lines:
389 return {'RUNNING_MODAL'}
391 return {'PASS_THROUGH'}
393 def draw_callback_px(self):
394 if self.bm:
395 self.draw_cache.draw_elem(self.snap_obj, self.bm, self.geom)
396 self.draw_cache.draw(self.type, self.location,
397 self.list_verts_co, self.vector_constrain, self.prevloc)
399 def invoke(self, context, event):
400 if context.space_data.type == 'VIEW_3D':
401 self.snap_context_init(context)
402 self.snap_context_update(context)
404 self.constrain = Constrain(
405 self.preferences, context.scene, self.obj)
407 self.intersect = self.preferences.intersect
408 self.create_face = self.preferences.create_face
409 self.navigation_ops = SnapNavigation(context, True)
410 self.charmap = CharMap(context)
412 self._init_snap_line_context(context)
414 # print('name', __name__, __package__)
416 # Store current state
417 self.select_mode = context.tool_settings.mesh_select_mode[:]
418 self.show_face_center = context.space_data.overlay.show_face_center
420 # Modify the current state
421 bpy.ops.mesh.select_all(action='DESELECT')
422 context.tool_settings.mesh_select_mode = (True, False, True)
423 context.space_data.overlay.show_face_center = True
425 # Store values from 3d view context
426 self.rv3d = context.region_data
427 self.rotMat = self.rv3d.view_matrix.copy()
428 # self.obj_matrix.transposed())
430 # modals
431 context.window_manager.modal_handler_add(self)
433 if not self.wait_for_input:
434 if not self.snapwidgets:
435 self.modal(context, event)
436 else:
437 mat_inv = self.obj.matrix_world.inverted_safe()
438 point = mat_inv @ self.location
439 self.list_verts_co = make_line(self, self.geom, point)
441 self._handle = bpy.types.SpaceView3D.draw_handler_add(
442 self.draw_callback_px, (), 'WINDOW', 'POST_VIEW')
444 return {'RUNNING_MODAL'}
445 else:
446 self.report({'WARNING'}, "Active space must be a View3d")
447 return {'CANCELLED'}
450 def register():
451 bpy.utils.register_class(SnapUtilitiesLine)
454 if __name__ == "__main__":
455 register()