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