1 # SPDX-License-Identifier: GPL-2.0-or-later
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 (
19 __package__
= "mesh_snap_utilities_line"
22 def get_closest_edge(bm
, point
, dist
):
27 # Test the BVH (AABB) first
30 isect
= v1
[i
] - dist
<= point
[i
] <= v2
[i
] + dist
32 isect
= v2
[i
] - dist
<= point
[i
] <= v1
[i
] + dist
37 ret
= intersect_point_line(point
, v1
, v2
)
46 new_dist
= (point
- tmp
).length
54 def get_loose_linked_edges(vert
):
55 linked
= [e
for e
in vert
.link_edges
if e
.is_wire
]
57 linked
+= [le
for v
in e
.verts
if v
.is_wire
for le
in v
.link_edges
if le
not in linked
]
61 def make_line(self
, bm_geom
, location
):
62 obj
= self
.main_snap_obj
.data
[0]
66 update_edit_mesh
= False
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
)
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:
89 vert
= bm_geom
.verts
[0]
91 vert
= bm_geom
.verts
[1]
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
])
118 self
.list_edges
.append(edge
)
120 if not v2
.link_edges
:
121 edge
= bm
.edges
.new([v1
, v2
])
122 self
.list_edges
.append(edge
)
124 v1_link_faces
= v1
.link_faces
125 v2_link_faces
= v2
.link_faces
126 if v1_link_faces
and v2_link_faces
:
128 set(v1_link_faces
).intersection(v2_link_faces
))
132 faces
= v1_link_faces
135 faces
= v2_link_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
)
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
)
153 facesp
= bmesh
.ops
.connect_vert_pair(
154 bm
, verts
=[v1
, v2
], verts_exclude
=bm
.verts
)
156 if not self
.intersect
or not facesp
['edges']:
157 edge
= bm
.edges
.new([v1
, v2
])
158 self
.list_edges
.append(edge
)
160 for edge
in facesp
['edges']:
161 self
.list_edges
.append(edge
)
162 update_edit_mesh
= True
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
:
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
181 if not edge
.is_wire
and not edge
.is_boundary
:
183 tmp_vert
= bm
.verts
.new(v2
.co
)
184 e1
= bm
.edges
.new([v1
, tmp_vert
])
185 e2
= bm
.edges
.new([tmp_vert
, v2
])
189 targetmap
[tmp_vert
] = v2
191 bmesh
.ops
.edgenet_fill(bm
, edges
=ed_list
+ ed_new
)
193 bmesh
.ops
.weld_verts(bm
, targetmap
=targetmap
)
195 update_edit_mesh
= True
196 # print('face created')
199 obj
.data
.update_gpu_tag()
200 obj
.data
.update_tag()
201 obj
.update_from_editmode()
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
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()
240 self
.list_verts_co
= []
241 self
.bool_update
= True
242 self
.vector_constrain
= ()
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
(
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':
267 if not self
.wait_for_input
:
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()
296 self
.bool_update
= False
298 self
.snap_obj
, self
.prevloc
, self
.location
, self
.type, self
.bm
, self
.geom
, self
.len = snap_utilities(
302 constrain
=self
.vector_constrain
,
304 self
.list_verts
[-1] if self
.list_verts
else None),
305 increment
=self
.incremental
)
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
)
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]
335 self
.vector_constrain
= (
336 loc
, loc
+ self
.constrain
.last_vec
, self
.constrain
.last_type
)
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
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
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':
370 self
.geom
.select
= True
374 snap_utilities
.cache
.clear()
375 self
.vector_constrain
= None
378 self
.list_verts_co
= []
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
):
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())
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
)
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'}
446 self
.report({'WARNING'}, "Active space must be a View3d")
451 bpy
.utils
.register_class(SnapUtilitiesLine
)
454 if __name__
== "__main__":