Export_3ds: Improved distance cue node search
[blender-addons.git] / mesh_snap_utilities_line / op_line.py
blobb2a7298c57502592a4b7a967bb02ed817f18b79e
1 # SPDX-FileCopyrightText: 2018-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
6 import bmesh
8 from mathutils import Vector
9 from mathutils.geometry import intersect_point_line
11 from .snap_context_l.utils_projection import intersect_ray_ray_fac
13 from .common_utilities import snap_utilities
14 from .common_classes import (
15 CharMap,
16 Constrain,
17 SnapNavigation,
18 SnapUtilities,
22 if not __package__:
23 __package__ = "mesh_snap_utilities_line"
26 def get_closest_edge(bm, point, dist):
27 r_edge = None
28 for edge in bm.edges:
29 v1 = edge.verts[0].co
30 v2 = edge.verts[1].co
31 # Test the BVH (AABB) first
32 for i in range(3):
33 if v1[i] <= v2[i]:
34 isect = v1[i] - dist <= point[i] <= v2[i] + dist
35 else:
36 isect = v2[i] - dist <= point[i] <= v1[i] + dist
38 if not isect:
39 break
40 else:
41 ret = intersect_point_line(point, v1, v2)
43 if ret[1] < 0.0:
44 tmp = v1
45 elif ret[1] > 1.0:
46 tmp = v2
47 else:
48 tmp = ret[0]
50 new_dist = (point - tmp).length
51 if new_dist <= dist:
52 dist = new_dist
53 r_edge = edge
55 return r_edge
58 def get_loose_linked_edges(vert):
59 linked = [e for e in vert.link_edges if e.is_wire]
60 for e in linked:
61 linked += [le for v in e.verts if v.is_wire for le in v.link_edges if le not in linked]
62 return linked
65 def make_line(self, bm_geom, location):
66 obj = self.main_snap_obj.data[0]
67 bm = self.main_bm
68 split_faces = set()
70 update_edit_mesh = False
72 if bm_geom is None:
73 vert = bm.verts.new(location)
74 self.list_verts.append(vert)
75 update_edit_mesh = True
77 elif isinstance(bm_geom, bmesh.types.BMVert):
78 if (bm_geom.co - location).length_squared < .001:
79 if self.list_verts == [] or self.list_verts[-1] != bm_geom:
80 self.list_verts.append(bm_geom)
81 else:
82 vert = bm.verts.new(location)
83 self.list_verts.append(vert)
84 update_edit_mesh = True
86 elif isinstance(bm_geom, bmesh.types.BMEdge):
87 self.list_edges.append(bm_geom)
88 ret = intersect_point_line(
89 location, bm_geom.verts[0].co, bm_geom.verts[1].co)
91 if (ret[0] - location).length_squared < .001:
92 if ret[1] == 0.0:
93 vert = bm_geom.verts[0]
94 elif ret[1] == 1.0:
95 vert = bm_geom.verts[1]
96 else:
97 edge, vert = bmesh.utils.edge_split(
98 bm_geom, bm_geom.verts[0], ret[1])
99 update_edit_mesh = True
101 if self.list_verts == [] or self.list_verts[-1] != vert:
102 self.list_verts.append(vert)
103 self.geom = vert # hack to highlight in the drawing
104 # self.list_edges.append(edge)
106 else: # constrain point is near
107 vert = bm.verts.new(location)
108 self.list_verts.append(vert)
109 update_edit_mesh = True
111 elif isinstance(bm_geom, bmesh.types.BMFace):
112 split_faces.add(bm_geom)
113 vert = bm.verts.new(location)
114 self.list_verts.append(vert)
115 update_edit_mesh = True
117 # draw, split and create face
118 if len(self.list_verts) >= 2:
119 v1, v2 = self.list_verts[-2:]
120 edge = bm.edges.get([v1, v2])
121 if edge:
122 self.list_edges.append(edge)
123 else:
124 if not v2.link_edges:
125 edge = bm.edges.new([v1, v2])
126 self.list_edges.append(edge)
127 else: # split face
128 v1_link_faces = v1.link_faces
129 v2_link_faces = v2.link_faces
130 if v1_link_faces and v2_link_faces:
131 split_faces.update(
132 set(v1_link_faces).intersection(v2_link_faces))
134 else:
135 if v1_link_faces:
136 faces = v1_link_faces
137 co2 = v2.co.copy()
138 else:
139 faces = v2_link_faces
140 co2 = v1.co.copy()
142 for face in faces:
143 if bmesh.geometry.intersect_face_point(face, co2):
144 co = co2 - face.calc_center_median()
145 if co.dot(face.normal) < 0.001:
146 split_faces.add(face)
148 if split_faces:
149 edge = bm.edges.new([v1, v2])
150 self.list_edges.append(edge)
151 ed_list = get_loose_linked_edges(v2)
152 for face in split_faces:
153 facesp = bmesh.utils.face_split_edgenet(face, ed_list)
154 del split_faces
155 else:
156 if self.intersect:
157 facesp = bmesh.ops.connect_vert_pair(
158 bm, verts=[v1, v2], verts_exclude=bm.verts)
159 # print(facesp)
160 if not self.intersect or not facesp['edges']:
161 edge = bm.edges.new([v1, v2])
162 self.list_edges.append(edge)
163 else:
164 for edge in facesp['edges']:
165 self.list_edges.append(edge)
166 update_edit_mesh = True
168 # create face
169 if self.create_face:
170 ed_list = set(self.list_edges)
171 for edge in v2.link_edges:
172 if edge not in ed_list and edge.other_vert(v2) in self.list_verts:
173 ed_list.add(edge)
174 break
176 ed_list.update(get_loose_linked_edges(v2))
177 ed_list = list(ed_list)
179 # WORKAROUND: `edgenet_fill` only works with loose edges or boundary
180 # edges, so remove the other edges and create temporary elements to
181 # replace them.
182 targetmap = {}
183 ed_new = []
184 for edge in ed_list:
185 if not edge.is_wire and not edge.is_boundary:
186 v1, v2 = edge.verts
187 tmp_vert = bm.verts.new(v2.co)
188 e1 = bm.edges.new([v1, tmp_vert])
189 e2 = bm.edges.new([tmp_vert, v2])
190 ed_list.remove(edge)
191 ed_new.append(e1)
192 ed_new.append(e2)
193 targetmap[tmp_vert] = v2
195 bmesh.ops.edgenet_fill(bm, edges=ed_list + ed_new)
196 if targetmap:
197 bmesh.ops.weld_verts(bm, targetmap=targetmap)
199 update_edit_mesh = True
200 # print('face created')
202 if update_edit_mesh:
203 obj.data.update_gpu_tag()
204 obj.data.update_tag()
205 obj.update_from_editmode()
206 obj.update_tag()
207 bmesh.update_edit_mesh(obj.data)
208 self.sctx.tag_update_drawn_snap_object(self.main_snap_obj)
209 # bm.verts.index_update()
211 bpy.ops.ed.undo_push(message="Undo draw line*")
213 return [obj.matrix_world @ v.co for v in self.list_verts]
216 class SnapUtilitiesLine(SnapUtilities, bpy.types.Operator):
217 """Make Lines. Connect them to split faces"""
218 bl_idname = "mesh.snap_utilities_line"
219 bl_label = "Line Tool"
220 bl_options = {'REGISTER'}
222 wait_for_input: bpy.props.BoolProperty(name="Wait for Input", default=True)
224 def _exit(self, context):
225 # avoids unpredictable crashes
226 del self.main_snap_obj
227 del self.main_bm
228 del self.list_edges
229 del self.list_verts
230 del self.list_verts_co
232 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
233 context.area.header_text_set(None)
234 self.snap_context_free()
236 # Restore initial state
237 context.tool_settings.mesh_select_mode = self.select_mode
238 context.space_data.overlay.show_face_center = self.show_face_center
240 def _init_snap_line_context(self, context):
241 self.prevloc = Vector()
242 self.list_verts = []
243 self.list_edges = []
244 self.list_verts_co = []
245 self.bool_update = True
246 self.vector_constrain = ()
247 self.len = 0
248 self.curr_dir = Vector()
250 if not (self.bm and self.obj):
251 self.obj = context.edit_object
252 self.bm = bmesh.from_edit_mesh(self.obj.data)
254 self.main_snap_obj = self.snap_obj = self.sctx._get_snap_obj_by_obj(
255 self.obj)
256 self.main_bm = self.bm
258 def _shift_contrain_callback(self):
259 if isinstance(self.geom, bmesh.types.BMEdge):
260 mat = self.main_snap_obj.mat
261 verts_co = [mat @ v.co for v in self.geom.verts]
262 return verts_co[1] - verts_co[0]
264 def modal(self, context, event):
265 if self.navigation_ops.run(context, event, self.prevloc if self.vector_constrain else self.location):
266 return {'RUNNING_MODAL'}
268 if event.ctrl and event.type == 'Z' and event.value == 'PRESS':
269 bpy.ops.ed.undo()
270 if not self.wait_for_input:
271 self._exit(context)
272 return {'FINISHED'}
273 else:
274 del self.bm
275 del self.main_bm
276 self.charmap.clear()
278 bpy.ops.object.mode_set(mode='EDIT') # just to be sure
279 bpy.ops.mesh.select_all(action='DESELECT')
280 context.tool_settings.mesh_select_mode = (True, False, True)
281 context.space_data.overlay.show_face_center = True
283 self.snap_context_update(context)
284 self._init_snap_line_context(context)
285 self.sctx.update_all()
287 return {'RUNNING_MODAL'}
289 is_making_lines = bool(self.list_verts_co)
291 if (event.type == 'MOUSEMOVE' or self.bool_update):
292 mval = Vector((event.mouse_region_x, event.mouse_region_y))
293 if self.charmap.length_entered_value != 0.0:
294 ray_dir, ray_orig = self.sctx.get_ray(mval)
295 loc = self.list_verts_co[-1]
296 fac = intersect_ray_ray_fac(loc, self.curr_dir, ray_orig, ray_dir)
297 if fac < 0.0:
298 self.curr_dir.negate()
299 self.location = loc - (self.location - loc)
300 else:
301 if self.rv3d.view_matrix != self.rotMat:
302 self.rotMat = self.rv3d.view_matrix.copy()
303 self.bool_update = True
304 snap_utilities.cache.clear()
305 else:
306 self.bool_update = False
308 self.snap_obj, self.prevloc, self.location, self.type, self.bm, self.geom, self.len = snap_utilities(
309 self.sctx,
310 self.main_snap_obj,
311 mval,
312 constrain=self.vector_constrain,
313 previous_vert=(
314 self.list_verts[-1] if self.list_verts else None),
315 increment=self.incremental)
317 self.snap_to_grid()
319 if is_making_lines:
320 loc = self.list_verts_co[-1]
321 self.curr_dir = self.location - loc
322 if self.preferences.auto_constrain:
323 vec, cons_type = self.constrain.update(
324 self.sctx.region, self.sctx.rv3d, mval, loc)
325 self.vector_constrain = [loc, loc + vec, cons_type]
327 elif event.value == 'PRESS':
328 if is_making_lines and self.charmap.modal_(context, event):
329 self.bool_update = self.charmap.length_entered_value == 0.0
331 if not self.bool_update:
332 text_value = self.charmap.length_entered_value
333 vector = self.curr_dir.normalized()
334 self.location = self.list_verts_co[-1] + (vector * text_value)
336 elif self.constrain.modal(event, self._shift_contrain_callback):
337 self.bool_update = True
338 if self.constrain.last_vec:
339 if self.list_verts_co:
340 loc = self.list_verts_co[-1]
341 else:
342 loc = self.location
344 self.vector_constrain = (
345 loc, loc + self.constrain.last_vec, self.constrain.last_type)
346 else:
347 self.vector_constrain = None
349 elif event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'}:
350 if event.type == 'LEFTMOUSE' or self.charmap.length_entered_value != 0.0:
351 if not is_making_lines and self.bm:
352 self.main_snap_obj = self.snap_obj
353 self.main_bm = self.bm
355 mat_inv = self.main_snap_obj.mat.inverted_safe()
356 point = mat_inv @ self.location
357 geom2 = self.geom
358 if geom2:
359 geom2.select = False
361 if self.vector_constrain:
362 geom2 = get_closest_edge(self.main_bm, point, .001)
364 self.list_verts_co = make_line(self, geom2, point)
366 self.vector_constrain = None
367 self.charmap.clear()
368 else:
369 self._exit(context)
370 return {'FINISHED'}
372 elif event.type == 'F8':
373 self.vector_constrain = None
374 self.constrain.toggle()
376 elif event.type in {'RIGHTMOUSE', 'ESC'}:
377 if not self.wait_for_input or not is_making_lines or event.type == 'ESC':
378 if self.geom:
379 self.geom.select = True
380 self._exit(context)
381 return {'FINISHED'}
382 else:
383 snap_utilities.cache.clear()
384 self.vector_constrain = None
385 self.list_edges = []
386 self.list_verts = []
387 self.list_verts_co = []
388 self.charmap.clear()
389 else:
390 return {'RUNNING_MODAL'}
392 a = ""
393 if is_making_lines:
394 a = 'length: ' + self.charmap.get_converted_length_str(self.len)
396 context.area.header_text_set(
397 text="hit: %.3f %.3f %.3f %s" % (*self.location, a))
399 context.area.tag_redraw()
400 return {'RUNNING_MODAL'}
402 def draw_callback_px(self):
403 if self.bm:
404 self.draw_cache.draw_elem(self.snap_obj, self.bm, self.geom)
405 self.draw_cache.draw(self.type, self.location,
406 self.list_verts_co, self.vector_constrain, self.prevloc)
408 def invoke(self, context, event):
409 if context.space_data.type == 'VIEW_3D':
410 self.snap_context_init(context)
411 self.snap_context_update(context)
413 self.constrain = Constrain(
414 self.preferences, context.scene, self.obj)
416 self.intersect = self.preferences.intersect
417 self.create_face = self.preferences.create_face
418 self.navigation_ops = SnapNavigation(context, True)
419 self.charmap = CharMap(context)
421 self._init_snap_line_context(context)
423 # print('name', __name__, __package__)
425 # Store current state
426 self.select_mode = context.tool_settings.mesh_select_mode[:]
427 self.show_face_center = context.space_data.overlay.show_face_center
429 # Modify the current state
430 bpy.ops.mesh.select_all(action='DESELECT')
431 context.tool_settings.mesh_select_mode = (True, False, True)
432 context.space_data.overlay.show_face_center = True
434 # Store values from 3d view context
435 self.rv3d = context.region_data
436 self.rotMat = self.rv3d.view_matrix.copy()
437 # self.obj_matrix.transposed())
439 # modals
440 context.window_manager.modal_handler_add(self)
442 if not self.wait_for_input:
443 if not self.snapwidgets:
444 self.modal(context, event)
445 else:
446 mat_inv = self.obj.matrix_world.inverted_safe()
447 point = mat_inv @ self.location
448 self.list_verts_co = make_line(self, self.geom, point)
450 self._handle = bpy.types.SpaceView3D.draw_handler_add(
451 self.draw_callback_px, (), 'WINDOW', 'POST_VIEW')
453 return {'RUNNING_MODAL'}
454 else:
455 self.report({'WARNING'}, "Active space must be a View3d")
456 return {'CANCELLED'}
459 def register():
460 bpy.utils.register_class(SnapUtilitiesLine)
463 if __name__ == "__main__":
464 register()