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