· 9 years ago · Jan 19, 2017, 12:30 PM
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
20bl_info = {
21 "name": "Enhanced 3D Cursor",
22 "description": "Cursor history and bookmarks; drag/snap cursor.",
23 "author": "dairin0d",
24 "version": (3, 0, 1),
25 "blender": (2, 7, 7),
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://github.com/dairin0d/enhanced-3d-cursor/issues",
31 "category": "3D View"}
32
33"""
34Breakdown:
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
56First step is to re-make the cursor addon (make something usable first).
57CAD tools should be done without the hassle.
58
59TODO:
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==============================================================================
100Borrowed code/logic:
101- space_view3d_panel_measure.py (Buerbaum Martin "Pontiac"):
102 - OpenGL state storing/restoring; working with projection matrices.
103"""
104
105import bpy
106import bgl
107import blf
108import bmesh
109
110from mathutils import Vector, Matrix, Quaternion, Euler
111
112from 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
120from bpy_extras.view3d_utils import (region_2d_to_location_3d,
121 location_3d_to_region_2d,
122 )
123
124import math
125import time
126
127# ====== MODULE GLOBALS / CONSTANTS ====== #
128tmp_name = chr(0x10ffff) # maximal Unicode value
129epsilon = 0.000001
130
131# ====== SET CURSOR OPERATOR ====== #
132class 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 '%':-1, # French keyboard has this in place of [
161 '\u03BC':1, # French keyboard has this in place of ]
162 '^':-1, # Just in case
163 '$':1, # Just in case
164 'J':'VIEW',
165 'K':"Surface",
166 'L':'LOCAL',
167 'B':'GLOBAL',
168 'N':'NORMAL',
169 'M':"Scaled",
170 }
171
172 key_pivot_map = {
173 'H':'ACTIVE',
174 'U':'CURSOR',
175 'I':'INDIVIDUAL',
176 'O':'CENTER',
177 'P':'MEDIAN',
178 }
179
180 key_snap_map = {
181 'C':'INCREMENT',
182 'V':'VERTEX',
183 'E':'EDGE',
184 'F':'FACE',
185 }
186
187 key_tfm_mode_map = {
188 'G':'MOVE',
189 'R':'ROTATE',
190 'S':'SCALE',
191 }
192
193 key_map = {
194 "confirm":{'ACTIONMOUSE'}, # also 'RET' ?
195 "cancel":{'SELECTMOUSE', 'ESC'},
196 "free_mouse":{'F10'},
197 "make_normal_snapshot":{'W'},
198 "make_tangential_snapshot":{'Q'},
199 "use_absolute_coords":{'A'},
200 "snap_to_raw_mesh":{'D'},
201 "use_object_centers":{'T'},
202 "precision_up":{'PAGE_UP'},
203 "precision_down":{'PAGE_DOWN'},
204 "move_caret_prev":{'LEFT_ARROW'},
205 "move_caret_next":{'RIGHT_ARROW'},
206 "move_caret_home":{'HOME'},
207 "move_caret_end":{'END'},
208 "change_current_axis":{'TAB', 'RET', 'NUMPAD_ENTER'},
209 "prev_axis":{'UP_ARROW'},
210 "next_axis":{'DOWN_ARROW'},
211 "remove_next_character":{'DEL'},
212 "remove_last_character":{'BACK_SPACE'},
213 "copy_axes":{'C'},
214 "paste_axes":{'V'},
215 "cut_axes":{'X'},
216 }
217
218 gizmo_factor = 0.15
219 click_period = 0.25
220
221 angle_grid_steps = {True:1.0, False:5.0}
222 scale_grid_steps = {True:0.01, False:0.1}
223
224 # ====== OPERATOR METHOD OVERLOADS ====== #
225 @classmethod
226 def poll(cls, context):
227 area_types = {'VIEW_3D',} # also: IMAGE_EDITOR ?
228 return ((context.area.type in area_types) and
229 (context.region.type == "WINDOW") and
230 (not find_settings().cursor_lock))
231
232 def modal(self, context, event):
233 context.area.tag_redraw()
234 return self.try_process_input(context, event)
235
236 def invoke(self, context, event):
237 # Attempt to launch the monitor
238 if bpy.ops.view3d.cursor3d_monitor.poll():
239 bpy.ops.view3d.cursor3d_monitor()
240
241 # Don't interfere with these modes when only mouse is pressed
242 if ('SCULPT' in context.mode) or ('PAINT' in context.mode):
243 if "MOUSE" in event.type:
244 return {'CANCELLED'}
245
246 CursorDynamicSettings.active_transform_operator = self
247
248 tool_settings = context.tool_settings
249
250 settings = find_settings()
251 tfm_opts = settings.transform_options
252
253 settings_scene = context.scene.cursor_3d_tools_settings
254
255 self.setup_keymaps(context, event)
256
257 # Coordinate System Utility
258 self.particles, self.csu = gather_particles(context=context)
259 self.particles = [View3D_Cursor(context)]
260
261 self.csu.source_pos = self.particles[0].get_location()
262 self.csu.source_rot = self.particles[0].get_rotation()
263 self.csu.source_scale = self.particles[0].get_scale()
264
265 # View3D Utility
266 self.vu = ViewUtility(context.region, context.space_data,
267 context.region_data)
268
269 # Snap Utility
270 self.su = SnapUtility(context)
271
272 # turn off view locking for the duration of the operator
273 self.view_pos = self.vu.get_position(True)
274 self.vu.set_position(self.vu.get_position(), True)
275 self.view_locks = self.vu.get_locks()
276 self.vu.set_locks({})
277
278 # Initialize runtime states
279 self.initiated_by_mouse = ("MOUSE" in event.type)
280 self.free_mouse = not self.initiated_by_mouse
281 self.use_object_centers = False
282 self.axes_values = ["", "", ""]
283 self.axes_coords = [None, None, None]
284 self.axes_eval_success = [True, True, True]
285 self.allowed_axes = [True, True, True]
286 self.current_axis = 0
287 self.caret_pos = 0
288 self.coord_format = "{:." + str(settings.free_coord_precision) + "f}"
289 self.transform_mode = 'MOVE'
290 self.init_xy_angle_distance(context, event)
291
292 self.click_start = time.time()
293 if not self.initiated_by_mouse:
294 self.click_start -= self.click_period
295
296 self.stick_obj_name = settings_scene.stick_obj_name
297 self.stick_obj_pos = settings_scene.stick_obj_pos
298
299 # Initial run
300 self.try_process_input(context, event, True)
301
302 context.window_manager.modal_handler_add(self)
303 return {'RUNNING_MODAL'}
304
305 def cancel(self, context):
306 for particle in self.particles:
307 particle.revert()
308
309 set_stick_obj(context.scene, self.stick_obj_name, self.stick_obj_pos)
310
311 self.finalize(context)
312
313 # ====== CLEANUP/FINALIZE ====== #
314 def finalize(self, context):
315 # restore view locking
316 self.vu.set_locks(self.view_locks)
317 self.vu.set_position(self.view_pos, True)
318
319 self.cleanup(context)
320
321 # This is to avoid "blinking" of
322 # between-history-positions line
323 settings = find_settings()
324 history = settings.history
325 # make sure the most recent history entry is displayed
326 history.curr_id = 0
327 history.last_id = 0
328
329 # Ensure there are no leftovers from draw_callback
330 context.area.tag_redraw()
331
332 return {'FINISHED'}
333
334 def cleanup(self, context):
335 self.particles = None
336 self.csu = None
337 self.vu = None
338 if self.su is not None:
339 self.su.dispose()
340 self.su = None
341
342 CursorDynamicSettings.active_transform_operator = None
343
344 # ====== USER INPUT PROCESSING ====== #
345 def setup_keymaps(self, context, event=None):
346 self.key_map = self.key_map.copy()
347
348 # There is no such event as 'ACTIONMOUSE',
349 # it's always 'LEFTMOUSE' or 'RIGHTMOUSE'
350 if event:
351 if event.type == 'LEFTMOUSE':
352 self.key_map["confirm"] = {'LEFTMOUSE'}
353 self.key_map["cancel"] = {'RIGHTMOUSE', 'ESC'}
354 elif event.type == 'RIGHTMOUSE':
355 self.key_map["confirm"] = {'RIGHTMOUSE'}
356 self.key_map["cancel"] = {'LEFTMOUSE', 'ESC'}
357 else:
358 event = None
359 if event is None:
360 select_mouse = context.user_preferences.inputs.select_mouse
361 if select_mouse == 'RIGHT':
362 self.key_map["confirm"] = {'LEFTMOUSE'}
363 self.key_map["cancel"] = {'RIGHTMOUSE', 'ESC'}
364 else:
365 self.key_map["confirm"] = {'RIGHTMOUSE'}
366 self.key_map["cancel"] = {'LEFTMOUSE', 'ESC'}
367
368 # Use user-defined "free mouse" key, if it exists
369 wm = context.window_manager
370 if '3D View' in wm.keyconfigs.user.keymaps:
371 km = wm.keyconfigs.user.keymaps['3D View']
372 for kmi in KeyMapItemSearch(EnhancedSetCursor.bl_idname, km):
373 if kmi.map_type == 'KEYBOARD':
374 self.key_map["free_mouse"] = {kmi.type,}
375 break
376
377 def try_process_input(self, context, event, initial_run=False):
378 try:
379 return self.process_input(context, event, initial_run)
380 except:
381 # If anything fails, at least dispose the resources
382 self.cleanup(context)
383 raise
384
385 def process_input(self, context, event, initial_run=False):
386 wm = context.window_manager
387 v3d = context.space_data
388
389 if event.type in self.key_map["confirm"]:
390 if self.free_mouse:
391 finished = (event.value == 'PRESS')
392 else:
393 finished = (event.value == 'RELEASE')
394
395 if finished:
396 return self.finalize(context)
397
398 if event.type in self.key_map["cancel"]:
399 self.cancel(context)
400 return {'CANCELLED'}
401
402 tool_settings = context.tool_settings
403
404 settings = find_settings()
405 tfm_opts = settings.transform_options
406
407 make_snapshot = False
408 tangential_snapshot = False
409
410 if event.value == 'PRESS':
411 if event.type in self.key_map["free_mouse"]:
412 if self.free_mouse and not initial_run:
413 # confirm if pressed second time
414 return self.finalize(context)
415 else:
416 self.free_mouse = True
417
418 if event.type in self.key_tfm_mode_map:
419 new_mode = self.key_tfm_mode_map[event.type]
420
421 if self.transform_mode != new_mode:
422 # snap cursor to its initial state
423 if new_mode != 'MOVE':
424 for particle in self.particles:
425 initial_matrix = particle.get_initial_matrix()
426 particle.set_matrix(initial_matrix)
427 # reset intial mouse position
428 self.init_xy_angle_distance(context, event)
429
430 self.transform_mode = new_mode
431
432 if event.type in self.key_map["make_normal_snapshot"]:
433 make_snapshot = True
434 tangential_snapshot = False
435
436 if event.type in self.key_map["make_tangential_snapshot"]:
437 make_snapshot = True
438 tangential_snapshot = True
439
440 if event.type in self.key_map["snap_to_raw_mesh"]:
441 tool_settings.use_snap_self = \
442 not tool_settings.use_snap_self
443
444 if (not event.alt) and (event.type in {'X', 'Y', 'Z'}):
445 axis_lock = [(event.type == 'X') != event.shift,
446 (event.type == 'Y') != event.shift,
447 (event.type == 'Z') != event.shift]
448
449 if self.allowed_axes != axis_lock:
450 self.allowed_axes = axis_lock
451 else:
452 self.allowed_axes = [True, True, True]
453
454 if event.type in self.key_map["use_absolute_coords"]:
455 tfm_opts.use_relative_coords = \
456 not tfm_opts.use_relative_coords
457
458 self.update_origin_projection(context)
459
460 incr = 0
461 if event.type in self.key_map["change_current_axis"]:
462 incr = (-1 if event.shift else 1)
463 elif event.type in self.key_map["next_axis"]:
464 incr = 1
465 elif event.type in self.key_map["prev_axis"]:
466 incr = -1
467
468 if incr != 0:
469 self.current_axis = (self.current_axis + incr) % 3
470 self.caret_pos = len(self.axes_values[self.current_axis])
471
472 incr = 0
473 if event.type in self.key_map["precision_up"]:
474 incr = 1
475 elif event.type in self.key_map["precision_down"]:
476 incr = -1
477
478 if incr != 0:
479 settings.free_coord_precision += incr
480 self.coord_format = "{:." + \
481 str(settings.free_coord_precision) + "f}"
482
483 new_orient1 = self.key_coordsys_map.get(event.type, None)
484 new_orient2 = self.key_coordsys_map.get(event.unicode, None)
485 new_orientation = (new_orient1 or new_orient2)
486 if new_orientation:
487 self.csu.set_orientation(new_orientation)
488
489 self.update_origin_projection(context)
490
491 if event.ctrl:
492 self.snap_to_system_origin()
493
494 if (event.type == 'ZERO') and event.ctrl:
495 self.snap_to_system_origin()
496 elif new_orientation is None: # avoid conflicting shortcuts
497 self.process_axis_input(event)
498
499 if event.alt:
500 jc = (", " if tfm_opts.use_comma_separator else "\t")
501 if event.type in self.key_map["copy_axes"]:
502 wm.clipboard = jc.join(self.get_axes_text(True))
503 elif event.type in self.key_map["cut_axes"]:
504 wm.clipboard = jc.join(self.get_axes_text(True))
505 self.set_axes_text("\t\t\t")
506 elif event.type in self.key_map["paste_axes"]:
507 if jc == "\t":
508 self.set_axes_text(wm.clipboard, True)
509 else:
510 jc = jc.strip()
511 ttext = ""
512 brackets = 0
513 for c in wm.clipboard:
514 if c in "[{(":
515 brackets += 1
516 elif c in "]})":
517 brackets -= 1
518 if (brackets == 0) and (c == jc):
519 c = "\t"
520 ttext += c
521 self.set_axes_text(ttext, True)
522
523 if event.type in self.key_map["use_object_centers"]:
524 v3d.use_pivot_point_align = not v3d.use_pivot_point_align
525
526 if event.type in self.key_pivot_map:
527 self.csu.set_pivot(self.key_pivot_map[event.type])
528
529 self.update_origin_projection(context)
530
531 if event.ctrl:
532 self.snap_to_system_origin(force_pivot=True)
533
534 if (not event.alt) and (event.type in self.key_snap_map):
535 snap_element = self.key_snap_map[event.type]
536 if tool_settings.snap_element == snap_element:
537 if snap_element == 'VERTEX':
538 snap_element = 'VOLUME'
539 elif snap_element == 'VOLUME':
540 snap_element = 'VERTEX'
541 tool_settings.snap_element = snap_element
542 # end if
543
544 use_snap = (tool_settings.use_snap != event.ctrl)
545 if use_snap:
546 snap_type = tool_settings.snap_element
547 else:
548 userprefs_view = context.user_preferences.view
549 if userprefs_view.use_mouse_depth_cursor:
550 # Suggested by Lissanro in the forum
551 use_snap = True
552 snap_type = 'FACE'
553 else:
554 snap_type = None
555
556 axes_coords = [None, None, None]
557 if self.transform_mode == 'MOVE':
558 for i in range(3):
559 if self.axes_coords[i] is not None:
560 axes_coords[i] = self.axes_coords[i]
561 elif not self.allowed_axes[i]:
562 axes_coords[i] = 0.0
563
564 self.su.set_modes(
565 interpolation=tfm_opts.snap_interpolate_normals_mode,
566 use_relative_coords=tfm_opts.use_relative_coords,
567 editmode=tool_settings.use_snap_self,
568 snap_type=snap_type,
569 snap_align=tool_settings.use_snap_align_rotation,
570 axes_coords=axes_coords,
571 )
572
573 self.do_raycast = ("MOUSE" in event.type)
574 self.grid_substep = event.shift
575 self.modify_surface_orientation = (len(self.particles) == 1)
576 self.xy = Vector((event.mouse_region_x, event.mouse_region_y))
577
578 self.use_object_centers = v3d.use_pivot_point_align
579
580 if event.type == 'MOUSEMOVE':
581 self.update_transform_mousemove()
582
583 if self.transform_mode == 'MOVE':
584 transform_func = self.transform_move
585 elif self.transform_mode == 'ROTATE':
586 transform_func = self.transform_rotate
587 elif self.transform_mode == 'SCALE':
588 transform_func = self.transform_scale
589
590 for particle in self.particles:
591 transform_func(particle)
592
593 if make_snapshot:
594 self.make_normal_snapshot(context.scene, tangential_snapshot)
595
596 return {'RUNNING_MODAL'}
597
598 def update_origin_projection(self, context):
599 r = context.region
600 rv3d = context.region_data
601
602 origin = self.csu.get_origin()
603 # prehaps not projection, but intersection with plane?
604 self.origin_xy = location_3d_to_region_2d(r, rv3d, origin)
605 if self.origin_xy is None:
606 self.origin_xy = Vector((r.width / 2, r.height / 2))
607
608 self.delta_xy = (self.start_xy - self.origin_xy).to_3d()
609 self.prev_delta_xy = self.delta_xy
610
611 def init_xy_angle_distance(self, context, event):
612 self.start_xy = Vector((event.mouse_region_x, event.mouse_region_y))
613
614 self.update_origin_projection(context)
615
616 # Distinction between angles has to be made because
617 # angles can go beyond 360 degrees (we cannot snap
618 # to increment the original ones).
619 self.raw_angles = [0.0, 0.0, 0.0]
620 self.angles = [0.0, 0.0, 0.0]
621 self.scales = [1.0, 1.0, 1.0]
622
623 def update_transform_mousemove(self):
624 delta_xy = (self.xy - self.origin_xy).to_3d()
625
626 n_axes = sum(int(v) for v in self.allowed_axes)
627 if n_axes == 1:
628 # rotate using angle as value
629 rd = self.prev_delta_xy.rotation_difference(delta_xy)
630 offset = -rd.angle * round(rd.axis[2])
631
632 sys_matrix = self.csu.get_matrix()
633
634 i_allowed = 0
635 for i in range(3):
636 if self.allowed_axes[i]:
637 i_allowed = i
638
639 view_dir = self.vu.get_direction()
640 if view_dir.dot(sys_matrix[i_allowed][:3]) < 0:
641 offset = -offset
642
643 for i in range(3):
644 if self.allowed_axes[i]:
645 self.raw_angles[i] += offset
646 elif n_axes == 2:
647 # rotate using XY coords as two values
648 offset = (delta_xy - self.prev_delta_xy) * (math.pi / 180.0)
649
650 if self.grid_substep:
651 offset *= 0.1
652 else:
653 offset *= 0.5
654
655 j = 0
656 for i in range(3):
657 if self.allowed_axes[i]:
658 self.raw_angles[i] += offset[1 - j]
659 j += 1
660 elif n_axes == 3:
661 # rotate around view direction
662 rd = self.prev_delta_xy.rotation_difference(delta_xy)
663 offset = -rd.angle * round(rd.axis[2])
664
665 view_dir = self.vu.get_direction()
666
667 sys_matrix = self.csu.get_matrix()
668
669 try:
670 view_dir = sys_matrix.inverted().to_3x3() * view_dir
671 except:
672 # this is some degenerate system
673 pass
674 view_dir.normalize()
675
676 rot = Matrix.Rotation(offset, 3, view_dir)
677
678 matrix = Euler(self.raw_angles, 'XYZ').to_matrix()
679 matrix.rotate(rot)
680
681 euler = matrix.to_euler('XYZ')
682 self.raw_angles[0] += clamp_angle(euler.x - self.raw_angles[0])
683 self.raw_angles[1] += clamp_angle(euler.y - self.raw_angles[1])
684 self.raw_angles[2] += clamp_angle(euler.z - self.raw_angles[2])
685
686 scale = delta_xy.length / self.delta_xy.length
687 if self.delta_xy.dot(delta_xy) < 0:
688 scale *= -1
689 for i in range(3):
690 if self.allowed_axes[i]:
691 self.scales[i] = scale
692
693 self.prev_delta_xy = delta_xy
694
695 def transform_move(self, particle):
696 global set_cursor_location__reset_stick
697
698 src_matrix = particle.get_matrix()
699 initial_matrix = particle.get_initial_matrix()
700
701 matrix = self.su.snap(
702 self.xy, src_matrix, initial_matrix,
703 self.do_raycast, self.grid_substep,
704 self.vu, self.csu,
705 self.modify_surface_orientation,
706 self.use_object_centers)
707
708 set_cursor_location__reset_stick = False
709 particle.set_matrix(matrix)
710 set_cursor_location__reset_stick = True
711
712 def rotate_matrix(self, matrix):
713 sys_matrix = self.csu.get_matrix()
714
715 try:
716 matrix = sys_matrix.inverted() * matrix
717 except:
718 # this is some degenerate system
719 pass
720
721 # Blender's order of rotation [in local axes]
722 rotation_order = [2, 1, 0]
723
724 # Seems that 4x4 matrix cannot be rotated using rotate() ?
725 sys_matrix3 = sys_matrix.to_3x3()
726
727 for i in range(3):
728 j = rotation_order[i]
729 axis = sys_matrix3[j]
730 angle = self.angles[j]
731
732 rot = angle_axis_to_quat(angle, axis)
733 # this seems to be buggy too
734 #rot = Matrix.Rotation(angle, 3, axis)
735
736 sys_matrix3 = rot.to_matrix() * sys_matrix3
737 # sys_matrix3.rotate has a bug? or I don't understand how it works?
738 #sys_matrix3.rotate(rot)
739
740 for i in range(3):
741 sys_matrix[i][:3] = sys_matrix3[i]
742
743 matrix = sys_matrix * matrix
744
745 return matrix
746
747 def transform_rotate(self, particle):
748 grid_step = self.angle_grid_steps[self.grid_substep]
749 grid_step *= (math.pi / 180.0)
750
751 for i in range(3):
752 if self.axes_values[i] and self.axes_eval_success[i]:
753 self.raw_angles[i] = self.axes_coords[i] * (math.pi / 180.0)
754
755 self.angles[i] = self.raw_angles[i]
756
757 if self.su.implementation.snap_type == 'INCREMENT':
758 for i in range(3):
759 self.angles[i] = round_step(self.angles[i], grid_step)
760
761 initial_matrix = particle.get_initial_matrix()
762 matrix = self.rotate_matrix(initial_matrix)
763
764 particle.set_matrix(matrix)
765
766 def scale_matrix(self, matrix):
767 sys_matrix = self.csu.get_matrix()
768
769 try:
770 matrix = sys_matrix.inverted() * matrix
771 except:
772 # this is some degenerate system
773 pass
774
775 for i in range(3):
776 sys_matrix[i] *= self.scales[i]
777
778 matrix = sys_matrix * matrix
779
780 return matrix
781
782 def transform_scale(self, particle):
783 grid_step = self.scale_grid_steps[self.grid_substep]
784
785 for i in range(3):
786 if self.axes_values[i] and self.axes_eval_success[i]:
787 self.scales[i] = self.axes_coords[i]
788
789 if self.su.implementation.snap_type == 'INCREMENT':
790 for i in range(3):
791 self.scales[i] = round_step(self.scales[i], grid_step)
792
793 initial_matrix = particle.get_initial_matrix()
794 matrix = self.scale_matrix(initial_matrix)
795
796 particle.set_matrix(matrix)
797
798 def set_axis_input(self, axis_id, axis_val):
799 if axis_val == self.axes_values[axis_id]:
800 return
801
802 self.axes_values[axis_id] = axis_val
803
804 if len(axis_val) == 0:
805 self.axes_coords[axis_id] = None
806 self.axes_eval_success[axis_id] = True
807 else:
808 try:
809 #self.axes_coords[axis_id] = float(eval(axis_val, {}, {}))
810 self.axes_coords[axis_id] = \
811 float(eval(axis_val, math.__dict__))
812 self.axes_eval_success[axis_id] = True
813 except:
814 self.axes_eval_success[axis_id] = False
815
816 def snap_to_system_origin(self, force_pivot=False):
817 if self.transform_mode == 'MOVE':
818 pivot = self.csu.get_pivot_name(raw=force_pivot)
819 p = self.csu.get_origin(relative=False, pivot=pivot)
820 m = self.csu.get_matrix()
821 try:
822 p = m.inverted() * p
823 except:
824 # this is some degenerate system
825 pass
826 for i in range(3):
827 self.set_axis_input(i, str(p[i]))
828 elif self.transform_mode == 'ROTATE':
829 for i in range(3):
830 self.set_axis_input(i, "0")
831 elif self.transform_mode == 'SCALE':
832 for i in range(3):
833 self.set_axis_input(i, "1")
834
835 def get_axes_values(self, as_string=False):
836 if self.transform_mode == 'MOVE':
837 localmat = CursorDynamicSettings.local_matrix
838 raw_axes = localmat.translation
839 elif self.transform_mode == 'ROTATE':
840 raw_axes = Vector(self.angles) * (180.0 / math.pi)
841 elif self.transform_mode == 'SCALE':
842 raw_axes = Vector(self.scales)
843
844 axes_values = []
845 for i in range(3):
846 if as_string and self.axes_values[i]:
847 value = self.axes_values[i]
848 elif self.axes_eval_success[i] and \
849 (self.axes_coords[i] is not None):
850 value = self.axes_coords[i]
851 else:
852 value = raw_axes[i]
853 if as_string:
854 value = self.coord_format.format(value)
855 axes_values.append(value)
856
857 return axes_values
858
859 def get_axes_text(self, offset=False):
860 axes_values = self.get_axes_values(as_string=True)
861
862 axes_text = []
863 for i in range(3):
864 j = i
865 if offset:
866 j = (i + self.current_axis) % 3
867
868 axes_text.append(axes_values[j])
869
870 return axes_text
871
872 def set_axes_text(self, text, offset=False):
873 if "\n" in text:
874 text = text.replace("\r", "")
875 else:
876 text = text.replace("\r", "\n")
877 text = text.replace("\n", "\t")
878 #text = text.replace(",", ".") # ???
879
880 axes_text = text.split("\t")
881 for i in range(min(len(axes_text), 3)):
882 j = i
883 if offset:
884 j = (i + self.current_axis) % 3
885 self.set_axis_input(j, axes_text[i])
886
887 def process_axis_input(self, event):
888 axis_id = self.current_axis
889 axis_val = self.axes_values[axis_id]
890
891 if event.type in self.key_map["remove_next_character"]:
892 if event.ctrl:
893 # clear all
894 for i in range(3):
895 self.set_axis_input(i, "")
896 self.caret_pos = 0
897 return
898 else:
899 axis_val = axis_val[0:self.caret_pos] + \
900 axis_val[self.caret_pos + 1:len(axis_val)]
901 elif event.type in self.key_map["remove_last_character"]:
902 if event.ctrl:
903 # clear current
904 axis_val = ""
905 else:
906 axis_val = axis_val[0:self.caret_pos - 1] + \
907 axis_val[self.caret_pos:len(axis_val)]
908 self.caret_pos -= 1
909 elif event.type in self.key_map["move_caret_next"]:
910 self.caret_pos += 1
911 if event.ctrl:
912 snap_chars = ".-+*/%()"
913 i = self.caret_pos
914 while axis_val[i:i + 1] not in snap_chars:
915 i += 1
916 self.caret_pos = i
917 elif event.type in self.key_map["move_caret_prev"]:
918 self.caret_pos -= 1
919 if event.ctrl:
920 snap_chars = ".-+*/%()"
921 i = self.caret_pos
922 while axis_val[i - 1:i] not in snap_chars:
923 i -= 1
924 self.caret_pos = i
925 elif event.type in self.key_map["move_caret_home"]:
926 self.caret_pos = 0
927 elif event.type in self.key_map["move_caret_end"]:
928 self.caret_pos = len(axis_val)
929 elif event.type in self.key_char_map:
930 # Currently accessing event.ascii seems to crash Blender
931 c = self.key_char_map[event.type]
932 if event.shift:
933 if c == "8":
934 c = "*"
935 elif c == "5":
936 c = "%"
937 elif c == "9":
938 c = "("
939 elif c == "0":
940 c = ")"
941 axis_val = axis_val[0:self.caret_pos] + c + \
942 axis_val[self.caret_pos:len(axis_val)]
943 self.caret_pos += 1
944
945 self.caret_pos = min(max(self.caret_pos, 0), len(axis_val))
946
947 self.set_axis_input(axis_id, axis_val)
948
949 # ====== DRAWING ====== #
950 def gizmo_distance(self, pos):
951 rv3d = self.vu.region_data
952 if rv3d.view_perspective == 'ORTHO':
953 dist = rv3d.view_distance
954 else:
955 view_pos = self.vu.get_viewpoint()
956 view_dir = self.vu.get_direction()
957 dist = (pos - view_pos).dot(view_dir)
958 return dist
959
960 def gizmo_scale(self, pos):
961 return self.gizmo_distance(pos) * self.gizmo_factor
962
963 def check_v3d_local(self, context):
964 csu_v3d = self.csu.space_data
965 v3d = context.space_data
966 if csu_v3d.local_view:
967 return csu_v3d != v3d
968 return v3d.local_view
969
970 def draw_3d(self, context):
971 if self.check_v3d_local(context):
972 return
973
974 if time.time() < (self.click_start + self.click_period):
975 return
976
977 settings = find_settings()
978 tfm_opts = settings.transform_options
979
980 initial_matrix = self.particles[0].get_initial_matrix()
981
982 sys_matrix = self.csu.get_matrix()
983 if tfm_opts.use_relative_coords:
984 sys_matrix.translation = initial_matrix.translation.copy()
985 sys_origin = sys_matrix.to_translation()
986 dest_point = self.particles[0].get_location()
987
988 if self.is_normal_visible():
989 p0, x, y, z, _x, _z = \
990 self.get_normal_params(tfm_opts, dest_point)
991
992 # use theme colors?
993 #ThemeView3D.normal
994 #ThemeView3D.vertex_normal
995
996 bgl.glDisable(bgl.GL_LINE_STIPPLE)
997
998 if settings.draw_N:
999 bgl.glColor4f(0, 1, 1, 1)
1000 draw_arrow(p0, _x, y, z) # Z (normal)
1001 if settings.draw_T1:
1002 bgl.glColor4f(1, 0, 1, 1)
1003 draw_arrow(p0, y, _z, x) # X (1st tangential)
1004 if settings.draw_T2:
1005 bgl.glColor4f(1, 1, 0, 1)
1006 draw_arrow(p0, _z, x, y) # Y (2nd tangential)
1007
1008 bgl.glEnable(bgl.GL_BLEND)
1009 bgl.glDisable(bgl.GL_DEPTH_TEST)
1010
1011 if settings.draw_N:
1012 bgl.glColor4f(0, 1, 1, 0.25)
1013 draw_arrow(p0, _x, y, z) # Z (normal)
1014 if settings.draw_T1:
1015 bgl.glColor4f(1, 0, 1, 0.25)
1016 draw_arrow(p0, y, _z, x) # X (1st tangential)
1017 if settings.draw_T2:
1018 bgl.glColor4f(1, 1, 0, 0.25)
1019 draw_arrow(p0, _z, x, y) # Y (2nd tangential)
1020
1021 if settings.draw_guides:
1022 p0 = dest_point
1023 try:
1024 p00 = sys_matrix.inverted() * p0
1025 except:
1026 # this is some degenerate system
1027 p00 = p0.copy()
1028
1029 axes_line_params = [
1030 (Vector((0, p00.y, p00.z)), (1, 0, 0)),
1031 (Vector((p00.x, 0, p00.z)), (0, 1, 0)),
1032 (Vector((p00.x, p00.y, 0)), (0, 0, 1)),
1033 ]
1034
1035 for i in range(3):
1036 p1, color = axes_line_params[i]
1037 p1 = sys_matrix * p1
1038 constrained = (self.axes_coords[i] is not None) or \
1039 (not self.allowed_axes[i])
1040 alpha = (0.25 if constrained else 1.0)
1041 draw_line_hidden_depth(p0, p1, color, \
1042 alpha, alpha, False, True)
1043
1044 # line from origin to cursor
1045 p0 = sys_origin
1046 p1 = dest_point
1047
1048 bgl.glEnable(bgl.GL_LINE_STIPPLE)
1049 bgl.glColor4f(1, 1, 0, 1)
1050
1051 draw_line_hidden_depth(p0, p1, (1, 1, 0), 1.0, 0.5, True, True)
1052
1053 if settings.draw_snap_elements:
1054 sui = self.su.implementation
1055 if sui.potential_snap_elements and (sui.snap_type == 'EDGE'):
1056 bgl.glDisable(bgl.GL_LINE_STIPPLE)
1057
1058 bgl.glEnable(bgl.GL_BLEND)
1059 bgl.glDisable(bgl.GL_DEPTH_TEST)
1060
1061 bgl.glLineWidth(2)
1062 bgl.glColor4f(0, 0, 1, 0.5)
1063
1064 bgl.glBegin(bgl.GL_LINE_LOOP)
1065 for p in sui.potential_snap_elements:
1066 bgl.glVertex3f(p[0], p[1], p[2])
1067 bgl.glEnd()
1068 elif sui.potential_snap_elements and (sui.snap_type == 'FACE'):
1069 bgl.glEnable(bgl.GL_BLEND)
1070 bgl.glDisable(bgl.GL_DEPTH_TEST)
1071
1072 bgl.glColor4f(0, 1, 0, 0.5)
1073
1074 co = sui.potential_snap_elements
1075 tris = tessellate_polygon([co])
1076 bgl.glBegin(bgl.GL_TRIANGLES)
1077 for tri in tris:
1078 for vi in tri:
1079 p = co[vi]
1080 bgl.glVertex3f(p[0], p[1], p[2])
1081 bgl.glEnd()
1082
1083 def draw_2d(self, context):
1084 if self.check_v3d_local(context):
1085 return
1086
1087 r = context.region
1088 rv3d = context.region_data
1089
1090 settings = find_settings()
1091
1092 if settings.draw_snap_elements:
1093 sui = self.su.implementation
1094
1095 snap_points = []
1096 if sui.potential_snap_elements and \
1097 (sui.snap_type in {'VERTEX', 'VOLUME'}):
1098 snap_points.extend(sui.potential_snap_elements)
1099 if sui.extra_snap_points:
1100 snap_points.extend(sui.extra_snap_points)
1101
1102 if snap_points:
1103 bgl.glEnable(bgl.GL_BLEND)
1104
1105 bgl.glPointSize(5)
1106 bgl.glColor4f(1, 0, 0, 0.5)
1107
1108 bgl.glBegin(bgl.GL_POINTS)
1109 for p in snap_points:
1110 p = location_3d_to_region_2d(r, rv3d, p)
1111 if p is not None:
1112 bgl.glVertex2f(p[0], p[1])
1113 bgl.glEnd()
1114
1115 bgl.glPointSize(1)
1116
1117 if self.transform_mode == 'MOVE':
1118 return
1119
1120 bgl.glEnable(bgl.GL_LINE_STIPPLE)
1121
1122 bgl.glLineWidth(1)
1123
1124 bgl.glColor4f(0, 0, 0, 1)
1125 draw_line_2d(self.origin_xy, self.xy)
1126
1127 bgl.glDisable(bgl.GL_LINE_STIPPLE)
1128
1129 line_width = 3
1130 bgl.glLineWidth(line_width)
1131
1132 L = 12.0
1133 arrow_len = 6.0
1134 arrow_width = 8.0
1135 arrow_space = 5.0
1136
1137 Lmax = arrow_space * 2 + L * 2 + line_width
1138
1139 pos = self.xy.to_2d()
1140 normal = self.prev_delta_xy.to_2d().normalized()
1141 dist = self.prev_delta_xy.length
1142 tangential = Vector((-normal[1], normal[0]))
1143
1144 if self.transform_mode == 'ROTATE':
1145 n_axes = sum(int(v) for v in self.allowed_axes)
1146 if n_axes == 2:
1147 bgl.glColor4f(0.4, 0.15, 0.15, 1)
1148 for sgn in (-1, 1):
1149 n = sgn * Vector((0, 1))
1150 p0 = pos + arrow_space * n
1151 draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
1152
1153 bgl.glColor4f(0.11, 0.51, 0.11, 1)
1154 for sgn in (-1, 1):
1155 n = sgn * Vector((1, 0))
1156 p0 = pos + arrow_space * n
1157 draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
1158 else:
1159 bgl.glColor4f(0, 0, 0, 1)
1160 for sgn in (-1, 1):
1161 n = sgn * tangential
1162 if dist < Lmax:
1163 n *= dist / Lmax
1164 p0 = pos + arrow_space * n
1165 draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
1166 elif self.transform_mode == 'SCALE':
1167 bgl.glColor4f(0, 0, 0, 1)
1168 for sgn in (-1, 1):
1169 n = sgn * normal
1170 p0 = pos + arrow_space * n
1171 draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
1172
1173 bgl.glLineWidth(1)
1174
1175 def draw_axes_coords(self, context, header_size):
1176 if self.check_v3d_local(context):
1177 return
1178
1179 if time.time() < (self.click_start + self.click_period):
1180 return
1181
1182 v3d = context.space_data
1183
1184 userprefs_view = context.user_preferences.view
1185
1186 tool_settings = context.tool_settings
1187
1188 settings = find_settings()
1189 tfm_opts = settings.transform_options
1190
1191 localmat = CursorDynamicSettings.local_matrix
1192
1193 font_id = 0 # default font
1194
1195 font_size = 11
1196 blf.size(font_id, font_size, 72) # font, point size, dpi
1197
1198 tet = context.user_preferences.themes[0].text_editor
1199
1200 # Prepare the table...
1201 if self.transform_mode == 'MOVE':
1202 axis_prefix = ("D" if tfm_opts.use_relative_coords else "")
1203 elif self.transform_mode == 'SCALE':
1204 axis_prefix = "S"
1205 else:
1206 axis_prefix = "R"
1207 axis_names = ["X", "Y", "Z"]
1208
1209 axis_cells = []
1210 coord_cells = []
1211 #caret_cell = TextCell("_", tet.cursor)
1212 caret_cell = TextCell("|", tet.cursor)
1213
1214 try:
1215 axes_text = self.get_axes_text()
1216
1217 for i in range(3):
1218 color = tet.space.text
1219 alpha = (1.0 if self.allowed_axes[i] else 0.5)
1220 text = axis_prefix + axis_names[i] + " : "
1221 axis_cells.append(TextCell(text, color, alpha))
1222
1223 if self.axes_values[i]:
1224 if self.axes_eval_success[i]:
1225 color = tet.syntax_numbers
1226 else:
1227 color = tet.syntax_string
1228 else:
1229 color = tet.space.text
1230 text = axes_text[i]
1231 coord_cells.append(TextCell(text, color))
1232 except Exception as e:
1233 print(repr(e))
1234
1235 mode_cells = []
1236
1237 try:
1238 snap_type = self.su.implementation.snap_type
1239 if snap_type is None:
1240 color = tet.space.text
1241 elif (not self.use_object_centers) or \
1242 (snap_type == 'INCREMENT'):
1243 color = tet.syntax_numbers
1244 else:
1245 color = tet.syntax_special
1246 text = snap_type or tool_settings.snap_element
1247 if text == 'VOLUME':
1248 text = "BBOX"
1249 mode_cells.append(TextCell(text, color))
1250
1251 if self.csu.tou.is_custom:
1252 color = tet.space.text
1253 else:
1254 color = tet.syntax_builtin
1255 text = self.csu.tou.get_title()
1256 mode_cells.append(TextCell(text, color))
1257
1258 color = tet.space.text
1259 text = self.csu.get_pivot_name(raw=True)
1260 if self.use_object_centers:
1261 color = tet.syntax_special
1262 mode_cells.append(TextCell(text, color))
1263 except Exception as e:
1264 print(repr(e))
1265
1266 hdr_w, hdr_h = header_size
1267
1268 try:
1269 xyz_x_start_min = 12
1270 xyz_x_start = xyz_x_start_min
1271 mode_x_start = 6
1272
1273 mode_margin = 4
1274 xyz_margin = 16
1275 blend_margin = 32
1276
1277 color = tet.space.back
1278 bgl.glColor4f(color[0], color[1], color[2], 1.0)
1279 draw_rect(0, 0, hdr_w, hdr_h)
1280
1281 if tool_settings.use_snap_self:
1282 x = hdr_w - mode_x_start
1283 y = hdr_h / 2
1284 cell = mode_cells[0]
1285 x -= cell.w
1286 y -= cell.h * 0.5
1287 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
1288 draw_rect(x, y, cell.w, cell.h, 1, True)
1289
1290 x = hdr_w - mode_x_start
1291 y = hdr_h / 2
1292 for cell in mode_cells:
1293 cell.draw(x, y, (1, 0.5))
1294 x -= (cell.w + mode_margin)
1295
1296 curr_axis_x_start = 0
1297 curr_axis_x_end = 0
1298 caret_x = 0
1299
1300 xyz_width = 0
1301 for i in range(3):
1302 if i == self.current_axis:
1303 curr_axis_x_start = xyz_width
1304
1305 xyz_width += axis_cells[i].w
1306
1307 if i == self.current_axis:
1308 char_offset = 0
1309 if self.axes_values[i]:
1310 char_offset = blf.dimensions(font_id,
1311 coord_cells[i].text[:self.caret_pos])[0]
1312 caret_x = xyz_width + char_offset
1313
1314 xyz_width += coord_cells[i].w
1315
1316 if i == self.current_axis:
1317 curr_axis_x_end = xyz_width
1318
1319 xyz_width += xyz_margin
1320
1321 xyz_width = int(xyz_width)
1322 xyz_width_ext = xyz_width + blend_margin
1323
1324 offset = (xyz_x_start + curr_axis_x_end) - hdr_w
1325 if offset > 0:
1326 xyz_x_start -= offset
1327
1328 offset = xyz_x_start_min - (xyz_x_start + curr_axis_x_start)
1329 if offset > 0:
1330 xyz_x_start += offset
1331
1332 offset = (xyz_x_start + caret_x) - hdr_w
1333 if offset > 0:
1334 xyz_x_start -= offset
1335
1336 # somewhy GL_BLEND should be set right here
1337 # to actually draw the box with blending %)
1338 # (perhaps due to text draw happened before)
1339 bgl.glEnable(bgl.GL_BLEND)
1340 bgl.glShadeModel(bgl.GL_SMOOTH)
1341 gl_enable(bgl.GL_SMOOTH, True)
1342 color = tet.space.back
1343 bgl.glBegin(bgl.GL_TRIANGLE_STRIP)
1344 bgl.glColor4f(color[0], color[1], color[2], 1.0)
1345 bgl.glVertex2i(0, 0)
1346 bgl.glVertex2i(0, hdr_h)
1347 bgl.glVertex2i(xyz_width, 0)
1348 bgl.glVertex2i(xyz_width, hdr_h)
1349 bgl.glColor4f(color[0], color[1], color[2], 0.0)
1350 bgl.glVertex2i(xyz_width_ext, 0)
1351 bgl.glVertex2i(xyz_width_ext, hdr_h)
1352 bgl.glEnd()
1353
1354 x = xyz_x_start
1355 y = hdr_h / 2
1356 for i in range(3):
1357 cell = axis_cells[i]
1358 cell.draw(x, y, (0, 0.5))
1359 x += cell.w
1360
1361 cell = coord_cells[i]
1362 cell.draw(x, y, (0, 0.5))
1363 x += (cell.w + xyz_margin)
1364
1365 caret_x -= blf.dimensions(font_id, caret_cell.text)[0] * 0.5
1366 caret_cell.draw(xyz_x_start + caret_x, y, (0, 0.5))
1367
1368 bgl.glEnable(bgl.GL_BLEND)
1369 bgl.glShadeModel(bgl.GL_SMOOTH)
1370 gl_enable(bgl.GL_SMOOTH, True)
1371 color = tet.space.back
1372 bgl.glBegin(bgl.GL_TRIANGLE_STRIP)
1373 bgl.glColor4f(color[0], color[1], color[2], 1.0)
1374 bgl.glVertex2i(0, 0)
1375 bgl.glVertex2i(0, hdr_h)
1376 bgl.glVertex2i(xyz_x_start_min, 0)
1377 bgl.glColor4f(color[0], color[1], color[2], 0.0)
1378 bgl.glVertex2i(xyz_x_start_min, hdr_h)
1379 bgl.glEnd()
1380
1381 except Exception as e:
1382 print(repr(e))
1383
1384 return
1385
1386 # ====== NORMAL SNAPSHOT ====== #
1387 def is_normal_visible(self):
1388 if self.csu.tou.get() == "Surface":
1389 return True
1390
1391 if self.use_object_centers:
1392 return False
1393
1394 return self.su.implementation.snap_type \
1395 not in {None, 'INCREMENT', 'VOLUME'}
1396
1397 def get_normal_params(self, tfm_opts, dest_point):
1398 surf_matrix = self.csu.get_matrix("Surface")
1399 if tfm_opts.use_relative_coords:
1400 surf_origin = dest_point
1401 else:
1402 surf_origin = surf_matrix.to_translation()
1403
1404 m3 = surf_matrix.to_3x3()
1405 p0 = surf_origin
1406 scl = self.gizmo_scale(p0)
1407
1408 # Normal and tangential are not always orthogonal
1409 # (e.g. when normal is interpolated)
1410 x = (m3 * Vector((1, 0, 0))).normalized()
1411 y = (m3 * Vector((0, 1, 0))).normalized()
1412 z = (m3 * Vector((0, 0, 1))).normalized()
1413
1414 _x = z.cross(y)
1415 _z = y.cross(x)
1416
1417 return p0, x * scl, y * scl, z * scl, _x * scl, _z * scl
1418
1419 def make_normal_snapshot(self, scene, tangential=False):
1420 settings = find_settings()
1421 tfm_opts = settings.transform_options
1422
1423 dest_point = self.particles[0].get_location()
1424
1425 if self.is_normal_visible():
1426 p0, x, y, z, _x, _z = \
1427 self.get_normal_params(tfm_opts, dest_point)
1428
1429 snapshot = bpy.data.objects.new("normal_snapshot", None)
1430
1431 if tangential:
1432 m = MatrixCompose(_z, y, x, p0)
1433 else:
1434 m = MatrixCompose(_x, y, z, p0)
1435 snapshot.matrix_world = m
1436
1437 snapshot.empty_draw_type = 'SINGLE_ARROW'
1438 #snapshot.empty_draw_type = 'ARROWS'
1439 #snapshot.layers = [True] * 20 # ?
1440 scene.objects.link(snapshot)
1441#============================================================================#
1442
1443
1444class Particle:
1445 pass
1446
1447class View3D_Cursor(Particle):
1448 def __init__(self, context):
1449 assert context.space_data.type == 'VIEW_3D'
1450 self.v3d = context.space_data
1451 self.initial_pos = self.get_location()
1452 self.initial_matrix = Matrix.Translation(self.initial_pos)
1453
1454 def revert(self):
1455 self.set_location(self.initial_pos)
1456
1457 def get_location(self):
1458 return get_cursor_location(v3d=self.v3d)
1459
1460 def set_location(self, value):
1461 set_cursor_location(Vector(value), v3d=self.v3d)
1462
1463 def get_rotation(self):
1464 return Quaternion()
1465
1466 def set_rotation(self, value):
1467 pass
1468
1469 def get_scale(self):
1470 return Vector((1.0, 1.0, 1.0))
1471
1472 def set_scale(self, value):
1473 pass
1474
1475 def get_matrix(self):
1476 return Matrix.Translation(self.get_location())
1477
1478 def set_matrix(self, value):
1479 self.set_location(value.to_translation())
1480
1481 def get_initial_matrix(self):
1482 return self.initial_matrix
1483
1484class View3D_Object(Particle):
1485 def __init__(self, obj):
1486 self.obj = obj
1487
1488 def get_location(self):
1489 # obj.location seems to be in parent's system...
1490 # or even maybe not bounded by constraints %)
1491 return self.obj.matrix_world.to_translation()
1492
1493class View3D_EditMesh_Vertex(Particle):
1494 pass
1495
1496class View3D_EditMesh_Edge(Particle):
1497 pass
1498
1499class View3D_EditMesh_Face(Particle):
1500 pass
1501
1502class View3D_EditSpline_Point(Particle):
1503 pass
1504
1505class View3D_EditSpline_BezierPoint(Particle):
1506 pass
1507
1508class View3D_EditSpline_BezierHandle(Particle):
1509 pass
1510
1511class View3D_EditMeta_Element(Particle):
1512 pass
1513
1514class View3D_EditBone_Bone(Particle):
1515 pass
1516
1517class View3D_EditBone_HeadTail(Particle):
1518 pass
1519
1520class View3D_PoseBone(Particle):
1521 pass
1522
1523class UV_Cursor(Particle):
1524 pass
1525
1526class UV_Vertex(Particle):
1527 pass
1528
1529class UV_Edge(Particle):
1530 pass
1531
1532class UV_Face(Particle):
1533 pass
1534
1535# Other types:
1536# NLA / Dopesheet / Graph editor ...
1537
1538# Particles are used in the following situations:
1539# - as subjects of transformation
1540# - as reference point(s) for cursor transformation
1541# Note: particles 'dragged' by Proportional Editing
1542# are a separate issue (they can come and go).
1543def gather_particles(**kwargs):
1544 context = kwargs.get("context", bpy.context)
1545
1546 area_type = kwargs.get("area_type", context.area.type)
1547
1548 scene = kwargs.get("scene", context.scene)
1549
1550 space_data = kwargs.get("space_data", context.space_data)
1551 region_data = kwargs.get("region_data", context.region_data)
1552
1553 particles = []
1554 pivots = {}
1555 normal_system = None
1556
1557 active_element = None
1558 cursor_pos = None
1559 median = None
1560
1561 if area_type == 'VIEW_3D':
1562 context_mode = kwargs.get("context_mode", context.mode)
1563
1564 selected_objects = kwargs.get("selected_objects",
1565 context.selected_objects)
1566
1567 active_object = kwargs.get("active_object",
1568 context.active_object)
1569
1570 if context_mode == 'OBJECT':
1571 for obj in selected_objects:
1572 particle = View3D_Object(obj)
1573 particles.append(particle)
1574
1575 if active_object:
1576 active_element = active_object.\
1577 matrix_world.to_translation()
1578
1579 # On Undo/Redo scene hash value is changed ->
1580 # -> the monitor tries to update the CSU ->
1581 # -> object.mode_set seem to somehow conflict
1582 # with Undo/Redo mechanisms.
1583 elif active_object and active_object.data and \
1584 (context_mode in {
1585 'EDIT_MESH', 'EDIT_METABALL',
1586 'EDIT_CURVE', 'EDIT_SURFACE',
1587 'EDIT_ARMATURE', 'POSE'}):
1588
1589 m = active_object.matrix_world
1590
1591 positions = []
1592 normal = Vector((0, 0, 0))
1593
1594 if context_mode == 'EDIT_MESH':
1595 bm = bmesh.from_edit_mesh(active_object.data)
1596
1597 if bm.select_history:
1598 elem = bm.select_history[-1]
1599 if isinstance(elem, bmesh.types.BMVert):
1600 active_element = elem.co.copy()
1601 else:
1602 active_element = Vector()
1603 for v in elem.verts:
1604 active_element += v.co
1605 active_element *= 1.0 / len(elem.verts)
1606
1607 for v in bm.verts:
1608 if v.select:
1609 positions.append(v.co)
1610 normal += v.normal
1611
1612 # mimic Blender's behavior (as of now,
1613 # order of selection is ignored)
1614 if len(positions) == 2:
1615 normal = positions[1] - positions[0]
1616 elif len(positions) == 3:
1617 a = positions[0] - positions[1]
1618 b = positions[2] - positions[1]
1619 normal = a.cross(b)
1620 elif context_mode == 'EDIT_METABALL':
1621 active_elem = active_object.data.elements.active
1622 if active_elem:
1623 active_element = active_elem.co.copy()
1624 active_element = active_object.\
1625 matrix_world * active_element
1626
1627 # Currently there is no API for element.select
1628 #for element in active_object.data.elements:
1629 # if element.select:
1630 # positions.append(element.co)
1631 elif context_mode == 'EDIT_ARMATURE':
1632 # active bone seems to have the same pivot
1633 # as median of the selection
1634 '''
1635 active_bone = active_object.data.edit_bones.active
1636 if active_bone:
1637 active_element = active_bone.head + \
1638 active_bone.tail
1639 active_element = active_object.\
1640 matrix_world * active_element
1641 '''
1642
1643 for bone in active_object.data.edit_bones:
1644 if bone.select_head:
1645 positions.append(bone.head)
1646 if bone.select_tail:
1647 positions.append(bone.tail)
1648 elif context_mode == 'POSE':
1649 active_bone = active_object.data.bones.active
1650 if active_bone:
1651 active_element = active_bone.\
1652 matrix_local.translation.to_3d()
1653 active_element = active_object.\
1654 matrix_world * active_element
1655
1656 # consider only topmost parents
1657 bones = set()
1658 for bone in active_object.data.bones:
1659 if bone.select:
1660 bones.add(bone)
1661
1662 parents = set()
1663 for bone in bones:
1664 if not set(bone.parent_recursive).intersection(bones):
1665 parents.add(bone)
1666
1667 for bone in parents:
1668 positions.append(bone.matrix_local.translation.to_3d())
1669 else:
1670 for spline in active_object.data.splines:
1671 for point in spline.bezier_points:
1672 if point.select_control_point:
1673 positions.append(point.co)
1674 else:
1675 if point.select_left_handle:
1676 positions.append(point.handle_left)
1677 if point.select_right_handle:
1678 positions.append(point.handle_right)
1679
1680 n = None
1681 nL = point.co - point.handle_left
1682 nR = point.co - point.handle_right
1683 #nL = point.handle_left.copy()
1684 #nR = point.handle_right.copy()
1685 if point.select_control_point:
1686 n = nL + nR
1687 elif point.select_left_handle or \
1688 point.select_right_handle:
1689 n = nL + nR
1690 else:
1691 if point.select_left_handle:
1692 n = -nL
1693 if point.select_right_handle:
1694 n = nR
1695
1696 if n is not None:
1697 if n.length_squared < epsilon:
1698 n = -nL
1699 normal += n.normalized()
1700
1701 for point in spline.points:
1702 if point.select:
1703 positions.append(point.co)
1704
1705 if len(positions) != 0:
1706 if normal.length_squared < epsilon:
1707 normal = Vector((0, 0, 1))
1708 normal.rotate(m)
1709 normal.normalize()
1710
1711 if (1.0 - abs(normal.z)) < epsilon:
1712 t1 = Vector((1, 0, 0))
1713 else:
1714 t1 = Vector((0, 0, 1)).cross(normal)
1715 t2 = t1.cross(normal)
1716 normal_system = MatrixCompose(t1, t2, normal)
1717
1718 median, bbox_center = calc_median_bbox_pivots(positions)
1719 median = m * median
1720 bbox_center = m * bbox_center
1721
1722 # Currently I don't know how to get active mesh element
1723 if active_element is None:
1724 if context_mode == 'EDIT_ARMATURE':
1725 # Somewhy EDIT_ARMATURE has such behavior
1726 active_element = bbox_center
1727 else:
1728 active_element = median
1729 else:
1730 if active_element is None:
1731 active_element = active_object.\
1732 matrix_world.to_translation()
1733
1734 median = active_element
1735 bbox_center = active_element
1736
1737 normal_system = active_object.matrix_world.to_3x3()
1738 normal_system.col[0].normalize()
1739 normal_system.col[1].normalize()
1740 normal_system.col[2].normalize()
1741 else:
1742 # paint/sculpt, etc.?
1743 particle = View3D_Object(active_object)
1744 particles.append(particle)
1745
1746 if active_object:
1747 active_element = active_object.\
1748 matrix_world.to_translation()
1749
1750 cursor_pos = get_cursor_location(v3d=space_data)
1751
1752 #elif area_type == 'IMAGE_EDITOR':
1753 # currently there is no way to get UV editor's
1754 # offset (and maybe some other parameters
1755 # required to implement these operators)
1756 #cursor_pos = space_data.uv_editor.cursor_location
1757
1758 #elif area_type == 'EMPTY':
1759 #elif area_type == 'GRAPH_EDITOR':
1760 #elif area_type == 'OUTLINER':
1761 #elif area_type == 'PROPERTIES':
1762 #elif area_type == 'FILE_BROWSER':
1763 #elif area_type == 'INFO':
1764 #elif area_type == 'SEQUENCE_EDITOR':
1765 #elif area_type == 'TEXT_EDITOR':
1766 #elif area_type == 'AUDIO_WINDOW':
1767 #elif area_type == 'DOPESHEET_EDITOR':
1768 #elif area_type == 'NLA_EDITOR':
1769 #elif area_type == 'SCRIPTS_WINDOW':
1770 #elif area_type == 'TIMELINE':
1771 #elif area_type == 'NODE_EDITOR':
1772 #elif area_type == 'LOGIC_EDITOR':
1773 #elif area_type == 'CONSOLE':
1774 #elif area_type == 'USER_PREFERENCES':
1775
1776 else:
1777 print("gather_particles() not implemented for '{}'".\
1778 format(area_type))
1779 return None, None
1780
1781 # 'INDIVIDUAL_ORIGINS' is not handled here
1782
1783 if cursor_pos:
1784 pivots['CURSOR'] = cursor_pos.copy()
1785
1786 if active_element:
1787 # in v3d: ACTIVE_ELEMENT
1788 pivots['ACTIVE'] = active_element.copy()
1789
1790 if (len(particles) != 0) and (median is None):
1791 positions = (p.get_location() for p in particles)
1792 median, bbox_center = calc_median_bbox_pivots(positions)
1793
1794 if median:
1795 # in v3d: MEDIAN_POINT, in UV editor: MEDIAN
1796 pivots['MEDIAN'] = median.copy()
1797 # in v3d: BOUNDING_BOX_CENTER, in UV editor: CENTER
1798 pivots['CENTER'] = bbox_center.copy()
1799
1800 csu = CoordinateSystemUtility(scene, space_data, region_data, \
1801 pivots, normal_system)
1802
1803 return particles, csu
1804
1805def calc_median_bbox_pivots(positions):
1806 median = None # pos can be 3D or 2D
1807 bbox = [None, None]
1808
1809 n = 0
1810 for pos in positions:
1811 extend_bbox(bbox, pos)
1812 try:
1813 median += pos
1814 except:
1815 median = pos.copy()
1816 n += 1
1817
1818 median = median / n
1819 bbox_center = (Vector(bbox[0]) + Vector(bbox[1])) * 0.5
1820
1821 return median, bbox_center
1822
1823def extend_bbox(bbox, pos):
1824 try:
1825 bbox[0] = tuple(min(e0, e1) for e0, e1 in zip(bbox[0], pos))
1826 bbox[1] = tuple(max(e0, e1) for e0, e1 in zip(bbox[1], pos))
1827 except:
1828 bbox[0] = tuple(pos)
1829 bbox[1] = tuple(pos)
1830
1831
1832# ====== COORDINATE SYSTEM UTILITY ====== #
1833class CoordinateSystemUtility:
1834 pivot_name_map = {
1835 'CENTER':'CENTER',
1836 'BOUNDING_BOX_CENTER':'CENTER',
1837 'MEDIAN':'MEDIAN',
1838 'MEDIAN_POINT':'MEDIAN',
1839 'CURSOR':'CURSOR',
1840 'INDIVIDUAL_ORIGINS':'INDIVIDUAL',
1841 'ACTIVE_ELEMENT':'ACTIVE',
1842 'WORLD':'WORLD',
1843 'SURFACE':'SURFACE', # ?
1844 'BOOKMARK':'BOOKMARK',
1845 }
1846 pivot_v3d_map = {
1847 'CENTER':'BOUNDING_BOX_CENTER',
1848 'MEDIAN':'MEDIAN_POINT',
1849 'CURSOR':'CURSOR',
1850 'INDIVIDUAL':'INDIVIDUAL_ORIGINS',
1851 'ACTIVE':'ACTIVE_ELEMENT',
1852 }
1853
1854 def __init__(self, scene, space_data, region_data, \
1855 pivots, normal_system):
1856 self.space_data = space_data
1857 self.region_data = region_data
1858
1859 if space_data.type == 'VIEW_3D':
1860 self.pivot_map_inv = self.pivot_v3d_map
1861
1862 self.tou = TransformOrientationUtility(
1863 scene, space_data, region_data)
1864 self.tou.normal_system = normal_system
1865
1866 self.pivots = pivots
1867
1868 # Assigned by caller (for cursor or selection)
1869 self.source_pos = None
1870 self.source_rot = None
1871 self.source_scale = None
1872
1873 def set_orientation(self, name):
1874 self.tou.set(name)
1875
1876 def set_pivot(self, pivot):
1877 self.space_data.pivot_point = self.pivot_map_inv[pivot]
1878
1879 def get_pivot_name(self, name=None, relative=None, raw=False):
1880 pivot = self.pivot_name_map[self.space_data.pivot_point]
1881 if raw:
1882 return pivot
1883
1884 if not name:
1885 name = self.tou.get()
1886
1887 if relative is None:
1888 settings = find_settings()
1889 tfm_opts = settings.transform_options
1890 relative = tfm_opts.use_relative_coords
1891
1892 if relative:
1893 pivot = "RELATIVE"
1894 elif (name == 'GLOBAL') or (pivot == 'WORLD'):
1895 pivot = 'WORLD'
1896 elif (name == "Surface") or (pivot == 'SURFACE'):
1897 pivot = "SURFACE"
1898
1899 return pivot
1900
1901 def get_origin(self, name=None, relative=None, pivot=None):
1902 if not pivot:
1903 pivot = self.get_pivot_name(name, relative)
1904
1905 if relative or (pivot == "RELATIVE"):
1906 # "relative" parameter overrides "pivot"
1907 return self.source_pos
1908 elif pivot == 'WORLD':
1909 return Vector()
1910 elif pivot == "SURFACE":
1911 runtime_settings = find_runtime_settings()
1912 return Vector(runtime_settings.surface_pos)
1913 else:
1914 if pivot == 'INDIVIDUAL':
1915 pivot = 'MEDIAN'
1916
1917 #if pivot == 'ACTIVE':
1918 # print(self.pivots)
1919
1920 try:
1921 return self.pivots[pivot]
1922 except:
1923 return Vector()
1924
1925 def get_matrix(self, name=None, relative=None, pivot=None):
1926 if not name:
1927 name = self.tou.get()
1928
1929 matrix = self.tou.get_matrix(name)
1930
1931 if isinstance(pivot, Vector):
1932 pos = pivot
1933 else:
1934 pos = self.get_origin(name, relative, pivot)
1935
1936 return to_matrix4x4(matrix, pos)
1937
1938# ====== TRANSFORM ORIENTATION UTILITIES ====== #
1939class TransformOrientationUtility:
1940 special_systems = {"Surface", "Scaled"}
1941 predefined_systems = {
1942 'GLOBAL', 'LOCAL', 'VIEW', 'NORMAL', 'GIMBAL',
1943 "Scaled", "Surface",
1944 }
1945
1946 def __init__(self, scene, v3d, rv3d):
1947 self.scene = scene
1948 self.v3d = v3d
1949 self.rv3d = rv3d
1950
1951 self.custom_systems = [item for item in scene.orientations \
1952 if item.name not in self.special_systems]
1953
1954 self.is_custom = False
1955 self.custom_id = -1
1956
1957 # This is calculated elsewhere
1958 self.normal_system = None
1959
1960 self.set(v3d.transform_orientation)
1961
1962 def get(self):
1963 return self.transform_orientation
1964
1965 def get_title(self):
1966 if self.is_custom:
1967 return self.transform_orientation
1968
1969 name = self.transform_orientation
1970 return name[:1].upper() + name[1:].lower()
1971
1972 def set(self, name, set_v3d=True):
1973 if isinstance(name, int):
1974 n = len(self.custom_systems)
1975 if n == 0:
1976 # No custom systems, do nothing
1977 return
1978
1979 increment = name
1980
1981 if self.is_custom:
1982 # If already custom, switch to next custom system
1983 self.custom_id = (self.custom_id + increment) % n
1984
1985 self.is_custom = True
1986
1987 name = self.custom_systems[self.custom_id].name
1988 else:
1989 self.is_custom = name not in self.predefined_systems
1990
1991 if self.is_custom:
1992 self.custom_id = next((i for i, v in \
1993 enumerate(self.custom_systems) if v.name == name), -1)
1994
1995 if name in self.special_systems:
1996 # Ensure such system exists
1997 self.get_custom(name)
1998
1999 self.transform_orientation = name
2000
2001 if set_v3d:
2002 self.v3d.transform_orientation = name
2003
2004 def get_matrix(self, name=None):
2005 active_obj = self.scene.objects.active
2006
2007 if not name:
2008 name = self.transform_orientation
2009
2010 if self.is_custom:
2011 matrix = self.custom_systems[self.custom_id].matrix.copy()
2012 else:
2013 if (name == 'VIEW') and self.rv3d:
2014 matrix = self.rv3d.view_rotation.to_matrix()
2015 elif name == "Surface":
2016 matrix = self.get_custom(name).matrix.copy()
2017 elif (name == 'GLOBAL') or (not active_obj):
2018 matrix = Matrix().to_3x3()
2019 elif (name == 'NORMAL') and self.normal_system:
2020 matrix = self.normal_system.copy()
2021 else:
2022 matrix = active_obj.matrix_world.to_3x3()
2023 if name == "Scaled":
2024 self.get_custom(name).matrix = matrix
2025 else: # 'LOCAL', 'GIMBAL', ['NORMAL'] for now
2026 matrix[0].normalize()
2027 matrix[1].normalize()
2028 matrix[2].normalize()
2029
2030 return matrix
2031
2032 def get_custom(self, name):
2033 try:
2034 return self.scene.orientations[name]
2035 except:
2036 return create_transform_orientation(
2037 self.scene, name, Matrix())
2038
2039# Is there a less cumbersome way to create transform orientation?
2040def create_transform_orientation(scene, name=None, matrix=None):
2041 active_obj = scene.objects.active
2042 prev_mode = None
2043
2044 if active_obj:
2045 prev_mode = active_obj.mode
2046 bpy.ops.object.mode_set(mode='OBJECT')
2047 else:
2048 bpy.ops.object.add()
2049
2050 # ATTENTION! This uses context's scene
2051 bpy.ops.transform.create_orientation()
2052
2053 tfm_orient = scene.orientations[-1]
2054
2055 if name is not None:
2056 basename = name
2057 i = 1
2058 while name in scene.orientations:
2059 name = "%s.%03i" % (basename, i)
2060 i += 1
2061 tfm_orient.name = name
2062
2063 if matrix:
2064 tfm_orient.matrix = matrix.to_3x3()
2065
2066 if active_obj:
2067 bpy.ops.object.mode_set(mode=prev_mode)
2068 else:
2069 bpy.ops.object.delete()
2070
2071 return tfm_orient
2072
2073# ====== VIEW UTILITY CLASS ====== #
2074class ViewUtility:
2075 methods = dict(
2076 get_locks = lambda: {},
2077 set_locks = lambda locks: None,
2078 get_position = lambda: Vector(),
2079 set_position = lambda: None,
2080 get_rotation = lambda: Quaternion(),
2081 get_direction = lambda: Vector((0, 0, 1)),
2082 get_viewpoint = lambda: Vector(),
2083 get_matrix = lambda: Matrix(),
2084 get_point = lambda xy, pos: \
2085 Vector((xy[0], xy[1], 0)),
2086 get_ray = lambda xy: tuple(
2087 Vector((xy[0], xy[1], 0)),
2088 Vector((xy[0], xy[1], 1)),
2089 False),
2090 )
2091
2092 def __init__(self, region, space_data, region_data):
2093 self.region = region
2094 self.space_data = space_data
2095 self.region_data = region_data
2096
2097 if space_data.type == 'VIEW_3D':
2098 self.implementation = View3DUtility(
2099 region, space_data, region_data)
2100 else:
2101 self.implementation = None
2102
2103 if self.implementation:
2104 for name in self.methods:
2105 setattr(self, name,
2106 getattr(self.implementation, name))
2107 else:
2108 for name, value in self.methods.items():
2109 setattr(self, name, value)
2110
2111class View3DUtility:
2112 lock_types = {"lock_cursor": False, "lock_object": None, "lock_bone": ""}
2113
2114 # ====== INITIALIZATION / CLEANUP ====== #
2115 def __init__(self, region, space_data, region_data):
2116 self.region = region
2117 self.space_data = space_data
2118 self.region_data = region_data
2119
2120 # ====== GET VIEW MATRIX AND ITS COMPONENTS ====== #
2121 def get_locks(self):
2122 v3d = self.space_data
2123 return {k:getattr(v3d, k) for k in self.lock_types}
2124
2125 def set_locks(self, locks):
2126 v3d = self.space_data
2127 for k in self.lock_types:
2128 setattr(v3d, k, locks.get(k, self.lock_types[k]))
2129
2130 def _get_lock_obj_bone(self):
2131 v3d = self.space_data
2132
2133 obj = v3d.lock_object
2134 if not obj:
2135 return None, None
2136
2137 if v3d.lock_bone:
2138 try:
2139 # this is not tested!
2140 if obj.mode == 'EDIT':
2141 bone = obj.data.edit_bones[v3d.lock_bone]
2142 else:
2143 bone = obj.data.bones[v3d.lock_bone]
2144 except:
2145 bone = None
2146
2147 return obj, bone
2148
2149 # TODO: learn how to get these values from
2150 # rv3d.perspective_matrix and rv3d.view_matrix ?
2151 def get_position(self, no_locks=False):
2152 v3d = self.space_data
2153 rv3d = self.region_data
2154
2155 if no_locks:
2156 return rv3d.view_location.copy()
2157
2158 # rv3d.perspective_matrix and rv3d.view_matrix
2159 # seem to have some weird translation components %)
2160
2161 if rv3d.view_perspective == 'CAMERA':
2162 p = v3d.camera.matrix_world.to_translation()
2163 d = self.get_direction()
2164 return p + d * rv3d.view_distance
2165 else:
2166 if v3d.lock_object:
2167 obj, bone = self._get_lock_obj_bone()
2168 if bone:
2169 return (obj.matrix_world * bone.matrix).to_translation()
2170 else:
2171 return obj.matrix_world.to_translation()
2172 elif v3d.lock_cursor:
2173 return get_cursor_location(v3d=v3d)
2174 else:
2175 return rv3d.view_location.copy()
2176
2177 def set_position(self, pos, no_locks=False):
2178 v3d = self.space_data
2179 rv3d = self.region_data
2180
2181 pos = pos.copy()
2182
2183 if no_locks:
2184 rv3d.view_location = pos
2185 return
2186
2187 if rv3d.view_perspective == 'CAMERA':
2188 d = self.get_direction()
2189 v3d.camera.matrix_world.translation = pos - d * rv3d.view_distance
2190 else:
2191 if v3d.lock_object:
2192 obj, bone = self._get_lock_obj_bone()
2193 if bone:
2194 try:
2195 bone.matrix.translation = \
2196 obj.matrix_world.inverted() * pos
2197 except:
2198 # this is some degenerate object
2199 bone.matrix.translation = pos
2200 else:
2201 obj.matrix_world.translation = pos
2202 elif v3d.lock_cursor:
2203 set_cursor_location(pos, v3d=v3d)
2204 else:
2205 rv3d.view_location = pos
2206
2207 def get_rotation(self):
2208 v3d = self.space_data
2209 rv3d = self.region_data
2210
2211 if rv3d.view_perspective == 'CAMERA':
2212 return v3d.camera.matrix_world.to_quaternion()
2213 else:
2214 return rv3d.view_rotation
2215
2216 def get_direction(self):
2217 # Camera (as well as viewport) looks in the direction of -Z;
2218 # Y is up, X is left
2219 d = self.get_rotation() * Vector((0, 0, -1))
2220 d.normalize()
2221 return d
2222
2223 def get_viewpoint(self):
2224 v3d = self.space_data
2225 rv3d = self.region_data
2226
2227 if rv3d.view_perspective == 'CAMERA':
2228 return v3d.camera.matrix_world.to_translation()
2229 else:
2230 p = self.get_position()
2231 d = self.get_direction()
2232 return p - d * rv3d.view_distance
2233
2234 def get_matrix(self):
2235 m = self.get_rotation().to_matrix()
2236 m.resize_4x4()
2237 m.translation = self.get_viewpoint()
2238 return m
2239
2240 def get_point(self, xy, pos):
2241 region = self.region
2242 rv3d = self.region_data
2243 return region_2d_to_location_3d(region, rv3d, xy, pos)
2244
2245 def get_ray(self, xy):
2246 region = self.region
2247 v3d = self.space_data
2248 rv3d = self.region_data
2249
2250 viewPos = self.get_viewpoint()
2251 viewDir = self.get_direction()
2252
2253 near = viewPos + viewDir * v3d.clip_start
2254 far = viewPos + viewDir * v3d.clip_end
2255
2256 a = region_2d_to_location_3d(region, rv3d, xy, near)
2257 b = region_2d_to_location_3d(region, rv3d, xy, far)
2258
2259 # When viewed from in-scene camera, near and far
2260 # planes clip geometry even in orthographic mode.
2261 clip = rv3d.is_perspective or (rv3d.view_perspective == 'CAMERA')
2262
2263 return a, b, clip
2264
2265# ====== SNAP UTILITY CLASS ====== #
2266class SnapUtility:
2267 def __init__(self, context):
2268 if context.area.type == 'VIEW_3D':
2269 v3d = context.space_data
2270 shade = v3d.viewport_shade
2271 self.implementation = Snap3DUtility(context.scene, shade)
2272 self.implementation.update_targets(
2273 context.visible_objects, [])
2274
2275 def dispose(self):
2276 self.implementation.dispose()
2277
2278 def update_targets(self, to_include, to_exclude):
2279 self.implementation.update_targets(to_include, to_exclude)
2280
2281 def set_modes(self, **kwargs):
2282 return self.implementation.set_modes(**kwargs)
2283
2284 def snap(self, *args, **kwargs):
2285 return self.implementation.snap(*args, **kwargs)
2286
2287class SnapUtilityBase:
2288 def __init__(self):
2289 self.targets = set()
2290 # TODO: set to current blend settings?
2291 self.interpolation = 'NEVER'
2292 self.editmode = False
2293 self.snap_type = None
2294 self.projection = [None, None, None]
2295 self.potential_snap_elements = None
2296 self.extra_snap_points = None
2297
2298 def update_targets(self, to_include, to_exclude):
2299 self.targets.update(to_include)
2300 self.targets.difference_update(to_exclude)
2301
2302 def set_modes(self, **kwargs):
2303 if "use_relative_coords" in kwargs:
2304 self.use_relative_coords = kwargs["use_relative_coords"]
2305 if "interpolation" in kwargs:
2306 # NEVER, ALWAYS, SMOOTH
2307 self.interpolation = kwargs["interpolation"]
2308 if "editmode" in kwargs:
2309 self.editmode = kwargs["editmode"]
2310 if "snap_align" in kwargs:
2311 self.snap_align = kwargs["snap_align"]
2312 if "snap_type" in kwargs:
2313 # 'INCREMENT', 'VERTEX', 'EDGE', 'FACE', 'VOLUME'
2314 self.snap_type = kwargs["snap_type"]
2315 if "axes_coords" in kwargs:
2316 # none, point, line, plane
2317 self.axes_coords = kwargs["axes_coords"]
2318
2319 # ====== CURSOR REPOSITIONING ====== #
2320 def snap(self, xy, src_matrix, initial_matrix, do_raycast, \
2321 alt_snap, vu, csu, modify_Surface, use_object_centers):
2322
2323 v3d = csu.space_data
2324
2325 grid_step = self.grid_steps[alt_snap] * v3d.grid_scale
2326
2327 su = self
2328 use_relative_coords = su.use_relative_coords
2329 snap_align = su.snap_align
2330 axes_coords = su.axes_coords
2331 snap_type = su.snap_type
2332
2333 runtime_settings = find_runtime_settings()
2334
2335 matrix = src_matrix.to_3x3()
2336 pos = src_matrix.to_translation().copy()
2337
2338 sys_matrix = csu.get_matrix()
2339 if use_relative_coords:
2340 sys_matrix.translation = initial_matrix.translation.copy()
2341
2342 # Axes of freedom and line/plane parameters
2343 start = Vector(((0 if v is None else v) for v in axes_coords))
2344 direction = Vector(((v is not None) for v in axes_coords))
2345 axes_of_freedom = 3 - int(sum(direction))
2346
2347 # do_raycast is False when mouse is not moving
2348 if do_raycast:
2349 su.hide_bbox(True)
2350
2351 self.potential_snap_elements = None
2352 self.extra_snap_points = None
2353
2354 set_stick_obj(csu.tou.scene, None)
2355
2356 raycast = None
2357 snap_to_obj = (snap_type != 'INCREMENT') #or use_object_centers
2358 snap_to_obj = snap_to_obj and (snap_type is not None)
2359 if snap_to_obj:
2360 a, b, clip = vu.get_ray(xy)
2361 view_dir = vu.get_direction()
2362 raycast = su.snap_raycast(a, b, clip, view_dir, csu, alt_snap)
2363
2364 if raycast:
2365 surf_matrix, face_id, obj, orig_obj = raycast
2366
2367 if not use_object_centers:
2368 self.potential_snap_elements = [
2369 (obj.matrix_world * obj.data.vertices[vi].co)
2370 for vi in obj.data.polygons[face_id].vertices
2371 ]
2372
2373 if use_object_centers:
2374 self.extra_snap_points = \
2375 [obj.matrix_world.to_translation()]
2376 elif alt_snap:
2377 pse = self.potential_snap_elements
2378 n = len(pse)
2379 if self.snap_type == 'EDGE':
2380 self.extra_snap_points = []
2381 for i in range(n):
2382 v0 = pse[i]
2383 v1 = pse[(i + 1) % n]
2384 self.extra_snap_points.append((v0 + v1) / 2)
2385 elif self.snap_type == 'FACE':
2386 self.extra_snap_points = []
2387 v0 = Vector()
2388 for v1 in pse:
2389 v0 += v1
2390 self.extra_snap_points.append(v0 / n)
2391
2392 if snap_align:
2393 matrix = surf_matrix.to_3x3()
2394
2395 if not use_object_centers:
2396 pos = surf_matrix.to_translation()
2397 else:
2398 pos = orig_obj.matrix_world.to_translation()
2399
2400 try:
2401 local_pos = orig_obj.matrix_world.inverted() * pos
2402 except:
2403 # this is some degenerate object
2404 local_pos = pos
2405
2406 set_stick_obj(csu.tou.scene, orig_obj.name, local_pos)
2407
2408 modify_Surface = modify_Surface and \
2409 (snap_type != 'VOLUME') and (not use_object_centers)
2410
2411 # === Update "Surface" orientation === #
2412 if modify_Surface:
2413 # Use raycast[0], not matrix! If snap_align == False,
2414 # matrix will be src_matrix!
2415 coordsys = csu.tou.get_custom("Surface")
2416 coordsys.matrix = surf_matrix.to_3x3()
2417 runtime_settings.surface_pos = pos
2418 if csu.tou.get() == "Surface":
2419 sys_matrix = to_matrix4x4(matrix, pos)
2420 else:
2421 if axes_of_freedom == 0:
2422 # Constrained in all axes, can't move.
2423 pass
2424 elif axes_of_freedom == 3:
2425 # Not constrained, move in view plane.
2426 pos = vu.get_point(xy, pos)
2427 else:
2428 a, b, clip = vu.get_ray(xy)
2429 view_dir = vu.get_direction()
2430
2431 start = sys_matrix * start
2432
2433 if axes_of_freedom == 1:
2434 direction = Vector((1, 1, 1)) - direction
2435 direction.rotate(sys_matrix)
2436
2437 if axes_of_freedom == 2:
2438 # Constrained in one axis.
2439 # Find intersection with plane.
2440 i_p = intersect_line_plane(a, b, start, direction)
2441 if i_p is not None:
2442 pos = i_p
2443 elif axes_of_freedom == 1:
2444 # Constrained in two axes.
2445 # Find nearest point to line.
2446 i_p = intersect_line_line(a, b, start,
2447 start + direction)
2448 if i_p is not None:
2449 pos = i_p[1]
2450 #end if do_raycast
2451
2452 try:
2453 sys_matrix_inv = sys_matrix.inverted()
2454 except:
2455 # this is some degenerate system
2456 sys_matrix_inv = Matrix()
2457
2458 _pos = sys_matrix_inv * pos
2459
2460 # don't snap when mouse hasn't moved
2461 if (snap_type == 'INCREMENT') and do_raycast:
2462 for i in range(3):
2463 _pos[i] = round_step(_pos[i], grid_step)
2464
2465 for i in range(3):
2466 if axes_coords[i] is not None:
2467 _pos[i] = axes_coords[i]
2468
2469 if (snap_type == 'INCREMENT') or (axes_of_freedom != 3):
2470 pos = sys_matrix * _pos
2471
2472 res_matrix = to_matrix4x4(matrix, pos)
2473
2474 CursorDynamicSettings.local_matrix = \
2475 sys_matrix_inv * res_matrix
2476
2477 return res_matrix
2478
2479class Snap3DUtility(SnapUtilityBase):
2480 grid_steps = {False:1.0, True:0.1}
2481
2482 cube_verts = [Vector((i, j, k))
2483 for i in (-1, 1)
2484 for j in (-1, 1)
2485 for k in (-1, 1)]
2486
2487 def __init__(self, scene, shade):
2488 SnapUtilityBase.__init__(self)
2489
2490 convert_types = {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}
2491 self.cache = MeshCache(scene, convert_types)
2492
2493 # ? seems that dict is enough
2494 self.bbox_cache = {}#collections.OrderedDict()
2495 self.sys_matrix_key = [0.0] * 9
2496
2497 bm = prepare_gridbox_mesh(subdiv=2)
2498 mesh = bpy.data.meshes.new(tmp_name)
2499 bm.to_mesh(mesh)
2500 mesh.update(calc_tessface=True)
2501 #mesh.calc_tessface()
2502
2503 self.bbox_obj = self.cache._make_obj(mesh, None)
2504 self.bbox_obj.hide = True
2505 self.bbox_obj.draw_type = 'WIRE'
2506 self.bbox_obj.name = "BoundBoxSnap"
2507
2508 self.shade_bbox = (shade == 'BOUNDBOX')
2509
2510 def update_targets(self, to_include, to_exclude):
2511 settings = find_settings()
2512 tfm_opts = settings.transform_options
2513 only_solid = tfm_opts.snap_only_to_solid
2514
2515 # Ensure this is a set and not some other
2516 # type of collection
2517 to_exclude = set(to_exclude)
2518
2519 for target in to_include:
2520 if only_solid and ((target.draw_type == 'BOUNDS') \
2521 or (target.draw_type == 'WIRE')):
2522 to_exclude.add(target)
2523
2524 SnapUtilityBase.update_targets(self, to_include, to_exclude)
2525
2526 def dispose(self):
2527 self.hide_bbox(True)
2528
2529 mesh = self.bbox_obj.data
2530 bpy.data.objects.remove(self.bbox_obj)
2531 bpy.data.meshes.remove(mesh)
2532
2533 self.cache.clear()
2534
2535 def hide_bbox(self, hide):
2536 if self.bbox_obj.hide == hide:
2537 return
2538
2539 self.bbox_obj.hide = hide
2540
2541 # We need to unlink bbox until required to show it,
2542 # because otherwise outliner will blink each
2543 # time cursor is clicked
2544 if hide:
2545 self.cache.scene.objects.unlink(self.bbox_obj)
2546 else:
2547 self.cache.scene.objects.link(self.bbox_obj)
2548
2549 def get_bbox_obj(self, obj, sys_matrix, sys_matrix_inv, is_local):
2550 if is_local:
2551 bbox = None
2552 else:
2553 bbox = self.bbox_cache.get(obj, None)
2554
2555 if bbox is None:
2556 m = obj.matrix_world
2557 if is_local:
2558 sys_matrix = m.copy()
2559 try:
2560 sys_matrix_inv = sys_matrix.inverted()
2561 except Exception:
2562 # this is some degenerate system
2563 sys_matrix_inv = Matrix()
2564 m_combined = sys_matrix_inv * m
2565 bbox = [None, None]
2566
2567 variant = ('RAW' if (self.editmode and
2568 (obj.type == 'MESH') and (obj.mode == 'EDIT'))
2569 else 'PREVIEW')
2570 mesh_obj = self.cache.get(obj, variant, reuse=False)
2571 if (mesh_obj is None) or self.shade_bbox or \
2572 (obj.draw_type == 'BOUNDS'):
2573 if is_local:
2574 bbox = [(-1, -1, -1), (1, 1, 1)]
2575 else:
2576 for p in self.cube_verts:
2577 extend_bbox(bbox, m_combined * p.copy())
2578 elif is_local:
2579 bbox = [mesh_obj.bound_box[0], mesh_obj.bound_box[6]]
2580 else:
2581 for v in mesh_obj.data.vertices:
2582 extend_bbox(bbox, m_combined * v.co.copy())
2583
2584 bbox = (Vector(bbox[0]), Vector(bbox[1]))
2585
2586 if not is_local:
2587 self.bbox_cache[obj] = bbox
2588
2589 half = (bbox[1] - bbox[0]) * 0.5
2590
2591 m = MatrixCompose(half[0], half[1], half[2])
2592 m = sys_matrix.to_3x3() * m
2593 m.resize_4x4()
2594 m.translation = sys_matrix * (bbox[0] + half)
2595 self.bbox_obj.matrix_world = m
2596
2597 return self.bbox_obj
2598
2599 # TODO: ?
2600 # - Sort snap targets according to raycasted distance?
2601 # - Ignore targets if their bounding sphere is further
2602 # than already picked position?
2603 # Perhaps these "optimizations" aren't worth the overhead.
2604
2605 def raycast(self, a, b, clip, view_dir, is_bbox, \
2606 sys_matrix, sys_matrix_inv, is_local, x_ray):
2607 # If we need to interpolate normals or snap to
2608 # vertices/edges, we must convert mesh.
2609 #force = (self.interpolation != 'NEVER') or \
2610 # (self.snap_type in {'VERTEX', 'EDGE'})
2611 # Actually, we have to always convert, since
2612 # we need to get face at least to find tangential.
2613 force = True
2614 edit = self.editmode
2615
2616 res = None
2617 L = None
2618
2619 for obj in self.targets:
2620 orig_obj = obj
2621
2622 if obj.name == self.bbox_obj.name:
2623 # is there a better check?
2624 # ("a is b" doesn't work here)
2625 continue
2626 if obj.show_x_ray != x_ray:
2627 continue
2628
2629 if is_bbox:
2630 obj = self.get_bbox_obj(obj, \
2631 sys_matrix, sys_matrix_inv, is_local)
2632 elif obj.draw_type == 'BOUNDS':
2633 # Outside of BBox, there is no meaningful visual snapping
2634 # for such display mode
2635 continue
2636
2637 m = obj.matrix_world.copy()
2638 try:
2639 mi = m.inverted()
2640 except:
2641 # this is some degenerate object
2642 continue
2643 la = mi * a
2644 lb = mi * b
2645
2646 # Bounding sphere check (to avoid unnecesary conversions
2647 # and to make ray 'infinite')
2648 bb_min = Vector(obj.bound_box[0])
2649 bb_max = Vector(obj.bound_box[6])
2650 c = (bb_min + bb_max) * 0.5
2651 r = (bb_max - bb_min).length * 0.5
2652 sec = intersect_line_sphere(la, lb, c, r, False)
2653 if sec[0] is None:
2654 continue # no intersection with the bounding sphere
2655
2656 if not is_bbox:
2657 # Ensure we work with raycastable object.
2658 variant = ('RAW' if (edit and
2659 (obj.type == 'MESH') and (obj.mode == 'EDIT'))
2660 else 'PREVIEW')
2661 obj = self.cache.get(obj, variant, reuse=(not force))
2662 if (obj is None) or (not obj.data.polygons):
2663 continue # the object has no raycastable geometry
2664
2665 # If ray must be infinite, ensure that
2666 # endpoints are outside of bounding volume
2667 if not clip:
2668 # Seems that intersect_line_sphere()
2669 # returns points in flipped order
2670 lb, la = sec
2671
2672 # Note: in 2.77 the ray_cast API has changed.
2673 # was: location, normal, index
2674 # now: result, location, normal, index
2675 def ray_cast(obj, la, lb):
2676 res = obj.ray_cast(la, lb)
2677 if bpy.app.version < (2, 77, 0):
2678 return ((res[-1] >= 0), res[0], res[1], res[2])
2679 return res
2680
2681 # Does ray actually intersect something?
2682 try:
2683 success, lp, ln, face_id = ray_cast(obj, la, lb)
2684 except Exception as e:
2685 # Somewhy this seems to happen when snapping cursor
2686 # in Local View mode at least since r55223:
2687 # <<Object "\U0010ffff" has no mesh data to be used
2688 # for raycasting>> despite obj.data.polygons
2689 # being non-empty.
2690 try:
2691 # Work-around: in Local View at least the object
2692 # in focus permits raycasting (modifiers are
2693 # applied in 'PREVIEW' mode)
2694 success, lp, ln, face_id = ray_cast(orig_obj, la, lb)
2695 except Exception as e:
2696 # However, in Edit mode in Local View we have
2697 # no luck -- during the edit mode, mesh is
2698 # inaccessible (thus no mesh data for raycasting).
2699 #print(repr(e))
2700 success = False
2701
2702 if not success:
2703 continue
2704
2705 # transform position to global space
2706 p = m * lp
2707
2708 # This works both for prespective and ortho
2709 l = p.dot(view_dir)
2710 if (L is None) or (l < L):
2711 res = (lp, ln, face_id, obj, p, m, la, lb, orig_obj)
2712 L = l
2713 #end for
2714
2715 return res
2716
2717 # Returns:
2718 # Matrix(X -- tangential,
2719 # Y -- 2nd tangential,
2720 # Z -- normal,
2721 # T -- raycasted/snapped position)
2722 # Face ID (-1 if not applicable)
2723 # Object (None if not applicable)
2724 def snap_raycast(self, a, b, clip, view_dir, csu, alt_snap):
2725 settings = find_settings()
2726 tfm_opts = settings.transform_options
2727
2728 if self.shade_bbox and tfm_opts.snap_only_to_solid:
2729 return None
2730
2731 # Since introduction of "use object centers",
2732 # this check is useless (use_object_centers overrides
2733 # even INCREMENT snapping)
2734 #if self.snap_type not in {'VERTEX', 'EDGE', 'FACE', 'VOLUME'}:
2735 # return None
2736
2737 # key shouldn't depend on system origin;
2738 # for bbox calculation origin is always zero
2739 #if csu.tou.get() != "Surface":
2740 # sys_matrix = csu.get_matrix().to_3x3()
2741 #else:
2742 # sys_matrix = csu.get_matrix('LOCAL').to_3x3()
2743 sys_matrix = csu.get_matrix().to_3x3()
2744 sys_matrix_key = list(c for v in sys_matrix for c in v)
2745 sys_matrix_key.append(self.editmode)
2746 sys_matrix = sys_matrix.to_4x4()
2747 try:
2748 sys_matrix_inv = sys_matrix.inverted()
2749 except:
2750 # this is some degenerate system
2751 return None
2752
2753 if self.sys_matrix_key != sys_matrix_key:
2754 self.bbox_cache.clear()
2755 self.sys_matrix_key = sys_matrix_key
2756
2757 # In this context, Volume represents BBox :P
2758 is_bbox = (self.snap_type == 'VOLUME')
2759 is_local = (csu.tou.get() in {'LOCAL', "Scaled"})
2760
2761 res = self.raycast(a, b, clip, view_dir, \
2762 is_bbox, sys_matrix, sys_matrix_inv, is_local, True)
2763
2764 if res is None:
2765 res = self.raycast(a, b, clip, view_dir, \
2766 is_bbox, sys_matrix, sys_matrix_inv, is_local, False)
2767
2768 # Occlusion-based edge/vertex snapping will be
2769 # too inefficient in Python (well, even without
2770 # the occlusion, iterating over all edges/vertices
2771 # of each object is inefficient too)
2772
2773 if not res:
2774 return None
2775
2776 lp, ln, face_id, obj, p, m, la, lb, orig_obj = res
2777
2778 if is_bbox:
2779 self.bbox_obj.matrix_world = m.copy()
2780 self.bbox_obj.show_x_ray = orig_obj.show_x_ray
2781 self.hide_bbox(False)
2782
2783 _ln = ln.copy()
2784
2785 face = obj.data.polygons[face_id]
2786 L = None
2787 t1 = None
2788
2789 if self.snap_type == 'VERTEX' or self.snap_type == 'VOLUME':
2790 for v0 in face.vertices:
2791 v = obj.data.vertices[v0]
2792 p0 = v.co
2793 l = (lp - p0).length_squared
2794 if (L is None) or (l < L):
2795 p = p0
2796 ln = v.normal.copy()
2797 #t1 = ln.cross(_ln)
2798 L = l
2799
2800 _ln = ln.copy()
2801 '''
2802 if t1.length < epsilon:
2803 if (1.0 - abs(ln.z)) < epsilon:
2804 t1 = Vector((1, 0, 0))
2805 else:
2806 t1 = Vector((0, 0, 1)).cross(_ln)
2807 '''
2808 p = m * p
2809 elif self.snap_type == 'EDGE':
2810 use_smooth = face.use_smooth
2811 if self.interpolation == 'NEVER':
2812 use_smooth = False
2813 elif self.interpolation == 'ALWAYS':
2814 use_smooth = True
2815
2816 for v0, v1 in face.edge_keys:
2817 p0 = obj.data.vertices[v0].co
2818 p1 = obj.data.vertices[v1].co
2819 dp = p1 - p0
2820 q = dp.dot(lp - p0) / dp.length_squared
2821 if (q >= 0.0) and (q <= 1.0):
2822 ep = p0 + dp * q
2823 l = (lp - ep).length_squared
2824 if (L is None) or (l < L):
2825 if alt_snap:
2826 p = (p0 + p1) * 0.5
2827 q = 0.5
2828 else:
2829 p = ep
2830 if not use_smooth:
2831 q = 0.5
2832 ln = obj.data.vertices[v1].normal * q + \
2833 obj.data.vertices[v0].normal * (1.0 - q)
2834 t1 = dp
2835 L = l
2836
2837 p = m * p
2838 else:
2839 if alt_snap:
2840 lp = face.center
2841 p = m * lp
2842
2843 if self.interpolation != 'NEVER':
2844 ln = self.interpolate_normal(
2845 obj, face_id, lp, la, lb - la)
2846
2847 # Comment this to make 1st tangential
2848 # always lie in the face's plane
2849 _ln = ln.copy()
2850
2851 '''
2852 for v0, v1 in face.edge_keys:
2853 p0 = obj.data.vertices[v0].co
2854 p1 = obj.data.vertices[v1].co
2855 dp = p1 - p0
2856 q = dp.dot(lp - p0) / dp.length_squared
2857 if (q >= 0.0) and (q <= 1.0):
2858 ep = p0 + dp * q
2859 l = (lp - ep).length_squared
2860 if (L is None) or (l < L):
2861 t1 = dp
2862 L = l
2863 '''
2864
2865 n = ln.copy()
2866 n.rotate(m)
2867 n.normalize()
2868
2869 if t1 is None:
2870 _ln.rotate(m)
2871 _ln.normalize()
2872 if (1.0 - abs(_ln.z)) < epsilon:
2873 t1 = Vector((1, 0, 0))
2874 else:
2875 t1 = Vector((0, 0, 1)).cross(_ln)
2876 t1.normalize()
2877 else:
2878 t1.rotate(m)
2879 t1.normalize()
2880
2881 t2 = t1.cross(n)
2882 t2.normalize()
2883
2884 matrix = MatrixCompose(t1, t2, n, p)
2885
2886 return (matrix, face_id, obj, orig_obj)
2887
2888 def interpolate_normal(self, obj, face_id, p, orig, ray):
2889 face = obj.data.polygons[face_id]
2890
2891 use_smooth = face.use_smooth
2892 if self.interpolation == 'NEVER':
2893 use_smooth = False
2894 elif self.interpolation == 'ALWAYS':
2895 use_smooth = True
2896
2897 if not use_smooth:
2898 return face.normal.copy()
2899
2900 # edge.use_edge_sharp affects smoothness only if
2901 # mesh has EdgeSplit modifier
2902
2903 # ATTENTION! Coords/Normals MUST be copied
2904 # (a bug in barycentric_transform implementation ?)
2905 # Somewhat strangely, the problem also disappears
2906 # if values passed to barycentric_transform
2907 # are print()ed beforehand.
2908
2909 co = [obj.data.vertices[vi].co.copy()
2910 for vi in face.vertices]
2911
2912 normals = [obj.data.vertices[vi].normal.copy()
2913 for vi in face.vertices]
2914
2915 if len(face.vertices) != 3:
2916 tris = tessellate_polygon([co])
2917 for tri in tris:
2918 i0, i1, i2 = tri
2919 if intersect_ray_tri(co[i0], co[i1], co[i2], ray, orig):
2920 break
2921 else:
2922 i0, i1, i2 = 0, 1, 2
2923
2924 n = barycentric_transform(p, co[i0], co[i1], co[i2],
2925 normals[i0], normals[i1], normals[i2])
2926 n.normalize()
2927
2928 return n
2929
2930# ====== CONVERTED-TO-MESH OBJECTS CACHE ====== #
2931#============================================================================#
2932class ToggleObjectMode:
2933 def __init__(self, mode='OBJECT'):
2934 if not isinstance(mode, str):
2935 mode = ('OBJECT' if mode else None)
2936
2937 self.mode = mode
2938
2939 def __enter__(self):
2940 if self.mode:
2941 edit_preferences = bpy.context.user_preferences.edit
2942
2943 self.global_undo = edit_preferences.use_global_undo
2944 self.prev_mode = bpy.context.object.mode
2945
2946 if self.prev_mode != self.mode:
2947 edit_preferences.use_global_undo = False
2948 bpy.ops.object.mode_set(mode=self.mode)
2949
2950 return self
2951
2952 def __exit__(self, type, value, traceback):
2953 if self.mode:
2954 edit_preferences = bpy.context.user_preferences.edit
2955
2956 if self.prev_mode != self.mode:
2957 bpy.ops.object.mode_set(mode=self.prev_mode)
2958 edit_preferences.use_global_undo = self.global_undo
2959
2960class MeshCacheItem:
2961 def __init__(self):
2962 self.variants = {}
2963
2964 def __getitem__(self, variant):
2965 return self.variants[variant][0]
2966
2967 def __setitem__(self, variant, conversion):
2968 mesh = conversion[0].data
2969 #mesh.update(calc_tessface=True)
2970 #mesh.calc_tessface()
2971 mesh.calc_normals()
2972
2973 self.variants[variant] = conversion
2974
2975 def __contains__(self, variant):
2976 return variant in self.variants
2977
2978 def dispose(self):
2979 for obj, converted in self.variants.values():
2980 if converted:
2981 mesh = obj.data
2982 bpy.data.objects.remove(obj)
2983 bpy.data.meshes.remove(mesh)
2984 self.variants = None
2985
2986class MeshCache:
2987 """
2988 Keeps a cache of mesh equivalents of requested objects.
2989 It is assumed that object's data does not change while
2990 the cache is in use.
2991 """
2992
2993 variants_enum = {'RAW', 'PREVIEW', 'RENDER'}
2994 variants_normalization = {
2995 'MESH':{},
2996 'CURVE':{},
2997 'SURFACE':{},
2998 'FONT':{},
2999 'META':{'RAW':'PREVIEW'},
3000 'ARMATURE':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3001 'LATTICE':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3002 'EMPTY':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3003 'CAMERA':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3004 'LAMP':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3005 'SPEAKER':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
3006 }
3007 conversible_types = {'MESH', 'CURVE', 'SURFACE', 'FONT',
3008 'META', 'ARMATURE', 'LATTICE'}
3009 convert_types = conversible_types
3010
3011 def __init__(self, scene, convert_types=None):
3012 self.scene = scene
3013 if convert_types:
3014 self.convert_types = convert_types
3015 self.cached = {}
3016
3017 def __del__(self):
3018 self.clear()
3019
3020 def clear(self, expect_zero_users=False):
3021 for cache_item in self.cached.values():
3022 if cache_item:
3023 try:
3024 cache_item.dispose()
3025 except RuntimeError:
3026 if expect_zero_users:
3027 raise
3028 self.cached.clear()
3029
3030 def __delitem__(self, obj):
3031 cache_item = self.cached.pop(obj, None)
3032 if cache_item:
3033 cache_item.dispose()
3034
3035 def __contains__(self, obj):
3036 return obj in self.cached
3037
3038 def __getitem__(self, obj):
3039 if isinstance(obj, tuple):
3040 return self.get(*obj)
3041 return self.get(obj)
3042
3043 def get(self, obj, variant='PREVIEW', reuse=True):
3044 if variant not in self.variants_enum:
3045 raise ValueError("Mesh variant must be one of %s" %
3046 self.variants_enum)
3047
3048 # Make sure the variant is proper for this type of object
3049 variant = (self.variants_normalization[obj.type].
3050 get(variant, variant))
3051
3052 if obj in self.cached:
3053 cache_item = self.cached[obj]
3054 try:
3055 # cache_item is None if object isn't conversible to mesh
3056 return (None if (cache_item is None)
3057 else cache_item[variant])
3058 except KeyError:
3059 pass
3060 else:
3061 cache_item = None
3062
3063 if obj.type not in self.conversible_types:
3064 self.cached[obj] = None
3065 return None
3066
3067 if not cache_item:
3068 cache_item = MeshCacheItem()
3069 self.cached[obj] = cache_item
3070
3071 conversion = self._convert(obj, variant, reuse)
3072 cache_item[variant] = conversion
3073
3074 return conversion[0]
3075
3076 def _convert(self, obj, variant, reuse=True):
3077 obj_type = obj.type
3078 obj_mode = obj.mode
3079 data = obj.data
3080
3081 if obj_type == 'MESH':
3082 if reuse and ((variant == 'RAW') or (len(obj.modifiers) == 0)):
3083 return (obj, False)
3084 else:
3085 force_objectmode = (obj_mode in {'EDIT', 'SCULPT'})
3086 return (self._to_mesh(obj, variant, force_objectmode), True)
3087 elif obj_type in {'CURVE', 'SURFACE', 'FONT'}:
3088 if variant == 'RAW':
3089 bm = bmesh.new()
3090 for spline in data.splines:
3091 for point in spline.bezier_points:
3092 bm.verts.new(point.co)
3093 bm.verts.new(point.handle_left)
3094 bm.verts.new(point.handle_right)
3095 for point in spline.points:
3096 bm.verts.new(point.co[:3])
3097 return (self._make_obj(bm, obj), True)
3098 else:
3099 if variant == 'RENDER':
3100 resolution_u = data.resolution_u
3101 resolution_v = data.resolution_v
3102 if data.render_resolution_u != 0:
3103 data.resolution_u = data.render_resolution_u
3104 if data.render_resolution_v != 0:
3105 data.resolution_v = data.render_resolution_v
3106
3107 result = (self._to_mesh(obj, variant), True)
3108
3109 if variant == 'RENDER':
3110 data.resolution_u = resolution_u
3111 data.resolution_v = resolution_v
3112
3113 return result
3114 elif obj_type == 'META':
3115 if variant == 'RAW':
3116 # To avoid the hassle of snapping metaelements
3117 # to themselves, we just create an empty mesh
3118 bm = bmesh.new()
3119 return (self._make_obj(bm, obj), True)
3120 else:
3121 if variant == 'RENDER':
3122 resolution = data.resolution
3123 data.resolution = data.render_resolution
3124
3125 result = (self._to_mesh(obj, variant), True)
3126
3127 if variant == 'RENDER':
3128 data.resolution = resolution
3129
3130 return result
3131 elif obj_type == 'ARMATURE':
3132 bm = bmesh.new()
3133 if obj_mode == 'EDIT':
3134 for bone in data.edit_bones:
3135 head = bm.verts.new(bone.head)
3136 tail = bm.verts.new(bone.tail)
3137 bm.edges.new((head, tail))
3138 elif obj_mode == 'POSE':
3139 for bone in obj.pose.bones:
3140 head = bm.verts.new(bone.head)
3141 tail = bm.verts.new(bone.tail)
3142 bm.edges.new((head, tail))
3143 else:
3144 for bone in data.bones:
3145 head = bm.verts.new(bone.head_local)
3146 tail = bm.verts.new(bone.tail_local)
3147 bm.edges.new((head, tail))
3148 return (self._make_obj(bm, obj), True)
3149 elif obj_type == 'LATTICE':
3150 bm = bmesh.new()
3151 for point in data.points:
3152 bm.verts.new(point.co_deform)
3153 return (self._make_obj(bm, obj), True)
3154
3155 def _to_mesh(self, obj, variant, force_objectmode=False):
3156 tmp_name = chr(0x10ffff) # maximal Unicode value
3157
3158 with ToggleObjectMode(force_objectmode):
3159 if variant == 'RAW':
3160 mesh = obj.to_mesh(self.scene, False, 'PREVIEW')
3161 else:
3162 mesh = obj.to_mesh(self.scene, True, variant)
3163 mesh.name = tmp_name
3164
3165 return self._make_obj(mesh, obj)
3166
3167 def _make_obj(self, mesh, src_obj):
3168 tmp_name = chr(0x10ffff) # maximal Unicode value
3169
3170 if isinstance(mesh, bmesh.types.BMesh):
3171 bm = mesh
3172 mesh = bpy.data.meshes.new(tmp_name)
3173 bm.to_mesh(mesh)
3174
3175 tmp_obj = bpy.data.objects.new(tmp_name, mesh)
3176
3177 if src_obj:
3178 tmp_obj.matrix_world = src_obj.matrix_world
3179
3180 # This is necessary for correct bbox display # TODO
3181 # (though it'd be better to change the logic in the raycasting)
3182 tmp_obj.show_x_ray = src_obj.show_x_ray
3183
3184 tmp_obj.dupli_faces_scale = src_obj.dupli_faces_scale
3185 tmp_obj.dupli_frames_end = src_obj.dupli_frames_end
3186 tmp_obj.dupli_frames_off = src_obj.dupli_frames_off
3187 tmp_obj.dupli_frames_on = src_obj.dupli_frames_on
3188 tmp_obj.dupli_frames_start = src_obj.dupli_frames_start
3189 tmp_obj.dupli_group = src_obj.dupli_group
3190 #tmp_obj.dupli_list = src_obj.dupli_list
3191 tmp_obj.dupli_type = src_obj.dupli_type
3192
3193 # Make Blender recognize object as having geometry
3194 # (is there a simpler way to do this?)
3195 self.scene.objects.link(tmp_obj)
3196 self.scene.update()
3197 # We don't need this object in scene
3198 self.scene.objects.unlink(tmp_obj)
3199
3200 return tmp_obj
3201
3202#============================================================================#
3203
3204# A base class for emulating ID-datablock behavior
3205class PseudoIDBlockBase(bpy.types.PropertyGroup):
3206 # TODO: use normal metaprogramming?
3207
3208 @staticmethod
3209 def create_props(type, name, options={'ANIMATABLE'}):
3210 def active_update(self, context):
3211 # necessary to avoid recursive calls
3212 if self._self_update[0]:
3213 return
3214
3215 if self._dont_rename[0]:
3216 return
3217
3218 if len(self.collection) == 0:
3219 return
3220
3221 # prepare data for renaming...
3222 old_key = (self.enum if self.enum else self.collection[0].name)
3223 new_key = (self.active if self.active else "Untitled")
3224
3225 if old_key == new_key:
3226 return
3227
3228 old_item = None
3229 new_item = None
3230 existing_names = []
3231
3232 for item in self.collection:
3233 if (item.name == old_key) and (not new_item):
3234 new_item = item
3235 elif (item.name == new_key) and (not old_item):
3236 old_item = item
3237 else:
3238 existing_names.append(item.name)
3239 existing_names.append(new_key)
3240
3241 # rename current item
3242 new_item.name = new_key
3243
3244 if old_item:
3245 # rename other item if it has that name
3246 name = new_key
3247 i = 1
3248 while name in existing_names:
3249 name = "{}.{:0>3}".format(new_key, i)
3250 i += 1
3251 old_item.name = name
3252
3253 # update the enum
3254 self._self_update[0] += 1
3255 self.update_enum()
3256 self._self_update[0] -= 1
3257 # end def
3258
3259 def enum_update(self, context):
3260 # necessary to avoid recursive calls
3261 if self._self_update[0]:
3262 return
3263
3264 self._dont_rename[0] = True
3265 self.active = self.enum
3266 self._dont_rename[0] = False
3267
3268 self.on_item_select()
3269 # end def
3270
3271 collection = bpy.props.CollectionProperty(
3272 type=type)
3273 active = bpy.props.StringProperty(
3274 name="Name",
3275 description="Name of the active {}".format(name),
3276 options=options,
3277 update=active_update)
3278 enum = bpy.props.EnumProperty(
3279 items=[],
3280 name="Choose",
3281 description="Choose {}".format(name),
3282 default=set(),
3283 options={'ENUM_FLAG'},
3284 update=enum_update)
3285
3286 return collection, active, enum
3287 # end def
3288
3289 def add(self, name="", **kwargs):
3290 if not name:
3291 name = 'Untitled'
3292 _name = name
3293
3294 existing_names = [item.name for item in self.collection]
3295 i = 1
3296 while name in existing_names:
3297 name = "{}.{:0>3}".format(_name, i)
3298 i += 1
3299
3300 instance = self.collection.add()
3301 instance.name = name
3302
3303 for key, value in kwargs.items():
3304 setattr(instance, key, value)
3305
3306 self._self_update[0] += 1
3307 self.active = name
3308 self.update_enum()
3309 self._self_update[0] -= 1
3310
3311 return instance
3312
3313 def remove(self, key):
3314 if isinstance(key, int):
3315 i = key
3316 else:
3317 i = self.indexof(key)
3318
3319 # Currently remove() ignores non-existing indices...
3320 # In the case this behavior changes, we have the try block.
3321 try:
3322 self.collection.remove(i)
3323 except:
3324 pass
3325
3326 self._self_update[0] += 1
3327 if len(self.collection) != 0:
3328 i = min(i, len(self.collection) - 1)
3329 self.active = self.collection[i].name
3330 else:
3331 self.active = ""
3332 self.update_enum()
3333 self._self_update[0] -= 1
3334
3335 def get_item(self, key=None):
3336 if key is None:
3337 i = self.indexof(self.active)
3338 elif isinstance(key, int):
3339 i = key
3340 else:
3341 i = self.indexof(key)
3342
3343 try:
3344 return self.collection[i]
3345 except:
3346 return None
3347
3348 def indexof(self, key):
3349 return next((i for i, v in enumerate(self.collection) \
3350 if v.name == key), -1)
3351
3352 # Which is more Pythonic?
3353
3354 #for i, item in enumerate(self.collection):
3355 # if item.name == key:
3356 # return i
3357 #return -1 # non-existing index
3358
3359 def update_enum(self):
3360 names = []
3361 items = []
3362 for item in self.collection:
3363 names.append(item.name)
3364 items.append((item.name, item.name, ""))
3365
3366 prop_class, prop_params = type(self).enum
3367 prop_params["items"] = items
3368 if len(items) == 0:
3369 prop_params["default"] = set()
3370 prop_params["options"] = {'ENUM_FLAG'}
3371 else:
3372 # Somewhy active may be left from previous times,
3373 # I don't want to dig now why that happens.
3374 if self.active not in names:
3375 self.active = items[0][0]
3376 prop_params["default"] = self.active
3377 prop_params["options"] = set()
3378
3379 # Can this cause problems? In the near future, shouldn't...
3380 type(self).enum = (prop_class, prop_params)
3381 #type(self).enum = bpy.props.EnumProperty(**prop_params)
3382
3383 if len(items) != 0:
3384 self.enum = self.active
3385
3386 def on_item_select(self):
3387 pass
3388
3389 data_name = ""
3390 op_new = ""
3391 op_delete = ""
3392 icon = 'DOT'
3393
3394 def draw(self, context, layout):
3395 if len(self.collection) == 0:
3396 if self.op_new:
3397 layout.operator(self.op_new, icon=self.icon)
3398 else:
3399 layout.label(
3400 text="({})".format(self.data_name),
3401 icon=self.icon)
3402 return
3403
3404 row = layout.row(align=True)
3405 row.prop_menu_enum(self, "enum", text="", icon=self.icon)
3406 row.prop(self, "active", text="")
3407 if self.op_new:
3408 row.operator(self.op_new, text="", icon='ZOOMIN')
3409 if self.op_delete:
3410 row.operator(self.op_delete, text="", icon='X')
3411# end class
3412#============================================================================#
3413# ===== PROPERTY DEFINITIONS ===== #
3414
3415# ===== TRANSFORM EXTRA OPTIONS ===== #
3416class TransformExtraOptionsProp(bpy.types.PropertyGroup):
3417 use_relative_coords = bpy.props.BoolProperty(
3418 name="Relative coordinates",
3419 description="Consider existing transformation as the starting point",
3420 default=True)
3421 snap_interpolate_normals_mode = bpy.props.EnumProperty(
3422 items=[('NEVER', "Never", "Don't interpolate normals"),
3423 ('ALWAYS', "Always", "Always interpolate normals"),
3424 ('SMOOTH', "Smoothness-based", "Interpolate normals only "\
3425 "for faces with smooth shading"),],
3426 name="Normal interpolation",
3427 description="Normal interpolation mode for snapping",
3428 default='SMOOTH')
3429 snap_only_to_solid = bpy.props.BoolProperty(
3430 name="Snap only to solid",
3431 description="Ignore wireframe/non-solid objects during snapping",
3432 default=False)
3433 snap_element_screen_size = bpy.props.IntProperty(
3434 name="Snap distance",
3435 description="Radius in pixels for snapping to edges/vertices",
3436 default=8,
3437 min=2,
3438 max=64)
3439 use_comma_separator = bpy.props.BoolProperty(
3440 name="Use comma separator",
3441 description="Use comma separator when copying/pasting"\
3442 "coordinate values (instead of Tab character)",
3443 default=True,
3444 options={'HIDDEN'})
3445
3446# ===== 3D VECTOR LOCATION ===== #
3447class LocationProp(bpy.types.PropertyGroup):
3448 pos = bpy.props.FloatVectorProperty(
3449 name="xyz", description="xyz coords",
3450 options={'HIDDEN'}, subtype='XYZ')
3451
3452# ===== HISTORY ===== #
3453def update_history_max_size(self, context):
3454 settings = find_settings()
3455
3456 history = settings.history
3457
3458 prop_class, prop_params = type(history).current_id
3459 old_max = prop_params["max"]
3460
3461 size = history.max_size
3462 try:
3463 int_size = int(size)
3464 int_size = max(int_size, 0)
3465 int_size = min(int_size, history.max_size_limit)
3466 except:
3467 int_size = old_max
3468
3469 if old_max != int_size:
3470 prop_params["max"] = int_size
3471 type(history).current_id = (prop_class, prop_params)
3472
3473 # also: clear immediately?
3474 for i in range(len(history.entries) - 1, int_size, -1):
3475 history.entries.remove(i)
3476
3477 if str(int_size) != size:
3478 # update history.max_size if it's not inside the limits
3479 history.max_size = str(int_size)
3480
3481def update_history_id(self, context):
3482 scene = bpy.context.scene
3483
3484 settings = find_settings()
3485 history = settings.history
3486
3487 pos = history.get_pos()
3488 if pos is not None:
3489 # History doesn't depend on view (?)
3490 cursor_pos = get_cursor_location(scene=scene)
3491
3492 if CursorHistoryProp.update_cursor_on_id_change:
3493 # Set cursor position anyway (we're changing v3d's
3494 # cursor, which may be separate from scene's)
3495 # This, however, should be done cautiously
3496 # from scripts, since, e.g., CursorMonitor
3497 # can supply wrong context -> cursor will be set
3498 # in a different view than required
3499 set_cursor_location(pos, v3d=context.space_data)
3500
3501 if pos != cursor_pos:
3502 if (history.current_id == 0) and (history.last_id <= 1):
3503 history.last_id = 1
3504 else:
3505 history.last_id = history.curr_id
3506 history.curr_id = history.current_id
3507
3508class CursorHistoryProp(bpy.types.PropertyGroup):
3509 max_size_limit = 500
3510
3511 update_cursor_on_id_change = True
3512
3513 show_trace = bpy.props.BoolProperty(
3514 name="Trace",
3515 description="Show history trace",
3516 default=False)
3517 max_size = bpy.props.StringProperty(
3518 name="Size",
3519 description="History max size",
3520 default=str(50),
3521 update=update_history_max_size)
3522 current_id = bpy.props.IntProperty(
3523 name="Index",
3524 description="Current position in cursor location history",
3525 default=50,
3526 min=0,
3527 max=50,
3528 update=update_history_id)
3529 entries = bpy.props.CollectionProperty(
3530 type=LocationProp)
3531
3532 curr_id = bpy.props.IntProperty(options={'HIDDEN'})
3533 last_id = bpy.props.IntProperty(options={'HIDDEN'})
3534
3535 def get_pos(self, id = None):
3536 if id is None:
3537 id = self.current_id
3538
3539 id = min(max(id, 0), len(self.entries) - 1)
3540
3541 if id < 0:
3542 # history is empty
3543 return None
3544
3545 return self.entries[id].pos
3546
3547 # for updating the upper bound on file load
3548 def update_max_size(self):
3549 prop_class, prop_params = type(self).current_id
3550 # self.max_size expected to be always a correct integer
3551 prop_params["max"] = int(self.max_size)
3552 type(self).current_id = (prop_class, prop_params)
3553
3554 def draw_trace(self, context):
3555 bgl.glColor4f(0.75, 1.0, 0.75, 1.0)
3556 bgl.glBegin(bgl.GL_LINE_STRIP)
3557 for entry in self.entries:
3558 p = entry.pos
3559 bgl.glVertex3f(p[0], p[1], p[2])
3560 bgl.glEnd()
3561
3562 def draw_offset(self, context):
3563 bgl.glShadeModel(bgl.GL_SMOOTH)
3564
3565 tfm_operator = CursorDynamicSettings.active_transform_operator
3566
3567 bgl.glBegin(bgl.GL_LINE_STRIP)
3568
3569 if tfm_operator:
3570 p = tfm_operator.particles[0]. \
3571 get_initial_matrix().to_translation()
3572 else:
3573 p = self.get_pos(self.last_id)
3574 bgl.glColor4f(1.0, 0.75, 0.5, 1.0)
3575 bgl.glVertex3f(p[0], p[1], p[2])
3576
3577 p = get_cursor_location(v3d=context.space_data)
3578 bgl.glColor4f(1.0, 1.0, 0.25, 1.0)
3579 bgl.glVertex3f(p[0], p[1], p[2])
3580
3581 bgl.glEnd()
3582
3583# ===== BOOKMARK ===== #
3584class BookmarkProp(bpy.types.PropertyGroup):
3585 name = bpy.props.StringProperty(
3586 name="name", description="bookmark name",
3587 options={'HIDDEN'})
3588 pos = bpy.props.FloatVectorProperty(
3589 name="xyz", description="xyz coords",
3590 options={'HIDDEN'}, subtype='XYZ')
3591
3592class BookmarkIDBlock(PseudoIDBlockBase):
3593 # Somewhy instance members aren't seen in update()
3594 # callbacks... but class members are.
3595 _self_update = [0]
3596 _dont_rename = [False]
3597
3598 data_name = "Bookmark"
3599 op_new = "scene.cursor_3d_new_bookmark"
3600 op_delete = "scene.cursor_3d_delete_bookmark"
3601 icon = 'CURSOR'
3602
3603 collection, active, enum = PseudoIDBlockBase.create_props(
3604 BookmarkProp, "Bookmark")
3605
3606class NewCursor3DBookmark(bpy.types.Operator):
3607 bl_idname = "scene.cursor_3d_new_bookmark"
3608 bl_label = "New Bookmark"
3609 bl_description = "Add a new bookmark"
3610
3611 name = bpy.props.StringProperty(
3612 name="Name",
3613 description="Name of the new bookmark",
3614 default="Mark")
3615
3616 @classmethod
3617 def poll(cls, context):
3618 return context.area.type == 'VIEW_3D'
3619
3620 def execute(self, context):
3621 settings = find_settings()
3622 library = settings.libraries.get_item()
3623 if not library:
3624 return {'CANCELLED'}
3625
3626 bookmark = library.bookmarks.add(name=self.name)
3627
3628 cusor_pos = get_cursor_location(v3d=context.space_data)
3629
3630 try:
3631 bookmark.pos = library.convert_from_abs(context.space_data,
3632 cusor_pos, True)
3633 except Exception as exc:
3634 self.report({'ERROR_INVALID_CONTEXT'}, exc.args[0])
3635 return {'CANCELLED'}
3636
3637 return {'FINISHED'}
3638
3639class DeleteCursor3DBookmark(bpy.types.Operator):
3640 bl_idname = "scene.cursor_3d_delete_bookmark"
3641 bl_label = "Delete Bookmark"
3642 bl_description = "Delete active bookmark"
3643
3644 def execute(self, context):
3645 settings = find_settings()
3646 library = settings.libraries.get_item()
3647 if not library:
3648 return {'CANCELLED'}
3649
3650 name = library.bookmarks.active
3651
3652 library.bookmarks.remove(key=name)
3653
3654 return {'FINISHED'}
3655
3656class OverwriteCursor3DBookmark(bpy.types.Operator):
3657 bl_idname = "scene.cursor_3d_overwrite_bookmark"
3658 bl_label = "Overwrite"
3659 bl_description = "Overwrite active bookmark "\
3660 "with the current cursor location"
3661
3662 @classmethod
3663 def poll(cls, context):
3664 return context.area.type == 'VIEW_3D'
3665
3666 def execute(self, context):
3667 settings = find_settings()
3668 library = settings.libraries.get_item()
3669 if not library:
3670 return {'CANCELLED'}
3671
3672 bookmark = library.bookmarks.get_item()
3673 if not bookmark:
3674 return {'CANCELLED'}
3675
3676 cusor_pos = get_cursor_location(v3d=context.space_data)
3677
3678 try:
3679 bookmark.pos = library.convert_from_abs(context.space_data,
3680 cusor_pos, True)
3681 except Exception as exc:
3682 self.report({'ERROR_INVALID_CONTEXT'}, exc.args[0])
3683 return {'CANCELLED'}
3684
3685 CursorDynamicSettings.recalc_csu(context, 'PRESS')
3686
3687 return {'FINISHED'}
3688
3689class RecallCursor3DBookmark(bpy.types.Operator):
3690 bl_idname = "scene.cursor_3d_recall_bookmark"
3691 bl_label = "Recall"
3692 bl_description = "Move cursor to the active bookmark"
3693
3694 @classmethod
3695 def poll(cls, context):
3696 return context.area.type == 'VIEW_3D'
3697
3698 def execute(self, context):
3699 settings = find_settings()
3700 library = settings.libraries.get_item()
3701 if not library:
3702 return {'CANCELLED'}
3703
3704 bookmark = library.bookmarks.get_item()
3705 if not bookmark:
3706 return {'CANCELLED'}
3707
3708 try:
3709 bookmark_pos = library.convert_to_abs(context.space_data,
3710 bookmark.pos, True)
3711 set_cursor_location(bookmark_pos, v3d=context.space_data)
3712 except Exception as exc:
3713 self.report({'ERROR_INVALID_CONTEXT'}, exc.args[0])
3714 return {'CANCELLED'}
3715
3716 CursorDynamicSettings.recalc_csu(context)
3717
3718 return {'FINISHED'}
3719
3720class SwapCursor3DBookmark(bpy.types.Operator):
3721 bl_idname = "scene.cursor_3d_swap_bookmark"
3722 bl_label = "Swap"
3723 bl_description = "Swap cursor position with the active bookmark"
3724
3725 @classmethod
3726 def poll(cls, context):
3727 return context.area.type == 'VIEW_3D'
3728
3729 def execute(self, context):
3730 settings = find_settings()
3731 library = settings.libraries.get_item()
3732 if not library:
3733 return {'CANCELLED'}
3734
3735 bookmark = library.bookmarks.get_item()
3736 if not bookmark:
3737 return {'CANCELLED'}
3738
3739 cusor_pos = get_cursor_location(v3d=context.space_data)
3740
3741 try:
3742 bookmark_pos = library.convert_to_abs(context.space_data,
3743 bookmark.pos, True)
3744
3745 set_cursor_location(bookmark_pos, v3d=context.space_data)
3746
3747 bookmark.pos = library.convert_from_abs(context.space_data,
3748 cusor_pos, True,
3749 use_history=False)
3750 except Exception as exc:
3751 self.report({'ERROR_INVALID_CONTEXT'}, exc.args[0])
3752 return {'CANCELLED'}
3753
3754 CursorDynamicSettings.recalc_csu(context)
3755
3756 return {'FINISHED'}
3757
3758# Will this be used?
3759class SnapSelectionToCursor3DBookmark(bpy.types.Operator):
3760 bl_idname = "scene.cursor_3d_snap_selection_to_bookmark"
3761 bl_label = "Snap Selection"
3762 bl_description = "Snap selection to the active bookmark"
3763
3764# Will this be used?
3765class AddEmptyAtCursor3DBookmark(bpy.types.Operator):
3766 bl_idname = "scene.cursor_3d_add_empty_at_bookmark"
3767 bl_label = "Add Empty"
3768 bl_description = "Add new Empty at the active bookmark"
3769
3770 @classmethod
3771 def poll(cls, context):
3772 return context.area.type == 'VIEW_3D'
3773
3774 def execute(self, context):
3775 settings = find_settings()
3776 library = settings.libraries.get_item()
3777 if not library:
3778 return {'CANCELLED'}
3779
3780 bookmark = library.bookmarks.get_item()
3781 if not bookmark:
3782 return {'CANCELLED'}
3783
3784 try:
3785 matrix = library.get_matrix(use_history=False,
3786 v3d=context.space_data, warn=True)
3787 bookmark_pos = matrix * bookmark.pos
3788 except Exception as exc:
3789 self.report({'ERROR_INVALID_CONTEXT'}, exc.args[0])
3790 return {'CANCELLED'}
3791
3792 name = "{}.{}".format(library.name, bookmark.name)
3793 obj = bpy.data.objects.new(name, None)
3794 obj.matrix_world = to_matrix4x4(matrix, bookmark_pos)
3795 context.scene.objects.link(obj)
3796
3797 """
3798 for sel_obj in list(context.selected_objects):
3799 sel_obj.select = False
3800 obj.select = True
3801 context.scene.objects.active = obj
3802
3803 # We need this to update bookmark position if
3804 # library's system is local/scaled/normal/etc.
3805 CursorDynamicSettings.recalc_csu(context, "PRESS")
3806 """
3807
3808 # TODO: exit from editmode? It has separate history!
3809 # If we just link object to scene, it will not trigger
3810 # addition of new entry to Undo history
3811 bpy.ops.ed.undo_push(message="Add Object")
3812
3813 return {'FINISHED'}
3814
3815# ===== BOOKMARK LIBRARY ===== #
3816class BookmarkLibraryProp(bpy.types.PropertyGroup):
3817 name = bpy.props.StringProperty(
3818 name="Name", description="Name of the bookmark library",
3819 options={'HIDDEN'})
3820 bookmarks = bpy.props.PointerProperty(
3821 type=BookmarkIDBlock,
3822 options={'HIDDEN'})
3823 system = bpy.props.EnumProperty(
3824 items=[
3825 ('GLOBAL', "Global", "Global (absolute) coordinates"),
3826 ('LOCAL', "Local", "Local coordinate system, "\
3827 "relative to the active object"),
3828 ('SCALED', "Scaled", "Scaled local coordinate system, "\
3829 "relative to the active object"),
3830 ('NORMAL', "Normal", "Normal coordinate system, "\
3831 "relative to the selected elements"),
3832 ('CONTEXT', "Context", "Current transform orientation; "\
3833 "origin depends on selection"),
3834 ],
3835 default="GLOBAL",
3836 name="System",
3837 description="Coordinate system in which to store/recall "\
3838 "cursor locations",
3839 options={'HIDDEN'})
3840 offset = bpy.props.BoolProperty(
3841 name="Offset",
3842 description="Store/recall relative to the last cursor position",
3843 default=False,
3844 options={'HIDDEN'})
3845
3846 # Returned None means "operation is not aplicable"
3847 def get_matrix(self, use_history, v3d, warn=True, **kwargs):
3848 #particles, csu = gather_particles(**kwargs)
3849
3850 # Ensure we have relevant CSU (Blender will crash
3851 # if we use the old one after Undo/Redo)
3852 CursorDynamicSettings.recalc_csu(bpy.context)
3853
3854 csu = CursorDynamicSettings.csu
3855
3856 if self.offset:
3857 # history? or keep separate for each scene?
3858 if not use_history:
3859 csu.source_pos = get_cursor_location(v3d=v3d)
3860 else:
3861 settings = find_settings()
3862 history = settings.history
3863 csu.source_pos = history.get_pos(history.last_id)
3864 else:
3865 csu.source_pos = Vector()
3866
3867 active_obj = csu.tou.scene.objects.active
3868
3869 if self.system == 'GLOBAL':
3870 sys_name = 'GLOBAL'
3871 pivot = 'WORLD'
3872 elif self.system == 'LOCAL':
3873 if not active_obj:
3874 if warn:
3875 raise Exception("There is no active object")
3876 return None
3877 sys_name = 'LOCAL'
3878 pivot = 'ACTIVE'
3879 elif self.system == 'SCALED':
3880 if not active_obj:
3881 if warn:
3882 raise Exception("There is no active object")
3883 return None
3884 sys_name = 'Scaled'
3885 pivot = 'ACTIVE'
3886 elif self.system == 'NORMAL':
3887 if not active_obj or active_obj.mode != 'EDIT':
3888 if warn:
3889 raise Exception("Active object must be in Edit mode")
3890 return None
3891 sys_name = 'NORMAL'
3892 pivot = 'MEDIAN' # ?
3893 elif self.system == 'CONTEXT':
3894 sys_name = None # use current orientation
3895 pivot = None
3896
3897 if active_obj and (active_obj.mode != 'OBJECT'):
3898 if len(particles) == 0:
3899 pivot = active_obj.matrix_world.to_translation()
3900
3901 return csu.get_matrix(sys_name, self.offset, pivot)
3902
3903 def convert_to_abs(self, v3d, pos, warn=False, **kwargs):
3904 kwargs.pop("use_history", None)
3905 matrix = self.get_matrix(False, v3d, warn, **kwargs)
3906 if not matrix:
3907 return None
3908 return matrix * pos
3909
3910 def convert_from_abs(self, v3d, pos, warn=False, **kwargs):
3911 use_history = kwargs.pop("use_history", True)
3912 matrix = self.get_matrix(use_history, v3d, warn, **kwargs)
3913 if not matrix:
3914 return None
3915
3916 try:
3917 return matrix.inverted() * pos
3918 except:
3919 # this is some degenerate object
3920 return Vector()
3921
3922 def draw_bookmark(self, context):
3923 r = context.region
3924 rv3d = context.region_data
3925
3926 bookmark = self.bookmarks.get_item()
3927 if not bookmark:
3928 return
3929
3930 pos = self.convert_to_abs(context.space_data, bookmark.pos)
3931 if pos is None:
3932 return
3933
3934 projected = location_3d_to_region_2d(r, rv3d, pos)
3935
3936 if projected:
3937 # Store previous OpenGL settings
3938 smooth_prev = gl_get(bgl.GL_SMOOTH)
3939
3940 pixelsize = 1
3941 dpi = context.user_preferences.system.dpi
3942 widget_unit = (pixelsize * dpi * 20.0 + 36.0) / 72.0
3943
3944 bgl.glShadeModel(bgl.GL_SMOOTH)
3945 bgl.glLineWidth(2)
3946 bgl.glColor4f(0.0, 1.0, 0.0, 1.0)
3947 bgl.glBegin(bgl.GL_LINE_STRIP)
3948 radius = widget_unit * 0.3 #6
3949 n = 8
3950 da = 2 * math.pi / n
3951 x, y = projected
3952 x, y = int(x), int(y)
3953 for i in range(n + 1):
3954 a = i * da
3955 dx = math.sin(a) * radius
3956 dy = math.cos(a) * radius
3957 if (i % 2) == 0:
3958 bgl.glColor4f(0.0, 1.0, 0.0, 1.0)
3959 else:
3960 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
3961 bgl.glVertex2i(x + int(dx), y + int(dy))
3962 bgl.glEnd()
3963
3964 # Restore previous OpenGL settings
3965 gl_enable(bgl.GL_SMOOTH, smooth_prev)
3966
3967class BookmarkLibraryIDBlock(PseudoIDBlockBase):
3968 # Somewhy instance members aren't seen in update()
3969 # callbacks... but class members are.
3970 _self_update = [0]
3971 _dont_rename = [False]
3972
3973 data_name = "Bookmark Library"
3974 op_new = "scene.cursor_3d_new_bookmark_library"
3975 op_delete = "scene.cursor_3d_delete_bookmark_library"
3976 icon = 'BOOKMARKS'
3977
3978 collection, active, enum = PseudoIDBlockBase.create_props(
3979 BookmarkLibraryProp, "Bookmark Library")
3980
3981 def on_item_select(self):
3982 library = self.get_item()
3983 library.bookmarks.update_enum()
3984
3985class NewCursor3DBookmarkLibrary(bpy.types.Operator):
3986 bl_idname = "scene.cursor_3d_new_bookmark_library"
3987 bl_label = "New Library"
3988 bl_description = "Add a new bookmark library"
3989
3990 name = bpy.props.StringProperty(
3991 name="Name",
3992 description="Name of the new library",
3993 default="Lib")
3994
3995 def execute(self, context):
3996 settings = find_settings()
3997
3998 settings.libraries.add(name=self.name)
3999
4000 return {'FINISHED'}
4001
4002class DeleteCursor3DBookmarkLibrary(bpy.types.Operator):
4003 bl_idname = "scene.cursor_3d_delete_bookmark_library"
4004 bl_label = "Delete Library"
4005 bl_description = "Delete active bookmark library"
4006
4007 def execute(self, context):
4008 settings = find_settings()
4009
4010 name = settings.libraries.active
4011
4012 settings.libraries.remove(key=name)
4013
4014 return {'FINISHED'}
4015
4016# ===== MAIN PROPERTIES ===== #
4017# TODO: ~a bug? Somewhy tooltip shows "Cursor3DToolsSettings.foo"
4018# instead of "bpy.types.Screen.cursor_3d_tools_settings.foo"
4019class Cursor3DToolsSettings(bpy.types.PropertyGroup):
4020 transform_options = bpy.props.PointerProperty(
4021 type=TransformExtraOptionsProp,
4022 options={'HIDDEN'})
4023
4024 cursor_visible = bpy.props.BoolProperty(
4025 name="Cursor visibility",
4026 description="Show/hide cursor. When hidden, "\
4027"Blender continuously redraws itself (eats CPU like crazy, "\
4028"and becomes the less responsive the more complex scene you have)!",
4029 default=True)
4030
4031 cursor_lock = bpy.props.BoolProperty(
4032 name="Lock cursor location",
4033 description="Prevent accidental cursor movement",
4034 default=False)
4035
4036 draw_guides = bpy.props.BoolProperty(
4037 name="Guides",
4038 description="Display guides",
4039 default=True)
4040
4041 draw_snap_elements = bpy.props.BoolProperty(
4042 name="Snap elements",
4043 description="Display snap elements",
4044 default=True)
4045
4046 draw_N = bpy.props.BoolProperty(
4047 name="Surface normal",
4048 description="Display surface normal",
4049 default=True)
4050
4051 draw_T1 = bpy.props.BoolProperty(
4052 name="Surface 1st tangential",
4053 description="Display 1st surface tangential",
4054 default=True)
4055
4056 draw_T2 = bpy.props.BoolProperty(
4057 name="Surface 2nd tangential",
4058 description="Display 2nd surface tangential",
4059 default=True)
4060
4061 stick_to_obj = bpy.props.BoolProperty(
4062 name="Stick to objects",
4063 description="Move cursor along with object it was snapped to",
4064 default=True)
4065
4066 # HISTORY-RELATED
4067 history = bpy.props.PointerProperty(
4068 type=CursorHistoryProp,
4069 options={'HIDDEN'})
4070
4071 # BOOKMARK-RELATED
4072 libraries = bpy.props.PointerProperty(
4073 type=BookmarkLibraryIDBlock,
4074 options={'HIDDEN'})
4075
4076 show_bookmarks = bpy.props.BoolProperty(
4077 name="Show bookmarks",
4078 description="Show active bookmark in 3D view",
4079 default=True,
4080 options={'HIDDEN'})
4081
4082 free_coord_precision = bpy.props.IntProperty(
4083 name="Coord precision",
4084 description="Numer of digits afer comma "\
4085 "for displayed coordinate values",
4086 default=4,
4087 min=0,
4088 max=10,
4089 options={'HIDDEN'})
4090
4091 auto_register_keymaps = bpy.props.BoolProperty(
4092 name="Auto Register Keymaps",
4093 default=True)
4094
4095class Cursor3DToolsSceneSettings(bpy.types.PropertyGroup):
4096 stick_obj_name = bpy.props.StringProperty(
4097 name="Stick-to-object name",
4098 description="Name of the object to stick cursor to",
4099 options={'HIDDEN'})
4100 stick_obj_pos = bpy.props.FloatVectorProperty(
4101 default=(0.0, 0.0, 0.0),
4102 options={'HIDDEN'},
4103 subtype='XYZ')
4104
4105# ===== CURSOR RUNTIME PROPERTIES ===== #
4106class CursorRuntimeSettings(bpy.types.PropertyGroup):
4107 current_monitor_id = bpy.props.IntProperty(
4108 default=0,
4109 options={'HIDDEN'})
4110
4111 surface_pos = bpy.props.FloatVectorProperty(
4112 default=(0.0, 0.0, 0.0),
4113 options={'HIDDEN'},
4114 subtype='XYZ')
4115
4116 use_cursor_monitor = bpy.props.BoolProperty(
4117 name="Enable Cursor Monitor",
4118 description="Record 3D cursor history "\
4119 "(uses a background modal operator)",
4120 default=True)
4121
4122class CursorDynamicSettings:
4123 local_matrix = Matrix()
4124
4125 active_transform_operator = None
4126
4127 csu = None
4128
4129 active_scene_hash = 0
4130
4131 @classmethod
4132 def recalc_csu(cls, context, event_value=None):
4133 scene_hash_changed = (cls.active_scene_hash != hash(context.scene))
4134 cls.active_scene_hash = hash(context.scene)
4135
4136 # Don't recalc if mouse is over some UI panel!
4137 # (otherwise, this may lead to applying operator
4138 # (e.g. Subdivide) in Edit Mode, even if user
4139 # just wants to change some operator setting)
4140 clicked = (event_value in {'PRESS', 'RELEASE'}) and \
4141 (context.region.type == 'WINDOW')
4142
4143 if clicked or scene_hash_changed:
4144 particles, cls.csu = gather_particles()
4145
4146#============================================================================#
4147# ===== PANELS AND DIALOGS ===== #
4148class TransformExtraOptions(bpy.types.Panel):
4149 bl_label = "Transform Extra Options"
4150 bl_idname = "OBJECT_PT_transform_extra_options"
4151 bl_space_type = "VIEW_3D"
4152 bl_region_type = "UI"
4153 #bl_context = "object"
4154 bl_options = {'DEFAULT_CLOSED'}
4155
4156 def draw(self, context):
4157 layout = self.layout
4158
4159 settings = find_settings()
4160 tfm_opts = settings.transform_options
4161
4162 layout.prop(tfm_opts, "use_relative_coords")
4163 layout.prop(tfm_opts, "snap_only_to_solid")
4164 layout.prop(tfm_opts, "snap_interpolate_normals_mode", text="")
4165 layout.prop(tfm_opts, "use_comma_separator")
4166 #layout.prop(tfm_opts, "snap_element_screen_size")
4167
4168class Cursor3DTools(bpy.types.Panel):
4169 bl_label = "3D Cursor Tools"
4170 bl_idname = "OBJECT_PT_cursor_3d_tools"
4171 bl_space_type = "VIEW_3D"
4172 bl_region_type = "UI"
4173 bl_options = {'DEFAULT_CLOSED'}
4174
4175 def draw(self, context):
4176 layout = self.layout
4177
4178 # Attempt to launch the monitor
4179 if bpy.ops.view3d.cursor3d_monitor.poll():
4180 bpy.ops.view3d.cursor3d_monitor()
4181 #=============================================#
4182
4183 wm = context.window_manager
4184 settings = find_settings()
4185
4186 row = layout.split(0.5)
4187 #row = layout.row()
4188 row.operator("view3d.set_cursor3d_dialog",
4189 "Set", 'CURSOR')
4190 row = row.split(1 / 3, align=True)
4191 #row = row.row(align=True)
4192 row.prop(settings, "draw_guides",
4193 text="", icon='MANIPUL', toggle=True)
4194 row.prop(settings, "draw_snap_elements",
4195 text="", icon='EDITMODE_HLT', toggle=True)
4196 row.prop(settings, "stick_to_obj",
4197 text="", icon='SNAP_ON', toggle=True)
4198
4199 row = layout.split(0.5)
4200 subrow = row.split(0.5)
4201 subrow.prop(settings, "cursor_lock", text="", toggle=True,
4202 icon=('LOCKED' if settings.cursor_lock else 'UNLOCKED'))
4203 subrow = subrow.split(1)
4204 subrow.alert = True
4205 subrow.prop(settings, "cursor_visible", text="", toggle=True,
4206 icon=('RESTRICT_VIEW_OFF' if settings.cursor_visible
4207 else 'RESTRICT_VIEW_ON'))
4208 row = row.split(1 / 3, align=True)
4209 row.prop(settings, "draw_N",
4210 text="N", toggle=True, index=0)
4211 row.prop(settings, "draw_T1",
4212 text="T1", toggle=True, index=1)
4213 row.prop(settings, "draw_T2",
4214 text="T2", toggle=True, index=2)
4215
4216 # === HISTORY === #
4217 history = settings.history
4218 row = layout.row(align=True)
4219 row.prop(wm.cursor_3d_runtime_settings, "use_cursor_monitor",
4220 text="", toggle=True, icon='REC')
4221 row.prop(history, "show_trace", text="", icon='SORTTIME')
4222 row = row.split(0.35, True)
4223 row.prop(history, "max_size", text="")
4224 row.prop(history, "current_id", text="")
4225
4226 # === BOOKMARK LIBRARIES === #
4227 settings.libraries.draw(context, layout)
4228
4229 library = settings.libraries.get_item()
4230
4231 if library is None:
4232 return
4233
4234 row = layout.row()
4235 row.prop(settings, "show_bookmarks",
4236 text="", icon='RESTRICT_VIEW_OFF')
4237 row = row.row(align=True)
4238 row.prop(library, "system", text="")
4239 row.prop(library, "offset", text="",
4240 icon='ARROW_LEFTRIGHT')
4241
4242 # === BOOKMARKS === #
4243 library.bookmarks.draw(context, layout)
4244
4245 if len(library.bookmarks.collection) == 0:
4246 return
4247
4248 row = layout.row()
4249 row = row.split(align=True)
4250 # PASTEDOWN
4251 # COPYDOWN
4252 row.operator("scene.cursor_3d_overwrite_bookmark",
4253 text="", icon='REC')
4254 row.operator("scene.cursor_3d_swap_bookmark",
4255 text="", icon='FILE_REFRESH')
4256 row.operator("scene.cursor_3d_recall_bookmark",
4257 text="", icon='FILE_TICK')
4258 row.operator("scene.cursor_3d_add_empty_at_bookmark",
4259 text="", icon='EMPTY_DATA')
4260 # Not implemented (and maybe shouldn't)
4261 #row.operator("scene.cursor_3d_snap_selection_to_bookmark",
4262 # text="", icon='SNAP_ON')
4263
4264class SetCursorDialog(bpy.types.Operator):
4265 bl_idname = "view3d.set_cursor3d_dialog"
4266 bl_label = "Set 3D Cursor"
4267 bl_description = "Set 3D Cursor XYZ values"
4268
4269 pos = bpy.props.FloatVectorProperty(
4270 name="Location",
4271 description="3D Cursor location in current coordinate system",
4272 subtype='XYZ',
4273 )
4274
4275 @classmethod
4276 def poll(cls, context):
4277 return context.area.type == 'VIEW_3D'
4278
4279 def execute(self, context):
4280 scene = context.scene
4281
4282 # "current system" / "relative" could have changed
4283 self.matrix = self.csu.get_matrix()
4284
4285 pos = self.matrix * self.pos
4286 set_cursor_location(pos, v3d=context.space_data)
4287
4288 return {'FINISHED'}
4289
4290 def invoke(self, context, event):
4291 scene = context.scene
4292
4293 cursor_pos = get_cursor_location(v3d=context.space_data)
4294
4295 particles, self.csu = gather_particles(context=context)
4296 self.csu.source_pos = cursor_pos
4297
4298 self.matrix = self.csu.get_matrix()
4299
4300 try:
4301 self.pos = self.matrix.inverted() * cursor_pos
4302 except:
4303 # this is some degenerate system
4304 self.pos = Vector()
4305
4306 wm = context.window_manager
4307 return wm.invoke_props_dialog(self, width=160)
4308
4309 def draw(self, context):
4310 layout = self.layout
4311
4312 settings = find_settings()
4313 tfm_opts = settings.transform_options
4314
4315 v3d = context.space_data
4316
4317 col = layout.column()
4318 col.prop(self, "pos", text="")
4319
4320 row = layout.row()
4321 row.prop(tfm_opts, "use_relative_coords", text="Relative")
4322 row.prop(v3d, "transform_orientation", text="")
4323
4324# Adapted from Chromoly's lock_cursor3d
4325def selection_global_positions(context):
4326 if context.mode == 'EDIT_MESH':
4327 ob = context.active_object
4328 mat = ob.matrix_world
4329 bm = bmesh.from_edit_mesh(ob.data)
4330 verts = [v for v in bm.verts if v.select]
4331 return [mat * v.co for v in verts]
4332 elif context.mode == 'OBJECT':
4333 return [ob.matrix_world.to_translation()
4334 for ob in context.selected_objects]
4335
4336# Adapted from Chromoly's lock_cursor3d
4337def center_of_circumscribed_circle(vecs):
4338 if len(vecs) == 1:
4339 return vecs[0]
4340 elif len(vecs) == 2:
4341 return (vecs[0] + vecs[1]) / 2
4342 elif len(vecs) == 3:
4343 v1, v2, v3 = vecs
4344 if v1 != v2 and v2 != v3 and v3 != v1:
4345 v12 = v2 - v1
4346 v13 = v3 - v1
4347 med12 = (v1 + v2) / 2
4348 med13 = (v1 + v3) / 2
4349 per12 = v13 - v13.project(v12)
4350 per13 = v12 - v12.project(v13)
4351 inter = intersect_line_line(med12, med12 + per12,
4352 med13, med13 + per13)
4353 if inter:
4354 return (inter[0] + inter[1]) / 2
4355 return (v1 + v2 + v3) / 3
4356 return None
4357
4358def center_of_inscribed_circle(vecs):
4359 if len(vecs) == 1:
4360 return vecs[0]
4361 elif len(vecs) == 2:
4362 return (vecs[0] + vecs[1]) / 2
4363 elif len(vecs) == 3:
4364 v1, v2, v3 = vecs
4365 L1 = (v3 - v2).magnitude
4366 L2 = (v3 - v1).magnitude
4367 L3 = (v2 - v1).magnitude
4368 return (L1*v1 + L2*v2 + L3*v3) / (L1 + L2 + L3)
4369 return None
4370
4371# Adapted from Chromoly's lock_cursor3d
4372class SnapCursor_Circumscribed(bpy.types.Operator):
4373 bl_idname = "view3d.snap_cursor_to_circumscribed"
4374 bl_label = "Cursor to Circumscribed"
4375 bl_description = "Snap cursor to the center of the circumscribed circle"
4376
4377 def execute(self, context):
4378 vecs = selection_global_positions(context)
4379 if vecs is None:
4380 self.report({'WARNING'}, 'Not implemented \
4381 for %s mode' % context.mode)
4382 return {'CANCELLED'}
4383
4384 pos = center_of_circumscribed_circle(vecs)
4385 if pos is None:
4386 self.report({'WARNING'}, 'Select 3 objects/elements')
4387 return {'CANCELLED'}
4388
4389 set_cursor_location(pos, v3d=context.space_data)
4390
4391 return {'FINISHED'}
4392
4393class SnapCursor_Inscribed(bpy.types.Operator):
4394 bl_idname = "view3d.snap_cursor_to_inscribed"
4395 bl_label = "Cursor to Inscribed"
4396 bl_description = "Snap cursor to the center of the inscribed circle"
4397
4398 def execute(self, context):
4399 vecs = selection_global_positions(context)
4400 if vecs is None:
4401 self.report({'WARNING'}, 'Not implemented \
4402 for %s mode' % context.mode)
4403 return {'CANCELLED'}
4404
4405 pos = center_of_inscribed_circle(vecs)
4406 if pos is None:
4407 self.report({'WARNING'}, 'Select 3 objects/elements')
4408 return {'CANCELLED'}
4409
4410 set_cursor_location(pos, v3d=context.space_data)
4411
4412 return {'FINISHED'}
4413
4414class AlignOrientationProperties(bpy.types.PropertyGroup):
4415 axes_items = [
4416 ('X', 'X', 'X axis'),
4417 ('Y', 'Y', 'Y axis'),
4418 ('Z', 'Z', 'Z axis'),
4419 ('-X', '-X', '-X axis'),
4420 ('-Y', '-Y', '-Y axis'),
4421 ('-Z', '-Z', '-Z axis'),
4422 ]
4423
4424 axes_items_ = [
4425 ('X', 'X', 'X axis'),
4426 ('Y', 'Y', 'Y axis'),
4427 ('Z', 'Z', 'Z axis'),
4428 (' ', ' ', 'Same as source axis'),
4429 ]
4430
4431 def get_orients(self, context):
4432 orients = []
4433 orients.append(('GLOBAL', "Global", ""))
4434 orients.append(('LOCAL', "Local", ""))
4435 orients.append(('GIMBAL', "Gimbal", ""))
4436 orients.append(('NORMAL', "Normal", ""))
4437 orients.append(('VIEW', "View", ""))
4438
4439 if context is not None:
4440 for orientation in context.scene.orientations:
4441 name = orientation.name
4442 orients.append((name, name, ""))
4443
4444 return orients
4445
4446 src_axis = bpy.props.EnumProperty(default='Z', items=axes_items,
4447 name="Initial axis")
4448 #src_orient = bpy.props.EnumProperty(default='GLOBAL', items=get_orients)
4449
4450 dest_axis = bpy.props.EnumProperty(default=' ', items=axes_items_,
4451 name="Final axis")
4452 dest_orient = bpy.props.EnumProperty(items=get_orients,
4453 name="Final orientation")
4454
4455class AlignOrientation(bpy.types.Operator):
4456 bl_idname = "view3d.align_orientation"
4457 bl_label = "Align Orientation"
4458 bl_description = "Rotates active object to match axis of current "\
4459 "orientation to axis of another orientation"
4460 bl_options = {'REGISTER', 'UNDO'}
4461
4462 axes_items = [
4463 ('X', 'X', 'X axis'),
4464 ('Y', 'Y', 'Y axis'),
4465 ('Z', 'Z', 'Z axis'),
4466 ('-X', '-X', '-X axis'),
4467 ('-Y', '-Y', '-Y axis'),
4468 ('-Z', '-Z', '-Z axis'),
4469 ]
4470
4471 axes_items_ = [
4472 ('X', 'X', 'X axis'),
4473 ('Y', 'Y', 'Y axis'),
4474 ('Z', 'Z', 'Z axis'),
4475 (' ', ' ', 'Same as source axis'),
4476 ]
4477
4478 axes_ids = {'X':0, 'Y':1, 'Z':2}
4479
4480 def get_orients(self, context):
4481 orients = []
4482 orients.append(('GLOBAL', "Global", ""))
4483 orients.append(('LOCAL', "Local", ""))
4484 orients.append(('GIMBAL', "Gimbal", ""))
4485 orients.append(('NORMAL', "Normal", ""))
4486 orients.append(('VIEW', "View", ""))
4487
4488 if context is not None:
4489 for orientation in context.scene.orientations:
4490 name = orientation.name
4491 orients.append((name, name, ""))
4492
4493 return orients
4494
4495 src_axis = bpy.props.EnumProperty(default='Z', items=axes_items,
4496 name="Initial axis")
4497 #src_orient = bpy.props.EnumProperty(default='GLOBAL', items=get_orients)
4498
4499 dest_axis = bpy.props.EnumProperty(default=' ', items=axes_items_,
4500 name="Final axis")
4501 dest_orient = bpy.props.EnumProperty(items=get_orients,
4502 name="Final orientation")
4503
4504 @classmethod
4505 def poll(cls, context):
4506 return (context.area.type == 'VIEW_3D') and context.object
4507
4508 def execute(self, context):
4509 wm = context.window_manager
4510 obj = context.object
4511 scene = context.scene
4512 v3d = context.space_data
4513 rv3d = context.region_data
4514
4515 particles, csu = gather_particles(context=context)
4516 tou = csu.tou
4517 #tou = TransformOrientationUtility(scene, v3d, rv3d)
4518
4519 aop = wm.align_orientation_properties # self
4520
4521 src_matrix = tou.get_matrix()
4522 src_axes = MatrixDecompose(src_matrix)
4523 src_axis_name = aop.src_axis
4524 if src_axis_name.startswith("-"):
4525 src_axis_name = src_axis_name[1:]
4526 src_axis = -src_axes[self.axes_ids[src_axis_name]]
4527 else:
4528 src_axis = src_axes[self.axes_ids[src_axis_name]]
4529
4530 tou.set(aop.dest_orient, False)
4531 dest_matrix = tou.get_matrix()
4532 dest_axes = MatrixDecompose(dest_matrix)
4533 if self.dest_axis != ' ':
4534 dest_axis_name = aop.dest_axis
4535 else:
4536 dest_axis_name = src_axis_name
4537 dest_axis = dest_axes[self.axes_ids[dest_axis_name]]
4538
4539 q = src_axis.rotation_difference(dest_axis)
4540
4541 m = obj.matrix_world.to_3x3()
4542 m.rotate(q)
4543 m.resize_4x4()
4544 m.translation = obj.matrix_world.translation.copy()
4545
4546 obj.matrix_world = m
4547
4548 #bpy.ops.ed.undo_push(message="Align Orientation")
4549
4550 return {'FINISHED'}
4551
4552 # ATTENTION!
4553 # This _must_ be a dialog, because with 'UNDO' option
4554 # the last selected orientation may revert to the previous state
4555 def invoke(self, context, event):
4556 wm = context.window_manager
4557 return wm.invoke_props_dialog(self, width=200)
4558
4559 def draw(self, context):
4560 layout = self.layout
4561 wm = context.window_manager
4562 aop = wm.align_orientation_properties # self
4563 layout.prop(aop, "src_axis")
4564 layout.prop(aop, "dest_axis")
4565 layout.prop(aop, "dest_orient")
4566
4567class CopyOrientation(bpy.types.Operator):
4568 bl_idname = "view3d.copy_orientation"
4569 bl_label = "Copy Orientation"
4570 bl_description = "Makes a copy of current orientation"
4571
4572 def execute(self, context):
4573 scene = context.scene
4574 v3d = context.space_data
4575 rv3d = context.region_data
4576
4577 particles, csu = gather_particles(context=context)
4578 tou = csu.tou
4579 #tou = TransformOrientationUtility(scene, v3d, rv3d)
4580
4581 orient = create_transform_orientation(scene,
4582 name=tou.get()+".copy", matrix=tou.get_matrix())
4583
4584 tou.set(orient.name)
4585
4586 return {'FINISHED'}
4587
4588def transform_orientations_panel_extension(self, context):
4589 row = self.layout.row()
4590 row.operator("view3d.align_orientation", text="Align")
4591 row.operator("view3d.copy_orientation", text="Copy")
4592
4593# ===== CURSOR MONITOR ===== #
4594class CursorMonitor(bpy.types.Operator):
4595 """Monitor changes in cursor location and write to history"""
4596 bl_idname = "view3d.cursor3d_monitor"
4597 bl_label = "Cursor Monitor"
4598
4599 # A class-level variable (it must be accessed from poll())
4600 is_running = False
4601
4602 storage = {}
4603
4604 _handle_view = None
4605 _handle_px = None
4606 _handle_header_px = None
4607
4608 script_reload_kmis = []
4609
4610 @staticmethod
4611 def handle_add(self, context):
4612 CursorMonitor._handle_view = bpy.types.SpaceView3D.draw_handler_add(
4613 draw_callback_view, (self, context), 'WINDOW', 'POST_VIEW')
4614 CursorMonitor._handle_px = bpy.types.SpaceView3D.draw_handler_add(
4615 draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL')
4616 CursorMonitor._handle_header_px = bpy.types.SpaceView3D.draw_handler_add(
4617 draw_callback_header_px, (self, context), 'HEADER', 'POST_PIXEL')
4618
4619 @staticmethod
4620 def handle_remove(context):
4621 if CursorMonitor._handle_view is not None:
4622 bpy.types.SpaceView3D.draw_handler_remove(CursorMonitor._handle_view, 'WINDOW')
4623 if CursorMonitor._handle_px is not None:
4624 bpy.types.SpaceView3D.draw_handler_remove(CursorMonitor._handle_px, 'WINDOW')
4625 if CursorMonitor._handle_header_px is not None:
4626 bpy.types.SpaceView3D.draw_handler_remove(CursorMonitor._handle_header_px, 'HEADER')
4627 CursorMonitor._handle_view = None
4628 CursorMonitor._handle_px = None
4629 CursorMonitor._handle_header_px = None
4630
4631 @classmethod
4632 def poll(cls, context):
4633 try:
4634 wm = context.window_manager
4635 if not wm.cursor_3d_runtime_settings.use_cursor_monitor:
4636 return False
4637
4638 runtime_settings = find_runtime_settings()
4639 if not runtime_settings:
4640 return False
4641
4642 # When addon is enabled by default and
4643 # user started another new scene, is_running
4644 # would still be True
4645 return (not CursorMonitor.is_running) or \
4646 (runtime_settings.current_monitor_id == 0)
4647 except Exception as e:
4648 print("Cursor monitor exeption in poll:\n" + repr(e))
4649 return False
4650
4651 def modal(self, context, event):
4652 wm = context.window_manager
4653 if not wm.cursor_3d_runtime_settings.use_cursor_monitor:
4654 self.cancel(context)
4655 return {'CANCELLED'}
4656
4657 # Scripts cannot be reloaded while modal operators are running
4658 # Intercept the corresponding event and shut down CursorMonitor
4659 # (it would be relaunched automatically afterwards)
4660 for kmi in CursorMonitor.script_reload_kmis:
4661 if IsKeyMapItemEvent(kmi, event):
4662 self.cancel(context)
4663 return {'CANCELLED'}
4664
4665 try:
4666 return self._modal(context, event)
4667 except Exception as e:
4668 print("Cursor monitor exeption in modal:\n" + repr(e))
4669 # Remove callbacks at any cost
4670 self.cancel(context)
4671 #raise
4672 return {'CANCELLED'}
4673
4674 def _modal(self, context, event):
4675 runtime_settings = find_runtime_settings()
4676
4677 # ATTENTION: will this work correctly when another
4678 # blend is loaded? (it should, since all scripts
4679 # seem to be reloaded in such case)
4680 if (runtime_settings is None) or \
4681 (self.id != runtime_settings.current_monitor_id):
4682 # Another (newer) monitor was launched;
4683 # this one should stop.
4684 # (OR addon was disabled)
4685 self.cancel(context)
4686 return {'CANCELLED'}
4687
4688 # Somewhy after addon re-registration
4689 # this permanently becomes False
4690 CursorMonitor.is_running = True
4691
4692 if self.update_storage(runtime_settings):
4693 # hmm... can this cause flickering of menus?
4694 context.area.tag_redraw()
4695
4696 settings = find_settings()
4697
4698 propagate_settings_to_all_screens(settings)
4699
4700 # ================== #
4701 # Update bookmark enums when addon is initialized.
4702 # Since CursorMonitor operator can be called from draw(),
4703 # we have to postpone all re-registration-related tasks
4704 # (such as redefining the enums).
4705 if self.just_initialized:
4706 # update the relevant enums, bounds and other options
4707 # (is_running becomes False once another scene is loaded,
4708 # so this operator gets restarted)
4709 settings.history.update_max_size()
4710 settings.libraries.update_enum()
4711 library = settings.libraries.get_item()
4712 if library:
4713 library.bookmarks.update_enum()
4714
4715 self.just_initialized = False
4716 # ================== #
4717
4718 # Seems like recalc_csu() in this place causes trouble
4719 # if space type is switched from 3D to e.g. UV
4720 '''
4721 tfm_operator = CursorDynamicSettings.active_transform_operator
4722 if tfm_operator:
4723 CursorDynamicSettings.csu = tfm_operator.csu
4724 else:
4725 CursorDynamicSettings.recalc_csu(context, event.value)
4726 '''
4727
4728 return {'PASS_THROUGH'}
4729
4730 def update_storage(self, runtime_settings):
4731 if CursorDynamicSettings.active_transform_operator:
4732 # Don't add to history while operator is running
4733 return False
4734
4735 new_pos = None
4736
4737 last_locations = {}
4738
4739 for scene in bpy.data.scenes:
4740 # History doesn't depend on view (?)
4741 curr_pos = get_cursor_location(scene=scene)
4742
4743 last_locations[scene.name] = curr_pos
4744
4745 # Ignore newly-created or some renamed scenes
4746 if scene.name in self.last_locations:
4747 if curr_pos != self.last_locations[scene.name]:
4748 new_pos = curr_pos
4749 elif runtime_settings.current_monitor_id == 0:
4750 # startup location should be added
4751 new_pos = curr_pos
4752
4753 # Seems like scene.cursor_location is fast enough here
4754 # -> no need to resort to v3d.cursor_location.
4755 """
4756 screen = bpy.context.screen
4757 scene = screen.scene
4758 v3d = None
4759 for area in screen.areas:
4760 for space in area.spaces:
4761 if space.type == 'VIEW_3D':
4762 v3d = space
4763 break
4764
4765 if v3d is not None:
4766 curr_pos = get_cursor_location(v3d=v3d)
4767
4768 last_locations[scene.name] = curr_pos
4769
4770 # Ignore newly-created or some renamed scenes
4771 if scene.name in self.last_locations:
4772 if curr_pos != self.last_locations[scene.name]:
4773 new_pos = curr_pos
4774 """
4775
4776 self.last_locations = last_locations
4777
4778 if new_pos is not None:
4779 settings = find_settings()
4780 history = settings.history
4781
4782 pos = history.get_pos()
4783 if (pos is not None):# and (history.current_id != 0): # ?
4784 if pos == new_pos:
4785 return False # self.just_initialized ?
4786
4787 entry = history.entries.add()
4788 entry.pos = new_pos
4789
4790 last_id = len(history.entries) - 1
4791 history.entries.move(last_id, 0)
4792
4793 if last_id > int(history.max_size):
4794 history.entries.remove(last_id)
4795
4796 # make sure the most recent history entry is displayed
4797
4798 CursorHistoryProp.update_cursor_on_id_change = False
4799 history.current_id = 0
4800 CursorHistoryProp.update_cursor_on_id_change = True
4801
4802 history.curr_id = history.current_id
4803 history.last_id = 1
4804
4805 return True
4806
4807 return False # self.just_initialized ?
4808
4809 def execute(self, context):
4810 print("Cursor monitor: launched")
4811
4812 CursorMonitor.script_reload_kmis = list(KeyMapItemSearch('script.reload'))
4813
4814 runtime_settings = find_runtime_settings()
4815
4816 self.just_initialized = True
4817
4818 self.id = 0
4819
4820 self.last_locations = {}
4821
4822 # Important! Call update_storage() before assigning
4823 # current_monitor_id (used to add startup cursor location)
4824 self.update_storage(runtime_settings)
4825
4826 # Indicate that this is the most recent monitor.
4827 # All others should shut down.
4828 self.id = runtime_settings.current_monitor_id + 1
4829 runtime_settings.current_monitor_id = self.id
4830
4831 CursorMonitor.is_running = True
4832
4833 CursorDynamicSettings.recalc_csu(context, 'PRESS')
4834
4835 # I suppose that cursor position would change
4836 # only with user interaction.
4837 #self._timer = context.window_manager. \
4838 # event_timer_add(0.1, context.window)
4839
4840 CursorMonitor.handle_add(self, context)
4841
4842 # Here we cannot return 'PASS_THROUGH',
4843 # or Blender will crash!
4844
4845 # Currently there seems to be only one window
4846 context.window_manager.modal_handler_add(self)
4847 return {'RUNNING_MODAL'}
4848
4849 def cancel(self, context):
4850 CursorMonitor.is_running = False
4851 #type(self).is_running = False
4852
4853 # Unregister callbacks...
4854 CursorMonitor.handle_remove(context)
4855
4856
4857# ===== MATH / GEOMETRY UTILITIES ===== #
4858def to_matrix4x4(orient, pos):
4859 if not isinstance(orient, Matrix):
4860 orient = orient.to_matrix()
4861 m = orient.to_4x4()
4862 m.translation = pos.to_3d()
4863 return m
4864
4865def MatrixCompose(*args):
4866 size = len(args)
4867 m = Matrix.Identity(size)
4868 axes = m.col # m.row
4869
4870 if size == 2:
4871 for i in (0, 1):
4872 c = args[i]
4873 if isinstance(c, Vector):
4874 axes[i] = c.to_2d()
4875 elif hasattr(c, "__iter__"):
4876 axes[i] = Vector(c).to_2d()
4877 else:
4878 axes[i][i] = c
4879 else:
4880 for i in (0, 1, 2):
4881 c = args[i]
4882 if isinstance(c, Vector):
4883 axes[i][:3] = c.to_3d()
4884 elif hasattr(c, "__iter__"):
4885 axes[i][:3] = Vector(c).to_3d()
4886 else:
4887 axes[i][i] = c
4888
4889 if size == 4:
4890 c = args[3]
4891 if isinstance(c, Vector):
4892 m.translation = c.to_3d()
4893 elif hasattr(c, "__iter__"):
4894 m.translation = Vector(c).to_3d()
4895
4896 return m
4897
4898def MatrixDecompose(m, res_size=None):
4899 size = len(m)
4900 axes = m.col # m.row
4901 if res_size is None:
4902 res_size = size
4903
4904 if res_size == 2:
4905 return (axes[0].to_2d(), axes[1].to_2d())
4906 else:
4907 x = axes[0].to_3d()
4908 y = axes[1].to_3d()
4909 z = (axes[2].to_3d() if size > 2 else Vector())
4910 if res_size == 3:
4911 return (x, y, z)
4912
4913 t = (m.translation.to_3d() if size == 4 else Vector())
4914 if res_size == 4:
4915 return (x, y, z, t)
4916
4917def angle_axis_to_quat(angle, axis):
4918 w = math.cos(angle / 2.0)
4919 xyz = axis.normalized() * math.sin(angle / 2.0)
4920 return Quaternion((w, xyz.x, xyz.y, xyz.z))
4921
4922def round_step(x, s=1.0):
4923 #return math.floor(x * s + 0.5) / s
4924 return math.floor(x / s + 0.5) * s
4925
4926twoPi = 2.0 * math.pi
4927def clamp_angle(ang):
4928 # Attention! In Python the behaviour is:
4929 # -359.0 % 180.0 == 1.0
4930 # -359.0 % -180.0 == -179.0
4931 ang = (ang % twoPi)
4932 return ((ang - twoPi) if (ang > math.pi) else ang)
4933
4934def prepare_grid_mesh(bm, nx=1, ny=1, sx=1.0, sy=1.0,
4935 z=0.0, xyz_indices=(0,1,2)):
4936 vertices = []
4937 for i in range(nx + 1):
4938 x = 2 * (i / nx) - 1
4939 x *= sx
4940 for j in range(ny + 1):
4941 y = 2 * (j / ny) - 1
4942 y *= sy
4943 pos = (x, y, z)
4944 vert = bm.verts.new((pos[xyz_indices[0]],
4945 pos[xyz_indices[1]],
4946 pos[xyz_indices[2]]))
4947 vertices.append(vert)
4948
4949 nxmax = nx + 1
4950 for i in range(nx):
4951 i1 = i + 1
4952 for j in range(ny):
4953 j1 = j + 1
4954 verts = [vertices[j + i * nxmax],
4955 vertices[j1 + i * nxmax],
4956 vertices[j1 + i1 * nxmax],
4957 vertices[j + i1 * nxmax]]
4958 bm.faces.new(verts)
4959 #return
4960
4961def prepare_gridbox_mesh(subdiv=1):
4962 bm = bmesh.new()
4963
4964 sides = [
4965 (-1, (0,1,2)), # -Z
4966 (1, (1,0,2)), # +Z
4967 (-1, (1,2,0)), # -Y
4968 (1, (0,2,1)), # +Y
4969 (-1, (2,0,1)), # -X
4970 (1, (2,1,0)), # +X
4971 ]
4972
4973 for side in sides:
4974 prepare_grid_mesh(bm, nx=subdiv, ny=subdiv,
4975 z=side[0], xyz_indices=side[1])
4976
4977 return bm
4978
4979# ===== DRAWING UTILITIES ===== #
4980class GfxCell:
4981 def __init__(self, w, h, color=None, alpha=None, draw=None):
4982 self.w = w
4983 self.h = h
4984
4985 self.color = (0, 0, 0, 1)
4986 self.set_color(color, alpha)
4987
4988 if draw:
4989 self.draw = draw
4990
4991 def set_color(self, color=None, alpha=None):
4992 if color is None:
4993 color = self.color
4994 if alpha is None:
4995 alpha = (color[3] if len(color) > 3 else self.color[3])
4996 self.color = Vector((color[0], color[1], color[2], alpha))
4997
4998 def prepare_draw(self, x, y, align=(0, 0)):
4999 if self.color[3] <= 0.0:
5000 return None
5001
5002 if (align[0] != 0) or (align[1] != 0):
5003 x -= self.w * align[0]
5004 y -= self.h * align[1]
5005
5006 x = int(math.floor(x + 0.5))
5007 y = int(math.floor(y + 0.5))
5008
5009 bgl.glColor4f(*self.color)
5010
5011 return x, y
5012
5013 def draw(self, x, y, align=(0, 0)):
5014 xy = self.prepare_draw(x, y, align)
5015 if not xy:
5016 return
5017
5018 draw_rect(xy[0], xy[1], w, h)
5019
5020class TextCell(GfxCell):
5021 font_id = 0
5022
5023 def __init__(self, text="", color=None, alpha=None, font_id=None):
5024 if font_id is None:
5025 font_id = TextCell.font_id
5026 self.font_id = font_id
5027
5028 self.set_text(text)
5029
5030 self.color = (0, 0, 0, 1)
5031 self.set_color(color, alpha)
5032
5033 def set_text(self, text):
5034 self.text = str(text)
5035 dims = blf.dimensions(self.font_id, self.text)
5036 self.w = dims[0]
5037 dims = blf.dimensions(self.font_id, "dp") # fontheight
5038 self.h = dims[1]
5039
5040 def draw(self, x, y, align=(0, 0)):
5041 xy = self.prepare_draw(x, y, align)
5042 if not xy:
5043 return
5044
5045 blf.position(self.font_id, xy[0], xy[1], 0)
5046 blf.draw(self.font_id, self.text)
5047
5048
5049def draw_text(x, y, value, font_id=0, align=(0, 0), font_height=None):
5050 value = str(value)
5051
5052 if (align[0] != 0) or (align[1] != 0):
5053 dims = blf.dimensions(font_id, value)
5054 if font_height is not None:
5055 dims = (dims[0], font_height)
5056 x -= dims[0] * align[0]
5057 y -= dims[1] * align[1]
5058
5059 x = int(math.floor(x + 0.5))
5060 y = int(math.floor(y + 0.5))
5061
5062 blf.position(font_id, x, y, 0)
5063 blf.draw(font_id, value)
5064
5065def draw_rect(x, y, w, h, margin=0, outline=False):
5066 if w < 0:
5067 x += w
5068 w = abs(w)
5069
5070 if h < 0:
5071 y += h
5072 h = abs(h)
5073
5074 x = int(x)
5075 y = int(y)
5076 w = int(w)
5077 h = int(h)
5078 margin = int(margin)
5079
5080 if outline:
5081 bgl.glBegin(bgl.GL_LINE_LOOP)
5082 else:
5083 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
5084 bgl.glVertex2i(x - margin, y - margin)
5085 bgl.glVertex2i(x + w + margin, y - margin)
5086 bgl.glVertex2i(x + w + margin, y + h + margin)
5087 bgl.glVertex2i(x - margin, y + h + margin)
5088 bgl.glEnd()
5089
5090def append_round_rect(verts, x, y, w, h, rw, rh=None):
5091 if rh is None:
5092 rh = rw
5093
5094 if w < 0:
5095 x += w
5096 w = abs(w)
5097
5098 if h < 0:
5099 y += h
5100 h = abs(h)
5101
5102 if rw < 0:
5103 rw = min(abs(rw), w * 0.5)
5104 x += rw
5105 w -= rw * 2
5106
5107 if rh < 0:
5108 rh = min(abs(rh), h * 0.5)
5109 y += rh
5110 h -= rh * 2
5111
5112 n = int(max(rw, rh) * math.pi / 2.0)
5113
5114 a0 = 0.0
5115 a1 = math.pi / 2.0
5116 append_oval_segment(verts, x + w, y + h, rw, rh, a0, a1, n)
5117
5118 a0 = math.pi / 2.0
5119 a1 = math.pi
5120 append_oval_segment(verts, x + w, y, rw, rh, a0, a1, n)
5121
5122 a0 = math.pi
5123 a1 = 3.0 * math.pi / 2.0
5124 append_oval_segment(verts, x, y, rw, rh, a0, a1, n)
5125
5126 a0 = 3.0 * math.pi / 2.0
5127 a1 = math.pi * 2.0
5128 append_oval_segment(verts, x, y + h, rw, rh, a0, a1, n)
5129
5130def append_oval_segment(verts, x, y, rw, rh, a0, a1, n, skip_last=False):
5131 nmax = n - 1
5132 da = a1 - a0
5133 for i in range(n - int(skip_last)):
5134 a = a0 + da * (i / nmax)
5135 dx = math.sin(a) * rw
5136 dy = math.cos(a) * rh
5137 verts.append((x + int(dx), y + int(dy)))
5138
5139def draw_line(p0, p1, c=None):
5140 if c is not None:
5141 bgl.glColor4f(c[0], c[1], c[2], \
5142 (c[3] if len(c) > 3 else 1.0))
5143 bgl.glBegin(bgl.GL_LINE_STRIP)
5144 bgl.glVertex3f(p0[0], p0[1], p0[2])
5145 bgl.glVertex3f(p1[0], p1[1], p1[2])
5146 bgl.glEnd()
5147
5148def draw_line_2d(p0, p1, c=None):
5149 if c is not None:
5150 bgl.glColor4f(c[0], c[1], c[2], \
5151 (c[3] if len(c) > 3 else 1.0))
5152 bgl.glBegin(bgl.GL_LINE_STRIP)
5153 bgl.glVertex2f(p0[0], p0[1])
5154 bgl.glVertex2f(p1[0], p1[1])
5155 bgl.glEnd()
5156
5157def draw_line_hidden_depth(p0, p1, c, a0=1.0, a1=0.5, s0=None, s1=None):
5158 bgl.glEnable(bgl.GL_DEPTH_TEST)
5159 bgl.glColor4f(c[0], c[1], c[2], a0)
5160 if s0 is not None:
5161 gl_enable(bgl.GL_LINE_STIPPLE, int(bool(s0)))
5162 draw_line(p0, p1)
5163 bgl.glDisable(bgl.GL_DEPTH_TEST)
5164 if (a1 == a0) and (s1 == s0):
5165 return
5166 bgl.glColor4f(c[0], c[1], c[2], a1)
5167 if s1 is not None:
5168 gl_enable(bgl.GL_LINE_STIPPLE, int(bool(s1)))
5169 draw_line(p0, p1)
5170
5171def draw_arrow(p0, x, y, z, n_scl=0.2, ort_scl=0.035):
5172 p1 = p0 + z
5173
5174 bgl.glBegin(bgl.GL_LINE_STRIP)
5175 bgl.glVertex3f(p0[0], p0[1], p0[2])
5176 bgl.glVertex3f(p1[0], p1[1], p1[2])
5177 bgl.glEnd()
5178
5179 p2 = p1 - z * n_scl
5180 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
5181 bgl.glVertex3f(p1[0], p1[1], p1[2])
5182 p3 = p2 + (x + y) * ort_scl
5183 bgl.glVertex3f(p3[0], p3[1], p3[2])
5184 p3 = p2 + (-x + y) * ort_scl
5185 bgl.glVertex3f(p3[0], p3[1], p3[2])
5186 p3 = p2 + (-x - y) * ort_scl
5187 bgl.glVertex3f(p3[0], p3[1], p3[2])
5188 p3 = p2 + (x - y) * ort_scl
5189 bgl.glVertex3f(p3[0], p3[1], p3[2])
5190 p3 = p2 + (x + y) * ort_scl
5191 bgl.glVertex3f(p3[0], p3[1], p3[2])
5192 bgl.glEnd()
5193
5194def draw_arrow_2d(p0, n, L, arrow_len, arrow_width):
5195 p1 = p0 + n * L
5196 t = Vector((-n[1], n[0]))
5197 pA = p1 - n * arrow_len + t * arrow_width
5198 pB = p1 - n * arrow_len - t * arrow_width
5199
5200 bgl.glBegin(bgl.GL_LINES)
5201
5202 bgl.glVertex2f(p0[0], p0[1])
5203 bgl.glVertex2f(p1[0], p1[1])
5204
5205 bgl.glVertex2f(p1[0], p1[1])
5206 bgl.glVertex2f(pA[0], pA[1])
5207
5208 bgl.glVertex2f(p1[0], p1[1])
5209 bgl.glVertex2f(pB[0], pB[1])
5210
5211 bgl.glEnd()
5212
5213# Store/restore OpenGL settings and working with
5214# projection matrices -- inspired by space_view3d_panel_measure
5215# of Buerbaum Martin (Pontiac).
5216
5217# OpenGl helper functions/data
5218gl_state_info = {
5219 bgl.GL_MATRIX_MODE:(bgl.GL_INT, 1),
5220 bgl.GL_PROJECTION_MATRIX:(bgl.GL_DOUBLE, 16),
5221 bgl.GL_LINE_WIDTH:(bgl.GL_FLOAT, 1),
5222 bgl.GL_BLEND:(bgl.GL_BYTE, 1),
5223 bgl.GL_LINE_STIPPLE:(bgl.GL_BYTE, 1),
5224 bgl.GL_COLOR:(bgl.GL_FLOAT, 4),
5225 bgl.GL_SMOOTH:(bgl.GL_BYTE, 1),
5226 bgl.GL_DEPTH_TEST:(bgl.GL_BYTE, 1),
5227 bgl.GL_DEPTH_WRITEMASK:(bgl.GL_BYTE, 1),
5228}
5229gl_type_getters = {
5230 bgl.GL_INT:bgl.glGetIntegerv,
5231 bgl.GL_DOUBLE:bgl.glGetFloatv, # ?
5232 bgl.GL_FLOAT:bgl.glGetFloatv,
5233 #bgl.GL_BYTE:bgl.glGetFloatv, # Why GetFloat for getting byte???
5234 bgl.GL_BYTE:bgl.glGetBooleanv, # maybe like that?
5235}
5236
5237def gl_get(state_id):
5238 type, size = gl_state_info[state_id]
5239 buf = bgl.Buffer(type, [size])
5240 gl_type_getters[type](state_id, buf)
5241 return (buf if (len(buf) != 1) else buf[0])
5242
5243def gl_enable(state_id, enable):
5244 if enable:
5245 bgl.glEnable(state_id)
5246 else:
5247 bgl.glDisable(state_id)
5248
5249def gl_matrix_to_buffer(m):
5250 tempMat = [m[i][j] for i in range(4) for j in range(4)]
5251 return bgl.Buffer(bgl.GL_FLOAT, 16, tempMat)
5252
5253
5254# ===== DRAWING CALLBACKS ===== #
5255cursor_save_location = Vector()
5256
5257def draw_callback_view(self, context):
5258 global cursor_save_location
5259
5260 settings = find_settings()
5261 if settings is None:
5262 return
5263
5264 update_stick_to_obj(context)
5265
5266 if "EDIT" not in context.mode:
5267 # It's nice to have bookmark position update interactively
5268 # However, this still can be slow if there are many
5269 # selected objects
5270
5271 # ATTENTION!!!
5272 # This eats a lot of processor time!
5273 #CursorDynamicSettings.recalc_csu(context, 'PRESS')
5274 pass
5275
5276 history = settings.history
5277
5278 tfm_operator = CursorDynamicSettings.active_transform_operator
5279
5280 is_drawing = history.show_trace or tfm_operator
5281
5282 if is_drawing:
5283 # Store previous OpenGL settings
5284 MatrixMode_prev = gl_get(bgl.GL_MATRIX_MODE)
5285 ProjMatrix_prev = gl_get(bgl.GL_PROJECTION_MATRIX)
5286 lineWidth_prev = gl_get(bgl.GL_LINE_WIDTH)
5287 blend_prev = gl_get(bgl.GL_BLEND)
5288 line_stipple_prev = gl_get(bgl.GL_LINE_STIPPLE)
5289 color_prev = gl_get(bgl.GL_COLOR)
5290 smooth_prev = gl_get(bgl.GL_SMOOTH)
5291 depth_test_prev = gl_get(bgl.GL_DEPTH_TEST)
5292 depth_mask_prev = gl_get(bgl.GL_DEPTH_WRITEMASK)
5293
5294 if history.show_trace:
5295 bgl.glDepthRange(0.0, 0.9999)
5296
5297 history.draw_trace(context)
5298
5299 library = settings.libraries.get_item()
5300 if library and library.offset:
5301 history.draw_offset(context)
5302
5303 bgl.glDepthRange(0.0, 1.0)
5304
5305 if tfm_operator:
5306 tfm_operator.draw_3d(context)
5307
5308 if is_drawing:
5309 # Restore previous OpenGL settings
5310 bgl.glLineWidth(lineWidth_prev)
5311 gl_enable(bgl.GL_BLEND, blend_prev)
5312 gl_enable(bgl.GL_LINE_STIPPLE, line_stipple_prev)
5313 gl_enable(bgl.GL_SMOOTH, smooth_prev)
5314 gl_enable(bgl.GL_DEPTH_TEST, depth_test_prev)
5315 bgl.glDepthMask(depth_mask_prev)
5316 bgl.glColor4f(color_prev[0],
5317 color_prev[1],
5318 color_prev[2],
5319 color_prev[3])
5320
5321 cursor_save_location = Vector(context.space_data.cursor_location)
5322 if not settings.cursor_visible:
5323 # This is causing problems! See <https://developer.blender.org/T33197>
5324 #bpy.context.space_data.cursor_location = Vector([float('nan')] * 3)
5325
5326 region = context.region
5327 v3d = context.space_data
5328 rv3d = context.region_data
5329
5330 pixelsize = 1
5331 dpi = context.user_preferences.system.dpi
5332 widget_unit = (pixelsize * dpi * 20.0 + 36.0) / 72.0
5333
5334 cursor_w = widget_unit*2
5335 cursor_h = widget_unit*2
5336
5337 viewinv = rv3d.view_matrix.inverted()
5338 persinv = rv3d.perspective_matrix.inverted()
5339
5340 origin_start = viewinv.translation
5341 view_direction = viewinv.col[2].xyz#.normalized()
5342 depth_location = origin_start - view_direction
5343
5344 coord = (-cursor_w, -cursor_h)
5345 dx = (2.0 * coord[0] / region.width) - 1.0
5346 dy = (2.0 * coord[1] / region.height) - 1.0
5347 p = ((persinv.col[0].xyz * dx) +
5348 (persinv.col[1].xyz * dy) +
5349 depth_location)
5350
5351 context.space_data.cursor_location = p
5352
5353def draw_callback_header_px(self, context):
5354 r = context.region
5355
5356 tfm_operator = CursorDynamicSettings.active_transform_operator
5357 if not tfm_operator:
5358 return
5359
5360 smooth_prev = gl_get(bgl.GL_SMOOTH)
5361
5362 tfm_operator.draw_axes_coords(context, (r.width, r.height))
5363
5364 gl_enable(bgl.GL_SMOOTH, smooth_prev)
5365
5366 bgl.glDisable(bgl.GL_BLEND)
5367 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
5368
5369def draw_callback_px(self, context):
5370 global cursor_save_location
5371 settings = find_settings()
5372 if settings is None:
5373 return
5374 library = settings.libraries.get_item()
5375
5376 if not settings.cursor_visible:
5377 context.space_data.cursor_location = cursor_save_location
5378
5379 tfm_operator = CursorDynamicSettings.active_transform_operator
5380
5381 if settings.show_bookmarks and library:
5382 library.draw_bookmark(context)
5383
5384 if tfm_operator:
5385 tfm_operator.draw_2d(context)
5386
5387 # restore opengl defaults
5388 bgl.glLineWidth(1)
5389 bgl.glDisable(bgl.GL_BLEND)
5390 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
5391
5392
5393# ===== UTILITY FUNCTIONS ===== #
5394cursor_stick_pos_cache = None
5395def update_stick_to_obj(context):
5396 global cursor_stick_pos_cache
5397
5398 settings = find_settings()
5399
5400 if not settings.stick_to_obj:
5401 cursor_stick_pos_cache = None
5402 return
5403
5404 scene = context.scene
5405
5406 settings_scene = scene.cursor_3d_tools_settings
5407
5408 name = settings_scene.stick_obj_name
5409 if (not name) or (name not in scene.objects):
5410 cursor_stick_pos_cache = None
5411 return
5412
5413 obj = scene.objects[name]
5414 pos = settings_scene.stick_obj_pos
5415 pos = obj.matrix_world * pos
5416
5417 if pos != cursor_stick_pos_cache:
5418 cursor_stick_pos_cache = pos
5419
5420 # THIS IS AN EXPENSIVE OPERATION!
5421 # (eats 50% of my CPU if called each frame)
5422 context.space_data.cursor_location = pos
5423
5424def get_cursor_location(v3d=None, scene=None):
5425 if v3d:
5426 pos = v3d.cursor_location
5427 elif scene:
5428 pos = scene.cursor_location
5429
5430 return pos.copy()
5431
5432set_cursor_location__reset_stick = True
5433def set_cursor_location(pos, v3d=None, scene=None):
5434 pos = pos.to_3d().copy()
5435
5436 if v3d:
5437 scene = bpy.context.scene
5438 # Accessing scene.cursor_location is SLOW
5439 # (well, at least assigning to it).
5440 # Accessing v3d.cursor_location is fast.
5441 v3d.cursor_location = pos
5442 elif scene:
5443 scene.cursor_location = pos
5444
5445 if set_cursor_location__reset_stick:
5446 set_stick_obj(scene, None)
5447
5448def set_stick_obj(scene, name=None, pos=None):
5449 settings_scene = scene.cursor_3d_tools_settings
5450
5451 if name:
5452 settings_scene.stick_obj_name = name
5453 else:
5454 settings_scene.stick_obj_name = ""
5455
5456 if pos is not None:
5457 settings_scene.stick_obj_pos = Vector(pos).to_3d()
5458
5459# WHERE TO STORE SETTINGS:
5460# Currently there are two types of ID blocks
5461# which properties don't change on Undo/Redo.
5462# - WindowManager seems to be unique (at least
5463# for majority of situations). However, the
5464# properties stored in it are not saved
5465# with the blend.
5466# - Screen. Properties are saved with blend,
5467# but there is some probability that any of
5468# the pre-chosen screen names may not exist
5469# in the user's blend.
5470
5471def propagate_settings_to_all_screens(settings):
5472 # At least the most vital "user preferences" stuff
5473 for screen in bpy.data.screens:
5474 _settings = screen.cursor_3d_tools_settings
5475 _settings.auto_register_keymaps = settings.auto_register_keymaps
5476 _settings.free_coord_precision = settings.free_coord_precision
5477
5478def find_settings():
5479 #wm = bpy.data.window_managers[0]
5480 #settings = wm.cursor_3d_tools_settings
5481
5482 try:
5483 screen = bpy.data.screens.get("Default", bpy.data.screens[0])
5484 except:
5485 # find_settings() was called from register()/unregister()
5486 screen = bpy.context.window_manager.windows[0].screen
5487
5488 try:
5489 settings = screen.cursor_3d_tools_settings
5490 except:
5491 # addon was unregistered
5492 settings = None
5493
5494 return settings
5495
5496def find_runtime_settings():
5497 wm = bpy.data.window_managers[0]
5498 try:
5499 runtime_settings = wm.cursor_3d_runtime_settings
5500 except:
5501 # addon was unregistered
5502 runtime_settings = None
5503
5504 return runtime_settings
5505
5506def KeyMapItemSearch(idname, place=None):
5507 if isinstance(place, bpy.types.KeyMap):
5508 for kmi in place.keymap_items:
5509 if kmi.idname == idname:
5510 yield kmi
5511 elif isinstance(place, bpy.types.KeyConfig):
5512 for keymap in place.keymaps:
5513 for kmi in KeyMapItemSearch(idname, keymap):
5514 yield kmi
5515 else:
5516 wm = bpy.context.window_manager
5517 for keyconfig in wm.keyconfigs:
5518 for kmi in KeyMapItemSearch(idname, keyconfig):
5519 yield kmi
5520
5521def IsKeyMapItemEvent(kmi, event):
5522 event_any = (event.shift or event.ctrl or event.alt or event.oskey)
5523 event_key_modifier = 'NONE' # no such info in event
5524 return ((kmi.type == event.type) and
5525 (kmi.value == event.value) and
5526 (kmi.shift == event.shift) and
5527 (kmi.ctrl == event.ctrl) and
5528 (kmi.alt == event.alt) and
5529 (kmi.oskey == event.oskey) and
5530 (kmi.any == event_any) and
5531 (kmi.key_modifier == event_key_modifier))
5532
5533# ===== REGISTRATION ===== #
5534def update_keymap(activate):
5535 enh_idname = EnhancedSetCursor.bl_idname
5536 cur_idname = 'view3d.cursor3d'
5537
5538 wm = bpy.context.window_manager
5539 userprefs = bpy.context.user_preferences
5540 addon_prefs = userprefs.addons[__name__].preferences
5541 settings = find_settings()
5542
5543 wm.cursor_3d_runtime_settings.use_cursor_monitor = \
5544 addon_prefs.use_cursor_monitor
5545
5546 auto_register_keymaps = settings.auto_register_keymaps
5547 auto_register_keymaps &= addon_prefs.auto_register_keymaps
5548 if not auto_register_keymaps:
5549 return
5550
5551 try:
5552 km = wm.keyconfigs.user.keymaps['3D View']
5553 except:
5554 # wm.keyconfigs.user is empty on Blender startup!
5555 return
5556
5557 # We need for the enhanced operator to take precedence over
5558 # the default cursor3d, but not over the manipulator.
5559 # If we add the operator to "addon" keymaps, it will
5560 # take precedence over both. If we add it to "user"
5561 # keymaps, the default will take precedence.
5562 # However, we may just simply turn it off or remove
5563 # (depending on what saves with blend).
5564
5565 items = list(KeyMapItemSearch(enh_idname, km))
5566 if activate and (len(items) == 0):
5567 kmi = km.keymap_items.new(enh_idname, 'ACTIONMOUSE', 'PRESS')
5568 for key in EnhancedSetCursor.key_map["free_mouse"]:
5569 kmi = km.keymap_items.new(enh_idname, key, 'PRESS')
5570 else:
5571 for kmi in items:
5572 if activate:
5573 kmi.active = activate
5574 else:
5575 km.keymap_items.remove(kmi)
5576
5577 for kmi in KeyMapItemSearch(cur_idname):
5578 kmi.active = not activate
5579
5580@bpy.app.handlers.persistent
5581def scene_update_post_kmreg(scene):
5582 bpy.app.handlers.scene_update_post.remove(scene_update_post_kmreg)
5583 update_keymap(True)
5584
5585class ThisAddonPreferences(bpy.types.AddonPreferences):
5586 # this must match the addon name, use '__package__'
5587 # when defining this in a submodule of a python package.
5588 bl_idname = __name__
5589
5590 auto_register_keymaps = bpy.props.BoolProperty(
5591 name="Auto Register Keymaps",
5592 default=True)
5593
5594 use_cursor_monitor = bpy.props.BoolProperty(
5595 name="Enable Cursor Monitor",
5596 description="Cursor monitor is a background modal operator "\
5597 "that records 3D cursor history",
5598 default=True)
5599
5600 def draw(self, context):
5601 layout = self.layout
5602 settings = find_settings()
5603 row = layout.row()
5604 row.prop(self, "auto_register_keymaps", text="")
5605 row.prop(settings, "auto_register_keymaps")
5606 row.prop(settings, "free_coord_precision")
5607 row.prop(self, "use_cursor_monitor")
5608
5609def extra_snap_menu_draw(self, context):
5610 layout = self.layout
5611 layout.operator("view3d.snap_cursor_to_circumscribed")
5612 layout.operator("view3d.snap_cursor_to_inscribed")
5613
5614
5615def register():
5616 bpy.utils.register_module(__name__)
5617
5618 bpy.types.Scene.cursor_3d_tools_settings = \
5619 bpy.props.PointerProperty(type=Cursor3DToolsSceneSettings)
5620
5621 bpy.types.Screen.cursor_3d_tools_settings = \
5622 bpy.props.PointerProperty(type=Cursor3DToolsSettings)
5623
5624 bpy.types.WindowManager.align_orientation_properties = \
5625 bpy.props.PointerProperty(type=AlignOrientationProperties)
5626
5627 bpy.types.WindowManager.cursor_3d_runtime_settings = \
5628 bpy.props.PointerProperty(type=CursorRuntimeSettings)
5629
5630 bpy.types.VIEW3D_PT_transform_orientations.append(
5631 transform_orientations_panel_extension)
5632
5633 # View properties panel is already long. Appending something
5634 # to it would make it too inconvenient
5635 #bpy.types.VIEW3D_PT_view3d_properties.append(draw_cursor_tools)
5636
5637 bpy.types.VIEW3D_MT_snap.append(extra_snap_menu_draw)
5638
5639 bpy.app.handlers.scene_update_post.append(scene_update_post_kmreg)
5640
5641
5642def unregister():
5643 # In case they are enabled/active
5644 CursorMonitor.handle_remove(bpy.context)
5645
5646 # Manually set this to False on unregister
5647 CursorMonitor.is_running = False
5648
5649 update_keymap(False)
5650
5651 bpy.utils.unregister_module(__name__)
5652
5653 if hasattr(bpy.types.Scene, "cursor_3d_tools_settings"):
5654 del bpy.types.Scene.cursor_3d_tools_settings
5655
5656 if hasattr(bpy.types.Screen, "cursor_3d_tools_settings"):
5657 del bpy.types.Screen.cursor_3d_tools_settings
5658
5659 if hasattr(bpy.types.WindowManager, "align_orientation_properties"):
5660 del bpy.types.WindowManager.align_orientation_properties
5661
5662 if hasattr(bpy.types.WindowManager, "cursor_3d_runtime_settings"):
5663 del bpy.types.WindowManager.cursor_3d_runtime_settings
5664
5665 bpy.types.VIEW3D_PT_transform_orientations.remove(
5666 transform_orientations_panel_extension)
5667
5668 #bpy.types.VIEW3D_PT_view3d_properties.remove(draw_cursor_tools)
5669
5670 bpy.types.VIEW3D_MT_snap.remove(extra_snap_menu_draw)
5671
5672
5673if __name__ == "__main__":
5674 # launched from the Blender text editor
5675 try:
5676 register()
5677 except Exception as e:
5678 print(repr(e))
5679 raise