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