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