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