Merge branch 'blender-v3.6-release'
[blender-addons.git] / mesh_snap_utilities_line / common_classes.py
blob0c5405664aa647d505d12130de45c74be4e6474f
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 import bpy
5 from mathutils import (
6 Vector,
7 Matrix,
9 from mathutils.geometry import intersect_point_line
10 from .drawing_utilities import SnapDrawn
11 from .common_utilities import (
12 convert_distance,
13 get_units_info,
14 location_3d_to_region_2d,
18 class SnapNavigation():
19 __slots__ = (
20 'use_ndof',
21 '_rotate',
22 '_move',
23 '_zoom',
24 '_ndof_all',
25 '_ndof_orbit',
26 '_ndof_orbit_zoom',
27 '_ndof_pan')
29 @staticmethod
30 def debug_key(key):
31 for member in dir(key):
32 print(member, getattr(key, member))
34 @staticmethod
35 def convert_to_flag(shift, ctrl, alt):
36 return (shift << 0) | (ctrl << 1) | (alt << 2)
38 def __init__(self, context, use_ndof):
39 # TO DO:
40 # 'View Orbit', 'View Pan', 'NDOF Orbit View', 'NDOF Pan View'
41 self.use_ndof = use_ndof and context.preferences.inputs.use_ndof
43 self._rotate = set()
44 self._move = set()
45 self._zoom = set()
47 if self.use_ndof:
48 self._ndof_all = set()
49 self._ndof_orbit = set()
50 self._ndof_orbit_zoom = set()
51 self._ndof_pan = set()
53 for key in context.window_manager.keyconfigs.user.keymaps['3D View'].keymap_items:
54 if key.idname == 'view3d.rotate':
55 self._rotate.add((self.convert_to_flag(
56 key.shift, key.ctrl, key.alt), key.type, key.value))
57 elif key.idname == 'view3d.move':
58 self._move.add((self.convert_to_flag(
59 key.shift, key.ctrl, key.alt), key.type, key.value))
60 elif key.idname == 'view3d.zoom':
61 if key.type == 'WHEELINMOUSE':
62 self._zoom.add((self.convert_to_flag(
63 key.shift, key.ctrl, key.alt), 'WHEELUPMOUSE', key.value, key.properties.delta))
64 elif key.type == 'WHEELOUTMOUSE':
65 self._zoom.add((self.convert_to_flag(
66 key.shift, key.ctrl, key.alt), 'WHEELDOWNMOUSE', key.value, key.properties.delta))
67 else:
68 self._zoom.add((self.convert_to_flag(
69 key.shift, key.ctrl, key.alt), key.type, key.value, key.properties.delta))
71 elif self.use_ndof:
72 if key.idname == 'view3d.ndof_all':
73 self._ndof_all.add((self.convert_to_flag(
74 key.shift, key.ctrl, key.alt), key.type))
75 elif key.idname == 'view3d.ndof_orbit':
76 self._ndof_orbit.add((self.convert_to_flag(
77 key.shift, key.ctrl, key.alt), key.type))
78 elif key.idname == 'view3d.ndof_orbit_zoom':
79 self._ndof_orbit_zoom.add(
80 (self.convert_to_flag(key.shift, key.ctrl, key.alt), key.type))
81 elif key.idname == 'view3d.ndof_pan':
82 self._ndof_pan.add((self.convert_to_flag(
83 key.shift, key.ctrl, key.alt), key.type))
85 def run(self, context, event, snap_location):
86 evkey = (self.convert_to_flag(event.shift, event.ctrl,
87 event.alt), event.type, event.value)
89 if evkey in self._rotate:
90 if snap_location:
91 bpy.ops.view3d.rotate_custom_pivot(
92 'INVOKE_DEFAULT', pivot=snap_location)
93 else:
94 bpy.ops.view3d.rotate('INVOKE_DEFAULT', use_cursor_init=True)
95 return True
97 if evkey in self._move:
98 bpy.ops.view3d.move('INVOKE_DEFAULT')
99 return True
101 for key in self._zoom:
102 if evkey == key[0:3]:
103 if key[3]:
104 if snap_location:
105 bpy.ops.view3d.zoom_custom_target(
106 'INVOKE_DEFAULT', delta=key[3], target=snap_location)
107 else:
108 bpy.ops.view3d.zoom('INVOKE_DEFAULT', delta=key[3])
109 else:
110 bpy.ops.view3d.zoom('INVOKE_DEFAULT')
111 return True
113 if self.use_ndof:
114 ndofkey = evkey[:2]
115 if ndofkey in self._ndof_all:
116 bpy.ops.view3d.ndof_all('INVOKE_DEFAULT')
117 return True
118 if ndofkey in self._ndof_orbit:
119 bpy.ops.view3d.ndof_orbit('INVOKE_DEFAULT')
120 return True
121 if ndofkey in self._ndof_orbit_zoom:
122 bpy.ops.view3d.ndof_orbit_zoom('INVOKE_DEFAULT')
123 return True
124 if ndofkey in self._ndof_pan:
125 bpy.ops.view3d.ndof_pan('INVOKE_DEFAULT')
126 return True
128 return False
131 class CharMap:
132 __slots__ = (
133 'unit_system',
134 'uinfo',
135 'length_entered',
136 'length_entered_value',
137 'line_pos')
139 ascii = {
140 ".", ",", "-", "+", "1", "2", "3",
141 "4", "5", "6", "7", "8", "9", "0",
142 "c", "m", "d", "k", "h", "a",
143 " ", "/", "*", "'", "\""
144 # "="
146 type = {
147 'BACK_SPACE', 'DEL',
148 'LEFT_ARROW', 'RIGHT_ARROW'
151 def __init__(self, context):
152 scale = context.scene.unit_settings.scale_length
153 separate_units = context.scene.unit_settings.use_separate
154 self.unit_system = context.scene.unit_settings.system
155 self.uinfo = get_units_info(scale, self.unit_system, separate_units)
157 self.clear()
159 def modal_(self, context, event):
160 if event.value == 'PRESS':
161 type = event.type
162 ascii = event.ascii
163 if (type in self.type) or (ascii in self.ascii):
164 if ascii:
165 pos = self.line_pos
166 if ascii == ",":
167 ascii = "."
168 self.length_entered = self.length_entered[:pos] + \
169 ascii + self.length_entered[pos:]
170 self.line_pos += 1
172 if self.length_entered:
173 pos = self.line_pos
174 if type == 'BACK_SPACE':
175 self.length_entered = self.length_entered[:pos -
176 1] + self.length_entered[pos:]
177 self.line_pos -= 1
179 elif type == 'DEL':
180 self.length_entered = self.length_entered[:pos] + \
181 self.length_entered[pos + 1:]
183 elif type == 'LEFT_ARROW':
184 self.line_pos = (
185 pos - 1) % (len(self.length_entered) + 1)
187 elif type == 'RIGHT_ARROW':
188 self.line_pos = (
189 pos + 1) % (len(self.length_entered) + 1)
191 try:
192 self.length_entered_value = bpy.utils.units.to_value(
193 self.unit_system, 'LENGTH', self.length_entered)
194 except: # ValueError:
195 self.length_entered_value = 0.0 # invalid
196 # self.report({'INFO'}, "Operation not supported yet")
197 else:
198 self.length_entered_value = 0.0
200 return True
202 return False
204 def get_converted_length_str(self, length):
205 if self.length_entered:
206 pos = self.line_pos
207 ret = self.length_entered[:pos] + '|' + self.length_entered[pos:]
208 else:
209 ret = convert_distance(length, self.uinfo)
211 return ret
213 def clear(self):
214 self.length_entered = ''
215 self.length_entered_value = 0.0
216 self.line_pos = 0
219 class Constrain:
220 def __init__(self, prefs, scene, obj):
221 self.last_type = None
222 self.last_vec = None
223 self.rotMat = None
224 self.preferences = prefs
225 trans_orient = scene.transform_orientation_slots[0]
226 self.orientation = [None, None]
227 if trans_orient.type == 'LOCAL':
228 self.orientation[0] = obj.matrix_world.to_3x3().transposed()
229 self.orientation[1] = Matrix.Identity(3)
230 else:
231 self.orientation[0] = Matrix.Identity(3)
232 self.orientation[1] = obj.matrix_world.to_3x3().transposed()
234 self.orientation_id = 0
235 self.center = Vector((0.0, 0.0, 0.0))
236 self.center_2d = Vector((0.0, 0.0))
237 self.projected_vecs = Matrix(([0.0, 0.0], [0.0, 0.0], [0.0, 0.0]))
239 def _constrain_set(self, mcursor):
240 vec = (mcursor - self.center_2d)
241 vec.normalize()
243 dot_x = abs(vec.dot(self.projected_vecs[0]))
244 dot_y = abs(vec.dot(self.projected_vecs[1]))
245 dot_z = abs(vec.dot(self.projected_vecs[2]))
247 if dot_x > dot_y and dot_x > dot_z:
248 vec = self.orientation[self.orientation_id][0]
249 type = 'X'
251 elif dot_y > dot_x and dot_y > dot_z:
252 vec = self.orientation[self.orientation_id][1]
253 type = 'Y'
255 else: # dot_z > dot_y and dot_z > dot_x:
256 vec = self.orientation[self.orientation_id][2]
257 type = 'Z'
259 return vec, type
261 def modal(self, event, shift_callback):
262 type = event.type
263 if self.last_type == type:
264 self.orientation_id += 1
266 if type == 'X':
267 if self.orientation_id < 2:
268 self.last_vec = self.orientation[self.orientation_id][0]
269 else:
270 self.orientation_id = 0
271 self.last_vec = type = None
272 elif type == 'Y':
273 if self.orientation_id < 2:
274 self.last_vec = self.orientation[self.orientation_id][1]
275 else:
276 self.orientation_id = 0
277 self.last_vec = type = None
278 elif type == 'Z':
279 if self.orientation_id < 2:
280 self.last_vec = self.orientation[self.orientation_id][2]
281 else:
282 self.orientation_id = 0
283 self.last_vec = type = None
284 elif shift_callback and type in {'RIGHT_SHIFT', 'LEFT_SHIFT'}:
285 if self.orientation_id < 1:
286 type = 'shift'
287 self.last_vec = shift_callback()
288 else:
289 self.orientation_id = 0
290 self.last_vec = type = None
291 else:
292 return False
294 self.preferences.auto_constrain = False
295 self.last_type = type
296 return True
298 def toggle(self):
299 self.rotMat = None # update
300 if self.preferences.auto_constrain:
301 self.orientation_id = (self.orientation_id + 1) % 2
302 self.preferences.auto_constrain = self.orientation_id != 0
303 else:
304 self.preferences.auto_constrain = True
306 def update(self, region, rv3d, mcursor, center):
307 if rv3d.view_matrix != self.rotMat or self.center != center:
308 self.rotMat = rv3d.view_matrix.copy()
310 self.center = center.copy()
311 self.center_2d = location_3d_to_region_2d(
312 region, rv3d, self.center)
314 vec = self.center + self.orientation[self.orientation_id][0]
315 self.projected_vecs[0] = location_3d_to_region_2d(
316 region, rv3d, vec) - self.center_2d
317 vec = self.center + self.orientation[self.orientation_id][1]
318 self.projected_vecs[1] = location_3d_to_region_2d(
319 region, rv3d, vec) - self.center_2d
320 vec = self.center + self.orientation[self.orientation_id][2]
321 self.projected_vecs[2] = location_3d_to_region_2d(
322 region, rv3d, vec) - self.center_2d
324 self.projected_vecs[0].normalize()
325 self.projected_vecs[1].normalize()
326 self.projected_vecs[2].normalize()
328 return self._constrain_set(mcursor)
331 class SnapUtilities:
333 __slots__ = (
334 "sctx",
335 "draw_cache",
336 "outer_verts",
337 "unit_system",
338 "rd",
339 "obj",
340 "bm",
341 "geom",
342 "type",
343 "location",
344 "preferences",
345 "normal",
346 "snap_vert",
347 "snap_edge",
348 "snap_face",
349 "incremental",
353 constrain_keys = {
354 'X': Vector((1, 0, 0)),
355 'Y': Vector((0, 1, 0)),
356 'Z': Vector((0, 0, 1)),
357 'RIGHT_SHIFT': 'shift',
358 'LEFT_SHIFT': 'shift',
361 snapwidgets = []
362 constrain = None
364 @staticmethod
365 def set_contrain(context, key):
366 widget = SnapUtilities.snapwidgets[-1] if SnapUtilities.snapwidgets else None
367 if SnapUtilities.constrain == key:
368 SnapUtilities.constrain = None
369 if hasattr(widget, "get_normal"):
370 widget.get_normal(context)
371 return
373 if hasattr(widget, "normal"):
374 if key == 'shift':
375 import bmesh
376 if isinstance(widget.geom, bmesh.types.BMEdge):
377 verts = widget.geom.verts
378 widget.normal = verts[1].co - verts[0].co
379 widget.normal.normalise()
380 else:
381 return
382 else:
383 widget.normal = SnapUtilities.constrain_keys[key]
385 SnapUtilities.constrain = key
387 def snap_context_update_and_return_moving_objects(self, context):
388 moving_objects = set()
389 moving_snp_objects = set()
390 children = set()
391 for obj in context.view_layer.objects.selected:
392 moving_objects.add(obj)
394 temp_children = set()
395 for obj in context.visible_objects:
396 temp_children.clear()
397 while obj.parent is not None:
398 temp_children.add(obj)
399 parent = obj.parent
400 if parent in moving_objects:
401 children.update(temp_children)
402 temp_children.clear()
403 obj = parent
405 del temp_children
407 moving_objects.difference_update(children)
409 self.sctx.clear_snap_objects(True)
411 for obj in context.visible_objects:
412 is_moving = obj in moving_objects or obj in children
413 snap_obj = self.sctx.add_obj(obj, obj.matrix_world)
414 if is_moving:
415 moving_snp_objects.add(snap_obj)
417 if obj.instance_type == 'COLLECTION':
418 mat = obj.matrix_world.copy()
419 for ob in obj.instance_collection.objects:
420 snap_obj = self.sctx.add_obj(ob, mat @ ob.matrix_world)
421 if is_moving:
422 moving_snp_objects.add(snap_obj)
424 del children
425 return moving_objects, moving_snp_objects
427 def snap_context_update(self, context):
428 def visible_objects_and_duplis():
429 if self.preferences.outer_verts:
430 for obj in context.visible_objects:
431 yield (obj, obj.matrix_world)
433 if obj.instance_type == 'COLLECTION':
434 mat = obj.matrix_world.copy()
435 for ob in obj.instance_collection.objects:
436 yield (ob, mat @ ob.matrix_world)
437 else:
438 for obj in context.objects_in_mode_unique_data:
439 yield (obj, obj.matrix_world)
441 self.sctx.clear_snap_objects(True)
443 for obj, matrix in visible_objects_and_duplis():
444 self.sctx.add_obj(obj, matrix)
446 def snap_context_init(self, context, snap_edge_and_vert=True):
447 from .snap_context_l import global_snap_context_get
449 # Create Snap Context
450 self.sctx = global_snap_context_get(
451 context.evaluated_depsgraph_get(), context.region, context.space_data)
452 ui_scale = context.preferences.system.ui_scale
453 self.sctx.set_pixel_dist(12 * ui_scale)
455 if SnapUtilities.snapwidgets:
456 widget = SnapUtilities.snapwidgets[-1]
458 self.obj = widget.snap_obj.data[0] if widget.snap_obj else context.active_object
459 self.bm = widget.bm
460 self.geom = widget.geom
461 self.type = widget.type
462 self.location = widget.location
463 self.preferences = widget.preferences
464 self.draw_cache = widget.draw_cache
465 if hasattr(widget, "normal"):
466 self.normal = widget.normal
468 else:
469 # init these variables to avoid errors
470 self.obj = context.active_object
471 self.bm = None
472 self.geom = None
473 self.type = 'OUT'
474 self.location = Vector()
476 preferences = context.preferences.addons[__package__].preferences
477 self.preferences = preferences
479 # Init DrawCache
480 self.draw_cache = SnapDrawn(
481 preferences.out_color,
482 preferences.face_color,
483 preferences.edge_color,
484 preferences.vert_color,
485 preferences.center_color,
486 preferences.perpendicular_color,
487 preferences.constrain_shift_color,
488 tuple(
489 context.preferences.themes[0].user_interface.axis_x) + (1.0,),
490 tuple(
491 context.preferences.themes[0].user_interface.axis_y) + (1.0,),
492 tuple(
493 context.preferences.themes[0].user_interface.axis_z) + (1.0,),
494 self.sctx.rv3d,
495 ui_scale)
497 self.snap_vert = self.snap_edge = snap_edge_and_vert
499 shading = context.space_data.shading
500 self.snap_face = not (snap_edge_and_vert and (
501 shading.show_xray or shading.type == 'WIREFRAME'))
503 self.sctx.set_snap_mode(self.snap_vert, self.snap_edge, self.snap_face)
505 # Configure the unit of measure
506 unit_system = context.scene.unit_settings.system
507 scale = context.scene.unit_settings.scale_length
508 scale /= context.space_data.overlay.grid_scale
509 self.rd = bpy.utils.units.to_value(
510 unit_system, 'LENGTH', str(1 / scale))
512 self.incremental = bpy.utils.units.to_value(
513 unit_system, 'LENGTH', str(self.preferences.incremental))
515 def snap_to_grid(self):
516 if self.type == 'OUT' and self.preferences.increments_grid:
517 loc = self.location / self.rd
518 self.location = Vector((round(loc.x),
519 round(loc.y),
520 round(loc.z))) * self.rd
522 def snap_context_free(self):
523 self.sctx = None
524 del self.sctx
526 del self.bm
527 del self.draw_cache
528 del self.geom
529 del self.location
530 del self.rd
531 del self.snap_face
532 del self.snap_obj
533 del self.type
535 del self.preferences
537 SnapUtilities.constrain = None