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