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