addons-contrib: more view_layer syntax updates
[blender-addons-contrib.git] / space_view3d_enhanced_3d_cursor.py
1 #  ***** BEGIN GPL LICENSE BLOCK *****
2 #
3 #  This program is free software: you can redistribute it and/or modify
4 #  it under the terms of the GNU General Public License as published by
5 #  the Free Software Foundation, either version 3 of the License, or
6 #  (at your option) any later version.
7 #
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.
12 #
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/>.
15 #
16 #  ***** END GPL LICENSE BLOCK *****
17
18 # <pep8-80 compliant>
19
20 bl_info = {
21     "name": "Enhanced 3D Cursor",
22     "description": "Cursor history and bookmarks; drag/snap cursor.",
23     "author": "dairin0d",
24     "version": (3, 0, 7),
25     "blender": (2, 77, 0),
26     "location": "View3D > Action mouse; F10; Properties panel",
27     "warning": "",
28     "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
29         "Scripts/3D_interaction/Enhanced_3D_Cursor",
30     "tracker_url": "https://github.com/dairin0d/enhanced-3d-cursor/issues",
31     "category": "3D View"}
32
33 """
34 Breakdown:
35     Addon registration
36     Keymap utils
37     Various utils (e.g. find_region)
38     OpenGL; drawing utils
39     Non-undoable data storage
40     Cursor utils
41     Stick-object
42     Cursor monitor
43     Addon's GUI
44     Addon's properties
45     Addon's operators
46     ID Block emulator
47     Mesh cache
48     Snap utils
49     View3D utils
50     Transform orientation / coordinate system utils
51     Generic transform utils
52     Main operator
53     ...
54 .
55
56 First step is to re-make the cursor addon (make something usable first).
57 CAD tools should be done without the hassle.
58
59 TODO:
60     strip trailing space? (one of campbellbarton's commits did that)
61
62     IDEAS:
63         - implement 'GIMBAL' orientation (euler axes)
64         - mini-Z-buffer in the vicinity of mouse coords (using raycasts)
65         - an orientation that points towards cursor
66           (from current selection to cursor)
67         - user coordinate systems (using e.g. empties to store different
68           systems; when user switches to such UCS, origin will be set to
69           "cursor", cursor will be sticked to the empty, and a custom
70           transform orientation will be aligned with the empty)
71           - "Stick" transform orientation that is always aligned with the
72             object cursor is "sticked" to?
73         - make 'NORMAL' system also work for bones?
74         - user preferences? (stored in a file)
75         - create spline/edge_mesh from history?
76         - API to access history/bookmarks/operators from other scripts?
77         - Snap selection to bookmark?
78         - Optimize
79         - Clean up code, move to several files?
80     LATER:
81     ISSUES:
82         Limitations:
83             - I need to emulate in Python some things that Blender doesn't
84               currently expose through API:
85               - obtaining matrix of predefined transform orientation
86               - obtaining position of pivot
87               For some kinds of information (e.g. active vertex/edge,
88               selected meta-elements), there is simply no workaround.
89             - Snapping to vertices/edges works differently than in Blender.
90               First of all, iteration over all vertices/edges of all
91               objects along the ray is likely to be very slow.
92               Second, it's more human-friendly to snap to visible
93               elements (or at least with approximately known position).
94             - In editmode I have to exit-and-enter it to get relevant
95               information about current selection. Thus any operator
96               would automatically get applied when you click on 3D View.
97         Mites:
98     QUESTIONS:
99 ==============================================================================
100 Borrowed code/logic:
101 - space_view3d_panel_measure.py (Buerbaum Martin "Pontiac"):
102   - OpenGL state storing/restoring; working with projection matrices.
103 """
104
105 import bpy
106 import bgl
107 import blf
108 import bmesh
109
110 from mathutils import Vector, Matrix, Quaternion, Euler
111
112 from mathutils.geometry import (intersect_line_sphere,
113                                 intersect_ray_tri,
114                                 barycentric_transform,
115                                 tessellate_polygon,
116                                 intersect_line_line,
117                                 intersect_line_plane,
118                                 )
119
120 from bpy_extras.view3d_utils import (region_2d_to_location_3d,
121                                      location_3d_to_region_2d,
122                                      )
123
124 import math
125 import time
126
127 # ====== MODULE GLOBALS / CONSTANTS ====== #
128 tmp_name = chr(0x10ffff) # maximal Unicode value
129 epsilon = 0.000001
130
131 # ====== SET CURSOR OPERATOR ====== #
132 class EnhancedSetCursor(bpy.types.Operator):
133     """Cursor history and bookmarks; drag/snap cursor."""
134     bl_idname = "view3d.cursor3d_enhanced"
135     bl_label = "Enhanced Set Cursor"
136
137     key_char_map = {
138         'PERIOD':".", 'NUMPAD_PERIOD':".",
139         'MINUS':"-", 'NUMPAD_MINUS':"-",
140         'EQUAL':"+", 'NUMPAD_PLUS':"+",
141         #'E':"e", # such big/small numbers aren't useful
142         'ONE':"1", 'NUMPAD_1':"1",
143         'TWO':"2", 'NUMPAD_2':"2",
144         'THREE':"3", 'NUMPAD_3':"3",
145         'FOUR':"4", 'NUMPAD_4':"4",
146         'FIVE':"5", 'NUMPAD_5':"5",
147         'SIX':"6", 'NUMPAD_6':"6",
148         'SEVEN':"7", 'NUMPAD_7':"7",
149         'EIGHT':"8", 'NUMPAD_8':"8",
150         'NINE':"9", 'NUMPAD_9':"9",
151         'ZERO':"0", 'NUMPAD_0':"0",
152         'SPACE':" ",
153         'SLASH':"/", 'NUMPAD_SLASH':"/",
154         'NUMPAD_ASTERIX':"*",
155     }
156
157     key_coordsys_map = {
158         'LEFT_BRACKET':-1,
159         'RIGHT_BRACKET':1,
160         ':':-1, # Instead of [ for French keyboards
161         '!':1, # Instead of ] for French keyboards
162         'J':'VIEW',
163         'K':"Surface",
164         'L':'LOCAL',
165         'B':'GLOBAL',
166         'N':'NORMAL',
167         'M':"Scaled",
168     }
169
170     key_pivot_map = {
171         'H':'ACTIVE',
172         'U':'CURSOR',
173         'I':'INDIVIDUAL',
174         'O':'CENTER',
175         'P':'MEDIAN',
176     }
177
178     key_snap_map = {
179         'C':'INCREMENT',
180         'V':'VERTEX',
181         'E':'EDGE',
182         'F':'FACE',
183     }
184
185     key_tfm_mode_map = {
186         'G':'MOVE',
187         'R':'ROTATE',
188         'S':'SCALE',
189     }
190
191     key_map = {
192         "confirm":{'ACTIONMOUSE'}, # also 'RET' ?
193         "cancel":{'SELECTMOUSE', 'ESC'},
194         "free_mouse":{'F10'},
195         "make_normal_snapshot":{'W'},
196         "make_tangential_snapshot":{'Q'},
197         "use_absolute_coords":{'A'},
198         "snap_to_raw_mesh":{'D'},
199         "use_object_centers":{'T'},
200         "precision_up":{'PAGE_UP'},
201         "precision_down":{'PAGE_DOWN'},
202         "move_caret_prev":{'LEFT_ARROW'},
203         "move_caret_next":{'RIGHT_ARROW'},
204         "move_caret_home":{'HOME'},
205         "move_caret_end":{'END'},
206         "change_current_axis":{'TAB', 'RET', 'NUMPAD_ENTER'},
207         "prev_axis":{'UP_ARROW'},
208         "next_axis":{'DOWN_ARROW'},
209         "remove_next_character":{'DEL'},
210         "remove_last_character":{'BACK_SPACE'},
211         "copy_axes":{'C'},
212         "paste_axes":{'V'},
213         "cut_axes":{'X'},
214     }
215
216     gizmo_factor = 0.15
217     click_period = 0.25
218
219     angle_grid_steps = {True:1.0, False:5.0}
220     scale_grid_steps = {True:0.01, False:0.1}
221
222     # ====== OPERATOR METHOD OVERLOADS ====== #
223     @classmethod
224     def poll(cls, context):
225         area_types = {'VIEW_3D',} # also: IMAGE_EDITOR ?
226         return ((context.area.type in area_types) and
227                 (context.region.type == "WINDOW") and
228                 (not find_settings().cursor_lock))
229
230     def modal(self, context, event):
231         context.area.tag_redraw()
232         return self.try_process_input(context, event)
233
234     def invoke(self, context, event):
235         # Attempt to launch the monitor
236         if bpy.ops.view3d.cursor3d_monitor.poll():
237             bpy.ops.view3d.cursor3d_monitor()
238
239         # Don't interfere with these modes when only mouse is pressed
240         if ('SCULPT' in context.mode) or ('PAINT' in context.mode):
241             if "MOUSE" in event.type:
242                 return {'CANCELLED'}
243
244         CursorDynamicSettings.active_transform_operator = self
245
246         tool_settings = context.tool_settings
247
248         settings = find_settings()
249         tfm_opts = settings.transform_options
250
251         settings_scene = context.scene.cursor_3d_tools_settings
252
253         self.setup_keymaps(context, event)
254
255         # Coordinate System Utility
256         self.particles, self.csu = gather_particles(context=context)
257         self.particles = [View3D_Cursor(context)]
258
259         self.csu.source_pos = self.particles[0].get_location()
260         self.csu.source_rot = self.particles[0].get_rotation()
261         self.csu.source_scale = self.particles[0].get_scale()
262
263         # View3D Utility
264         self.vu = ViewUtility(context.region, context.space_data,
265             context.region_data)
266
267         # Snap Utility
268         self.su = SnapUtility(context)
269
270         # turn off view locking for the duration of the operator
271         self.view_pos = self.vu.get_position(True)
272         self.vu.set_position(self.vu.get_position(), True)
273         self.view_locks = self.vu.get_locks()
274         self.vu.set_locks({})
275
276         # Initialize runtime states
277         self.initiated_by_mouse = ("MOUSE" in event.type)
278         self.free_mouse = not self.initiated_by_mouse
279         self.use_object_centers = False
280         self.axes_values = ["", "", ""]
281         self.axes_coords = [None, None, None]
282         self.axes_eval_success = [True, True, True]
283         self.allowed_axes = [True, True, True]
284         self.current_axis = 0
285         self.caret_pos = 0
286         self.coord_format = "{:." + str(settings.free_coord_precision) + "f}"
287         self.transform_mode = 'MOVE'
288         self.init_xy_angle_distance(context, event)
289
290         self.click_start = time.time()
291         if not self.initiated_by_mouse:
292             self.click_start -= self.click_period
293
294         self.stick_obj_name = settings_scene.stick_obj_name
295         self.stick_obj_pos = settings_scene.stick_obj_pos
296
297         # Initial run
298         self.try_process_input(context, event, True)
299
300         context.window_manager.modal_handler_add(self)
301         return {'RUNNING_MODAL'}
302
303     def cancel(self, context):
304         for particle in self.particles:
305             particle.revert()
306
307         set_stick_obj(context.scene, self.stick_obj_name, self.stick_obj_pos)
308
309         self.finalize(context)
310
311     # ====== CLEANUP/FINALIZE ====== #
312     def finalize(self, context):
313         # restore view locking
314         self.vu.set_locks(self.view_locks)
315         self.vu.set_position(self.view_pos, True)
316
317         self.cleanup(context)
318
319         # This is to avoid "blinking" of
320         # between-history-positions line
321         settings = find_settings()
322         history = settings.history
323         # make sure the most recent history entry is displayed
324         history.curr_id = 0
325         history.last_id = 0
326
327         # Ensure there are no leftovers from draw_callback
328         context.area.tag_redraw()
329
330         return {'FINISHED'}
331
332     def cleanup(self, context):
333         self.particles = None
334         self.csu = None
335         self.vu = None
336         if self.su is not None:
337             self.su.dispose()
338         self.su = None
339
340         CursorDynamicSettings.active_transform_operator = None
341
342     # ====== USER INPUT PROCESSING ====== #
343     def setup_keymaps(self, context, event=None):
344         self.key_map = self.key_map.copy()
345
346         wm = context.window_manager
347
348         # There is no such event as 'ACTIONMOUSE',
349         # it's always 'LEFTMOUSE' or 'RIGHTMOUSE'
350         if event:
351             if event.type == 'LEFTMOUSE':
352                 self.key_map["confirm"] = {'LEFTMOUSE'}
353                 self.key_map["cancel"] = {'RIGHTMOUSE', 'ESC'}
354             elif event.type == 'RIGHTMOUSE':
355                 self.key_map["confirm"] = {'RIGHTMOUSE'}
356                 self.key_map["cancel"] = {'LEFTMOUSE', 'ESC'}
357             else:
358                 event = None
359         if event is None:
360             keyconfig = wm.keyconfigs.active
361             select_mouse = getattr(keyconfig.preferences, "select_mouse", "LEFT")
362             if select_mouse == 'RIGHT':
363                 self.key_map["confirm"] = {'LEFTMOUSE'}
364                 self.key_map["cancel"] = {'RIGHTMOUSE', 'ESC'}
365             else:
366                 self.key_map["confirm"] = {'RIGHTMOUSE'}
367                 self.key_map["cancel"] = {'LEFTMOUSE', 'ESC'}
368
369         # Use user-defined "free mouse" key, if it exists
370         if '3D View' in wm.keyconfigs.user.keymaps:
371             km = wm.keyconfigs.user.keymaps['3D View']
372             for kmi in KeyMapItemSearch(EnhancedSetCursor.bl_idname, km):
373                 if kmi.map_type == 'KEYBOARD':
374                     self.key_map["free_mouse"] = {kmi.type,}
375                     break
376
377     def try_process_input(self, context, event, initial_run=False):
378         try:
379             return self.process_input(context, event, initial_run)
380         except:
381             # If anything fails, at least dispose the resources
382             self.cleanup(context)
383             raise
384
385     def process_input(self, context, event, initial_run=False):
386         wm = context.window_manager
387         v3d = context.space_data
388
389         if event.type in self.key_map["confirm"]:
390             if self.free_mouse:
391                 finished = (event.value == 'PRESS')
392             else:
393                 finished = (event.value == 'RELEASE')
394
395             if finished:
396                 return self.finalize(context)
397
398         if event.type in self.key_map["cancel"]:
399             self.cancel(context)
400             return {'CANCELLED'}
401
402         tool_settings = context.tool_settings
403
404         settings = find_settings()
405         tfm_opts = settings.transform_options
406
407         make_snapshot = False
408         tangential_snapshot = False
409
410         if event.value == 'PRESS':
411             if event.type in self.key_map["free_mouse"]:
412                 if self.free_mouse and not initial_run:
413                     # confirm if pressed second time
414                     return self.finalize(context)
415                 else:
416                     self.free_mouse = True
417
418             if event.type in self.key_tfm_mode_map:
419                 new_mode = self.key_tfm_mode_map[event.type]
420
421                 if self.transform_mode != new_mode:
422                     # snap cursor to its initial state
423                     if new_mode != 'MOVE':
424                         for particle in self.particles:
425                             initial_matrix = particle.get_initial_matrix()
426                             particle.set_matrix(initial_matrix)
427                     # reset intial mouse position
428                     self.init_xy_angle_distance(context, event)
429
430                 self.transform_mode = new_mode
431
432             if event.type in self.key_map["make_normal_snapshot"]:
433                 make_snapshot = True
434                 tangential_snapshot = False
435
436             if event.type in self.key_map["make_tangential_snapshot"]:
437                 make_snapshot = True
438                 tangential_snapshot = True
439
440             if event.type in self.key_map["snap_to_raw_mesh"]:
441                 tool_settings.use_snap_self = \
442                     not tool_settings.use_snap_self
443
444             if (not event.alt) and (event.type in {'X', 'Y', 'Z'}):
445                 axis_lock = [(event.type == 'X') != event.shift,
446                              (event.type == 'Y') != event.shift,
447                              (event.type == 'Z') != event.shift]
448
449                 if self.allowed_axes != axis_lock:
450                     self.allowed_axes = axis_lock
451                 else:
452                     self.allowed_axes = [True, True, True]
453
454             if event.type in self.key_map["use_absolute_coords"]:
455                 tfm_opts.use_relative_coords = \
456                     not tfm_opts.use_relative_coords
457
458                 self.update_origin_projection(context)
459
460             incr = 0
461             if event.type in self.key_map["change_current_axis"]:
462                 incr = (-1 if event.shift else 1)
463             elif event.type in self.key_map["next_axis"]:
464                 incr = 1
465             elif event.type in self.key_map["prev_axis"]:
466                 incr = -1
467
468             if incr != 0:
469                 self.current_axis = (self.current_axis + incr) % 3
470                 self.caret_pos = len(self.axes_values[self.current_axis])
471
472             incr = 0
473             if event.type in self.key_map["precision_up"]:
474                 incr = 1
475             elif event.type in self.key_map["precision_down"]:
476                 incr = -1
477
478             if incr != 0:
479                 settings.free_coord_precision += incr
480                 self.coord_format = "{:." + \
481                     str(settings.free_coord_precision) + "f}"
482
483             new_orient1 = self.key_coordsys_map.get(event.type, None)
484             new_orient2 = self.key_coordsys_map.get(event.unicode, None)
485             new_orientation = (new_orient1 or new_orient2)
486             if new_orientation:
487                 self.csu.set_orientation(new_orientation)
488
489                 self.update_origin_projection(context)
490
491                 if event.ctrl:
492                     self.snap_to_system_origin()
493
494             if (event.type == 'ZERO') and event.ctrl:
495                 self.snap_to_system_origin()
496             elif new_orientation is None: # avoid conflicting shortcuts
497                 self.process_axis_input(event)
498
499             if event.alt:
500                 jc = (", " if tfm_opts.use_comma_separator else "\t")
501                 if event.type in self.key_map["copy_axes"]:
502                     wm.clipboard = jc.join(self.get_axes_text(True))
503                 elif event.type in self.key_map["cut_axes"]:
504                     wm.clipboard = jc.join(self.get_axes_text(True))
505                     self.set_axes_text("\t\t\t")
506                 elif event.type in self.key_map["paste_axes"]:
507                     if jc == "\t":
508                         self.set_axes_text(wm.clipboard, True)
509                     else:
510                         jc = jc.strip()
511                         ttext = ""
512                         brackets = 0
513                         for c in wm.clipboard:
514                             if c in "[{(":
515                                 brackets += 1
516                             elif c in "]})":
517                                 brackets -= 1
518                             if (brackets == 0) and (c == jc):
519                                 c = "\t"
520                             ttext += c
521                         self.set_axes_text(ttext, True)
522
523             if event.type in self.key_map["use_object_centers"]:
524                 v3d.use_pivot_point_align = not v3d.use_pivot_point_align
525
526             if event.type in self.key_pivot_map:
527                 self.csu.set_pivot(self.key_pivot_map[event.type])
528
529                 self.update_origin_projection(context)
530
531                 if event.ctrl:
532                     self.snap_to_system_origin(force_pivot=True)
533
534             if (not event.alt) and (event.type in self.key_snap_map):
535                 snap_element = self.key_snap_map[event.type]
536                 if tool_settings.snap_element == snap_element:
537                     if snap_element == 'VERTEX':
538                         snap_element = 'VOLUME'
539                     elif snap_element == 'VOLUME':
540                         snap_element = 'VERTEX'
541                 tool_settings.snap_element = snap_element
542         # end if
543
544         use_snap = (tool_settings.use_snap != event.ctrl)
545         if use_snap:
546             snap_type = tool_settings.snap_element
547         else:
548             userprefs_input = context.preferences.input
549             if userprefs_input.use_mouse_depth_cursor:
550                 # Suggested by Lissanro in the forum
551                 use_snap = True
552                 snap_type = 'FACE'
553             else:
554                 snap_type = None
555
556         axes_coords = [None, None, None]
557         if self.transform_mode == 'MOVE':
558             for i in range(3):
559                 if self.axes_coords[i] is not None:
560                     axes_coords[i] = self.axes_coords[i]
561                 elif not self.allowed_axes[i]:
562                     axes_coords[i] = 0.0
563
564         self.su.set_modes(
565             interpolation=tfm_opts.snap_interpolate_normals_mode,
566             use_relative_coords=tfm_opts.use_relative_coords,
567             editmode=tool_settings.use_snap_self,
568             snap_type=snap_type,
569             snap_align=tool_settings.use_snap_align_rotation,
570             axes_coords=axes_coords,
571             )
572
573         self.do_raycast = ("MOUSE" in event.type)
574         self.grid_substep = event.shift
575         self.modify_surface_orientation = (len(self.particles) == 1)
576         self.xy = Vector((event.mouse_region_x, event.mouse_region_y))
577
578         self.use_object_centers = v3d.use_pivot_point_align
579
580         if event.type == 'MOUSEMOVE':
581             self.update_transform_mousemove()
582
583         if self.transform_mode == 'MOVE':
584             transform_func = self.transform_move
585         elif self.transform_mode == 'ROTATE':
586             transform_func = self.transform_rotate
587         elif self.transform_mode == 'SCALE':
588             transform_func = self.transform_scale
589
590         for particle in self.particles:
591             transform_func(particle)
592
593         if make_snapshot:
594             self.make_normal_snapshot(context.scene, tangential_snapshot)
595
596         return {'RUNNING_MODAL'}
597
598     def update_origin_projection(self, context):
599         r = context.region
600         rv3d = context.region_data
601
602         origin = self.csu.get_origin()
603         # prehaps not projection, but intersection with plane?
604         self.origin_xy = location_3d_to_region_2d(r, rv3d, origin)
605         if self.origin_xy is None:
606             self.origin_xy = Vector((r.width / 2, r.height / 2))
607
608         self.delta_xy = (self.start_xy - self.origin_xy).to_3d()
609         self.prev_delta_xy = self.delta_xy
610
611     def init_xy_angle_distance(self, context, event):
612         self.start_xy = Vector((event.mouse_region_x, event.mouse_region_y))
613
614         self.update_origin_projection(context)
615
616         # Distinction between angles has to be made because
617         # angles can go beyond 360 degrees (we cannot snap
618         # to increment the original ones).
619         self.raw_angles = [0.0, 0.0, 0.0]
620         self.angles = [0.0, 0.0, 0.0]
621         self.scales = [1.0, 1.0, 1.0]
622
623     def update_transform_mousemove(self):
624         delta_xy = (self.xy - self.origin_xy).to_3d()
625
626         n_axes = sum(int(v) for v in self.allowed_axes)
627         if n_axes == 1:
628             # rotate using angle as value
629             rd = self.prev_delta_xy.rotation_difference(delta_xy)
630             offset = -rd.angle * round(rd.axis[2])
631
632             sys_matrix = self.csu.get_matrix()
633
634             i_allowed = 0
635             for i in range(3):
636                 if self.allowed_axes[i]:
637                     i_allowed = i
638
639             view_dir = self.vu.get_direction()
640             if view_dir.dot(sys_matrix[i_allowed][:3]) < 0:
641                 offset = -offset
642
643             for i in range(3):
644                 if self.allowed_axes[i]:
645                     self.raw_angles[i] += offset
646         elif n_axes == 2:
647             # rotate using XY coords as two values
648             offset = (delta_xy - self.prev_delta_xy) * (math.pi / 180.0)
649
650             if self.grid_substep:
651                 offset *= 0.1
652             else:
653                 offset *= 0.5
654
655             j = 0
656             for i in range(3):
657                 if self.allowed_axes[i]:
658                     self.raw_angles[i] += offset[1 - j]
659                     j += 1
660         elif n_axes == 3:
661             # rotate around view direction
662             rd = self.prev_delta_xy.rotation_difference(delta_xy)
663             offset = -rd.angle * round(rd.axis[2])
664
665             view_dir = self.vu.get_direction()
666
667             sys_matrix = self.csu.get_matrix()
668
669             try:
670                 view_dir = sys_matrix.inverted().to_3x3() * view_dir
671             except:
672                 # this is some degenerate system
673                 pass
674             view_dir.normalize()
675
676             rot = Matrix.Rotation(offset, 3, view_dir)
677
678             matrix = Euler(self.raw_angles, 'XYZ').to_matrix()
679             matrix.rotate(rot)
680
681             euler = matrix.to_euler('XYZ')
682             self.raw_angles[0] += clamp_angle(euler.x - self.raw_angles[0])
683             self.raw_angles[1] += clamp_angle(euler.y - self.raw_angles[1])
684             self.raw_angles[2] += clamp_angle(euler.z - self.raw_angles[2])
685
686         scale = delta_xy.length / self.delta_xy.length
687         if self.delta_xy.dot(delta_xy) < 0:
688             scale *= -1
689         for i in range(3):
690             if self.allowed_axes[i]:
691                 self.scales[i] = scale
692
693         self.prev_delta_xy = delta_xy
694
695     def transform_move(self, particle):
696         global set_cursor_location__reset_stick
697
698         src_matrix = particle.get_matrix()
699         initial_matrix = particle.get_initial_matrix()
700
701         matrix = self.su.snap(
702             self.xy, src_matrix, initial_matrix,
703             self.do_raycast, self.grid_substep,
704             self.vu, self.csu,
705             self.modify_surface_orientation,
706             self.use_object_centers)
707
708         set_cursor_location__reset_stick = False
709         particle.set_matrix(matrix)
710         set_cursor_location__reset_stick = True
711
712     def rotate_matrix(self, matrix):
713         sys_matrix = self.csu.get_matrix()
714
715         try:
716             matrix = sys_matrix.inverted() * matrix
717         except:
718             # this is some degenerate system
719             pass
720
721         # Blender's order of rotation [in local axes]
722         rotation_order = [2, 1, 0]
723
724         # Seems that 4x4 matrix cannot be rotated using rotate() ?
725         sys_matrix3 = sys_matrix.to_3x3()
726
727         for i in range(3):
728             j = rotation_order[i]
729             axis = sys_matrix3[j]
730             angle = self.angles[j]
731
732             rot = angle_axis_to_quat(angle, axis)
733             # this seems to be buggy too
734             #rot = Matrix.Rotation(angle, 3, axis)
735
736             sys_matrix3 = rot.to_matrix() * sys_matrix3
737             # sys_matrix3.rotate has a bug? or I don't understand how it works?
738             #sys_matrix3.rotate(rot)
739
740         for i in range(3):
741             sys_matrix[i][:3] = sys_matrix3[i]
742
743         matrix = sys_matrix * matrix
744
745         return matrix
746
747     def transform_rotate(self, particle):
748         grid_step = self.angle_grid_steps[self.grid_substep]
749         grid_step *= (math.pi / 180.0)
750
751         for i in range(3):
752             if self.axes_values[i] and self.axes_eval_success[i]:
753                 self.raw_angles[i] = self.axes_coords[i] * (math.pi / 180.0)
754
755             self.angles[i] = self.raw_angles[i]
756
757         if self.su.implementation.snap_type == 'INCREMENT':
758             for i in range(3):
759                 self.angles[i] = round_step(self.angles[i], grid_step)
760
761         initial_matrix = particle.get_initial_matrix()
762         matrix = self.rotate_matrix(initial_matrix)
763
764         particle.set_matrix(matrix)
765
766     def scale_matrix(self, matrix):
767         sys_matrix = self.csu.get_matrix()
768
769         try:
770             matrix = sys_matrix.inverted() * matrix
771         except:
772             # this is some degenerate system
773             pass
774
775         for i in range(3):
776             sys_matrix[i] *= self.scales[i]
777
778         matrix = sys_matrix * matrix
779
780         return matrix
781
782     def transform_scale(self, particle):
783         grid_step = self.scale_grid_steps[self.grid_substep]
784
785         for i in range(3):
786             if self.axes_values[i] and self.axes_eval_success[i]:
787                 self.scales[i] = self.axes_coords[i]
788
789         if self.su.implementation.snap_type == 'INCREMENT':
790             for i in range(3):
791                 self.scales[i] = round_step(self.scales[i], grid_step)
792
793         initial_matrix = particle.get_initial_matrix()
794         matrix = self.scale_matrix(initial_matrix)
795
796         particle.set_matrix(matrix)
797
798     def set_axis_input(self, axis_id, axis_val):
799         if axis_val == self.axes_values[axis_id]:
800             return
801
802         self.axes_values[axis_id] = axis_val
803
804         if len(axis_val) == 0:
805             self.axes_coords[axis_id] = None
806             self.axes_eval_success[axis_id] = True
807         else:
808             try:
809                 #self.axes_coords[axis_id] = float(eval(axis_val, {}, {}))
810                 self.axes_coords[axis_id] = \
811                     float(eval(axis_val, math.__dict__))
812                 self.axes_eval_success[axis_id] = True
813             except:
814                 self.axes_eval_success[axis_id] = False
815
816     def snap_to_system_origin(self, force_pivot=False):
817         if self.transform_mode == 'MOVE':
818             pivot = self.csu.get_pivot_name(raw=force_pivot)
819             p = self.csu.get_origin(relative=False, pivot=pivot)
820             m = self.csu.get_matrix()
821             try:
822                 p = m.inverted() * p
823             except:
824                 # this is some degenerate system
825                 pass
826             for i in range(3):
827                 self.set_axis_input(i, str(p[i]))
828         elif self.transform_mode == 'ROTATE':
829             for i in range(3):
830                 self.set_axis_input(i, "0")
831         elif self.transform_mode == 'SCALE':
832             for i in range(3):
833                 self.set_axis_input(i, "1")
834
835     def get_axes_values(self, as_string=False):
836         if self.transform_mode == 'MOVE':
837             localmat = CursorDynamicSettings.local_matrix
838             raw_axes = localmat.translation
839         elif self.transform_mode == 'ROTATE':
840             raw_axes = Vector(self.angles) * (180.0 / math.pi)
841         elif self.transform_mode == 'SCALE':
842             raw_axes = Vector(self.scales)
843
844         axes_values = []
845         for i in range(3):
846             if as_string and self.axes_values[i]:
847                 value = self.axes_values[i]
848             elif self.axes_eval_success[i] and \
849                     (self.axes_coords[i] is not None):
850                 value = self.axes_coords[i]
851             else:
852                 value = raw_axes[i]
853                 if as_string:
854                     value = self.coord_format.format(value)
855             axes_values.append(value)
856
857         return axes_values
858
859     def get_axes_text(self, offset=False):
860         axes_values = self.get_axes_values(as_string=True)
861
862         axes_text = []
863         for i in range(3):
864             j = i
865             if offset:
866                 j = (i + self.current_axis) % 3
867
868             axes_text.append(axes_values[j])
869
870         return axes_text
871
872     def set_axes_text(self, text, offset=False):
873         if "\n" in text:
874             text = text.replace("\r", "")
875         else:
876             text = text.replace("\r", "\n")
877         text = text.replace("\n", "\t")
878         #text = text.replace(",", ".") # ???
879
880         axes_text = text.split("\t")
881         for i in range(min(len(axes_text), 3)):
882             j = i
883             if offset:
884                 j = (i + self.current_axis) % 3
885             self.set_axis_input(j, axes_text[i])
886
887     def process_axis_input(self, event):
888         axis_id = self.current_axis
889         axis_val = self.axes_values[axis_id]
890
891         if event.type in self.key_map["remove_next_character"]:
892             if event.ctrl:
893                 # clear all
894                 for i in range(3):
895                     self.set_axis_input(i, "")
896                 self.caret_pos = 0
897                 return
898             else:
899                 axis_val = axis_val[0:self.caret_pos] + \
900                            axis_val[self.caret_pos + 1:len(axis_val)]
901         elif event.type in self.key_map["remove_last_character"]:
902             if event.ctrl:
903                 # clear current
904                 axis_val = ""
905             else:
906                 axis_val = axis_val[0:self.caret_pos - 1] + \
907                            axis_val[self.caret_pos:len(axis_val)]
908                 self.caret_pos -= 1
909         elif event.type in self.key_map["move_caret_next"]:
910             self.caret_pos += 1
911             if event.ctrl:
912                 snap_chars = ".-+*/%()"
913                 i = self.caret_pos
914                 while axis_val[i:i + 1] not in snap_chars:
915                     i += 1
916                 self.caret_pos = i
917         elif event.type in self.key_map["move_caret_prev"]:
918             self.caret_pos -= 1
919             if event.ctrl:
920                 snap_chars = ".-+*/%()"
921                 i = self.caret_pos
922                 while axis_val[i - 1:i] not in snap_chars:
923                     i -= 1
924                 self.caret_pos = i
925         elif event.type in self.key_map["move_caret_home"]:
926             self.caret_pos = 0
927         elif event.type in self.key_map["move_caret_end"]:
928             self.caret_pos = len(axis_val)
929         elif event.type in self.key_char_map:
930             # Currently accessing event.ascii seems to crash Blender
931             c = self.key_char_map[event.type]
932             if event.shift:
933                 if c == "8":
934                     c = "*"
935                 elif c == "5":
936                     c = "%"
937                 elif c == "9":
938                     c = "("
939                 elif c == "0":
940                     c = ")"
941             axis_val = axis_val[0:self.caret_pos] + c + \
942                        axis_val[self.caret_pos:len(axis_val)]
943             self.caret_pos += 1
944
945         self.caret_pos = min(max(self.caret_pos, 0), len(axis_val))
946
947         self.set_axis_input(axis_id, axis_val)
948
949     # ====== DRAWING ====== #
950     def gizmo_distance(self, pos):
951         rv3d = self.vu.region_data
952         if rv3d.view_perspective == 'ORTHO':
953             dist = rv3d.view_distance
954         else:
955             view_pos = self.vu.get_viewpoint()
956             view_dir = self.vu.get_direction()
957             dist = (pos - view_pos).dot(view_dir)
958         return dist
959
960     def gizmo_scale(self, pos):
961         return self.gizmo_distance(pos) * self.gizmo_factor
962
963     def check_v3d_local(self, context):
964         csu_v3d = self.csu.space_data
965         v3d = context.space_data
966         if csu_v3d.local_view:
967             return csu_v3d != v3d
968         return v3d.local_view
969
970     def draw_3d(self, context):
971         if self.check_v3d_local(context):
972             return
973
974         if time.time() < (self.click_start + self.click_period):
975             return
976
977         settings = find_settings()
978         tfm_opts = settings.transform_options
979
980         initial_matrix = self.particles[0].get_initial_matrix()
981
982         sys_matrix = self.csu.get_matrix()
983         if tfm_opts.use_relative_coords:
984             sys_matrix.translation = initial_matrix.translation.copy()
985         sys_origin = sys_matrix.to_translation()
986         dest_point = self.particles[0].get_location()
987
988         if self.is_normal_visible():
989             p0, x, y, z, _x, _z = \
990                 self.get_normal_params(tfm_opts, dest_point)
991
992             # use theme colors?
993             #ThemeView3D.normal
994             #ThemeView3D.vertex_normal
995
996             bgl.glDisable(bgl.GL_LINE_STIPPLE)
997
998             if settings.draw_N:
999                 bgl.glColor4f(0, 1, 1, 1)
1000                 draw_arrow(p0, _x, y, z) # Z (normal)
1001             if settings.draw_T1:
1002                 bgl.glColor4f(1, 0, 1, 1)
1003                 draw_arrow(p0, y, _z, x) # X (1st tangential)
1004             if settings.draw_T2:
1005                 bgl.glColor4f(1, 1, 0, 1)
1006                 draw_arrow(p0, _z, x, y) # Y (2nd tangential)
1007
1008             bgl.glEnable(bgl.GL_BLEND)
1009             bgl.glDisable(bgl.GL_DEPTH_TEST)
1010
1011             if settings.draw_N:
1012                 bgl.glColor4f(0, 1, 1, 0.25)
1013                 draw_arrow(p0, _x, y, z) # Z (normal)
1014             if settings.draw_T1:
1015                 bgl.glColor4f(1, 0, 1, 0.25)
1016                 draw_arrow(p0, y, _z, x) # X (1st tangential)
1017             if settings.draw_T2:
1018                 bgl.glColor4f(1, 1, 0, 0.25)
1019                 draw_arrow(p0, _z, x, y) # Y (2nd tangential)
1020
1021         if settings.draw_guides:
1022             p0 = dest_point
1023             try:
1024                 p00 = sys_matrix.inverted() * p0
1025             except:
1026                 # this is some degenerate system
1027                 p00 = p0.copy()
1028
1029             axes_line_params = [
1030                 (Vector((0, p00.y, p00.z)), (1, 0, 0)),
1031                 (Vector((p00.x, 0, p00.z)), (0, 1, 0)),
1032                 (Vector((p00.x, p00.y, 0)), (0, 0, 1)),
1033             ]
1034
1035             for i in range(3):
1036                 p1, color = axes_line_params[i]
1037                 p1 = sys_matrix * p1
1038                 constrained = (self.axes_coords[i] is not None) or \
1039                     (not self.allowed_axes[i])
1040                 alpha = (0.25 if constrained else 1.0)
1041                 draw_line_hidden_depth(p0, p1, color, \
1042                     alpha, alpha, False, True)
1043
1044             # line from origin to cursor
1045             p0 = sys_origin
1046             p1 = dest_point
1047
1048             bgl.glEnable(bgl.GL_LINE_STIPPLE)
1049             bgl.glColor4f(1, 1, 0, 1)
1050
1051             draw_line_hidden_depth(p0, p1, (1, 1, 0), 1.0, 0.5, True, True)
1052
1053         if settings.draw_snap_elements:
1054             sui = self.su.implementation
1055             if sui.potential_snap_elements and (sui.snap_type == 'EDGE'):
1056                 bgl.glDisable(bgl.GL_LINE_STIPPLE)
1057
1058                 bgl.glEnable(bgl.GL_BLEND)
1059                 bgl.glDisable(bgl.GL_DEPTH_TEST)
1060
1061                 bgl.glLineWidth(2)
1062                 bgl.glColor4f(0, 0, 1, 0.5)
1063
1064                 bgl.glBegin(bgl.GL_LINE_LOOP)
1065                 for p in sui.potential_snap_elements:
1066                     bgl.glVertex3f(p[0], p[1], p[2])
1067                 bgl.glEnd()
1068             elif sui.potential_snap_elements and (sui.snap_type == 'FACE'):
1069                 bgl.glEnable(bgl.GL_BLEND)
1070                 bgl.glDisable(bgl.GL_DEPTH_TEST)
1071
1072                 bgl.glColor4f(0, 1, 0, 0.5)
1073
1074                 co = sui.potential_snap_elements
1075                 tris = tessellate_polygon([co])
1076                 bgl.glBegin(bgl.GL_TRIANGLES)
1077                 for tri in tris:
1078                     for vi in tri:
1079                         p = co[vi]
1080                         bgl.glVertex3f(p[0], p[1], p[2])
1081                 bgl.glEnd()
1082
1083     def draw_2d(self, context):
1084         if self.check_v3d_local(context):
1085             return
1086
1087         r = context.region
1088         rv3d = context.region_data
1089
1090         settings = find_settings()
1091
1092         if settings.draw_snap_elements:
1093             sui = self.su.implementation
1094
1095             snap_points = []
1096             if sui.potential_snap_elements and \
1097                     (sui.snap_type in {'VERTEX', 'VOLUME'}):
1098                 snap_points.extend(sui.potential_snap_elements)
1099             if sui.extra_snap_points:
1100                 snap_points.extend(sui.extra_snap_points)
1101
1102             if snap_points:
1103                 bgl.glEnable(bgl.GL_BLEND)
1104
1105                 bgl.glPointSize(5)
1106                 bgl.glColor4f(1, 0, 0, 0.5)
1107
1108                 bgl.glBegin(bgl.GL_POINTS)
1109                 for p in snap_points:
1110                     p = location_3d_to_region_2d(r, rv3d, p)
1111                     if p is not None:
1112                         bgl.glVertex2f(p[0], p[1])
1113                 bgl.glEnd()
1114
1115                 bgl.glPointSize(1)
1116
1117         if self.transform_mode == 'MOVE':
1118             return
1119
1120         bgl.glEnable(bgl.GL_LINE_STIPPLE)
1121
1122         bgl.glLineWidth(1)
1123
1124         bgl.glColor4f(0, 0, 0, 1)
1125         draw_line_2d(self.origin_xy, self.xy)
1126
1127         bgl.glDisable(bgl.GL_LINE_STIPPLE)
1128
1129         line_width = 3
1130         bgl.glLineWidth(line_width)
1131
1132         L = 12.0
1133         arrow_len = 6.0
1134         arrow_width = 8.0
1135         arrow_space = 5.0
1136
1137         Lmax = arrow_space * 2 + L * 2 + line_width
1138
1139         pos = self.xy.to_2d()
1140         normal = self.prev_delta_xy.to_2d().normalized()
1141         dist = self.prev_delta_xy.length
1142         tangential = Vector((-normal[1], normal[0]))
1143
1144         if self.transform_mode == 'ROTATE':
1145             n_axes = sum(int(v) for v in self.allowed_axes)
1146             if n_axes == 2:
1147                 bgl.glColor4f(0.4, 0.15, 0.15, 1)
1148                 for sgn in (-1, 1):
1149                     n = sgn * Vector((0, 1))
1150                     p0 = pos + arrow_space * n
1151                     draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
1152
1153                 bgl.glColor4f(0.11, 0.51, 0.11, 1)
1154                 for sgn in (-1, 1):
1155                     n = sgn * Vector((1, 0))
1156                     p0 = pos + arrow_space * n
1157                     draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
1158             else:
1159                 bgl.glColor4f(0, 0, 0, 1)
1160                 for sgn in (-1, 1):
1161                     n = sgn * tangential
1162                     if dist < Lmax:
1163                         n *= dist / Lmax
1164                     p0 = pos + arrow_space * n
1165                     draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
1166         elif self.transform_mode == 'SCALE':
1167             bgl.glColor4f(0, 0, 0, 1)
1168             for sgn in (-1, 1):
1169                 n = sgn * normal
1170                 p0 = pos + arrow_space * n
1171                 draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
1172
1173         bgl.glLineWidth(1)
1174
1175     def draw_axes_coords(self, context, header_size):
1176         if self.check_v3d_local(context):
1177             return
1178
1179         if time.time() < (self.click_start + self.click_period):
1180             return
1181
1182         v3d = context.space_data
1183
1184         userprefs_view = context.preferences.view
1185
1186         tool_settings = context.tool_settings
1187
1188         settings = find_settings()
1189         tfm_opts = settings.transform_options
1190
1191         localmat = CursorDynamicSettings.local_matrix
1192
1193         font_id = 0 # default font
1194
1195         font_size = 11
1196         blf.size(font_id, font_size, 72) # font, point size, dpi
1197
1198         tet = context.preferences.themes[0].text_editor
1199
1200         # Prepare the table...
1201         if self.transform_mode == 'MOVE':
1202             axis_prefix = ("D" if tfm_opts.use_relative_coords else "")
1203         elif self.transform_mode == 'SCALE':
1204             axis_prefix = "S"
1205         else:
1206             axis_prefix = "R"
1207         axis_names = ["X", "Y", "Z"]
1208
1209         axis_cells = []
1210         coord_cells = []
1211         #caret_cell = TextCell("_", tet.cursor)
1212         caret_cell = TextCell("|", tet.cursor)
1213
1214         try:
1215             axes_text = self.get_axes_text()
1216
1217             for i in range(3):
1218                 color = tet.space.text
1219                 alpha = (1.0 if self.allowed_axes[i] else 0.5)
1220                 text = axis_prefix + axis_names[i] + " : "
1221                 axis_cells.append(TextCell(text, color, alpha))
1222
1223                 if self.axes_values[i]:
1224                     if self.axes_eval_success[i]:
1225                         color = tet.syntax_numbers
1226                     else:
1227                         color = tet.syntax_string
1228                 else:
1229                     color = tet.space.text
1230                 text = axes_text[i]
1231                 coord_cells.append(TextCell(text, color))
1232         except Exception as e:
1233             print(repr(e))
1234
1235         mode_cells = []
1236
1237         try:
1238             snap_type = self.su.implementation.snap_type
1239             if snap_type is None:
1240                 color = tet.space.text
1241             elif (not self.use_object_centers) or \
1242                     (snap_type == 'INCREMENT'):
1243                 color = tet.syntax_numbers
1244             else:
1245                 color = tet.syntax_special
1246             text = snap_type or tool_settings.snap_element
1247             if text == 'VOLUME':
1248                 text = "BBOX"
1249             mode_cells.append(TextCell(text, color))
1250
1251             if self.csu.tou.is_custom:
1252                 color = tet.space.text
1253             else:
1254                 color = tet.syntax_builtin
1255             text = self.csu.tou.get_title()
1256             mode_cells.append(TextCell(text, color))
1257
1258             color = tet.space.text
1259             text = self.csu.get_pivot_name(raw=True)
1260             if self.use_object_centers:
1261                 color = tet.syntax_special
1262             mode_cells.append(TextCell(text, color))
1263         except Exception as e:
1264             print(repr(e))
1265
1266         hdr_w, hdr_h = header_size
1267
1268         try:
1269             xyz_x_start_min = 12
1270             xyz_x_start = xyz_x_start_min
1271             mode_x_start = 6
1272
1273             mode_margin = 4
1274             xyz_margin = 16
1275             blend_margin = 32
1276
1277             color = tet.space.back
1278             bgl.glColor4f(color[0], color[1], color[2], 1.0)
1279             draw_rect(0, 0, hdr_w, hdr_h)
1280
1281             if tool_settings.use_snap_self:
1282                 x = hdr_w - mode_x_start
1283                 y = hdr_h / 2
1284                 cell = mode_cells[0]
1285                 x -= cell.w
1286                 y -= cell.h * 0.5
1287                 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
1288                 draw_rect(x, y, cell.w, cell.h, 1, True)
1289
1290             x = hdr_w - mode_x_start
1291             y = hdr_h / 2
1292             for cell in mode_cells:
1293                 cell.draw(x, y, (1, 0.5))
1294                 x -= (cell.w + mode_margin)
1295
1296             curr_axis_x_start = 0
1297             curr_axis_x_end = 0
1298             caret_x = 0
1299
1300             xyz_width = 0
1301             for i in range(3):
1302                 if i == self.current_axis:
1303                     curr_axis_x_start = xyz_width
1304
1305                 xyz_width += axis_cells[i].w
1306
1307                 if i == self.current_axis:
1308                     char_offset = 0
1309                     if self.axes_values[i]:
1310                         char_offset = blf.dimensions(font_id,
1311                             coord_cells[i].text[:self.caret_pos])[0]
1312                     caret_x = xyz_width + char_offset
1313
1314                 xyz_width += coord_cells[i].w
1315
1316                 if i == self.current_axis:
1317                     curr_axis_x_end = xyz_width
1318
1319                 xyz_width += xyz_margin
1320
1321             xyz_width = int(xyz_width)
1322             xyz_width_ext = xyz_width + blend_margin
1323
1324             offset = (xyz_x_start + curr_axis_x_end) - hdr_w
1325             if offset > 0:
1326                 xyz_x_start -= offset
1327
1328             offset = xyz_x_start_min - (xyz_x_start + curr_axis_x_start)
1329             if offset > 0:
1330                 xyz_x_start += offset
1331
1332             offset = (xyz_x_start + caret_x) - hdr_w
1333             if offset > 0:
1334                 xyz_x_start -= offset
1335
1336             # somewhy GL_BLEND should be set right here
1337             # to actually draw the box with blending %)
1338             # (perhaps due to text draw happened before)
1339             bgl.glEnable(bgl.GL_BLEND)
1340             bgl.glShadeModel(bgl.GL_SMOOTH)
1341             gl_enable(bgl.GL_SMOOTH, True)
1342             color = tet.space.back
1343             bgl.glBegin(bgl.GL_TRIANGLE_STRIP)
1344             bgl.glColor4f(color[0], color[1], color[2], 1.0)
1345             bgl.glVertex2i(0, 0)
1346             bgl.glVertex2i(0, hdr_h)
1347             bgl.glVertex2i(xyz_width, 0)
1348             bgl.glVertex2i(xyz_width, hdr_h)
1349             bgl.glColor4f(color[0], color[1], color[2], 0.0)
1350             bgl.glVertex2i(xyz_width_ext, 0)
1351             bgl.glVertex2i(xyz_width_ext, hdr_h)
1352             bgl.glEnd()
1353
1354             x = xyz_x_start
1355             y = hdr_h / 2
1356             for i in range(3):
1357                 cell = axis_cells[i]
1358                 cell.draw(x, y, (0, 0.5))
1359                 x += cell.w
1360
1361                 cell = coord_cells[i]
1362                 cell.draw(x, y, (0, 0.5))
1363                 x += (cell.w + xyz_margin)
1364
1365             caret_x -= blf.dimensions(font_id, caret_cell.text)[0] * 0.5
1366             caret_cell.draw(xyz_x_start + caret_x, y, (0, 0.5))
1367
1368             bgl.glEnable(bgl.GL_BLEND)
1369             bgl.glShadeModel(bgl.GL_SMOOTH)
1370             gl_enable(bgl.GL_SMOOTH, True)
1371             color = tet.space.back
1372             bgl.glBegin(bgl.GL_TRIANGLE_STRIP)
1373             bgl.glColor4f(color[0], color[1], color[2], 1.0)
1374             bgl.glVertex2i(0, 0)
1375             bgl.glVertex2i(0, hdr_h)
1376             bgl.glVertex2i(xyz_x_start_min, 0)
1377             bgl.glColor4f(color[0], color[1], color[2], 0.0)
1378             bgl.glVertex2i(xyz_x_start_min, hdr_h)
1379             bgl.glEnd()
1380
1381         except Exception as e:
1382             print(repr(e))
1383
1384         return
1385
1386     # ====== NORMAL SNAPSHOT ====== #
1387     def is_normal_visible(self):
1388         if self.csu.tou.get() == "Surface":
1389             return True
1390
1391         if self.use_object_centers:
1392             return False
1393
1394         return self.su.implementation.snap_type \
1395             not in {None, 'INCREMENT', 'VOLUME'}
1396
1397     def get_normal_params(self, tfm_opts, dest_point):
1398         surf_matrix = self.csu.get_matrix("Surface")
1399         if tfm_opts.use_relative_coords:
1400             surf_origin = dest_point
1401         else:
1402             surf_origin = surf_matrix.to_translation()
1403
1404         m3 = surf_matrix.to_3x3()
1405         p0 = surf_origin
1406         scl = self.gizmo_scale(p0)
1407
1408         # Normal and tangential are not always orthogonal
1409         # (e.g. when normal is interpolated)
1410         x = (m3 * Vector((1, 0, 0))).normalized()
1411         y = (m3 * Vector((0, 1, 0))).normalized()
1412         z = (m3 * Vector((0, 0, 1))).normalized()
1413
1414         _x = z.cross(y)
1415         _z = y.cross(x)
1416
1417         return p0, x * scl, y * scl, z * scl, _x * scl, _z * scl
1418
1419     def make_normal_snapshot(self, scene, tangential=False):
1420         settings = find_settings()
1421         tfm_opts = settings.transform_options
1422
1423         dest_point = self.particles[0].get_location()
1424
1425         if self.is_normal_visible():
1426             p0, x, y, z, _x, _z = \
1427                 self.get_normal_params(tfm_opts, dest_point)
1428
1429             snapshot = bpy.data.objects.new("normal_snapshot", None)
1430
1431             if tangential:
1432                 m = MatrixCompose(_z, y, x, p0)
1433             else:
1434                 m = MatrixCompose(_x, y, z, p0)
1435             snapshot.matrix_world = m
1436
1437             snapshot.empty_display_type = 'SINGLE_ARROW'
1438             #snapshot.empty_display_type = 'ARROWS'
1439             #snapshot.layers = [True] * 20 # ?
1440             scene.objects.link(snapshot)
1441 #============================================================================#
1442
1443
1444 class Particle:
1445     pass
1446
1447 class View3D_Cursor(Particle):
1448     def __init__(self, context):
1449         assert context.space_data.type == 'VIEW_3D'
1450         self.v3d = context.space_data
1451         self.initial_pos = self.get_location()
1452         self.initial_matrix = Matrix.Translation(self.initial_pos)
1453
1454     def revert(self):
1455         self.set_location(self.initial_pos)
1456
1457     def get_location(self):
1458         return get_cursor_location(v3d=self.v3d)
1459
1460     def set_location(self, value):
1461         set_cursor_location(Vector(value), v3d=self.v3d)
1462
1463     def get_rotation(self):
1464         return Quaternion()
1465
1466     def set_rotation(self, value):
1467         pass
1468
1469     def get_scale(self):
1470         return Vector((1.0, 1.0, 1.0))
1471
1472     def set_scale(self, value):
1473         pass
1474
1475     def get_matrix(self):
1476         return Matrix.Translation(self.get_location())
1477
1478     def set_matrix(self, value):
1479         self.set_location(value.to_translation())
1480
1481     def get_initial_matrix(self):
1482         return self.initial_matrix
1483
1484 class View3D_Object(Particle):
1485     def __init__(self, obj):
1486         self.obj = obj
1487
1488     def get_location(self):
1489         # obj.location seems to be in parent's system...
1490         # or even maybe not bounded by constraints %)
1491         return self.obj.matrix_world.to_translation()
1492
1493 class View3D_EditMesh_Vertex(Particle):
1494     pass
1495
1496 class View3D_EditMesh_Edge(Particle):
1497     pass
1498
1499 class View3D_EditMesh_Face(Particle):
1500     pass
1501
1502 class View3D_EditSpline_Point(Particle):
1503     pass
1504
1505 class View3D_EditSpline_BezierPoint(Particle):
1506     pass
1507
1508 class View3D_EditSpline_BezierHandle(Particle):
1509     pass
1510
1511 class View3D_EditMeta_Element(Particle):
1512     pass
1513
1514 class View3D_EditBone_Bone(Particle):
1515     pass
1516
1517 class View3D_EditBone_HeadTail(Particle):
1518     pass
1519
1520 class View3D_PoseBone(Particle):
1521     pass
1522
1523 class UV_Cursor(Particle):
1524     pass
1525
1526 class UV_Vertex(Particle):
1527     pass
1528
1529 class UV_Edge(Particle):
1530     pass
1531
1532 class UV_Face(Particle):
1533     pass
1534
1535 # Other types:
1536 # NLA / Dopesheet / Graph editor ...
1537
1538 # Particles are used in the following situations:
1539 # - as subjects of transformation
1540 # - as reference point(s) for cursor transformation
1541 # Note: particles 'dragged' by Proportional Editing
1542 # are a separate issue (they can come and go).
1543 def gather_particles(**kwargs):
1544     context = kwargs.get("context", bpy.context)
1545
1546     area_type = kwargs.get("area_type", context.area.type)
1547
1548     scene = kwargs.get("scene", context.scene)
1549         
1550     view_layer = kwargs.get("view_layer", context.view_layer)
1551
1552     space_data = kwargs.get("space_data", context.space_data)
1553     region_data = kwargs.get("region_data", context.region_data)
1554
1555     particles = []
1556     pivots = {}
1557     normal_system = None
1558
1559     active_element = None
1560     cursor_pos = None
1561     median = None
1562
1563     if area_type == 'VIEW_3D':
1564         context_mode = kwargs.get("context_mode", context.mode)
1565
1566         selected_objects = kwargs.get("selected_objects",
1567             context.selected_objects)
1568
1569         active_object = kwargs.get("active_object",
1570             context.active_object)
1571
1572         if context_mode == 'OBJECT':
1573             for obj in selected_objects:
1574                 particle = View3D_Object(obj)
1575                 particles.append(particle)
1576
1577             if active_object:
1578                 active_element = active_object.\
1579                     matrix_world.to_translation()
1580
1581         # On Undo/Redo scene hash value is changed ->
1582         # -> the monitor tries to update the CSU ->
1583         # -> object.mode_set seem to somehow conflict
1584         # with Undo/Redo mechanisms.
1585         elif active_object and active_object.data and \
1586         (context_mode in {
1587         'EDIT_MESH', 'EDIT_METABALL',
1588         'EDIT_CURVE', 'EDIT_SURFACE',
1589         'EDIT_ARMATURE', 'POSE'}):
1590
1591             m = active_object.matrix_world
1592
1593             positions = []
1594             normal = Vector((0, 0, 0))
1595
1596             if context_mode == 'EDIT_MESH':
1597                 bm = bmesh.from_edit_mesh(active_object.data)
1598
1599                 if bm.select_history:
1600                     elem = bm.select_history[-1]
1601                     if isinstance(elem, bmesh.types.BMVert):
1602                         active_element = elem.co.copy()
1603                     else:
1604                         active_element = Vector()
1605                         for v in elem.verts:
1606                             active_element += v.co
1607                         active_element *= 1.0 / len(elem.verts)
1608
1609                 for v in bm.verts:
1610                     if v.select:
1611                         positions.append(v.co)
1612                         normal += v.normal
1613
1614                 # mimic Blender's behavior (as of now,
1615                 # order of selection is ignored)
1616                 if len(positions) == 2:
1617                     normal = positions[1] - positions[0]
1618                 elif len(positions) == 3:
1619                     a = positions[0] - positions[1]
1620                     b = positions[2] - positions[1]
1621                     normal = a.cross(b)
1622             elif context_mode == 'EDIT_METABALL':
1623                 active_elem = active_object.data.elements.active
1624                 if active_elem:
1625                     active_element = active_elem.co.copy()
1626                     active_element = active_object.\
1627                         matrix_world * active_element
1628
1629                 # Currently there is no API for element.select
1630                 #for element in active_object.data.elements:
1631                 #    if element.select:
1632                 #        positions.append(element.co)
1633             elif context_mode == 'EDIT_ARMATURE':
1634                 # active bone seems to have the same pivot
1635                 # as median of the selection
1636                 '''
1637                 active_bone = active_object.data.edit_bones.active
1638                 if active_bone:
1639                     active_element = active_bone.head + \
1640                                      active_bone.tail
1641                     active_element = active_object.\
1642                         matrix_world * active_element
1643                 '''
1644
1645                 for bone in active_object.data.edit_bones:
1646                     if bone.select_head:
1647                         positions.append(bone.head)
1648                     if bone.select_tail:
1649                         positions.append(bone.tail)
1650             elif context_mode == 'POSE':
1651                 active_bone = active_object.data.bones.active
1652                 if active_bone:
1653                     active_element = active_bone.\
1654                         matrix_local.translation.to_3d()
1655                     active_element = active_object.\
1656                         matrix_world * active_element
1657
1658                 # consider only topmost parents
1659                 bones = set()
1660                 for bone in active_object.data.bones:
1661                     if bone.select:
1662                         bones.add(bone)
1663
1664                 parents = set()
1665                 for bone in bones:
1666                     if not set(bone.parent_recursive).intersection(bones):
1667                         parents.add(bone)
1668
1669                 for bone in parents:
1670                     positions.append(bone.matrix_local.translation.to_3d())
1671             else:
1672                 for spline in active_object.data.splines:
1673                     for point in spline.bezier_points:
1674                         if point.select_control_point:
1675                             positions.append(point.co)
1676                         else:
1677                             if point.select_left_handle:
1678                                 positions.append(point.handle_left)
1679                             if point.select_right_handle:
1680                                 positions.append(point.handle_right)
1681
1682                         n = None
1683                         nL = point.co - point.handle_left
1684                         nR = point.co - point.handle_right
1685                         #nL = point.handle_left.copy()
1686                         #nR = point.handle_right.copy()
1687                         if point.select_control_point:
1688                             n = nL + nR
1689                         elif point.select_left_handle or \
1690                              point.select_right_handle:
1691                             n = nL + nR
1692                         else:
1693                             if point.select_left_handle:
1694                                 n = -nL
1695                             if point.select_right_handle:
1696                                 n = nR
1697
1698                         if n is not None:
1699                             if n.length_squared < epsilon:
1700                                 n = -nL
1701                             normal += n.normalized()
1702
1703                     for point in spline.points:
1704                         if point.select:
1705                             positions.append(point.co)
1706
1707             if len(positions) != 0:
1708                 if normal.length_squared < epsilon:
1709                     normal = Vector((0, 0, 1))
1710                 normal.rotate(m)
1711                 normal.normalize()
1712
1713                 if (1.0 - abs(normal.z)) < epsilon:
1714                     t1 = Vector((1, 0, 0))
1715                 else:
1716                     t1 = Vector((0, 0, 1)).cross(normal)
1717                 t2 = t1.cross(normal)
1718                 normal_system = MatrixCompose(t1, t2, normal)
1719
1720                 median, bbox_center = calc_median_bbox_pivots(positions)
1721                 median = m * median
1722                 bbox_center = m * bbox_center
1723
1724                 # Currently I don't know how to get active mesh element
1725                 if active_element is None:
1726                     if context_mode == 'EDIT_ARMATURE':
1727                         # Somewhy EDIT_ARMATURE has such behavior
1728                         active_element = bbox_center
1729                     else:
1730                         active_element = median
1731             else:
1732                 if active_element is None:
1733                     active_element = active_object.\
1734                         matrix_world.to_translation()
1735
1736                 median = active_element
1737                 bbox_center = active_element
1738
1739                 normal_system = active_object.matrix_world.to_3x3()
1740                 normal_system.col[0].normalize()
1741                 normal_system.col[1].normalize()
1742                 normal_system.col[2].normalize()
1743         else:
1744             # paint/sculpt, etc.?
1745             particle = View3D_Object(active_object)
1746             particles.append(particle)
1747
1748             if active_object:
1749                 active_element = active_object.\
1750                     matrix_world.to_translation()
1751
1752         cursor_pos = get_cursor_location(v3d=space_data)
1753
1754     #elif area_type == 'IMAGE_EDITOR':
1755         # currently there is no way to get UV editor's
1756         # offset (and maybe some other parameters
1757         # required to implement these operators)
1758         #cursor_pos = space_data.uv_editor.cursor_location
1759
1760     #elif area_type == 'EMPTY':
1761     #elif area_type == 'GRAPH_EDITOR':
1762     #elif area_type == 'OUTLINER':
1763     #elif area_type == 'PROPERTIES':
1764     #elif area_type == 'FILE_BROWSER':
1765     #elif area_type == 'INFO':
1766     #elif area_type == 'SEQUENCE_EDITOR':
1767     #elif area_type == 'TEXT_EDITOR':
1768     #elif area_type == 'AUDIO_WINDOW':
1769     #elif area_type == 'DOPESHEET_EDITOR':
1770     #elif area_type == 'NLA_EDITOR':
1771     #elif area_type == 'SCRIPTS_WINDOW':
1772     #elif area_type == 'TIMELINE':
1773     #elif area_type == 'NODE_EDITOR':
1774     #elif area_type == 'LOGIC_EDITOR':
1775     #elif area_type == 'CONSOLE':
1776     #elif area_type == 'USER_PREFERENCES':
1777
1778     else:
1779         print("gather_particles() not implemented for '{}'".\
1780               format(area_type))
1781         return None, None
1782
1783     # 'INDIVIDUAL_ORIGINS' is not handled here
1784
1785     if cursor_pos:
1786         pivots['CURSOR'] = cursor_pos.copy()
1787
1788     if active_element:
1789         # in v3d: ACTIVE_ELEMENT
1790         pivots['ACTIVE'] = active_element.copy()
1791
1792     if (len(particles) != 0) and (median is None):
1793         positions = (p.get_location() for p in particles)
1794         median, bbox_center = calc_median_bbox_pivots(positions)
1795
1796     if median:
1797         # in v3d: MEDIAN_POINT, in UV editor: MEDIAN
1798         pivots['MEDIAN'] = median.copy()
1799         # in v3d: BOUNDING_BOX_CENTER, in UV editor: CENTER
1800         pivots['CENTER'] = bbox_center.copy()
1801
1802     csu = CoordinateSystemUtility(scene, space_data, region_data, \
1803         pivots, normal_system, view_layer)
1804
1805     return particles, csu
1806
1807 def calc_median_bbox_pivots(positions):
1808     median = None # pos can be 3D or 2D
1809     bbox = [None, None]
1810
1811     n = 0
1812     for pos in positions:
1813         extend_bbox(bbox, pos)
1814         try:
1815             median += pos
1816         except:
1817             median = pos.copy()
1818         n += 1
1819
1820     median = median / n
1821     bbox_center = (Vector(bbox[0]) + Vector(bbox[1])) * 0.5
1822
1823     return median, bbox_center
1824
1825 def extend_bbox(bbox, pos):
1826     try:
1827         bbox[0] = tuple(min(e0, e1) for e0, e1 in zip(bbox[0], pos))
1828         bbox[1] = tuple(max(e0, e1) for e0, e1 in zip(bbox[1], pos))
1829     except:
1830         bbox[0] = tuple(pos)
1831         bbox[1] = tuple(pos)
1832
1833
1834 # ====== COORDINATE SYSTEM UTILITY ====== #
1835 class CoordinateSystemUtility:
1836     pivot_name_map = {
1837         'CENTER':'CENTER',
1838         'BOUNDING_BOX_CENTER':'CENTER',
1839         'MEDIAN':'MEDIAN',
1840         'MEDIAN_POINT':'MEDIAN',
1841         'CURSOR':'CURSOR',
1842         'INDIVIDUAL_ORIGINS':'INDIVIDUAL',
1843         'ACTIVE_ELEMENT':'ACTIVE',
1844         'WORLD':'WORLD',
1845         'SURFACE':'SURFACE', # ?
1846         'BOOKMARK':'BOOKMARK',
1847     }
1848     pivot_v3d_map = {
1849         'CENTER':'BOUNDING_BOX_CENTER',
1850         'MEDIAN':'MEDIAN_POINT',
1851         'CURSOR':'CURSOR',
1852         'INDIVIDUAL':'INDIVIDUAL_ORIGINS',
1853         'ACTIVE':'ACTIVE_ELEMENT',
1854     }
1855
1856     def __init__(self, scene, space_data, region_data, \
1857                  pivots, normal_system, view_layer):
1858         self.space_data = space_data
1859         self.region_data = region_data
1860
1861         if space_data.type == 'VIEW_3D':
1862             self.pivot_map_inv = self.pivot_v3d_map
1863
1864         self.tou = TransformOrientationUtility(
1865             scene, space_data, region_data, view_layer)
1866         self.tou.normal_system = normal_system
1867
1868         self.pivots = pivots
1869
1870         # Assigned by caller (for cursor or selection)
1871         self.source_pos = None
1872         self.source_rot = None
1873         self.source_scale = None
1874
1875     def set_orientation(self, name):
1876         self.tou.set(name)
1877
1878     def set_pivot(self, pivot):
1879         self.space_data.pivot_point = self.pivot_map_inv[pivot]
1880
1881     def get_pivot_name(self, name=None, relative=None, raw=False):
1882         pivot = self.pivot_name_map[self.space_data.pivot_point]
1883         if raw:
1884             return pivot
1885
1886         if not name:
1887             name = self.tou.get()
1888
1889         if relative is None:
1890             settings = find_settings()
1891             tfm_opts = settings.transform_options
1892             relative = tfm_opts.use_relative_coords
1893
1894         if relative:
1895             pivot = "RELATIVE"
1896         elif (name == 'GLOBAL') or (pivot == 'WORLD'):
1897             pivot = 'WORLD'
1898         elif (name == "Surface") or (pivot == 'SURFACE'):
1899             pivot = "SURFACE"
1900
1901         return pivot
1902
1903     def get_origin(self, name=None, relative=None, pivot=None):
1904         if not pivot:
1905             pivot = self.get_pivot_name(name, relative)
1906
1907         if relative or (pivot == "RELATIVE"):
1908             # "relative" parameter overrides "pivot"
1909             return self.source_pos
1910         elif pivot == 'WORLD':
1911             return Vector()
1912         elif pivot == "SURFACE":
1913             runtime_settings = find_runtime_settings()
1914             return Vector(runtime_settings.surface_pos)
1915         else:
1916             if pivot == 'INDIVIDUAL':
1917                 pivot = 'MEDIAN'
1918
1919             #if pivot == 'ACTIVE':
1920             #    print(self.pivots)
1921
1922             try:
1923                 return self.pivots[pivot]
1924             except:
1925                 return Vector()
1926
1927     def get_matrix(self, name=None, relative=None, pivot=None):
1928         if not name:
1929             name = self.tou.get()
1930
1931         matrix = self.tou.get_matrix(name)
1932
1933         if isinstance(pivot, Vector):
1934             pos = pivot
1935         else:
1936             pos = self.get_origin(name, relative, pivot)
1937
1938         return to_matrix4x4(matrix, pos)
1939
1940 # ====== TRANSFORM ORIENTATION UTILITIES ====== #
1941 class TransformOrientationUtility:
1942     special_systems = {"Surface", "Scaled"}
1943     predefined_systems = {
1944         'GLOBAL', 'LOCAL', 'VIEW', 'NORMAL', 'GIMBAL',
1945         "Scaled", "Surface",
1946     }
1947
1948     def __init__(self, scene, v3d, rv3d, vwly):
1949         self.scene = scene
1950         self.v3d = v3d
1951         self.rv3d = rv3d
1952         self.view_layer = vwly
1953
1954         self.custom_systems = [item for item in scene.orientations \
1955             if item.name not in self.special_systems]
1956
1957         self.is_custom = False
1958         self.custom_id = -1
1959
1960         # This is calculated elsewhere
1961         self.normal_system = None
1962
1963         self.set(v3d.transform_orientation)
1964
1965     def get(self):
1966         return self.transform_orientation
1967
1968     def get_title(self):
1969         if self.is_custom:
1970             return self.transform_orientation
1971
1972         name = self.transform_orientation
1973         return name[:1].upper() + name[1:].lower()
1974
1975     def set(self, name, set_v3d=True):
1976         if isinstance(name, int):
1977             n = len(self.custom_systems)
1978             if n == 0:
1979                 # No custom systems, do nothing
1980                 return
1981
1982             increment = name
1983
1984             if self.is_custom:
1985                 # If already custom, switch to next custom system
1986                 self.custom_id = (self.custom_id + increment) % n
1987
1988             self.is_custom = True
1989
1990             name = self.custom_systems[self.custom_id].name
1991         else:
1992             self.is_custom = name not in self.predefined_systems
1993
1994             if self.is_custom:
1995                 self.custom_id = next((i for i, v in \
1996                     enumerate(self.custom_systems) if v.name == name), -1)
1997
1998             if name in self.special_systems:
1999                 # Ensure such system exists
2000                 self.get_custom(name)
2001
2002         self.transform_orientation = name
2003
2004         if set_v3d:
2005             self.v3d.transform_orientation = name
2006
2007     def get_matrix(self, name=None):
2008         active_obj = self.view_layer.objects.active
2009
2010         if not name:
2011             name = self.transform_orientation
2012
2013         if self.is_custom:
2014             matrix = self.custom_systems[self.custom_id].matrix.copy()
2015         else:
2016             if (name == 'VIEW') and self.rv3d:
2017                 matrix = self.rv3d.view_rotation.to_matrix()
2018             elif name == "Surface":
2019                 matrix = self.get_custom(name).matrix.copy()
2020             elif (name == 'GLOBAL') or (not active_obj):
2021                 matrix = Matrix().to_3x3()
2022             elif (name == 'NORMAL') and self.normal_system:
2023                 matrix = self.normal_system.copy()
2024             else:
2025                 matrix = active_obj.matrix_world.to_3x3()
2026                 if name == "Scaled":
2027                     self.get_custom(name).matrix = matrix
2028                 else: # 'LOCAL', 'GIMBAL', ['NORMAL'] for now
2029                     matrix[0].normalize()
2030                     matrix[1].normalize()
2031                     matrix[2].normalize()
2032
2033         return matrix
2034
2035     def get_custom(self, name):
2036         try:
2037             return self.scene.orientations[name]
2038         except:
2039             return create_transform_orientation(
2040                 self.scene, name, Matrix())
2041
2042 # Is there a less cumbersome way to create transform orientation?
2043 def create_transform_orientation(scene, name=None, matrix=None):
2044     active_obj = view_layer.objects.active
2045     prev_mode = None
2046
2047     if active_obj:
2048         prev_mode = active_obj.mode
2049         bpy.ops.object.mode_set(mode='OBJECT')
2050     else:
2051         bpy.ops.object.add()
2052
2053     # ATTENTION! This uses context's scene
2054     bpy.ops.transform.create_orientation()
2055
2056     tfm_orient = scene.orientations[-1]
2057
2058     if name is not None:
2059         basename = name
2060         i = 1
2061         while name in scene.orientations:
2062             name = "%s.%03i" % (basename, i)
2063             i += 1
2064         tfm_orient.name = name
2065
2066     if matrix:
2067         tfm_orient.matrix = matrix.to_3x3()
2068
2069     if active_obj:
2070         bpy.ops.object.mode_set(mode=prev_mode)
2071     else:
2072         bpy.ops.object.delete()
2073
2074     return tfm_orient
2075
2076 # ====== VIEW UTILITY CLASS ====== #
2077 class ViewUtility:
2078     methods = dict(
2079         get_locks = lambda: {},
2080         set_locks = lambda locks: None,
2081         get_position = lambda: Vector(),
2082         set_position = lambda: None,
2083         get_rotation = lambda: Quaternion(),
2084         get_direction = lambda: Vector((0, 0, 1)),
2085         get_viewpoint = lambda: Vector(),
2086         get_matrix = lambda: Matrix(),
2087         get_point = lambda xy, pos: \
2088             Vector((xy[0], xy[1], 0)),
2089         get_ray = lambda xy: tuple(
2090             Vector((xy[0], xy[1], 0)),
2091             Vector((xy[0], xy[1], 1)),
2092             False),
2093     )
2094
2095     def __init__(self, region, space_data, region_data):
2096         self.region = region
2097         self.space_data = space_data
2098         self.region_data = region_data
2099
2100         if space_data.type == 'VIEW_3D':
2101             self.implementation = View3DUtility(
2102                 region, space_data, region_data)
2103         else:
2104             self.implementation = None
2105
2106         if self.implementation:
2107             for name in self.methods:
2108                 setattr(self, name,
2109                     getattr(self.implementation, name))
2110         else:
2111             for name, value in self.methods.items():
2112                 setattr(self, name, value)
2113
2114 class View3DUtility:
2115     lock_types = {"lock_cursor": False, "lock_object": None, "lock_bone": ""}
2116
2117     # ====== INITIALIZATION / CLEANUP ====== #
2118     def __init__(self, region, space_data, region_data):
2119         self.region = region
2120         self.space_data = space_data
2121         self.region_data = region_data
2122
2123     # ====== GET VIEW MATRIX AND ITS COMPONENTS ====== #
2124     def get_locks(self):
2125         v3d = self.space_data
2126         return {k:getattr(v3d, k) for k in self.lock_types}
2127
2128     def set_locks(self, locks):
2129         v3d = self.space_data
2130         for k in self.lock_types:
2131             setattr(v3d, k, locks.get(k, self.lock_types[k]))
2132
2133     def _get_lock_obj_bone(self):
2134         v3d = self.space_data
2135
2136         obj = v3d.lock_object
2137         if not obj:
2138             return None, None
2139
2140         if v3d.lock_bone:
2141             try:
2142                 # this is not tested!
2143                 if obj.mode == 'EDIT':
2144                     bone = obj.data.edit_bones[v3d.lock_bone]
2145                 else:
2146                     bone = obj.data.bones[v3d.lock_bone]
2147             except:
2148                 bone = None
2149         else:
2150             bone = None
2151
2152         return obj, bone
2153
2154     # TODO: learn how to get these values from
2155     # rv3d.perspective_matrix and rv3d.view_matrix ?
2156     def get_position(self, no_locks=False):
2157         v3d = self.space_data
2158         rv3d = self.region_data
2159
2160         if no_locks:
2161             return rv3d.view_location.copy()
2162
2163         # rv3d.perspective_matrix and rv3d.view_matrix
2164         # seem to have some weird translation components %)
2165
2166         if rv3d.view_perspective == 'CAMERA':
2167             p = v3d.camera.matrix_world.to_translation()
2168             d = self.get_direction()
2169             return p + d * rv3d.view_distance
2170         else:
2171             if v3d.lock_object:
2172                 obj, bone = self._get_lock_obj_bone()
2173                 if bone:
2174                     return (obj.matrix_world * bone.matrix).to_translation()
2175                 else:
2176                     return obj.matrix_world.to_translation()
2177             elif v3d.lock_cursor:
2178                 return get_cursor_location(v3d=v3d)
2179             else:
2180                 return rv3d.view_location.copy()
2181
2182     def set_position(self, pos, no_locks=False):
2183         v3d = self.space_data
2184         rv3d = self.region_data
2185
2186         pos = pos.copy()
2187
2188         if no_locks:
2189             rv3d.view_location = pos
2190             return
2191
2192         if rv3d.view_perspective == 'CAMERA':
2193             d = self.get_direction()
2194             v3d.camera.matrix_world.translation = pos - d * rv3d.view_distance
2195         else:
2196             if v3d.lock_object:
2197                 obj, bone = self._get_lock_obj_bone()
2198                 if bone:
2199                     try:
2200                         bone.matrix.translation = \
2201                             obj.matrix_world.inverted() * pos
2202                     except:
2203                         # this is some degenerate object
2204                         bone.matrix.translation = pos
2205                 else:
2206                     obj.matrix_world.translation = pos
2207             elif v3d.lock_cursor:
2208                 set_cursor_location(pos, v3d=v3d)
2209             else:
2210                 rv3d.view_location = pos
2211
2212     def get_rotation(self):
2213         v3d = self.space_data
2214         rv3d = self.region_data
2215
2216         if rv3d.view_perspective == 'CAMERA':
2217             return v3d.camera.matrix_world.to_quaternion()
2218         else:
2219             return rv3d.view_rotation
2220
2221     def get_direction(self):
2222         # Camera (as well as viewport) looks in the direction of -Z;
2223         # Y is up, X is left
2224         d = self.get_rotation() * Vector((0, 0, -1))
2225         d.normalize()
2226         return d
2227
2228     def get_viewpoint(self):
2229         v3d = self.space_data
2230         rv3d = self.region_data
2231
2232         if rv3d.view_perspective == 'CAMERA':
2233             return v3d.camera.matrix_world.to_translation()
2234         else:
2235             p = self.get_position()
2236             d = self.get_direction()
2237             return p - d * rv3d.view_distance
2238
2239     def get_matrix(self):
2240         m = self.get_rotation().to_matrix()
2241         m.resize_4x4()
2242         m.translation = self.get_viewpoint()
2243         return m
2244
2245     def get_point(self, xy, pos):
2246         region = self.region
2247         rv3d = self.region_data
2248         return region_2d_to_location_3d(region, rv3d, xy, pos)
2249
2250     def get_ray(self, xy):
2251         region = self.region
2252         v3d = self.space_data
2253         rv3d = self.region_data
2254
2255         viewPos = self.get_viewpoint()
2256         viewDir = self.get_direction()
2257
2258         near = viewPos + viewDir * v3d.clip_start
2259         far = viewPos + viewDir * v3d.clip_end
2260
2261         a = region_2d_to_location_3d(region, rv3d, xy, near)
2262         b = region_2d_to_location_3d(region, rv3d, xy, far)
2263
2264         # When viewed from in-scene camera, near and far
2265         # planes clip geometry even in orthographic mode.
2266         clip = rv3d.is_perspective or (rv3d.view_perspective == 'CAMERA')
2267
2268         return a, b, clip
2269
2270 # ====== SNAP UTILITY CLASS ====== #
2271 class SnapUtility:
2272     def __init__(self, context):
2273         if context.area.type == 'VIEW_3D':
2274             v3d = context.space_data
2275             shade = v3d.viewport_shade
2276             self.implementation = Snap3DUtility(context.scene, shade)
2277             self.implementation.update_targets(
2278                 context.visible_objects, [])
2279
2280     def dispose(self):
2281         self.implementation.dispose()
2282
2283     def update_targets(self, to_include, to_exclude):
2284         self.implementation.update_targets(to_include, to_exclude)
2285
2286     def set_modes(self, **kwargs):
2287         return self.implementation.set_modes(**kwargs)
2288
2289     def snap(self, *args, **kwargs):
2290         return self.implementation.snap(*args, **kwargs)
2291
2292 class SnapUtilityBase:
2293     def __init__(self):
2294         self.targets = set()
2295         # TODO: set to current blend settings?
2296         self.interpolation = 'NEVER'
2297         self.editmode = False
2298         self.snap_type = None
2299         self.projection = [None, None, None]
2300         self.potential_snap_elements = None
2301         self.extra_snap_points = None
2302
2303     def update_targets(self, to_include, to_exclude):
2304         self.targets.update(to_include)
2305         self.targets.difference_update(to_exclude)
2306
2307     def set_modes(self, **kwargs):
2308         if "use_relative_coords" in kwargs:
2309             self.use_relative_coords = kwargs["use_relative_coords"]
2310         if "interpolation" in kwargs:
2311             # NEVER, ALWAYS, SMOOTH
2312             self.interpolation = kwargs["interpolation"]
2313         if "editmode" in kwargs:
2314             self.editmode = kwargs["editmode"]
2315         if "snap_align" in kwargs:
2316             self.snap_align = kwargs["snap_align"]
2317         if "snap_type" in kwargs:
2318             # 'INCREMENT', 'VERTEX', 'EDGE', 'FACE', 'VOLUME'
2319             self.snap_type = kwargs["snap_type"]
2320         if "axes_coords" in kwargs:
2321             # none, point, line, plane
2322             self.axes_coords = kwargs["axes_coords"]
2323
2324     # ====== CURSOR REPOSITIONING ====== #
2325     def snap(self, xy, src_matrix, initial_matrix, do_raycast, \
2326         alt_snap, vu, csu, modify_Surface, use_object_centers):
2327
2328         v3d = csu.space_data
2329
2330         grid_step = self.grid_steps[alt_snap] * v3d.grid_scale
2331
2332         su = self
2333         use_relative_coords = su.use_relative_coords
2334         snap_align = su.snap_align
2335         axes_coords = su.axes_coords
2336         snap_type = su.snap_type
2337
2338         runtime_settings = find_runtime_settings()
2339
2340         matrix = src_matrix.to_3x3()
2341         pos = src_matrix.to_translation().copy()
2342
2343         sys_matrix = csu.get_matrix()
2344         if use_relative_coords:
2345             sys_matrix.translation = initial_matrix.translation.copy()
2346
2347         # Axes of freedom and line/plane parameters
2348         start = Vector(((0 if v is None else v) for v in axes_coords))
2349         direction = Vector(((v is not None) for v in axes_coords))
2350         axes_of_freedom = 3 - int(sum(direction))
2351
2352         # do_raycast is False when mouse is not moving
2353         if do_raycast:
2354             su.hide_bbox(True)
2355
2356             self.potential_snap_elements = None
2357             self.extra_snap_points = None
2358
2359             set_stick_obj(csu.tou.scene, None)
2360
2361             raycast = None
2362             snap_to_obj = (snap_type != 'INCREMENT') #or use_object_centers
2363             snap_to_obj = snap_to_obj and (snap_type is not None)
2364             if snap_to_obj:
2365                 a, b, clip = vu.get_ray(xy)
2366                 view_dir = vu.get_direction()
2367                 raycast = su.snap_raycast(a, b, clip, view_dir, csu, alt_snap)
2368
2369             if raycast:
2370                 surf_matrix, face_id, obj, orig_obj = raycast
2371
2372                 if not use_object_centers:
2373                     self.potential_snap_elements = [
2374                         (obj.matrix_world * obj.data.vertices[vi].co)
2375                         for vi in obj.data.polygons[face_id].vertices
2376                     ]
2377
2378                 if use_object_centers:
2379                     self.extra_snap_points = \
2380                         [obj.matrix_world.to_translation()]
2381                 elif alt_snap:
2382                     pse = self.potential_snap_elements
2383                     n = len(pse)
2384                     if self.snap_type == 'EDGE':
2385                         self.extra_snap_points = []
2386                         for i in range(n):
2387                             v0 = pse[i]
2388                             v1 = pse[(i + 1) % n]
2389                             self.extra_snap_points.append((v0 + v1) / 2)
2390                     elif self.snap_type == 'FACE':
2391                         self.extra_snap_points = []
2392                         v0 = Vector()
2393                         for v1 in pse:
2394                             v0 += v1
2395                         self.extra_snap_points.append(v0 / n)
2396
2397                 if snap_align:
2398                     matrix = surf_matrix.to_3x3()
2399
2400                 if not use_object_centers:
2401                     pos = surf_matrix.to_translation()
2402                 else:
2403                     pos = orig_obj.matrix_world.to_translation()
2404
2405                 try:
2406                     local_pos = orig_obj.matrix_world.inverted() * pos
2407                 except:
2408                     # this is some degenerate object
2409                     local_pos = pos
2410
2411                 set_stick_obj(csu.tou.scene, orig_obj.name, local_pos)
2412
2413                 modify_Surface = modify_Surface and \
2414                     (snap_type != 'VOLUME') and (not use_object_centers)
2415
2416                 # === Update "Surface" orientation === #
2417                 if modify_Surface:
2418                     # Use raycast[0], not matrix! If snap_align == False,
2419                     # matrix will be src_matrix!
2420                     coordsys = csu.tou.get_custom("Surface")
2421                     coordsys.matrix = surf_matrix.to_3x3()
2422                     runtime_settings.surface_pos = pos
2423                     if csu.tou.get() == "Surface":
2424                         sys_matrix = to_matrix4x4(matrix, pos)
2425             else:
2426                 if axes_of_freedom == 0:
2427                     # Constrained in all axes, can't move.
2428                     pass
2429                 elif axes_of_freedom == 3:
2430                     # Not constrained, move in view plane.
2431                     pos = vu.get_point(xy, pos)
2432                 else:
2433                     a, b, clip = vu.get_ray(xy)
2434                     view_dir = vu.get_direction()
2435
2436                     start = sys_matrix * start
2437
2438                     if axes_of_freedom == 1:
2439                         direction = Vector((1, 1, 1)) - direction
2440                     direction.rotate(sys_matrix)
2441
2442                     if axes_of_freedom == 2:
2443                         # Constrained in one axis.
2444                         # Find intersection with plane.
2445                         i_p = intersect_line_plane(a, b, start, direction)
2446                         if i_p is not None:
2447                             pos = i_p
2448                     elif axes_of_freedom == 1:
2449                         # Constrained in two axes.
2450                         # Find nearest point to line.
2451                         i_p = intersect_line_line(a, b, start,
2452                                                   start + direction)
2453                         if i_p is not None:
2454                             pos = i_p[1]
2455         #end if do_raycast
2456
2457         try:
2458             sys_matrix_inv = sys_matrix.inverted()
2459         except:
2460             # this is some degenerate system
2461             sys_matrix_inv = Matrix()
2462
2463         _pos = sys_matrix_inv * pos
2464
2465         # don't snap when mouse hasn't moved
2466         if (snap_type == 'INCREMENT') and do_raycast:
2467             for i in range(3):
2468                 _pos[i] = round_step(_pos[i], grid_step)
2469
2470         for i in range(3):
2471             if axes_coords[i] is not None:
2472                 _pos[i] = axes_coords[i]
2473
2474         if (snap_type == 'INCREMENT') or (axes_of_freedom != 3):
2475             pos = sys_matrix * _pos
2476
2477         res_matrix = to_matrix4x4(matrix, pos)
2478
2479         CursorDynamicSettings.local_matrix = \
2480             sys_matrix_inv * res_matrix
2481
2482         return res_matrix
2483
2484 class Snap3DUtility(SnapUtilityBase):
2485     grid_steps = {False:1.0, True:0.1}
2486
2487     cube_verts = [Vector((i, j, k))
2488         for i in (-1, 1)
2489         for j in (-1, 1)
2490         for k in (-1, 1)]
2491
2492     def __init__(self, scene, shade):
2493         SnapUtilityBase.__init__(self)
2494
2495         convert_types = {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}
2496         self.cache = MeshCache(scene, convert_types)
2497
2498         # ? seems that dict is enough
2499         self.bbox_cache = {}#collections.OrderedDict()
2500         self.sys_matrix_key = [0.0] * 9
2501
2502         bm = prepare_gridbox_mesh(subdiv=2)
2503         mesh = bpy.data.meshes.new(tmp_name)
2504         bm.to_mesh(mesh)
2505         mesh.update(calc_tessface=True)
2506         #mesh.calc_tessface()
2507
2508         self.bbox_obj = self.cache._make_obj(mesh, None)
2509         self.bbox_obj.hide = True
2510         self.bbox_obj.display_type = 'WIRE'
2511         self.bbox_obj.name = "BoundBoxSnap"
2512
2513         self.shade_bbox = (shade == 'BOUNDBOX')
2514
2515     def update_targets(self, to_include, to_exclude):
2516         settings = find_settings()
2517         tfm_opts = settings.transform_options
2518         only_solid = tfm_opts.snap_only_to_solid
2519
2520         # Ensure this is a set and not some other
2521         # type of collection
2522         to_exclude = set(to_exclude)
2523
2524         for target in to_include:
2525             if only_solid and ((target.display_type == 'BOUNDS') \
2526                     or (target.display_type == 'WIRE')):
2527                 to_exclude.add(target)
2528
2529         SnapUtilityBase.update_targets(self, to_include, to_exclude)
2530
2531     def dispose(self):
2532         self.hide_bbox(True)
2533
2534         mesh = self.bbox_obj.data
2535         bpy.data.objects.remove(self.bbox_obj)
2536         bpy.data.meshes.remove(mesh)
2537
2538         self.cache.clear()
2539
2540     def hide_bbox(self, hide):
2541         if self.bbox_obj.hide == hide:
2542             return
2543
2544         self.bbox_obj.hide = hide
2545
2546         # We need to unlink bbox until required to show it,
2547         # because otherwise outliner will blink each
2548         # time cursor is clicked
2549         if hide:
2550             self.cache.scene.objects.unlink(self.bbox_obj)
2551         else:
2552             self.cache.scene.objects.link(self.bbox_obj)
2553
2554     def get_bbox_obj(self, obj, sys_matrix, sys_matrix_inv, is_local):
2555         if is_local:
2556             bbox = None
2557         else:
2558             bbox = self.bbox_cache.get(obj, None)
2559
2560         if bbox is None:
2561             m = obj.matrix_world
2562             if is_local:
2563                 sys_matrix = m.copy()
2564                 try:
2565                     sys_matrix_inv = sys_matrix.inverted()
2566                 except Exception:
2567                     # this is some degenerate system
2568                     sys_matrix_inv = Matrix()
2569             m_combined = sys_matrix_inv * m
2570             bbox = [None, None]
2571
2572             variant = ('RAW' if (self.editmode and
2573                        (obj.type == 'MESH') and (obj.mode == 'EDIT'))
2574                        else 'PREVIEW')
2575             mesh_obj = self.cache.get(obj, variant, reuse=False)
2576             if (mesh_obj is None) or self.shade_bbox or \
2577                     (obj.display_type == 'BOUNDS'):
2578                 if is_local:
2579                     bbox = [(-1, -1, -1), (1, 1, 1)]
2580                 else:
2581                     for p in self.cube_verts:
2582                         extend_bbox(bbox, m_combined * p.copy())
2583             elif is_local:
2584                 bbox = [mesh_obj.bound_box[0], mesh_obj.bound_box[6]]
2585             else:
2586                 for v in mesh_obj.data.vertices:
2587                     extend_bbox(bbox, m_combined * v.co.copy())
2588
2589             bbox = (Vector(bbox[0]), Vector(bbox[1]))
2590
2591             if not is_local:
2592                 self.bbox_cache[obj] = bbox
2593
2594         half = (bbox[1] - bbox[0]) * 0.5
2595
2596         m = MatrixCompose(half[0], half[1], half[2])
2597         m = sys_matrix.to_3x3() * m
2598         m.resize_4x4()
2599         m.translation = sys_matrix * (bbox[0] + half)
2600         self.bbox_obj.matrix_world = m
2601
2602         return self.bbox_obj
2603
2604     # TODO: ?
2605     # - Sort snap targets according to raycasted distance?
2606     # - Ignore targets if their bounding sphere is further
2607     #   than already picked position?
2608     # Perhaps these "optimizations" aren't worth the overhead.
2609
2610     def raycast(self, a, b, clip, view_dir, is_bbox, \
2611                 sys_matrix, sys_matrix_inv, is_local, x_ray):
2612         # If we need to interpolate normals or snap to
2613         # vertices/edges, we must convert mesh.
2614         #force = (self.interpolation != 'NEVER') or \
2615         #    (self.snap_type in {'VERTEX', 'EDGE'})
2616         # Actually, we have to always convert, since
2617         # we need to get face at least to find tangential.
2618         force = True
2619         edit = self.editmode
2620
2621         res = None
2622         L = None
2623
2624         for obj in self.targets:
2625             orig_obj = obj
2626
2627             if obj.name == self.bbox_obj.name:
2628                 # is there a better check?
2629                 # ("a is b" doesn't work here)
2630                 continue
2631             if obj.show_in_front != x_ray:
2632                 continue
2633
2634             if is_bbox:
2635                 obj = self.get_bbox_obj(obj, \
2636                     sys_matrix, sys_matrix_inv, is_local)
2637             elif obj.display_type == 'BOUNDS':
2638                 # Outside of BBox, there is no meaningful visual snapping
2639                 # for such display mode
2640                 continue
2641
2642             m = obj.matrix_world.copy()
2643             try:
2644                 mi = m.inverted()
2645             except:
2646                 # this is some degenerate object
2647                 continue
2648             la = mi * a
2649             lb = mi * b
2650
2651             # Bounding sphere check (to avoid unnecesary conversions
2652             # and to make ray 'infinite')
2653             bb_min = Vector(obj.bound_box[0])
2654             bb_max = Vector(obj.bound_box[6])
2655             c = (bb_min + bb_max) * 0.5
2656             r = (bb_max - bb_min).length * 0.5
2657             sec = intersect_line_sphere(la, lb, c, r, False)
2658             if sec[0] is None:
2659                 continue # no intersection with the bounding sphere
2660
2661             if not is_bbox:
2662                 # Ensure we work with raycastable object.
2663                 variant = ('RAW' if (edit and
2664                            (obj.type == 'MESH') and (obj.mode == 'EDIT'))
2665                            else 'PREVIEW')
2666                 obj = self.cache.get(obj, variant, reuse=(not force))
2667                 if (obj is None) or (not obj.data.polygons):
2668                     continue # the object has no raycastable geometry
2669
2670             # If ray must be infinite, ensure that
2671             # endpoints are outside of bounding volume
2672             if not clip:
2673                 # Seems that intersect_line_sphere()
2674                 # returns points in flipped order
2675                 lb, la = sec
2676
2677             def ray_cast(obj, la, lb):
2678                 if bpy.app.version < (2, 77, 0):
2679                     # Object.ray_cast(start, end)
2680                     # returns (location, normal, index)
2681                     res = obj.ray_cast(la, lb)
2682                     return ((res[-1] >= 0), res[0], res[1], res[2])
2683                 else:
2684                     # Object.ray_cast(origin, direction, [distance])
2685                     # returns (result, location, normal, index)
2686                     ld = lb - la
2687                     return obj.ray_cast(la, ld, ld.magnitude)
2688
2689             # Does ray actually intersect something?
2690             try:
2691                 success, lp, ln, face_id = ray_cast(obj, la, lb)
2692             except Exception as e:
2693                 # Somewhy this seems to happen when snapping cursor
2694                 # in Local View mode at least since r55223:
2695                 # <<Object "\U0010ffff" has no mesh data to be used
2696                 # for raycasting>> despite obj.data.polygons
2697                 # being non-empty.
2698                 try:
2699                     # Work-around: in Local View at least the object
2700                     # in focus permits raycasting (modifiers are
2701                     # applied in 'PREVIEW' mode)
2702                     success, lp, ln, face_id = ray_cast(orig_obj, la, lb)
2703                 except Exception as e:
2704                     # However, in Edit mode in Local View we have
2705                     # no luck -- during the edit mode, mesh is
2706                     # inaccessible (thus no mesh data for raycasting).
2707                     #print(repr(e))
2708                     success = False
2709
2710             if not success:
2711                 continue
2712
2713             # transform position to global space
2714             p = m * lp
2715
2716             # This works both for prespective and ortho
2717             l = p.dot(view_dir)
2718             if (L is None) or (l < L):
2719                 res = (lp, ln, face_id, obj, p, m, la, lb, orig_obj)
2720                 L = l
2721         #end for
2722
2723         return res
2724
2725     # Returns:
2726     # Matrix(X -- tangential,
2727     #        Y -- 2nd tangential,
2728     #        Z -- normal,
2729     #        T -- raycasted/snapped position)
2730     # Face ID (-1 if not applicable)
2731     # Object (None if not applicable)
2732     def snap_raycast(self, a, b, clip, view_dir, csu, alt_snap):
2733         settings = find_settings()
2734         tfm_opts = settings.transform_options
2735
2736         if self.shade_bbox and tfm_opts.snap_only_to_solid:
2737             return None
2738
2739         # Since introduction of "use object centers",
2740         # this check is useless (use_object_centers overrides
2741         # even INCREMENT snapping)
2742         #if self.snap_type not in {'VERTEX', 'EDGE', 'FACE', 'VOLUME'}:
2743         #    return None
2744
2745         # key shouldn't depend on system origin;
2746         # for bbox calculation origin is always zero
2747         #if csu.tou.get() != "Surface":
2748         #    sys_matrix = csu.get_matrix().to_3x3()
2749         #else:
2750         #    sys_matrix = csu.get_matrix('LOCAL').to_3x3()
2751         sys_matrix = csu.get_matrix().to_3x3()
2752         sys_matrix_key = list(c for v in sys_matrix for c in v)
2753         sys_matrix_key.append(self.editmode)
2754         sys_matrix = sys_matrix.to_4x4()
2755         try:
2756             sys_matrix_inv = sys_matrix.inverted()
2757         except:
2758             # this is some degenerate system
2759             return None
2760
2761         if self.sys_matrix_key != sys_matrix_key:
2762             self.bbox_cache.clear()
2763             self.sys_matrix_key = sys_matrix_key
2764
2765         # In this context, Volume represents BBox :P
2766         is_bbox = (self.snap_type == 'VOLUME')
2767         is_local = (csu.tou.get() in {'LOCAL', "Scaled"})
2768
2769         res = self.raycast(a, b, clip, view_dir, \
2770             is_bbox, sys_matrix, sys_matrix_inv, is_local, True)
2771
2772         if res is None:
2773             res = self.raycast(a, b, clip, view_dir, \
2774                 is_bbox, sys_matrix, sys_matrix_inv, is_local, False)
2775
2776         # Occlusion-based edge/vertex snapping will be
2777         # too inefficient in Python (well, even without
2778         # the occlusion, iterating over all edges/vertices
2779         # of each object is inefficient too)
2780
2781         if not res:
2782             return None
2783
2784         lp, ln, face_id, obj, p, m, la, lb, orig_obj = res
2785
2786         if is_bbox:
2787             self.bbox_obj.matrix_world = m.copy()
2788             self.bbox_obj.show_in_front = orig_obj.show_in_front
2789             self.hide_bbox(False)
2790
2791         _ln = ln.copy()
2792
2793         face = obj.data.polygons[face_id]
2794         L = None
2795         t1 = None
2796
2797         if self.snap_type == 'VERTEX' or self.snap_type == 'VOLUME':
2798             for v0 in face.vertices:
2799                 v = obj.data.vertices[v0]
2800                 p0 = v.co
2801                 l = (lp - p0).length_squared
2802                 if (L is None) or (l < L):
2803                     p = p0
2804                     ln = v.normal.copy()
2805                     #t1 = ln.cross(_ln)
2806                     L = l
2807
2808             _ln = ln.copy()
2809             '''
2810             if t1.length < epsilon:
2811                 if (1.0 - abs(ln.z)) < epsilon:
2812                     t1 = Vector((1, 0, 0))
2813                 else:
2814                     t1 = Vector((0, 0, 1)).cross(_ln)
2815             '''
2816             p = m * p
2817         elif self.snap_type == 'EDGE':
2818             use_smooth = face.use_smooth
2819             if self.interpolation == 'NEVER':
2820                 use_smooth = False
2821             elif self.interpolation == 'ALWAYS':
2822                 use_smooth = True
2823
2824             for v0, v1 in face.edge_keys:
2825                 p0 = obj.data.vertices[v0].co
2826                 p1 = obj.data.vertices[v1].co
2827                 dp = p1 - p0
2828                 q = dp.dot(lp - p0) / dp.length_squared
2829                 if (q >= 0.0) and (q <= 1.0):
2830                     ep = p0 + dp * q
2831                     l = (lp - ep).length_squared
2832                     if (L is None) or (l < L):
2833                         if alt_snap:
2834                             p = (p0 + p1) * 0.5
2835                             q = 0.5
2836                         else:
2837                             p = ep
2838                         if not use_smooth:
2839                             q = 0.5
2840                         ln = obj.data.vertices[v1].normal * q + \
2841                              obj.data.vertices[v0].normal * (1.0 - q)
2842                         t1 = dp
2843                         L = l
2844
2845             p = m * p
2846         else:
2847             if alt_snap:
2848                 lp = face.center
2849                 p = m * lp
2850
2851             if self.interpolation != 'NEVER':
2852                 ln = self.interpolate_normal(
2853                     obj, face_id, lp, la, lb - la)
2854
2855             # Comment this to make 1st tangential
2856             # always lie in the face's plane
2857             _ln = ln.copy()
2858
2859             '''
2860             for v0, v1 in face.edge_keys:
2861                 p0 = obj.data.vertices[v0].co
2862                 p1 = obj.data.vertices[v1].co
2863                 dp = p1 - p0
2864                 q = dp.dot(lp - p0) / dp.length_squared
2865                 if (q >= 0.0) and (q <= 1.0):
2866                     ep = p0 + dp * q
2867                     l = (lp - ep).length_squared
2868                     if (L is None) or (l < L):
2869                         t1 = dp
2870                         L = l
2871             '''
2872
2873         n = ln.copy()
2874         n.rotate(m)
2875         n.normalize()
2876
2877         if t1 is None:
2878             _ln.rotate(m)
2879             _ln.normalize()
2880             if (1.0 - abs(_ln.z)) < epsilon:
2881                 t1 = Vector((1, 0, 0))
2882             else:
2883                 t1 = Vector((0, 0, 1)).cross(_ln)
2884             t1.normalize()
2885         else:
2886             t1.rotate(m)
2887             t1.normalize()
2888
2889         t2 = t1.cross(n)
2890         t2.normalize()
2891
2892         matrix = MatrixCompose(t1, t2, n, p)
2893
2894         return (matrix, face_id, obj, orig_obj)
2895
2896     def interpolate_normal(self, obj, face_id, p, orig, ray):
2897         face = obj.data.polygons[face_id]
2898
2899         use_smooth = face.use_smooth
2900         if self.interpolation == 'NEVER':
2901             use_smooth = False
2902         elif self.interpolation == 'ALWAYS':
2903             use_smooth = True
2904
2905         if not use_smooth:
2906             return face.normal.copy()
2907
2908         # edge.use_edge_sharp affects smoothness only if
2909         # mesh has EdgeSplit modifier
2910
2911         # ATTENTION! Coords/Normals MUST be copied
2912         # (a bug in barycentric_transform implementation ?)
2913         # Somewhat strangely, the problem also disappears
2914         # if values passed to barycentric_transform
2915         # are print()ed beforehand.
2916
2917         co = [obj.data.vertices[vi].co.copy()
2918             for vi in face.vertices]
2919
2920         normals = [obj.data.vertices[vi].normal.copy()
2921             for vi in face.vertices]
2922
2923         if len(face.vertices) != 3:
2924             tris = tessellate_polygon([co])
2925             for tri in tris:
2926                 i0, i1, i2 = tri
2927                 if intersect_ray_tri(co[i0], co[i1], co[i2], ray, orig):
2928                     break
2929         else:
2930             i0, i1, i2 = 0, 1, 2
2931
2932         n = barycentric_transform(p, co[i0], co[i1], co[i2],
2933             normals[i0], normals[i1], normals[i2])
2934         n.normalize()
2935
2936         return n
2937
2938 # ====== CONVERTED-TO-MESH OBJECTS CACHE ====== #
2939 #============================================================================#
2940 class ToggleObjectMode:
2941     def __init__(self, mode='OBJECT'):
2942         if not isinstance(mode, str):
2943             mode = ('OBJECT' if mode else None)
2944
2945         self.mode = mode
2946
2947     def __enter__(self):
2948         if self.mode:
2949             edit_preferences = bpy.context.preferences.edit
2950
2951             self.global_undo = edit_preferences.use_global_undo
2952             self.prev_mode = bpy.context.object.mode
2953
2954             if self.prev_mode != self.mode:
2955                 edit_preferences.use_global_undo = False
2956                 bpy.ops.object.mode_set(mode=self.mode)
2957
2958         return self
2959
2960     def __exit__(self, type, value, traceback):
2961         if self.mode:
2962             edit_preferences = bpy.context.preferences.edit
2963
2964             if self.prev_mode != self.mode:
2965                 bpy.ops.object.mode_set(mode=self.prev_mode)
2966                 edit_preferences.use_global_undo = self.global_undo
2967
2968 class MeshCacheItem:
2969     def __init__(self):
2970         self.variants = {}
2971
2972     def __getitem__(self, variant):
2973         return self.variants[variant][0]
2974
2975     def __setitem__(self, variant, conversion):
2976         mesh = conversion[0].data
2977         #mesh.update(calc_tessface=True)
2978         #mesh.calc_tessface()
2979         mesh.calc_normals()
2980
2981         self.variants[variant] = conversion
2982
2983     def __contains__(self, variant):
2984         return variant in self.variants
2985
2986     def dispose(self):
2987         for obj, converted in self.variants.values():
2988             if converted:
2989                 mesh = obj.data
2990                 bpy.data.objects.remove(obj)
2991                 bpy.data.meshes.remove(mesh)
2992         self.variants = None
2993
2994 class MeshCache:
2995     """
2996     Keeps a cache of mesh equivalents of requested objects.
2997     It is assumed that object's data does not change while
2998     the cache is in use.
2999     """
3000
3001     variants_enum = {'RAW', 'PREVIEW', 'RENDER'}
3002     variants_normalization = {
3003         'MESH':{},
3004         'CURVE':{},
3005         'SURFACE':{},
3006         'FONT':{},
3007         'META':{'RAW':'PREVIEW'},
3008         'ARMATURE':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3009         'LATTICE':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3010         'EMPTY':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3011         'CAMERA':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3012         'LAMP':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3013         'SPEAKER':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3014     }
3015     conversible_types = {'MESH', 'CURVE', 'SURFACE', 'FONT',
3016                          'META', 'ARMATURE', 'LATTICE'}
3017     convert_types = conversible_types
3018
3019     def __init__(self, scene, convert_types=None):
3020         self.scene = scene
3021         if convert_types:
3022             self.convert_types = convert_types
3023         self.cached = {}
3024
3025     def __del__(self):
3026         self.clear()
3027
3028     def clear(self, expect_zero_users=False):
3029         for cache_item in self.cached.values():
3030             if cache_item:
3031                 try:
3032                     cache_item.dispose()
3033                 except RuntimeError:
3034                     if expect_zero_users:
3035                         raise
3036         self.cached.clear()
3037
3038     def __delitem__(self, obj):
3039         cache_item = self.cached.pop(obj, None)
3040         if cache_item:
3041             cache_item.dispose()
3042
3043     def __contains__(self, obj):
3044         return obj in self.cached
3045
3046     def __getitem__(self, obj):
3047         if isinstance(obj, tuple):
3048             return self.get(*obj)
3049         return self.get(obj)
3050
3051     def get(self, obj, variant='PREVIEW', reuse=True):
3052         if variant not in self.variants_enum:
3053             raise ValueError("Mesh variant must be one of %s" %
3054                              self.variants_enum)
3055
3056         # Make sure the variant is proper for this type of object
3057         variant = (self.variants_normalization[obj.type].
3058                    get(variant, variant))
3059
3060         if obj in self.cached:
3061             cache_item = self.cached[obj]
3062             try:
3063                 # cache_item is None if object isn't conversible to mesh
3064                 return (None if (cache_item is None)
3065                         else cache_item[variant])
3066             except KeyError:
3067                 pass
3068         else:
3069             cache_item = None
3070
3071         if obj.type not in self.conversible_types:
3072             self.cached[obj] = None
3073             return None
3074
3075         if not cache_item:
3076             cache_item = MeshCacheItem()
3077             self.cached[obj] = cache_item
3078
3079         conversion = self._convert(obj, variant, reuse)
3080         cache_item[variant] = conversion
3081
3082         return conversion[0]
3083
3084     def _convert(self, obj, variant, reuse=True):
3085         obj_type = obj.type
3086         obj_mode = obj.mode
3087         data = obj.data
3088
3089         if obj_type == 'MESH':
3090             if reuse and ((variant == 'RAW') or (len(obj.modifiers) == 0)):
3091                 return (obj, False)
3092             else:
3093                 force_objectmode = (obj_mode in {'EDIT', 'SCULPT'})
3094                 return (self._to_mesh(obj, variant, force_objectmode), True)
3095         elif obj_type in {'CURVE', 'SURFACE', 'FONT'}:
3096             if variant == 'RAW':
3097                 bm = bmesh.new()
3098                 for spline in data.splines:
3099                     for point in spline.bezier_points:
3100                         bm.verts.new(point.co)
3101                         bm.verts.new(point.handle_left)
3102                         bm.verts.new(point.handle_right)
3103                     for point in spline.points:
3104                         bm.verts.new(point.co[:3])
3105                 return (self._make_obj(bm, obj), True)
3106             else:
3107                 if variant == 'RENDER':
3108                     resolution_u = data.resolution_u
3109                     resolution_v = data.resolution_v
3110                     if data.render_resolution_u != 0:
3111                         data.resolution_u = data.render_resolution_u
3112                     if data.render_resolution_v != 0:
3113                         data.resolution_v = data.render_resolution_v
3114
3115                 result = (self._to_mesh(obj, variant), True)
3116
3117                 if variant == 'RENDER':
3118                     data.resolution_u = resolution_u
3119                     data.resolution_v = resolution_v
3120
3121                 return result
3122         elif obj_type == 'META':
3123             if variant == 'RAW':
3124                 # To avoid the hassle of snapping metaelements
3125                 # to themselves, we just create an empty mesh
3126                 bm = bmesh.new()
3127                 return (self._make_obj(bm, obj), True)
3128             else:
3129                 if variant == 'RENDER':
3130                     resolution = data.resolution
3131                     data.resolution = data.render_resolution
3132
3133                 result = (self._to_mesh(obj, variant), True)
3134
3135                 if variant == 'RENDER':
3136                     data.resolution = resolution
3137
3138                 return result
3139         elif obj_type == 'ARMATURE':
3140             bm = bmesh.new()
3141             if obj_mode == 'EDIT':
3142                 for bone in data.edit_bones:
3143                     head = bm.verts.new(bone.head)
3144                     tail = bm.verts.new(bone.tail)
3145                     bm.edges.new((head, tail))
3146             elif obj_mode == 'POSE':
3147                 for bone in obj.pose.bones:
3148                     head = bm.verts.new(bone.head)
3149                     tail = bm.verts.new(bone.tail)
3150                     bm.edges.new((head, tail))
3151             else:
3152                 for bone in data.bones:
3153                     head = bm.verts.new(bone.head_local)
3154                     tail = bm.verts.new(bone.tail_local)
3155                     bm.edges.new((head, tail))
3156             return (self._make_obj(bm, obj), True)
3157         elif obj_type == 'LATTICE':
3158             bm = bmesh.new()
3159             for point in data.points:
3160                 bm.verts.new(point.co_deform)
3161             return (self._make_obj(bm, obj), True)
3162
3163     def _to_mesh(self, obj, variant, force_objectmode=False):
3164         tmp_name = chr(0x10ffff) # maximal Unicode value
3165
3166         with ToggleObjectMode(force_objectmode):
3167             if variant == 'RAW':
3168                 mesh = obj.to_mesh(self.scene, False, 'PREVIEW')
3169             else:
3170                 mesh = obj.to_mesh(self.scene, True, variant)
3171             mesh.name = tmp_name
3172
3173         return self._make_obj(mesh, obj)
3174
3175     def _make_obj(self, mesh, src_obj):
3176         tmp_name = chr(0x10ffff) # maximal Unicode value
3177
3178         if isinstance(mesh, bmesh.types.BMesh):
3179             bm = mesh
3180             mesh = bpy.data.meshes.new(tmp_name)
3181             bm.to_mesh(mesh)
3182
3183         tmp_obj = bpy.data.objects.new(tmp_name, mesh)
3184
3185         if src_obj:
3186             tmp_obj.matrix_world = src_obj.matrix_world
3187
3188             # This is necessary for correct bbox display # TODO
3189             # (though it'd be better to change the logic in the raycasting)
3190             tmp_obj.show_in_front = src_obj.show_in_front
3191
3192             tmp_obj.instance_faces_scale = src_obj.instance_faces_scale
3193             tmp_obj.instance_frames_end = src_obj.instance_frames_end
3194             tmp_obj.instance_frames_off = src_obj.instance_frames_off
3195             tmp_obj.instance_frames_on = src_obj.instance_frames_on
3196             tmp_obj.instance_frames_start = src_obj.instance_frames_start
3197             tmp_obj.instance_collection = src_obj.instance_collection
3198             #tmp_obj.dupli_list = src_obj.dupli_list
3199             tmp_obj.instance_type = src_obj.instance_type
3200
3201         # Make Blender recognize object as having geometry
3202         # (is there a simpler way to do this?)
3203         self.scene.objects.link(tmp_obj)
3204         self.scene.update()
3205         # We don't need this object in scene
3206         self.scene.objects.unlink(tmp_obj)
3207
3208         return tmp_obj
3209
3210 #============================================================================#
3211
3212 # A base class for emulating ID-datablock behavior
3213 class PseudoIDBlockBase(bpy.types.PropertyGroup):
3214     # TODO: use normal metaprogramming?
3215
3216     @staticmethod
3217     def create_props(type, name, options={'ANIMATABLE'}):
3218         def active_update(self, context):
3219             # necessary to avoid recursive calls
3220             if self._self_update[0]:
3221                 return
3222
3223             if self._dont_rename[0]:
3224                 return
3225
3226             if len(self.collection) == 0:
3227                 return
3228
3229             # prepare data for renaming...
3230             old_key = (self.enum if self.enum else self.collection[0].name)
3231             new_key = (self.active if self.active else "Untitled")
3232
3233             if old_key == new_key:
3234                 return
3235
3236             old_item = None
3237             new_item = None
3238             existing_names = []
3239
3240             for item in self.collection:
3241                 if (item.name == old_key) and (not new_item):
3242                     new_item = item
3243                 elif (item.name == new_key) and (not old_item):
3244                     old_item = item
3245                 else:
3246                     existing_names.append(item.name)
3247             existing_names.append(new_key)
3248
3249             # rename current item
3250             new_item.name = new_key
3251
3252             if old_item:
3253                 # rename other item if it has that name
3254                 name = new_key
3255                 i = 1
3256                 while name in existing_names:
3257                     name = "{}.{:0>3}".format(new_key, i)
3258                     i += 1
3259                 old_item.name = name
3260
3261             # update the enum
3262             self._self_update[0] += 1
3263             self.update_enum()
3264             self._self_update[0] -= 1
3265         # end def
3266
3267         def enum_update(self, context):
3268             # necessary to avoid recursive calls
3269             if self._self_update[0]:
3270                 return
3271
3272             self._dont_rename[0] = True
3273             self.active = self.enum
3274             self._dont_rename[0] = False
3275
3276             self.on_item_select()
3277         # end def
3278
3279         collection = bpy.props.CollectionProperty(
3280             type=type)
3281         active = bpy.props.StringProperty(
3282             name="Name",
3283             description="Name of the active {}".format(name),
3284             options=options,
3285             update=active_update)
3286         enum = bpy.props.EnumProperty(
3287             items=[],
3288             name="Choose",
3289             description="Choose {}".format(name),
3290             default=set(),
3291             options={'ENUM_FLAG