1 # SPDX-FileCopyrightText: 2018-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
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 (
23 __package__
= "mesh_snap_utilities_line"
26 def get_closest_edge(bm
, point
, dist
):
31 # Test the BVH (AABB) first
34 isect
= v1
[i
] - dist
<= point
[i
] <= v2
[i
] + dist
36 isect
= v2
[i
] - dist
<= point
[i
] <= v1
[i
] + dist
41 ret
= intersect_point_line(point
, v1
, v2
)
50 new_dist
= (point
- tmp
).length
58 def get_loose_linked_edges(vert
):
59 linked
= [e
for e
in vert
.link_edges
if e
.is_wire
]
61 linked
+= [le
for v
in e
.verts
if v
.is_wire
for le
in v
.link_edges
if le
not in linked
]
65 def make_line(self
, bm_geom
, location
):
66 obj
= self
.main_snap_obj
.data
[0]
70 update_edit_mesh
= False
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
)
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:
93 vert
= bm_geom
.verts
[0]
95 vert
= bm_geom
.verts
[1]
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
])
122 self
.list_edges
.append(edge
)
124 if not v2
.link_edges
:
125 edge
= bm
.edges
.new([v1
, v2
])
126 self
.list_edges
.append(edge
)
128 v1_link_faces
= v1
.link_faces
129 v2_link_faces
= v2
.link_faces
130 if v1_link_faces
and v2_link_faces
:
132 set(v1_link_faces
).intersection(v2_link_faces
))
136 faces
= v1_link_faces
139 faces
= v2_link_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
)
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
)
157 facesp
= bmesh
.ops
.connect_vert_pair(
158 bm
, verts
=[v1
, v2
], verts_exclude
=bm
.verts
)
160 if not self
.intersect
or not facesp
['edges']:
161 edge
= bm
.edges
.new([v1
, v2
])
162 self
.list_edges
.append(edge
)
164 for edge
in facesp
['edges']:
165 self
.list_edges
.append(edge
)
166 update_edit_mesh
= True
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
:
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
185 if not edge
.is_wire
and not edge
.is_boundary
:
187 tmp_vert
= bm
.verts
.new(v2
.co
)
188 e1
= bm
.edges
.new([v1
, tmp_vert
])
189 e2
= bm
.edges
.new([tmp_vert
, v2
])
193 targetmap
[tmp_vert
] = v2
195 bmesh
.ops
.edgenet_fill(bm
, edges
=ed_list
+ ed_new
)
197 bmesh
.ops
.weld_verts(bm
, targetmap
=targetmap
)
199 update_edit_mesh
= True
200 # print('face created')
203 obj
.data
.update_gpu_tag()
204 obj
.data
.update_tag()
205 obj
.update_from_editmode()
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
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()
244 self
.list_verts_co
= []
245 self
.bool_update
= True
246 self
.vector_constrain
= ()
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
(
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':
270 if not self
.wait_for_input
:
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
)
298 self
.curr_dir
.negate()
299 self
.location
= loc
- (self
.location
- loc
)
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()
306 self
.bool_update
= False
308 self
.snap_obj
, self
.prevloc
, self
.location
, self
.type, self
.bm
, self
.geom
, self
.len = snap_utilities(
312 constrain
=self
.vector_constrain
,
314 self
.list_verts
[-1] if self
.list_verts
else None),
315 increment
=self
.incremental
)
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]
344 self
.vector_constrain
= (
345 loc
, loc
+ self
.constrain
.last_vec
, self
.constrain
.last_type
)
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
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
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':
379 self
.geom
.select
= True
383 snap_utilities
.cache
.clear()
384 self
.vector_constrain
= None
387 self
.list_verts_co
= []
390 return {'RUNNING_MODAL'}
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
):
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())
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
)
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'}
455 self
.report({'WARNING'}, "Active space must be a View3d")
460 bpy
.utils
.register_class(SnapUtilitiesLine
)
463 if __name__
== "__main__":