Fix T70724: PLY import fails with strings
[blender-addons.git] / mesh_snap_utilities_line / op_line.py
blob685ebca404dff5bd3ce7c4b778a50e6822e6826b
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 import bpy
19 import bmesh
21 from mathutils import Vector
22 from mathutils.geometry import intersect_point_line
24 from .drawing_utilities import SnapDrawn
25 from .common_utilities import snap_utilities
26 from .common_classes import (
27 CharMap,
28 Constrain,
29 SnapNavigation,
30 SnapUtilities,
34 if not __package__:
35 __package__ = "mesh_snap_utilities_line"
38 def get_closest_edge(bm, point, dist):
39 r_edge = None
40 for edge in bm.edges:
41 v1 = edge.verts[0].co
42 v2 = edge.verts[1].co
43 # Test the BVH (AABB) first
44 for i in range(3):
45 if v1[i] <= v2[i]:
46 isect = v1[i] - dist <= point[i] <= v2[i] + dist
47 else:
48 isect = v2[i] - dist <= point[i] <= v1[i] + dist
50 if not isect:
51 break
52 else:
53 ret = intersect_point_line(point, v1, v2)
55 if ret[1] < 0.0:
56 tmp = v1
57 elif ret[1] > 1.0:
58 tmp = v2
59 else:
60 tmp = ret[0]
62 new_dist = (point - tmp).length
63 if new_dist <= dist:
64 dist = new_dist
65 r_edge = edge
67 return r_edge
70 def get_loose_linked_edges(bmvert):
71 linked = [e for e in bmvert.link_edges if not e.link_faces]
72 for e in linked:
73 linked += [le for v in e.verts if not v.link_faces for le in v.link_edges if le not in linked]
74 return linked
77 def make_line(self, bm_geom, location):
78 obj = self.main_snap_obj.data[0]
79 bm = self.main_bm
80 split_faces = set()
82 update_edit_mesh = False
84 if bm_geom is None:
85 vert = bm.verts.new(location)
86 self.list_verts.append(vert)
87 update_edit_mesh = True
89 elif isinstance(bm_geom, bmesh.types.BMVert):
90 if (bm_geom.co - location).length_squared < .001:
91 if self.list_verts == [] or self.list_verts[-1] != bm_geom:
92 self.list_verts.append(bm_geom)
93 else:
94 vert = bm.verts.new(location)
95 self.list_verts.append(vert)
96 update_edit_mesh = True
98 elif isinstance(bm_geom, bmesh.types.BMEdge):
99 self.list_edges.append(bm_geom)
100 ret = intersect_point_line(location, bm_geom.verts[0].co, bm_geom.verts[1].co)
102 if (ret[0] - location).length_squared < .001:
103 if ret[1] == 0.0:
104 vert = bm_geom.verts[0]
105 elif ret[1] == 1.0:
106 vert = bm_geom.verts[1]
107 else:
108 edge, vert = bmesh.utils.edge_split(bm_geom, bm_geom.verts[0], ret[1])
109 update_edit_mesh = True
111 if self.list_verts == [] or self.list_verts[-1] != vert:
112 self.list_verts.append(vert)
113 self.geom = vert # hack to highlight in the drawing
114 # self.list_edges.append(edge)
116 else: # constrain point is near
117 vert = bm.verts.new(location)
118 self.list_verts.append(vert)
119 update_edit_mesh = True
121 elif isinstance(bm_geom, bmesh.types.BMFace):
122 split_faces.add(bm_geom)
123 vert = bm.verts.new(location)
124 self.list_verts.append(vert)
125 update_edit_mesh = True
127 # draw, split and create face
128 if len(self.list_verts) >= 2:
129 v1, v2 = self.list_verts[-2:]
130 edge = bm.edges.get([v1, v2])
131 if edge:
132 self.list_edges.append(edge)
133 else:
134 if not v2.link_edges:
135 edge = bm.edges.new([v1, v2])
136 self.list_edges.append(edge)
137 else: # split face
138 v1_link_faces = v1.link_faces
139 v2_link_faces = v2.link_faces
140 if v1_link_faces and v2_link_faces:
141 split_faces.update(set(v1_link_faces).intersection(v2_link_faces))
143 else:
144 if v1_link_faces:
145 faces = v1_link_faces
146 co2 = v2.co.copy()
147 else:
148 faces = v2_link_faces
149 co2 = v1.co.copy()
151 for face in faces:
152 if bmesh.geometry.intersect_face_point(face, co2):
153 co = co2 - face.calc_center_median()
154 if co.dot(face.normal) < 0.001:
155 split_faces.add(face)
157 if split_faces:
158 edge = bm.edges.new([v1, v2])
159 self.list_edges.append(edge)
160 ed_list = get_loose_linked_edges(v2)
161 for face in split_faces:
162 facesp = bmesh.utils.face_split_edgenet(face, ed_list)
163 del split_faces
164 else:
165 if self.intersect:
166 facesp = bmesh.ops.connect_vert_pair(bm, verts=[v1, v2], verts_exclude=bm.verts)
167 # print(facesp)
168 if not self.intersect or not facesp['edges']:
169 edge = bm.edges.new([v1, v2])
170 self.list_edges.append(edge)
171 else:
172 for edge in facesp['edges']:
173 self.list_edges.append(edge)
174 update_edit_mesh = True
176 # create face
177 if self.create_face:
178 ed_list = set(self.list_edges)
179 for edge in v2.link_edges:
180 if edge not in ed_list and edge.other_vert(v2) in self.list_verts:
181 ed_list.add(edge)
182 break
184 ed_list.update(get_loose_linked_edges(v2))
186 bmesh.ops.edgenet_fill(bm, edges=list(ed_list))
187 update_edit_mesh = True
188 # print('face created')
190 if update_edit_mesh:
191 obj.data.update_gpu_tag()
192 obj.data.update_tag()
193 obj.update_from_editmode()
194 obj.update_tag()
195 bmesh.update_edit_mesh(obj.data)
196 self.sctx.tag_update_drawn_snap_object(self.main_snap_obj)
197 #bm.verts.index_update()
199 bpy.ops.ed.undo_push(message="Undo draw line*")
201 return [obj.matrix_world @ v.co for v in self.list_verts]
204 class SnapUtilitiesLine(SnapUtilities, bpy.types.Operator):
205 """Make Lines. Connect them to split faces"""
206 bl_idname = "mesh.snap_utilities_line"
207 bl_label = "Line Tool"
208 bl_options = {'REGISTER'}
210 wait_for_input : bpy.props.BoolProperty(name="Wait for Input", default=True)
212 def _exit(self, context):
213 #avoids unpredictable crashes
214 del self.main_snap_obj
215 del self.main_bm
216 del self.list_edges
217 del self.list_verts
218 del self.list_verts_co
220 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
221 context.area.header_text_set(None)
222 self.snap_context_free()
224 #Restore initial state
225 context.tool_settings.mesh_select_mode = self.select_mode
226 context.space_data.overlay.show_face_center = self.show_face_center
228 def _init_snap_line_context(self, context):
229 self.prevloc = Vector()
230 self.list_verts = []
231 self.list_edges = []
232 self.list_verts_co = []
233 self.bool_update = True
234 self.vector_constrain = ()
235 self.len = 0
237 if not (self.bm and self.obj):
238 self.obj = context.edit_object
239 self.bm = bmesh.from_edit_mesh(self.obj.data)
241 self.main_snap_obj = self.snap_obj = self.sctx._get_snap_obj_by_obj(self.obj)
242 self.main_bm = self.bm
244 def _shift_contrain_callback(self):
245 if isinstance(self.geom, bmesh.types.BMEdge):
246 mat = self.main_snap_obj.mat
247 verts_co = [mat @ v.co for v in self.geom.verts]
248 return verts_co[1] - verts_co[0]
250 def modal(self, context, event):
251 if self.navigation_ops.run(context, event, self.prevloc if self.vector_constrain else self.location):
252 return {'RUNNING_MODAL'}
254 context.area.tag_redraw()
256 if event.ctrl and event.type == 'Z' and event.value == 'PRESS':
257 bpy.ops.ed.undo()
258 if not self.wait_for_input:
259 self._exit(context)
260 return {'FINISHED'}
261 else:
262 del self.bm
263 del self.main_bm
264 self.charmap.clear()
266 bpy.ops.object.mode_set(mode='EDIT') # just to be sure
267 bpy.ops.mesh.select_all(action='DESELECT')
268 context.tool_settings.mesh_select_mode = (True, False, True)
269 context.space_data.overlay.show_face_center = True
271 self.snap_context_update(context)
272 self._init_snap_line_context(context)
273 self.sctx.update_all()
275 return {'RUNNING_MODAL'}
277 is_making_lines = bool(self.list_verts_co)
279 if (event.type == 'MOUSEMOVE' or self.bool_update) and self.charmap.length_entered_value == 0.0:
280 mval = Vector((event.mouse_region_x, event.mouse_region_y))
282 if self.rv3d.view_matrix != self.rotMat:
283 self.rotMat = self.rv3d.view_matrix.copy()
284 self.bool_update = True
285 snap_utilities.cache.clear()
286 else:
287 self.bool_update = False
289 self.snap_obj, self.prevloc, self.location, self.type, self.bm, self.geom, self.len = snap_utilities(
290 self.sctx,
291 self.main_snap_obj,
292 mval,
293 constrain=self.vector_constrain,
294 previous_vert=(self.list_verts[-1] if self.list_verts else None),
295 increment=self.incremental)
297 self.snap_to_grid()
299 if is_making_lines and self.preferences.auto_constrain:
300 loc = self.list_verts_co[-1]
301 vec, type = self.constrain.update(self.sctx.region, self.sctx.rv3d, mval, loc)
302 self.vector_constrain = [loc, loc + vec, type]
304 if event.value == 'PRESS':
305 if is_making_lines and self.charmap.modal_(context, event):
306 self.bool_update = self.charmap.length_entered_value == 0.0
308 if not self.bool_update:
309 text_value = self.charmap.length_entered_value
310 vector = (self.location - self.list_verts_co[-1]).normalized()
311 self.location = self.list_verts_co[-1] + (vector * text_value)
312 del vector
314 elif self.constrain.modal(event, self._shift_contrain_callback):
315 self.bool_update = True
316 if self.constrain.last_vec:
317 if self.list_verts_co:
318 loc = self.list_verts_co[-1]
319 else:
320 loc = self.location
322 self.vector_constrain = (loc, loc + self.constrain.last_vec, self.constrain.last_type)
323 else:
324 self.vector_constrain = None
326 elif event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'}:
327 if event.type == 'LEFTMOUSE' or self.charmap.length_entered_value != 0.0:
328 if not is_making_lines and self.bm:
329 self.main_snap_obj = self.snap_obj
330 self.main_bm = self.bm
332 mat_inv = self.main_snap_obj.mat.inverted_safe()
333 point = mat_inv @ self.location
334 geom2 = self.geom
335 if geom2:
336 geom2.select = False
338 if self.vector_constrain:
339 geom2 = get_closest_edge(self.main_bm, point, .001)
341 self.list_verts_co = make_line(self, geom2, point)
343 self.vector_constrain = None
344 self.charmap.clear()
345 else:
346 self._exit(context)
347 return {'FINISHED'}
349 elif event.type == 'F8':
350 self.vector_constrain = None
351 self.constrain.toogle()
353 elif event.type in {'RIGHTMOUSE', 'ESC'}:
354 if not self.wait_for_input or not is_making_lines or event.type == 'ESC':
355 if self.geom:
356 self.geom.select = True
357 self._exit(context)
358 return {'FINISHED'}
359 else:
360 snap_utilities.cache.clear()
361 self.vector_constrain = None
362 self.list_edges = []
363 self.list_verts = []
364 self.list_verts_co = []
365 self.charmap.clear()
367 a = ""
368 if is_making_lines:
369 a = 'length: ' + self.charmap.get_converted_length_str(self.len)
371 context.area.header_text_set(text = "hit: %.3f %.3f %.3f %s" % (*self.location, a))
373 if True or is_making_lines:
374 return {'RUNNING_MODAL'}
376 return {'PASS_THROUGH'}
378 def draw_callback_px(self):
379 if self.bm:
380 self.draw_cache.draw_elem(self.snap_obj, self.bm, self.geom)
381 self.draw_cache.draw(self.type, self.location, self.list_verts_co, self.vector_constrain, self.prevloc)
383 def invoke(self, context, event):
384 if context.space_data.type == 'VIEW_3D':
385 self.snap_context_init(context)
386 self.snap_context_update(context)
388 self.constrain = Constrain(self.preferences, context.scene, self.obj)
390 self.intersect = self.preferences.intersect
391 self.create_face = self.preferences.create_face
392 self.navigation_ops = SnapNavigation(context, True)
393 self.charmap = CharMap(context)
395 self._init_snap_line_context(context)
397 # print('name', __name__, __package__)
399 #Store current state
400 self.select_mode = context.tool_settings.mesh_select_mode[:]
401 self.show_face_center = context.space_data.overlay.show_face_center
403 #Modify the current state
404 bpy.ops.mesh.select_all(action='DESELECT')
405 context.tool_settings.mesh_select_mode = (True, False, True)
406 context.space_data.overlay.show_face_center = True
408 #Store values from 3d view context
409 self.rv3d = context.region_data
410 self.rotMat = self.rv3d.view_matrix.copy()
411 # self.obj_glmatrix = bgl.Buffer(bgl.GL_FLOAT, [4, 4],
412 # self.obj_matrix.transposed())
414 #modals
415 context.window_manager.modal_handler_add(self)
417 if not self.wait_for_input:
418 mat_inv = self.obj.matrix_world.inverted_safe()
419 point = mat_inv @ self.location
420 self.list_verts_co = make_line(self, self.geom, point)
422 self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (), 'WINDOW', 'POST_VIEW')
424 return {'RUNNING_MODAL'}
425 else:
426 self.report({'WARNING'}, "Active space must be a View3d")
427 return {'CANCELLED'}
430 def register():
431 bpy.utils.register_class(SnapUtilitiesLine)
433 if __name__ == "__main__":
434 register()