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