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