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