# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# ***** END GPL LICENSE BLOCK *****
#
bl_info = {
"name": "Enhanced 3D Cursor",
"description": "Cursor history and bookmarks; drag/snap cursor.",
"author": "dairin0d",
"version": (3, 0, 8),
"blender": (2, 7, 7),
"location": "View3D > Action mouse; F10; Properties panel",
"warning": "",
"wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
"Scripts/3D_interaction/Enhanced_3D_Cursor",
"tracker_url": "https://github.com/dairin0d/enhanced-3d-cursor/issues",
"category": "3D View"}
"""
Breakdown:
Addon registration
Keymap utils
Various utils (e.g. find_region)
OpenGL; drawing utils
Non-undoable data storage
Cursor utils
Stick-object
Cursor monitor
Addon's GUI
Addon's properties
Addon's operators
ID Block emulator
Mesh cache
Snap utils
View3D utils
Transform orientation / coordinate system utils
Generic transform utils
Main operator
...
.
First step is to re-make the cursor addon (make something usable first).
CAD tools should be done without the hassle.
TODO:
Make 3D/2D/header visualizations independent from CursorMonitor
(right now, disabling the monitor also disables visualization)
strip trailing space? (one of campbellbarton's commits did that)
IDEAS:
- implement 'GIMBAL' orientation (euler axes)
- mini-Z-buffer in the vicinity of mouse coords (using raycasts)
- an orientation that points towards cursor
(from current selection to cursor)
- user coordinate systems (using e.g. empties to store different
systems; when user switches to such UCS, origin will be set to
"cursor", cursor will be sticked to the empty, and a custom
transform orientation will be aligned with the empty)
- "Stick" transform orientation that is always aligned with the
object cursor is "sticked" to?
- make 'NORMAL' system also work for bones?
- user preferences? (stored in a file)
- create spline/edge_mesh from history?
- API to access history/bookmarks/operators from other scripts?
- Snap selection to bookmark?
- Optimize
- Clean up code, move to several files?
LATER:
ISSUES:
Limitations:
- I need to emulate in Python some things that Blender doesn't
currently expose through API:
- obtaining matrix of predefined transform orientation
- obtaining position of pivot
For some kinds of information (e.g. active vertex/edge,
selected meta-elements), there is simply no workaround.
- Snapping to vertices/edges works differently than in Blender.
First of all, iteration over all vertices/edges of all
objects along the ray is likely to be very slow.
Second, it's more human-friendly to snap to visible
elements (or at least with approximately known position).
- In editmode I have to exit-and-enter it to get relevant
information about current selection. Thus any operator
would automatically get applied when you click on 3D View.
Mites:
QUESTIONS:
==============================================================================
Borrowed code/logic:
- space_view3d_panel_measure.py (Buerbaum Martin "Pontiac"):
- OpenGL state storing/restoring; working with projection matrices.
"""
import bpy
import bgl
import blf
import bmesh
from mathutils import Vector, Matrix, Quaternion, Euler
from mathutils.geometry import (intersect_line_sphere,
intersect_ray_tri,
barycentric_transform,
tessellate_polygon,
intersect_line_line,
intersect_line_plane,
)
from bpy_extras.view3d_utils import (region_2d_to_location_3d,
location_3d_to_region_2d,
)
import math
import time
# ====== MODULE GLOBALS / CONSTANTS ====== #
tmp_name = chr(0x10ffff) # maximal Unicode value
epsilon = 0.000001
# ====== SET CURSOR OPERATOR ====== #
class EnhancedSetCursor(bpy.types.Operator):
"""Cursor history and bookmarks; drag/snap cursor."""
bl_idname = "view3d.cursor3d_enhanced"
bl_label = "Enhanced Set Cursor"
key_char_map = {
'PERIOD':".", 'NUMPAD_PERIOD':".",
'MINUS':"-", 'NUMPAD_MINUS':"-",
'EQUAL':"+", 'NUMPAD_PLUS':"+",
#'E':"e", # such big/small numbers aren't useful
'ONE':"1", 'NUMPAD_1':"1",
'TWO':"2", 'NUMPAD_2':"2",
'THREE':"3", 'NUMPAD_3':"3",
'FOUR':"4", 'NUMPAD_4':"4",
'FIVE':"5", 'NUMPAD_5':"5",
'SIX':"6", 'NUMPAD_6':"6",
'SEVEN':"7", 'NUMPAD_7':"7",
'EIGHT':"8", 'NUMPAD_8':"8",
'NINE':"9", 'NUMPAD_9':"9",
'ZERO':"0", 'NUMPAD_0':"0",
'SPACE':" ",
'SLASH':"/", 'NUMPAD_SLASH':"/",
'NUMPAD_ASTERIX':"*",
}
key_coordsys_map = {
'LEFT_BRACKET':-1,
'RIGHT_BRACKET':1,
':':-1, # Instead of [ for French keyboards
'!':1, # Instead of ] for French keyboards
'J':'VIEW',
'K':"Surface",
'L':'LOCAL',
'B':'GLOBAL',
'N':'NORMAL',
'M':"Scaled",
}
key_pivot_map = {
'H':'ACTIVE',
'U':'CURSOR',
'I':'INDIVIDUAL',
'O':'CENTER',
'P':'MEDIAN',
}
key_snap_map = {
'C':'INCREMENT',
'V':'VERTEX',
'E':'EDGE',
'F':'FACE',
}
key_tfm_mode_map = {
'G':'MOVE',
'R':'ROTATE',
'S':'SCALE',
}
key_map = {
"confirm":{'ACTIONMOUSE'}, # also 'RET' ?
"cancel":{'SELECTMOUSE', 'ESC'},
"free_mouse":{'F10'},
"make_normal_snapshot":{'W'},
"make_tangential_snapshot":{'Q'},
"use_absolute_coords":{'A'},
"snap_to_raw_mesh":{'D'},
"use_object_centers":{'T'},
"precision_up":{'PAGE_UP'},
"precision_down":{'PAGE_DOWN'},
"move_caret_prev":{'LEFT_ARROW'},
"move_caret_next":{'RIGHT_ARROW'},
"move_caret_home":{'HOME'},
"move_caret_end":{'END'},
"change_current_axis":{'TAB', 'RET', 'NUMPAD_ENTER'},
"prev_axis":{'UP_ARROW'},
"next_axis":{'DOWN_ARROW'},
"remove_next_character":{'DEL'},
"remove_last_character":{'BACK_SPACE'},
"copy_axes":{'C'},
"paste_axes":{'V'},
"cut_axes":{'X'},
}
gizmo_factor = 0.15
click_period = 0.25
@staticmethod
def angle_grid_step(flags):
s = (1.0 if flags[0] else 5.0)
return s * (0.1 if flags[1] else 1.0)
@staticmethod
def trackball_grid_step(flags):
s = (0.1 if flags[0] else 0.5)
return s * (0.1 if flags[1] else 1.0)
@staticmethod
def scale_grid_step(flags):
s = (0.01 if flags[0] else 0.1)
return s * (0.1 if flags[1] else 1.0)
# ====== OPERATOR METHOD OVERLOADS ====== #
@classmethod
def poll(cls, context):
area_types = {'VIEW_3D',} # also: IMAGE_EDITOR ?
return ((context.area.type in area_types) and
(context.region.type == "WINDOW") and
(not find_settings().cursor_lock))
def modal(self, context, event):
context.area.tag_redraw()
return self.try_process_input(context, event)
def invoke(self, context, event):
# Attempt to launch the monitor
if bpy.ops.view3d.cursor3d_monitor.poll():
bpy.ops.view3d.cursor3d_monitor()
# Don't interfere with these modes when only mouse is pressed
if ('SCULPT' in context.mode) or ('PAINT' in context.mode):
if "MOUSE" in event.type:
return {'CANCELLED'}
CursorDynamicSettings.active_transform_operator = self
tool_settings = context.tool_settings
settings = find_settings()
tfm_opts = settings.transform_options
settings_scene = context.scene.cursor_3d_tools_settings
self.setup_keymaps(context, event)
# Coordinate System Utility
self.particles, self.csu = gather_particles(context=context)
self.particles = [View3D_Cursor(context)]
self.csu.source_pos = self.particles[0].get_location()
self.csu.source_rot = self.particles[0].get_rotation()
self.csu.source_scale = self.particles[0].get_scale()
# View3D Utility
self.vu = ViewUtility(context.region, context.space_data,
context.region_data)
# Snap Utility
self.su = SnapUtility(context)
# turn off view locking for the duration of the operator
self.view_pos = self.vu.get_position(True)
self.vu.set_position(self.vu.get_position(), True)
self.view_locks = self.vu.get_locks()
self.vu.set_locks({})
# Initialize runtime states
self.initiated_by_mouse = ("MOUSE" in event.type)
self.free_mouse = not self.initiated_by_mouse
self.use_object_centers = False
self.axes_values = ["", "", ""]
self.axes_coords = [None, None, None]
self.axes_eval_success = [True, True, True]
self.allowed_axes = [True, True, True]
self.current_axis = 0
self.caret_pos = 0
self.coord_format = "{:." + str(settings.free_coord_precision) + "f}"
self.transform_mode = 'MOVE'
self.init_xy_angle_distance(context, event)
self.click_start = time.time()
if not self.initiated_by_mouse:
self.click_start -= self.click_period
self.stick_obj_name = settings_scene.stick_obj_name
self.stick_obj_pos = settings_scene.stick_obj_pos
# Initial run
self.try_process_input(context, event, True)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
for particle in self.particles:
particle.revert()
set_stick_obj(context.scene, self.stick_obj_name, self.stick_obj_pos)
self.finalize(context)
# ====== CLEANUP/FINALIZE ====== #
def finalize(self, context):
# restore view locking
self.vu.set_locks(self.view_locks)
self.vu.set_position(self.view_pos, True)
self.cleanup(context)
# This is to avoid "blinking" of
# between-history-positions line
settings = find_settings()
history = settings.history
# make sure the most recent history entry is displayed
history.curr_id = 0
history.last_id = 0
# Ensure there are no leftovers from draw_callback
context.area.tag_redraw()
return {'FINISHED'}
def cleanup(self, context):
self.particles = None
self.csu = None
self.vu = None
if self.su is not None:
self.su.dispose()
self.su = None
CursorDynamicSettings.active_transform_operator = None
# ====== USER INPUT PROCESSING ====== #
def setup_keymaps(self, context, event=None):
self.key_map = self.key_map.copy()
# There is no such event as 'ACTIONMOUSE',
# it's always 'LEFTMOUSE' or 'RIGHTMOUSE'
if event:
if event.type == 'LEFTMOUSE':
self.key_map["confirm"] = {'LEFTMOUSE'}
self.key_map["cancel"] = {'RIGHTMOUSE', 'ESC'}
elif event.type == 'RIGHTMOUSE':
self.key_map["confirm"] = {'RIGHTMOUSE'}
self.key_map["cancel"] = {'LEFTMOUSE', 'ESC'}
else:
event = None
if event is None:
select_mouse = context.user_preferences.inputs.select_mouse
if select_mouse == 'RIGHT':
self.key_map["confirm"] = {'LEFTMOUSE'}
self.key_map["cancel"] = {'RIGHTMOUSE', 'ESC'}
else:
self.key_map["confirm"] = {'RIGHTMOUSE'}
self.key_map["cancel"] = {'LEFTMOUSE', 'ESC'}
# Use user-defined "free mouse" key, if it exists
wm = context.window_manager
if '3D View' in wm.keyconfigs.user.keymaps:
km = wm.keyconfigs.user.keymaps['3D View']
for kmi in KeyMapItemSearch(EnhancedSetCursor.bl_idname, km):
if kmi.map_type == 'KEYBOARD':
self.key_map["free_mouse"] = {kmi.type,}
break
def try_process_input(self, context, event, initial_run=False):
try:
return self.process_input(context, event, initial_run)
except:
# If anything fails, at least dispose the resources
self.cleanup(context)
raise
def process_input(self, context, event, initial_run=False):
wm = context.window_manager
v3d = context.space_data
if event.type in self.key_map["confirm"]:
if self.free_mouse:
finished = (event.value == 'PRESS')
else:
finished = (event.value == 'RELEASE')
if finished:
return self.finalize(context)
if event.type in self.key_map["cancel"]:
self.cancel(context)
return {'CANCELLED'}
tool_settings = context.tool_settings
settings = find_settings()
tfm_opts = settings.transform_options
make_snapshot = False
tangential_snapshot = False
if event.value == 'PRESS':
if event.type in self.key_map["free_mouse"]:
if self.free_mouse and not initial_run:
# confirm if pressed second time
return self.finalize(context)
else:
self.free_mouse = True
if event.type in self.key_tfm_mode_map:
new_mode = self.key_tfm_mode_map[event.type]
if self.transform_mode != new_mode:
# snap cursor to its initial state
if new_mode != 'MOVE':
for particle in self.particles:
initial_matrix = particle.get_initial_matrix()
particle.set_matrix(initial_matrix)
# reset intial mouse position
self.init_xy_angle_distance(context, event)
self.transform_mode = new_mode
if event.type in self.key_map["make_normal_snapshot"]:
make_snapshot = True
tangential_snapshot = False
if event.type in self.key_map["make_tangential_snapshot"]:
make_snapshot = True
tangential_snapshot = True
if event.type in self.key_map["snap_to_raw_mesh"]:
tool_settings.use_snap_self = \
not tool_settings.use_snap_self
if (not event.alt) and (event.type in {'X', 'Y', 'Z'}):
axis_lock = [(event.type == 'X') != event.shift,
(event.type == 'Y') != event.shift,
(event.type == 'Z') != event.shift]
if self.allowed_axes != axis_lock:
self.allowed_axes = axis_lock
else:
self.allowed_axes = [True, True, True]
if event.type in self.key_map["use_absolute_coords"]:
tfm_opts.use_relative_coords = \
not tfm_opts.use_relative_coords
self.update_origin_projection(context)
incr = 0
if event.type in self.key_map["change_current_axis"]:
incr = (-1 if event.shift else 1)
elif event.type in self.key_map["next_axis"]:
incr = 1
elif event.type in self.key_map["prev_axis"]:
incr = -1
if incr != 0:
self.current_axis = (self.current_axis + incr) % 3
self.caret_pos = len(self.axes_values[self.current_axis])
incr = 0
if event.type in self.key_map["precision_up"]:
incr = 1
elif event.type in self.key_map["precision_down"]:
incr = -1
if incr != 0:
settings.free_coord_precision += incr
self.coord_format = "{:." + \
str(settings.free_coord_precision) + "f}"
new_orient1 = self.key_coordsys_map.get(event.type, None)
new_orient2 = self.key_coordsys_map.get(event.unicode, None)
new_orientation = (new_orient1 or new_orient2)
if new_orientation:
self.csu.set_orientation(new_orientation)
self.update_origin_projection(context)
if event.ctrl:
self.snap_to_system_origin()
if (event.type == 'ZERO') and event.ctrl:
self.snap_to_system_origin()
elif new_orientation is None: # avoid conflicting shortcuts
self.process_axis_input(event)
if event.alt:
jc = (", " if tfm_opts.use_comma_separator else "\t")
if event.type in self.key_map["copy_axes"]:
wm.clipboard = jc.join(self.get_axes_text(True))
elif event.type in self.key_map["cut_axes"]:
wm.clipboard = jc.join(self.get_axes_text(True))
self.set_axes_text("\t\t\t")
elif event.type in self.key_map["paste_axes"]:
if jc == "\t":
self.set_axes_text(wm.clipboard, True)
else:
jc = jc.strip()
ttext = ""
brackets = 0
for c in wm.clipboard:
if c in "[{(":
brackets += 1
elif c in "]})":
brackets -= 1
if (brackets == 0) and (c == jc):
c = "\t"
ttext += c
self.set_axes_text(ttext, True)
if event.type in self.key_map["use_object_centers"]:
v3d.use_pivot_point_align = not v3d.use_pivot_point_align
if event.type in self.key_pivot_map:
self.csu.set_pivot(self.key_pivot_map[event.type])
self.update_origin_projection(context)
if event.ctrl:
self.snap_to_system_origin(force_pivot=True)
if (not event.alt) and (event.type in self.key_snap_map):
snap_element = self.key_snap_map[event.type]
if tool_settings.snap_element == snap_element:
if snap_element == 'VERTEX':
snap_element = 'VOLUME'
elif snap_element == 'VOLUME':
snap_element = 'VERTEX'
tool_settings.snap_element = snap_element
# end if
use_snap = (tool_settings.use_snap != event.ctrl)
if use_snap:
snap_type = tool_settings.snap_element
else:
userprefs_view = context.user_preferences.view
if userprefs_view.use_mouse_depth_cursor:
# Suggested by Lissanro in the forum
use_snap = True
snap_type = 'FACE'
else:
snap_type = None
axes_coords = [None, None, None]
if self.transform_mode == 'MOVE':
for i in range(3):
if self.axes_coords[i] is not None:
axes_coords[i] = self.axes_coords[i]
elif not self.allowed_axes[i]:
axes_coords[i] = 0.0
self.su.set_modes(
interpolation=tfm_opts.snap_interpolate_normals_mode,
use_relative_coords=tfm_opts.use_relative_coords,
editmode=tool_settings.use_snap_self,
snap_type=snap_type,
snap_align=tool_settings.use_snap_align_rotation,
axes_coords=axes_coords,
)
self.do_raycast = ("MOUSE" in event.type)
self.grid_substep = (event.shift, event.alt)
self.modify_surface_orientation = (len(self.particles) == 1)
self.xy = Vector((event.mouse_region_x, event.mouse_region_y))
self.use_object_centers = v3d.use_pivot_point_align
if event.type == 'MOUSEMOVE':
self.update_transform_mousemove()
if self.transform_mode == 'MOVE':
transform_func = self.transform_move
elif self.transform_mode == 'ROTATE':
transform_func = self.transform_rotate
elif self.transform_mode == 'SCALE':
transform_func = self.transform_scale
for particle in self.particles:
transform_func(particle)
if make_snapshot:
self.make_normal_snapshot(context.scene, tangential_snapshot)
return {'RUNNING_MODAL'}
def update_origin_projection(self, context):
r = context.region
rv3d = context.region_data
origin = self.csu.get_origin()
# prehaps not projection, but intersection with plane?
self.origin_xy = location_3d_to_region_2d(r, rv3d, origin)
if self.origin_xy is None:
self.origin_xy = Vector((r.width / 2, r.height / 2))
self.delta_xy = (self.start_xy - self.origin_xy).to_3d()
self.prev_delta_xy = self.delta_xy
def init_xy_angle_distance(self, context, event):
self.start_xy = Vector((event.mouse_region_x, event.mouse_region_y))
self.update_origin_projection(context)
# Distinction between angles has to be made because
# angles can go beyond 360 degrees (we cannot snap
# to increment the original ones).
self.raw_angles = [0.0, 0.0, 0.0]
self.angles = [0.0, 0.0, 0.0]
self.scales = [1.0, 1.0, 1.0]
def update_transform_mousemove(self):
delta_xy = (self.xy - self.origin_xy).to_3d()
n_axes = sum(int(v) for v in self.allowed_axes)
if n_axes == 1:
# rotate using angle as value
rd = self.prev_delta_xy.rotation_difference(delta_xy)
offset = -rd.angle * round(rd.axis[2])
sys_matrix = self.csu.get_matrix()
i_allowed = 0
for i in range(3):
if self.allowed_axes[i]:
i_allowed = i
view_dir = self.vu.get_direction()
if view_dir.dot(sys_matrix[i_allowed][:3]) < 0:
offset = -offset
for i in range(3):
if self.allowed_axes[i]:
self.raw_angles[i] += offset
elif n_axes == 2:
# rotate using XY coords as two values
offset = (delta_xy - self.prev_delta_xy) * (math.pi / 180.0)
offset *= self.trackball_grid_step(self.grid_substep)
j = 0
for i in range(3):
if self.allowed_axes[i]:
self.raw_angles[i] += offset[1 - j]
j += 1
elif n_axes == 3:
# rotate around view direction
rd = self.prev_delta_xy.rotation_difference(delta_xy)
offset = -rd.angle * round(rd.axis[2])
view_dir = self.vu.get_direction()
sys_matrix = self.csu.get_matrix()
try:
view_dir = sys_matrix.inverted().to_3x3() * view_dir
except:
# this is some degenerate system
pass
view_dir.normalize()
rot = Matrix.Rotation(offset, 3, view_dir)
matrix = Euler(self.raw_angles, 'XYZ').to_matrix()
matrix.rotate(rot)
euler = matrix.to_euler('XYZ')
self.raw_angles[0] += clamp_angle(euler.x - self.raw_angles[0])
self.raw_angles[1] += clamp_angle(euler.y - self.raw_angles[1])
self.raw_angles[2] += clamp_angle(euler.z - self.raw_angles[2])
scale = delta_xy.length / self.delta_xy.length
if self.delta_xy.dot(delta_xy) < 0:
scale *= -1
for i in range(3):
if self.allowed_axes[i]:
self.scales[i] = scale
self.prev_delta_xy = delta_xy
def transform_move(self, particle):
global set_cursor_location__reset_stick
src_matrix = particle.get_matrix()
initial_matrix = particle.get_initial_matrix()
matrix = self.su.snap(
self.xy, src_matrix, initial_matrix,
self.do_raycast, self.grid_substep,
self.vu, self.csu,
self.modify_surface_orientation,
self.use_object_centers)
set_cursor_location__reset_stick = False
particle.set_matrix(matrix)
set_cursor_location__reset_stick = True
def rotate_matrix(self, matrix):
sys_matrix = self.csu.get_matrix()
try:
matrix = sys_matrix.inverted() * matrix
except:
# this is some degenerate system
pass
# Blender's order of rotation [in local axes]
rotation_order = [2, 1, 0]
# Seems that 4x4 matrix cannot be rotated using rotate() ?
sys_matrix3 = sys_matrix.to_3x3()
for i in range(3):
j = rotation_order[i]
axis = sys_matrix3[j]
angle = self.angles[j]
rot = angle_axis_to_quat(angle, axis)
# this seems to be buggy too
#rot = Matrix.Rotation(angle, 3, axis)
sys_matrix3 = rot.to_matrix() * sys_matrix3
# sys_matrix3.rotate has a bug? or I don't understand how it works?
#sys_matrix3.rotate(rot)
for i in range(3):
sys_matrix[i][:3] = sys_matrix3[i]
matrix = sys_matrix * matrix
return matrix
def transform_rotate(self, particle):
grid_step = self.angle_grid_step(self.grid_substep)
grid_step *= (math.pi / 180.0)
for i in range(3):
if self.axes_values[i] and self.axes_eval_success[i]:
self.raw_angles[i] = self.axes_coords[i] * (math.pi / 180.0)
self.angles[i] = self.raw_angles[i]
if self.su.implementation.snap_type == 'INCREMENT':
for i in range(3):
self.angles[i] = round_step(self.angles[i], grid_step)
initial_matrix = particle.get_initial_matrix()
matrix = self.rotate_matrix(initial_matrix)
particle.set_matrix(matrix)
def scale_matrix(self, matrix):
sys_matrix = self.csu.get_matrix()
try:
matrix = sys_matrix.inverted() * matrix
except:
# this is some degenerate system
pass
for i in range(3):
sys_matrix[i] *= self.scales[i]
matrix = sys_matrix * matrix
return matrix
def transform_scale(self, particle):
grid_step = self.scale_grid_step(self.grid_substep)
for i in range(3):
if self.axes_values[i] and self.axes_eval_success[i]:
self.scales[i] = self.axes_coords[i]
if self.su.implementation.snap_type == 'INCREMENT':
for i in range(3):
self.scales[i] = round_step(self.scales[i], grid_step)
initial_matrix = particle.get_initial_matrix()
matrix = self.scale_matrix(initial_matrix)
particle.set_matrix(matrix)
def set_axis_input(self, axis_id, axis_val):
if axis_val == self.axes_values[axis_id]:
return
self.axes_values[axis_id] = axis_val
if len(axis_val) == 0:
self.axes_coords[axis_id] = None
self.axes_eval_success[axis_id] = True
else:
try:
#self.axes_coords[axis_id] = float(eval(axis_val, {}, {}))
self.axes_coords[axis_id] = \
float(eval(axis_val, math.__dict__))
self.axes_eval_success[axis_id] = True
except:
self.axes_eval_success[axis_id] = False
def snap_to_system_origin(self, force_pivot=False):
if self.transform_mode == 'MOVE':
pivot = self.csu.get_pivot_name(raw=force_pivot)
p = self.csu.get_origin(relative=False, pivot=pivot)
m = self.csu.get_matrix()
try:
p = m.inverted() * p
except:
# this is some degenerate system
pass
for i in range(3):
self.set_axis_input(i, str(p[i]))
elif self.transform_mode == 'ROTATE':
for i in range(3):
self.set_axis_input(i, "0")
elif self.transform_mode == 'SCALE':
for i in range(3):
self.set_axis_input(i, "1")
def get_axes_values(self, as_string=False):
if self.transform_mode == 'MOVE':
localmat = CursorDynamicSettings.local_matrix
raw_axes = localmat.translation
elif self.transform_mode == 'ROTATE':
raw_axes = Vector(self.angles) * (180.0 / math.pi)
elif self.transform_mode == 'SCALE':
raw_axes = Vector(self.scales)
axes_values = []
for i in range(3):
if as_string and self.axes_values[i]:
value = self.axes_values[i]
elif self.axes_eval_success[i] and \
(self.axes_coords[i] is not None):
value = self.axes_coords[i]
else:
value = raw_axes[i]
if as_string:
value = self.coord_format.format(value)
axes_values.append(value)
return axes_values
def get_axes_text(self, offset=False):
axes_values = self.get_axes_values(as_string=True)
axes_text = []
for i in range(3):
j = i
if offset:
j = (i + self.current_axis) % 3
axes_text.append(axes_values[j])
return axes_text
def set_axes_text(self, text, offset=False):
if "\n" in text:
text = text.replace("\r", "")
else:
text = text.replace("\r", "\n")
text = text.replace("\n", "\t")
#text = text.replace(",", ".") # ???
axes_text = text.split("\t")
for i in range(min(len(axes_text), 3)):
j = i
if offset:
j = (i + self.current_axis) % 3
self.set_axis_input(j, axes_text[i])
def process_axis_input(self, event):
axis_id = self.current_axis
axis_val = self.axes_values[axis_id]
if event.type in self.key_map["remove_next_character"]:
if event.ctrl:
# clear all
for i in range(3):
self.set_axis_input(i, "")
self.caret_pos = 0
return
else:
axis_val = axis_val[0:self.caret_pos] + \
axis_val[self.caret_pos + 1:len(axis_val)]
elif event.type in self.key_map["remove_last_character"]:
if event.ctrl:
# clear current
axis_val = ""
else:
axis_val = axis_val[0:self.caret_pos - 1] + \
axis_val[self.caret_pos:len(axis_val)]
self.caret_pos -= 1
elif event.type in self.key_map["move_caret_next"]:
self.caret_pos += 1
if event.ctrl:
snap_chars = ".-+*/%()"
i = self.caret_pos
while axis_val[i:i + 1] not in snap_chars:
i += 1
self.caret_pos = i
elif event.type in self.key_map["move_caret_prev"]:
self.caret_pos -= 1
if event.ctrl:
snap_chars = ".-+*/%()"
i = self.caret_pos
while axis_val[i - 1:i] not in snap_chars:
i -= 1
self.caret_pos = i
elif event.type in self.key_map["move_caret_home"]:
self.caret_pos = 0
elif event.type in self.key_map["move_caret_end"]:
self.caret_pos = len(axis_val)
elif event.type in self.key_char_map:
# Currently accessing event.ascii seems to crash Blender
c = self.key_char_map[event.type]
if event.shift:
if c == "8":
c = "*"
elif c == "5":
c = "%"
elif c == "9":
c = "("
elif c == "0":
c = ")"
axis_val = axis_val[0:self.caret_pos] + c + \
axis_val[self.caret_pos:len(axis_val)]
self.caret_pos += 1
self.caret_pos = min(max(self.caret_pos, 0), len(axis_val))
self.set_axis_input(axis_id, axis_val)
# ====== DRAWING ====== #
def gizmo_distance(self, pos):
rv3d = self.vu.region_data
if rv3d.view_perspective == 'ORTHO':
dist = rv3d.view_distance
else:
view_pos = self.vu.get_viewpoint()
view_dir = self.vu.get_direction()
dist = (pos - view_pos).dot(view_dir)
return dist
def gizmo_scale(self, pos):
return self.gizmo_distance(pos) * self.gizmo_factor
def check_v3d_local(self, context):
csu_v3d = self.csu.space_data
v3d = context.space_data
if csu_v3d.local_view:
return csu_v3d != v3d
return v3d.local_view
def draw_3d(self, context):
if self.check_v3d_local(context):
return
if time.time() < (self.click_start + self.click_period):
return
settings = find_settings()
tfm_opts = settings.transform_options
initial_matrix = self.particles[0].get_initial_matrix()
sys_matrix = self.csu.get_matrix()
if tfm_opts.use_relative_coords:
sys_matrix.translation = initial_matrix.translation.copy()
sys_origin = sys_matrix.to_translation()
dest_point = self.particles[0].get_location()
if self.is_normal_visible():
p0, x, y, z, _x, _z = \
self.get_normal_params(tfm_opts, dest_point)
# use theme colors?
#ThemeView3D.normal
#ThemeView3D.vertex_normal
bgl.glDisable(bgl.GL_LINE_STIPPLE)
if settings.draw_N:
bgl.glColor4f(0, 1, 1, 1)
draw_arrow(p0, _x, y, z) # Z (normal)
if settings.draw_T1:
bgl.glColor4f(1, 0, 1, 1)
draw_arrow(p0, y, _z, x) # X (1st tangential)
if settings.draw_T2:
bgl.glColor4f(1, 1, 0, 1)
draw_arrow(p0, _z, x, y) # Y (2nd tangential)
bgl.glEnable(bgl.GL_BLEND)
bgl.glDisable(bgl.GL_DEPTH_TEST)
if settings.draw_N:
bgl.glColor4f(0, 1, 1, 0.25)
draw_arrow(p0, _x, y, z) # Z (normal)
if settings.draw_T1:
bgl.glColor4f(1, 0, 1, 0.25)
draw_arrow(p0, y, _z, x) # X (1st tangential)
if settings.draw_T2:
bgl.glColor4f(1, 1, 0, 0.25)
draw_arrow(p0, _z, x, y) # Y (2nd tangential)
if settings.draw_guides:
p0 = dest_point
try:
p00 = sys_matrix.inverted() * p0
except:
# this is some degenerate system
p00 = p0.copy()
axes_line_params = [
(Vector((0, p00.y, p00.z)), (1, 0, 0)),
(Vector((p00.x, 0, p00.z)), (0, 1, 0)),
(Vector((p00.x, p00.y, 0)), (0, 0, 1)),
]
for i in range(3):
p1, color = axes_line_params[i]
p1 = sys_matrix * p1
constrained = (self.axes_coords[i] is not None) or \
(not self.allowed_axes[i])
alpha = (0.25 if constrained else 1.0)
draw_line_hidden_depth(p0, p1, color, \
alpha, alpha, False, True)
# line from origin to cursor
p0 = sys_origin
p1 = dest_point
bgl.glEnable(bgl.GL_LINE_STIPPLE)
bgl.glColor4f(1, 1, 0, 1)
draw_line_hidden_depth(p0, p1, (1, 1, 0), 1.0, 0.5, True, True)
if settings.draw_snap_elements:
sui = self.su.implementation
if sui.potential_snap_elements and (sui.snap_type == 'EDGE'):
bgl.glDisable(bgl.GL_LINE_STIPPLE)
bgl.glEnable(bgl.GL_BLEND)
bgl.glDisable(bgl.GL_DEPTH_TEST)
bgl.glLineWidth(2)
bgl.glColor4f(0, 0, 1, 0.5)
bgl.glBegin(bgl.GL_LINE_LOOP)
for p in sui.potential_snap_elements:
bgl.glVertex3f(p[0], p[1], p[2])
bgl.glEnd()
elif sui.potential_snap_elements and (sui.snap_type == 'FACE'):
bgl.glEnable(bgl.GL_BLEND)
bgl.glDisable(bgl.GL_DEPTH_TEST)
bgl.glColor4f(0, 1, 0, 0.5)
co = sui.potential_snap_elements
tris = tessellate_polygon([co])
bgl.glBegin(bgl.GL_TRIANGLES)
for tri in tris:
for vi in tri:
p = co[vi]
bgl.glVertex3f(p[0], p[1], p[2])
bgl.glEnd()
def draw_2d(self, context):
if self.check_v3d_local(context):
return
r = context.region
rv3d = context.region_data
settings = find_settings()
if settings.draw_snap_elements:
sui = self.su.implementation
snap_points = []
if sui.potential_snap_elements and \
(sui.snap_type in {'VERTEX', 'VOLUME'}):
snap_points.extend(sui.potential_snap_elements)
if sui.extra_snap_points:
snap_points.extend(sui.extra_snap_points)
if snap_points:
bgl.glEnable(bgl.GL_BLEND)
bgl.glPointSize(5)
bgl.glColor4f(1, 0, 0, 0.5)
bgl.glBegin(bgl.GL_POINTS)
for p in snap_points:
p = location_3d_to_region_2d(r, rv3d, p)
if p is not None:
bgl.glVertex2f(p[0], p[1])
bgl.glEnd()
bgl.glPointSize(1)
if self.transform_mode == 'MOVE':
return
bgl.glEnable(bgl.GL_LINE_STIPPLE)
bgl.glLineWidth(1)
bgl.glColor4f(0, 0, 0, 1)
draw_line_2d(self.origin_xy, self.xy)
bgl.glDisable(bgl.GL_LINE_STIPPLE)
line_width = 3
bgl.glLineWidth(line_width)
L = 12.0
arrow_len = 6.0
arrow_width = 8.0
arrow_space = 5.0
Lmax = arrow_space * 2 + L * 2 + line_width
pos = self.xy.to_2d()
normal = self.prev_delta_xy.to_2d().normalized()
dist = self.prev_delta_xy.length
tangential = Vector((-normal[1], normal[0]))
if self.transform_mode == 'ROTATE':
n_axes = sum(int(v) for v in self.allowed_axes)
if n_axes == 2:
bgl.glColor4f(0.4, 0.15, 0.15, 1)
for sgn in (-1, 1):
n = sgn * Vector((0, 1))
p0 = pos + arrow_space * n
draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
bgl.glColor4f(0.11, 0.51, 0.11, 1)
for sgn in (-1, 1):
n = sgn * Vector((1, 0))
p0 = pos + arrow_space * n
draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
else:
bgl.glColor4f(0, 0, 0, 1)
for sgn in (-1, 1):
n = sgn * tangential
if dist < Lmax:
n *= dist / Lmax
p0 = pos + arrow_space * n
draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
elif self.transform_mode == 'SCALE':
bgl.glColor4f(0, 0, 0, 1)
for sgn in (-1, 1):
n = sgn * normal
p0 = pos + arrow_space * n
draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
bgl.glLineWidth(1)
def draw_axes_coords(self, context, header_size):
if self.check_v3d_local(context):
return
if time.time() < (self.click_start + self.click_period):
return
v3d = context.space_data
userprefs_view = context.user_preferences.view
tool_settings = context.tool_settings
settings = find_settings()
tfm_opts = settings.transform_options
localmat = CursorDynamicSettings.local_matrix
font_id = 0 # default font
font_size = 11
blf.size(font_id, font_size, 72) # font, point size, dpi
tet = context.user_preferences.themes[0].text_editor
# Prepare the table...
if self.transform_mode == 'MOVE':
axis_prefix = ("D" if tfm_opts.use_relative_coords else "")
elif self.transform_mode == 'SCALE':
axis_prefix = "S"
else:
axis_prefix = "R"
axis_names = ["X", "Y", "Z"]
axis_cells = []
coord_cells = []
#caret_cell = TextCell("_", tet.cursor)
caret_cell = TextCell("|", tet.cursor)
try:
axes_text = self.get_axes_text()
for i in range(3):
color = tet.space.text
alpha = (1.0 if self.allowed_axes[i] else 0.5)
text = axis_prefix + axis_names[i] + " : "
axis_cells.append(TextCell(text, color, alpha))
if self.axes_values[i]:
if self.axes_eval_success[i]:
color = tet.syntax_numbers
else:
color = tet.syntax_string
else:
color = tet.space.text
text = axes_text[i]
coord_cells.append(TextCell(text, color))
except Exception as e:
print(repr(e))
mode_cells = []
try:
snap_type = self.su.implementation.snap_type
if snap_type is None:
color = tet.space.text
elif (not self.use_object_centers) or \
(snap_type == 'INCREMENT'):
color = tet.syntax_numbers
else:
color = tet.syntax_special
text = snap_type or tool_settings.snap_element
if text == 'VOLUME':
text = "BBOX"
mode_cells.append(TextCell(text, color))
if self.csu.tou.is_custom:
color = tet.space.text
else:
color = tet.syntax_builtin
text = self.csu.tou.get_title()
mode_cells.append(TextCell(text, color))
color = tet.space.text
text = self.csu.get_pivot_name(raw=True)
if self.use_object_centers:
color = tet.syntax_special
mode_cells.append(TextCell(text, color))
except Exception as e:
print(repr(e))
hdr_w, hdr_h = header_size
try:
xyz_x_start_min = 12
xyz_x_start = xyz_x_start_min
mode_x_start = 6
mode_margin = 4
xyz_margin = 16
blend_margin = 32
color = tet.space.back
bgl.glColor4f(color[0], color[1], color[2], 1.0)
draw_rect(0, 0, hdr_w, hdr_h)
if tool_settings.use_snap_self:
x = hdr_w - mode_x_start
y = hdr_h / 2
cell = mode_cells[0]
x -= cell.w
y -= cell.h * 0.5
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
draw_rect(x, y, cell.w, cell.h, 1, True)
x = hdr_w - mode_x_start
y = hdr_h / 2
for cell in mode_cells:
cell.draw(x, y, (1, 0.5))
x -= (cell.w + mode_margin)
curr_axis_x_start = 0
curr_axis_x_end = 0
caret_x = 0
xyz_width = 0
for i in range(3):
if i == self.current_axis:
curr_axis_x_start = xyz_width
xyz_width += axis_cells[i].w
if i == self.current_axis:
char_offset = 0
if self.axes_values[i]:
char_offset = blf.dimensions(font_id,
coord_cells[i].text[:self.caret_pos])[0]
caret_x = xyz_width + char_offset
xyz_width += coord_cells[i].w
if i == self.current_axis:
curr_axis_x_end = xyz_width
xyz_width += xyz_margin
xyz_width = int(xyz_width)
xyz_width_ext = xyz_width + blend_margin
offset = (xyz_x_start + curr_axis_x_end) - hdr_w
if offset > 0:
xyz_x_start -= offset
offset = xyz_x_start_min - (xyz_x_start + curr_axis_x_start)
if offset > 0:
xyz_x_start += offset
offset = (xyz_x_start + caret_x) - hdr_w
if offset > 0:
xyz_x_start -= offset
# somewhy GL_BLEND should be set right here
# to actually draw the box with blending %)
# (perhaps due to text draw happened before)
bgl.glEnable(bgl.GL_BLEND)
bgl.glShadeModel(bgl.GL_SMOOTH)
gl_enable(bgl.GL_SMOOTH, True)
color = tet.space.back
bgl.glBegin(bgl.GL_TRIANGLE_STRIP)
bgl.glColor4f(color[0], color[1], color[2], 1.0)
bgl.glVertex2i(0, 0)
bgl.glVertex2i(0, hdr_h)
bgl.glVertex2i(xyz_width, 0)
bgl.glVertex2i(xyz_width, hdr_h)
bgl.glColor4f(color[0], color[1], color[2], 0.0)
bgl.glVertex2i(xyz_width_ext, 0)
bgl.glVertex2i(xyz_width_ext, hdr_h)
bgl.glEnd()
x = xyz_x_start
y = hdr_h / 2
for i in range(3):
cell = axis_cells[i]
cell.draw(x, y, (0, 0.5))
x += cell.w
cell = coord_cells[i]
cell.draw(x, y, (0, 0.5))
x += (cell.w + xyz_margin)
caret_x -= blf.dimensions(font_id, caret_cell.text)[0] * 0.5
caret_cell.draw(xyz_x_start + caret_x, y, (0, 0.5))
bgl.glEnable(bgl.GL_BLEND)
bgl.glShadeModel(bgl.GL_SMOOTH)
gl_enable(bgl.GL_SMOOTH, True)
color = tet.space.back
bgl.glBegin(bgl.GL_TRIANGLE_STRIP)
bgl.glColor4f(color[0], color[1], color[2], 1.0)
bgl.glVertex2i(0, 0)
bgl.glVertex2i(0, hdr_h)
bgl.glVertex2i(xyz_x_start_min, 0)
bgl.glColor4f(color[0], color[1], color[2], 0.0)
bgl.glVertex2i(xyz_x_start_min, hdr_h)
bgl.glEnd()
except Exception as e:
print(repr(e))
return
# ====== NORMAL SNAPSHOT ====== #
def is_normal_visible(self):
if self.csu.tou.get() == "Surface":
return True
if self.use_object_centers:
return False
return self.su.implementation.snap_type \
not in {None, 'INCREMENT', 'VOLUME'}
def get_normal_params(self, tfm_opts, dest_point):
surf_matrix = self.csu.get_matrix("Surface")
if tfm_opts.use_relative_coords:
surf_origin = dest_point
else:
surf_origin = surf_matrix.to_translation()
m3 = surf_matrix.to_3x3()
p0 = surf_origin
scl = self.gizmo_scale(p0)
# Normal and tangential are not always orthogonal
# (e.g. when normal is interpolated)
x = (m3 * Vector((1, 0, 0))).normalized()
y = (m3 * Vector((0, 1, 0))).normalized()
z = (m3 * Vector((0, 0, 1))).normalized()
_x = z.cross(y)
_z = y.cross(x)
return p0, x * scl, y * scl, z * scl, _x * scl, _z * scl
def make_normal_snapshot(self, scene, tangential=False):
settings = find_settings()
tfm_opts = settings.transform_options
dest_point = self.particles[0].get_location()
if self.is_normal_visible():
p0, x, y, z, _x, _z = \
self.get_normal_params(tfm_opts, dest_point)
snapshot = bpy.data.objects.new("normal_snapshot", None)
if tangential:
m = MatrixCompose(_z, y, x, p0)
else:
m = MatrixCompose(_x, y, z, p0)
snapshot.matrix_world = m
snapshot.empty_draw_type = 'SINGLE_ARROW'
#snapshot.empty_draw_type = 'ARROWS'
#snapshot.layers = [True] * 20 # ?
scene.objects.link(snapshot)
#============================================================================#
class Particle:
pass
class View3D_Cursor(Particle):
def __init__(self, context):
assert context.space_data.type == 'VIEW_3D'
self.v3d = context.space_data
self.initial_pos = self.get_location()
self.initial_matrix = Matrix.Translation(self.initial_pos)
def revert(self):
self.set_location(self.initial_pos)
def get_location(self):
return get_cursor_location(v3d=self.v3d)
def set_location(self, value):
set_cursor_location(Vector(value), v3d=self.v3d)
def get_rotation(self):
return Quaternion()
def set_rotation(self, value):
pass
def get_scale(self):
return Vector((1.0, 1.0, 1.0))
def set_scale(self, value):
pass
def get_matrix(self):
return Matrix.Translation(self.get_location())
def set_matrix(self, value):
self.set_location(value.to_translation())
def get_initial_matrix(self):
return self.initial_matrix
class View3D_Object(Particle):
def __init__(self, obj):
self.obj = obj
def get_location(self):
# obj.location seems to be in parent's system...
# or even maybe not bounded by constraints %)
return self.obj.matrix_world.to_translation()
class View3D_EditMesh_Vertex(Particle):
pass
class View3D_EditMesh_Edge(Particle):
pass
class View3D_EditMesh_Face(Particle):
pass
class View3D_EditSpline_Point(Particle):
pass
class View3D_EditSpline_BezierPoint(Particle):
pass
class View3D_EditSpline_BezierHandle(Particle):
pass
class View3D_EditMeta_Element(Particle):
pass
class View3D_EditBone_Bone(Particle):
pass
class View3D_EditBone_HeadTail(Particle):
pass
class View3D_PoseBone(Particle):
pass
class UV_Cursor(Particle):
pass
class UV_Vertex(Particle):
pass
class UV_Edge(Particle):
pass
class UV_Face(Particle):
pass
# Other types:
# NLA / Dopesheet / Graph editor ...
# Particles are used in the following situations:
# - as subjects of transformation
# - as reference point(s) for cursor transformation
# Note: particles 'dragged' by Proportional Editing
# are a separate issue (they can come and go).
def gather_particles(**kwargs):
context = kwargs.get("context", bpy.context)
area_type = kwargs.get("area_type", context.area.type)
scene = kwargs.get("scene", context.scene)
space_data = kwargs.get("space_data", context.space_data)
region_data = kwargs.get("region_data", context.region_data)
particles = []
pivots = {}
normal_system = None
active_element = None
cursor_pos = None
median = None
if area_type == 'VIEW_3D':
context_mode = kwargs.get("context_mode", context.mode)
selected_objects = kwargs.get("selected_objects",
context.selected_objects)
active_object = kwargs.get("active_object",
context.active_object)
if context_mode == 'OBJECT':
for obj in selected_objects:
particle = View3D_Object(obj)
particles.append(particle)
if active_object:
active_element = active_object.\
matrix_world.to_translation()
# On Undo/Redo scene hash value is changed ->
# -> the monitor tries to update the CSU ->
# -> object.mode_set seem to somehow conflict
# with Undo/Redo mechanisms.
elif active_object and active_object.data and \
(context_mode in {
'EDIT_MESH', 'EDIT_METABALL',
'EDIT_CURVE', 'EDIT_SURFACE',
'EDIT_ARMATURE', 'POSE'}):
m = active_object.matrix_world
positions = []
normal = Vector((0, 0, 0))
if context_mode == 'EDIT_MESH':
bm = bmesh.from_edit_mesh(active_object.data)
if bm.select_history:
elem = bm.select_history[-1]
if isinstance(elem, bmesh.types.BMVert):
active_element = elem.co.copy()
else:
active_element = Vector()
for v in elem.verts:
active_element += v.co
active_element *= 1.0 / len(elem.verts)
for v in bm.verts:
if v.select:
positions.append(v.co)
normal += v.normal
# mimic Blender's behavior (as of now,
# order of selection is ignored)
if len(positions) == 2:
normal = positions[1] - positions[0]
elif len(positions) == 3:
a = positions[0] - positions[1]
b = positions[2] - positions[1]
normal = a.cross(b)
elif context_mode == 'EDIT_METABALL':
active_elem = active_object.data.elements.active
if active_elem:
active_element = active_elem.co.copy()
active_element = active_object.\
matrix_world * active_element
# Currently there is no API for element.select
#for element in active_object.data.elements:
# if element.select:
# positions.append(element.co)
elif context_mode == 'EDIT_ARMATURE':
# active bone seems to have the same pivot
# as median of the selection
'''
active_bone = active_object.data.edit_bones.active
if active_bone:
active_element = active_bone.head + \
active_bone.tail
active_element = active_object.\
matrix_world * active_element
'''
for bone in active_object.data.edit_bones:
if bone.select_head:
positions.append(bone.head)
if bone.select_tail:
positions.append(bone.tail)
elif context_mode == 'POSE':
active_bone = active_object.data.bones.active
if active_bone:
active_element = active_bone.\
matrix_local.translation.to_3d()
active_element = active_object.\
matrix_world * active_element
# consider only topmost parents
bones = set()
for bone in active_object.data.bones:
if bone.select:
bones.add(bone)
parents = set()
for bone in bones:
if not set(bone.parent_recursive).intersection(bones):
parents.add(bone)
for bone in parents:
positions.append(bone.matrix_local.translation.to_3d())
else:
for spline in active_object.data.splines:
for point in spline.bezier_points:
if point.select_control_point:
positions.append(point.co)
else:
if point.select_left_handle:
positions.append(point.handle_left)
if point.select_right_handle:
positions.append(point.handle_right)
n = None
nL = point.co - point.handle_left
nR = point.co - point.handle_right
#nL = point.handle_left.copy()
#nR = point.handle_right.copy()
if point.select_control_point:
n = nL + nR
elif point.select_left_handle or \
point.select_right_handle:
n = nL + nR
else:
if point.select_left_handle:
n = -nL
if point.select_right_handle:
n = nR
if n is not None:
if n.length_squared < epsilon:
n = -nL
normal += n.normalized()
for point in spline.points:
if point.select:
positions.append(point.co)
if len(positions) != 0:
if normal.length_squared < epsilon:
normal = Vector((0, 0, 1))
normal.rotate(m)
normal.normalize()
if (1.0 - abs(normal.z)) < epsilon:
t1 = Vector((1, 0, 0))
else:
t1 = Vector((0, 0, 1)).cross(normal)
t2 = t1.cross(normal)
normal_system = MatrixCompose(t1, t2, normal)
median, bbox_center = calc_median_bbox_pivots(positions)
median = m * median
bbox_center = m * bbox_center
# Currently I don't know how to get active mesh element
if active_element is None:
if context_mode == 'EDIT_ARMATURE':
# Somewhy EDIT_ARMATURE has such behavior
active_element = bbox_center
else:
active_element = median
else:
if active_element is None:
active_element = active_object.\
matrix_world.to_translation()
median = active_element
bbox_center = active_element
normal_system = active_object.matrix_world.to_3x3()
normal_system.col[0].normalize()
normal_system.col[1].normalize()
normal_system.col[2].normalize()
else:
# paint/sculpt, etc.?
particle = View3D_Object(active_object)
particles.append(particle)
if active_object:
active_element = active_object.\
matrix_world.to_translation()
cursor_pos = get_cursor_location(v3d=space_data)
#elif area_type == 'IMAGE_EDITOR':
# currently there is no way to get UV editor's
# offset (and maybe some other parameters
# required to implement these operators)
#cursor_pos = space_data.uv_editor.cursor_location
#elif area_type == 'EMPTY':
#elif area_type == 'GRAPH_EDITOR':
#elif area_type == 'OUTLINER':
#elif area_type == 'PROPERTIES':
#elif area_type == 'FILE_BROWSER':
#elif area_type == 'INFO':
#elif area_type == 'SEQUENCE_EDITOR':
#elif area_type == 'TEXT_EDITOR':
#elif area_type == 'AUDIO_WINDOW':
#elif area_type == 'DOPESHEET_EDITOR':
#elif area_type == 'NLA_EDITOR':
#elif area_type == 'SCRIPTS_WINDOW':
#elif area_type == 'TIMELINE':
#elif area_type == 'NODE_EDITOR':
#elif area_type == 'LOGIC_EDITOR':
#elif area_type == 'CONSOLE':
#elif area_type == 'USER_PREFERENCES':
else:
print("gather_particles() not implemented for '{}'".\
format(area_type))
return None, None
# 'INDIVIDUAL_ORIGINS' is not handled here
if cursor_pos:
pivots['CURSOR'] = cursor_pos.copy()
if active_element:
# in v3d: ACTIVE_ELEMENT
pivots['ACTIVE'] = active_element.copy()
if (len(particles) != 0) and (median is None):
positions = (p.get_location() for p in particles)
median, bbox_center = calc_median_bbox_pivots(positions)
if median:
# in v3d: MEDIAN_POINT, in UV editor: MEDIAN
pivots['MEDIAN'] = median.copy()
# in v3d: BOUNDING_BOX_CENTER, in UV editor: CENTER
pivots['CENTER'] = bbox_center.copy()
csu = CoordinateSystemUtility(scene, space_data, region_data, \
pivots, normal_system)
return particles, csu
def calc_median_bbox_pivots(positions):
median = None # pos can be 3D or 2D
bbox = [None, None]
n = 0
for pos in positions:
extend_bbox(bbox, pos)
try:
median += pos
except:
median = pos.copy()
n += 1
median = median / n
bbox_center = (Vector(bbox[0]) + Vector(bbox[1])) * 0.5
return median, bbox_center
def extend_bbox(bbox, pos):
try:
bbox[0] = tuple(min(e0, e1) for e0, e1 in zip(bbox[0], pos))
bbox[1] = tuple(max(e0, e1) for e0, e1 in zip(bbox[1], pos))
except:
bbox[0] = tuple(pos)
bbox[1] = tuple(pos)
# ====== COORDINATE SYSTEM UTILITY ====== #
class CoordinateSystemUtility:
pivot_name_map = {
'CENTER':'CENTER',
'BOUNDING_BOX_CENTER':'CENTER',
'MEDIAN':'MEDIAN',
'MEDIAN_POINT':'MEDIAN',
'CURSOR':'CURSOR',
'INDIVIDUAL_ORIGINS':'INDIVIDUAL',
'ACTIVE_ELEMENT':'ACTIVE',
'WORLD':'WORLD',
'SURFACE':'SURFACE', # ?
'BOOKMARK':'BOOKMARK',
}
pivot_v3d_map = {
'CENTER':'BOUNDING_BOX_CENTER',
'MEDIAN':'MEDIAN_POINT',
'CURSOR':'CURSOR',
'INDIVIDUAL':'INDIVIDUAL_ORIGINS',
'ACTIVE':'ACTIVE_ELEMENT',
}
def __init__(self, scene, space_data, region_data, \
pivots, normal_system):
self.space_data = space_data
self.region_data = region_data
if space_data.type == 'VIEW_3D':
self.pivot_map_inv = self.pivot_v3d_map
self.tou = TransformOrientationUtility(
scene, space_data, region_data)
self.tou.normal_system = normal_system
self.pivots = pivots
# Assigned by caller (for cursor or selection)
self.source_pos = None
self.source_rot = None
self.source_scale = None
def set_orientation(self, name):
self.tou.set(name)
def set_pivot(self, pivot):
self.space_data.pivot_point = self.pivot_map_inv[pivot]
def get_pivot_name(self, name=None, relative=None, raw=False):
pivot = self.pivot_name_map[self.space_data.pivot_point]
if raw:
return pivot
if not name:
name = self.tou.get()
if relative is None:
settings = find_settings()
tfm_opts = settings.transform_options
relative = tfm_opts.use_relative_coords
if relative:
pivot = "RELATIVE"
elif (name == 'GLOBAL') or (pivot == 'WORLD'):
pivot = 'WORLD'
elif (name == "Surface") or (pivot == 'SURFACE'):
pivot = "SURFACE"
return pivot
def get_origin(self, name=None, relative=None, pivot=None):
if not pivot:
pivot = self.get_pivot_name(name, relative)
if relative or (pivot == "RELATIVE"):
# "relative" parameter overrides "pivot"
return self.source_pos
elif pivot == 'WORLD':
return Vector()
elif pivot == "SURFACE":
runtime_settings = find_runtime_settings()
return Vector(runtime_settings.surface_pos)
else:
if pivot == 'INDIVIDUAL':
pivot = 'MEDIAN'
#if pivot == 'ACTIVE':
# print(self.pivots)
try:
return self.pivots[pivot]
except:
return Vector()
def get_matrix(self, name=None, relative=None, pivot=None):
if not name:
name = self.tou.get()
matrix = self.tou.get_matrix(name)
if isinstance(pivot, Vector):
pos = pivot
else:
pos = self.get_origin(name, relative, pivot)
return to_matrix4x4(matrix, pos)
# ====== TRANSFORM ORIENTATION UTILITIES ====== #
class TransformOrientationUtility:
special_systems = {"Surface", "Scaled"}
predefined_systems = {
'GLOBAL', 'LOCAL', 'VIEW', 'NORMAL', 'GIMBAL',
"Scaled", "Surface",
}
def __init__(self, scene, v3d, rv3d):
self.scene = scene
self.v3d = v3d
self.rv3d = rv3d
self.custom_systems = [item for item in scene.orientations \
if item.name not in self.special_systems]
self.is_custom = False
self.custom_id = -1
# This is calculated elsewhere
self.normal_system = None
self.set(v3d.transform_orientation)
def get(self):
return self.transform_orientation
def get_title(self):
if self.is_custom:
return self.transform_orientation
name = self.transform_orientation
return name[:1].upper() + name[1:].lower()
def set(self, name, set_v3d=True):
if isinstance(name, int):
n = len(self.custom_systems)
if n == 0:
# No custom systems, do nothing
return
increment = name
if self.is_custom:
# If already custom, switch to next custom system
self.custom_id = (self.custom_id + increment) % n
self.is_custom = True
name = self.custom_systems[self.custom_id].name
else:
self.is_custom = name not in self.predefined_systems
if self.is_custom:
self.custom_id = next((i for i, v in \
enumerate(self.custom_systems) if v.name == name), -1)
if name in self.special_systems:
# Ensure such system exists
self.get_custom(name)
self.transform_orientation = name
if set_v3d:
self.v3d.transform_orientation = name
def get_matrix(self, name=None):
active_obj = self.scene.objects.active
if not name:
name = self.transform_orientation
if self.is_custom:
matrix = self.custom_systems[self.custom_id].matrix.copy()
else:
if (name == 'VIEW') and self.rv3d:
matrix = self.rv3d.view_rotation.to_matrix()
elif name == "Surface":
matrix = self.get_custom(name).matrix.copy()
elif (name == 'GLOBAL') or (not active_obj):
matrix = Matrix().to_3x3()
elif (name == 'NORMAL') and self.normal_system:
matrix = self.normal_system.copy()
else:
matrix = active_obj.matrix_world.to_3x3()
if name == "Scaled":
self.get_custom(name).matrix = matrix
else: # 'LOCAL', 'GIMBAL', ['NORMAL'] for now
matrix[0].normalize()
matrix[1].normalize()
matrix[2].normalize()
return matrix
def get_custom(self, name):
try:
return self.scene.orientations[name]
except:
return create_transform_orientation(
self.scene, name, Matrix())
# Is there a less cumbersome way to create transform orientation?
def create_transform_orientation(scene, name=None, matrix=None):
active_obj = scene.objects.active
prev_mode = None
if active_obj:
prev_mode = active_obj.mode
bpy.ops.object.mode_set(mode='OBJECT')
else:
bpy.ops.object.add()
# ATTENTION! This uses context's scene
bpy.ops.transform.create_orientation()
tfm_orient = scene.orientations[-1]
if name is not None:
basename = name
i = 1
while name in scene.orientations:
name = "%s.%03i" % (basename, i)
i += 1
tfm_orient.name = name
if matrix:
tfm_orient.matrix = matrix.to_3x3()
if active_obj:
bpy.ops.object.mode_set(mode=prev_mode)
else:
bpy.ops.object.delete()
return tfm_orient
# ====== VIEW UTILITY CLASS ====== #
class ViewUtility:
methods = dict(
get_locks = lambda: {},
set_locks = lambda locks: None,
get_position = lambda: Vector(),
set_position = lambda: None,
get_rotation = lambda: Quaternion(),
get_direction = lambda: Vector((0, 0, 1)),
get_viewpoint = lambda: Vector(),
get_matrix = lambda: Matrix(),
get_point = lambda xy, pos: \
Vector((xy[0], xy[1], 0)),
get_ray = lambda xy: tuple(
Vector((xy[0], xy[1], 0)),
Vector((xy[0], xy[1], 1)),
False),
)
def __init__(self, region, space_data, region_data):
self.region = region
self.space_data = space_data
self.region_data = region_data
if space_data.type == 'VIEW_3D':
self.implementation = View3DUtility(
region, space_data, region_data)
else:
self.implementation = None
if self.implementation:
for name in self.methods:
setattr(self, name,
getattr(self.implementation, name))
else:
for name, value in self.methods.items():
setattr(self, name, value)
class View3DUtility:
lock_types = {"lock_cursor": False, "lock_object": None, "lock_bone": ""}
# ====== INITIALIZATION / CLEANUP ====== #
def __init__(self, region, space_data, region_data):
self.region = region
self.space_data = space_data
self.region_data = region_data
# ====== GET VIEW MATRIX AND ITS COMPONENTS ====== #
def get_locks(self):
v3d = self.space_data
return {k:getattr(v3d, k) for k in self.lock_types}
def set_locks(self, locks):
v3d = self.space_data
for k in self.lock_types:
setattr(v3d, k, locks.get(k, self.lock_types[k]))
def _get_lock_obj_bone(self):
v3d = self.space_data
obj = v3d.lock_object
if not obj:
return None, None
if v3d.lock_bone:
try:
# this is not tested!
if obj.mode == 'EDIT':
bone = obj.data.edit_bones[v3d.lock_bone]
else:
bone = obj.data.bones[v3d.lock_bone]
except:
bone = None
else:
bone = None
return obj, bone
# TODO: learn how to get these values from
# rv3d.perspective_matrix and rv3d.view_matrix ?
def get_position(self, no_locks=False):
v3d = self.space_data
rv3d = self.region_data
if no_locks:
return rv3d.view_location.copy()
# rv3d.perspective_matrix and rv3d.view_matrix
# seem to have some weird translation components %)
if rv3d.view_perspective == 'CAMERA':
p = v3d.camera.matrix_world.to_translation()
d = self.get_direction()
return p + d * rv3d.view_distance
else:
if v3d.lock_object:
obj, bone = self._get_lock_obj_bone()
if bone:
return (obj.matrix_world * bone.matrix).to_translation()
else:
return obj.matrix_world.to_translation()
elif v3d.lock_cursor:
return get_cursor_location(v3d=v3d)
else:
return rv3d.view_location.copy()
def set_position(self, pos, no_locks=False):
v3d = self.space_data
rv3d = self.region_data
pos = pos.copy()
if no_locks:
rv3d.view_location = pos
return
if rv3d.view_perspective == 'CAMERA':
d = self.get_direction()
v3d.camera.matrix_world.translation = pos - d * rv3d.view_distance
else:
if v3d.lock_object:
obj, bone = self._get_lock_obj_bone()
if bone:
try:
bone.matrix.translation = \
obj.matrix_world.inverted() * pos
except:
# this is some degenerate object
bone.matrix.translation = pos
else:
obj.matrix_world.translation = pos
elif v3d.lock_cursor:
set_cursor_location(pos, v3d=v3d)
else:
rv3d.view_location = pos
def get_rotation(self):
v3d = self.space_data
rv3d = self.region_data
if rv3d.view_perspective == 'CAMERA':
return v3d.camera.matrix_world.to_quaternion()
else:
return rv3d.view_rotation
def get_direction(self):
# Camera (as well as viewport) looks in the direction of -Z;
# Y is up, X is left
d = self.get_rotation() * Vector((0, 0, -1))
d.normalize()
return d
def get_viewpoint(self):
v3d = self.space_data
rv3d = self.region_data
if rv3d.view_perspective == 'CAMERA':
return v3d.camera.matrix_world.to_translation()
else:
p = self.get_position()
d = self.get_direction()
return p - d * rv3d.view_distance
def get_matrix(self):
m = self.get_rotation().to_matrix()
m.resize_4x4()
m.translation = self.get_viewpoint()
return m
def get_point(self, xy, pos):
region = self.region
rv3d = self.region_data
return region_2d_to_location_3d(region, rv3d, xy, pos)
def get_ray(self, xy):
region = self.region
v3d = self.space_data
rv3d = self.region_data
viewPos = self.get_viewpoint()
viewDir = self.get_direction()
near = viewPos + viewDir * v3d.clip_start
far = viewPos + viewDir * v3d.clip_end
a = region_2d_to_location_3d(region, rv3d, xy, near)
b = region_2d_to_location_3d(region, rv3d, xy, far)
# When viewed from in-scene camera, near and far
# planes clip geometry even in orthographic mode.
clip = rv3d.is_perspective or (rv3d.view_perspective == 'CAMERA')
return a, b, clip
# ====== SNAP UTILITY CLASS ====== #
class SnapUtility:
def __init__(self, context):
if context.area.type == 'VIEW_3D':
v3d = context.space_data
shade = v3d.viewport_shade
self.implementation = Snap3DUtility(context.scene, shade)
self.implementation.update_targets(
context.visible_objects, [])
def dispose(self):
self.implementation.dispose()
def update_targets(self, to_include, to_exclude):
self.implementation.update_targets(to_include, to_exclude)
def set_modes(self, **kwargs):
return self.implementation.set_modes(**kwargs)
def snap(self, *args, **kwargs):
return self.implementation.snap(*args, **kwargs)
class SnapUtilityBase:
def __init__(self):
self.targets = set()
# TODO: set to current blend settings?
self.interpolation = 'NEVER'
self.editmode = False
self.snap_type = None
self.projection = [None, None, None]
self.potential_snap_elements = None
self.extra_snap_points = None
def update_targets(self, to_include, to_exclude):
self.targets.update(to_include)
self.targets.difference_update(to_exclude)
def set_modes(self, **kwargs):
if "use_relative_coords" in kwargs:
self.use_relative_coords = kwargs["use_relative_coords"]
if "interpolation" in kwargs:
# NEVER, ALWAYS, SMOOTH
self.interpolation = kwargs["interpolation"]
if "editmode" in kwargs:
self.editmode = kwargs["editmode"]
if "snap_align" in kwargs:
self.snap_align = kwargs["snap_align"]
if "snap_type" in kwargs:
# 'INCREMENT', 'VERTEX', 'EDGE', 'FACE', 'VOLUME'
self.snap_type = kwargs["snap_type"]
if "axes_coords" in kwargs:
# none, point, line, plane
self.axes_coords = kwargs["axes_coords"]
# ====== CURSOR REPOSITIONING ====== #
def snap(self, xy, src_matrix, initial_matrix, do_raycast, \
alt_snap, vu, csu, modify_Surface, use_object_centers):
v3d = csu.space_data
grid_step = self.grid_step(alt_snap, v3d)
su = self
use_relative_coords = su.use_relative_coords
snap_align = su.snap_align
axes_coords = su.axes_coords
snap_type = su.snap_type
runtime_settings = find_runtime_settings()
matrix = src_matrix.to_3x3()
pos = src_matrix.to_translation().copy()
sys_matrix = csu.get_matrix()
if use_relative_coords:
sys_matrix.translation = initial_matrix.translation.copy()
# Axes of freedom and line/plane parameters
start = Vector(((0 if v is None else v) for v in axes_coords))
direction = Vector(((v is not None) for v in axes_coords))
axes_of_freedom = 3 - int(sum(direction))
# do_raycast is False when mouse is not moving
if do_raycast:
su.hide_bbox(True)
self.potential_snap_elements = None
self.extra_snap_points = None
set_stick_obj(csu.tou.scene, None)
raycast = None
snap_to_obj = (snap_type != 'INCREMENT') #or use_object_centers
snap_to_obj = snap_to_obj and (snap_type is not None)
if snap_to_obj:
a, b, clip = vu.get_ray(xy)
view_dir = vu.get_direction()
raycast = su.snap_raycast(a, b, clip, view_dir, csu, alt_snap)
if raycast:
surf_matrix, face_id, obj, orig_obj = raycast
if not use_object_centers:
self.potential_snap_elements = [
(obj.matrix_world * obj.data.vertices[vi].co)
for vi in obj.data.polygons[face_id].vertices
]
if use_object_centers:
self.extra_snap_points = \
[obj.matrix_world.to_translation()]
elif alt_snap:
pse = self.potential_snap_elements
n = len(pse)
if self.snap_type == 'EDGE':
self.extra_snap_points = []
for i in range(n):
v0 = pse[i]
v1 = pse[(i + 1) % n]
self.extra_snap_points.append((v0 + v1) / 2)
elif self.snap_type == 'FACE':
self.extra_snap_points = []
v0 = Vector()
for v1 in pse:
v0 += v1
self.extra_snap_points.append(v0 / n)
if snap_align:
matrix = surf_matrix.to_3x3()
if not use_object_centers:
pos = surf_matrix.to_translation()
else:
pos = orig_obj.matrix_world.to_translation()
try:
local_pos = orig_obj.matrix_world.inverted() * pos
except:
# this is some degenerate object
local_pos = pos
set_stick_obj(csu.tou.scene, orig_obj.name, local_pos)
modify_Surface = modify_Surface and \
(snap_type != 'VOLUME') and (not use_object_centers)
# === Update "Surface" orientation === #
if modify_Surface:
# Use raycast[0], not matrix! If snap_align == False,
# matrix will be src_matrix!
coordsys = csu.tou.get_custom("Surface")
coordsys.matrix = surf_matrix.to_3x3()
runtime_settings.surface_pos = pos
if csu.tou.get() == "Surface":
sys_matrix = to_matrix4x4(matrix, pos)
else:
if axes_of_freedom == 0:
# Constrained in all axes, can't move.
pass
elif axes_of_freedom == 3:
# Not constrained, move in view plane.
pos = vu.get_point(xy, pos)
else:
a, b, clip = vu.get_ray(xy)
view_dir = vu.get_direction()
start = sys_matrix * start
if axes_of_freedom == 1:
direction = Vector((1, 1, 1)) - direction
direction.rotate(sys_matrix)
if axes_of_freedom == 2:
# Constrained in one axis.
# Find intersection with plane.
i_p = intersect_line_plane(a, b, start, direction)
if i_p is not None:
pos = i_p
elif axes_of_freedom == 1:
# Constrained in two axes.
# Find nearest point to line.
i_p = intersect_line_line(a, b, start,
start + direction)
if i_p is not None:
pos = i_p[1]
#end if do_raycast
try:
sys_matrix_inv = sys_matrix.inverted()
except:
# this is some degenerate system
sys_matrix_inv = Matrix()
_pos = sys_matrix_inv * pos
# don't snap when mouse hasn't moved
if (snap_type == 'INCREMENT') and do_raycast:
for i in range(3):
_pos[i] = round_step(_pos[i], grid_step)
for i in range(3):
if axes_coords[i] is not None:
_pos[i] = axes_coords[i]
if (snap_type == 'INCREMENT') or (axes_of_freedom != 3):
pos = sys_matrix * _pos
res_matrix = to_matrix4x4(matrix, pos)
CursorDynamicSettings.local_matrix = \
sys_matrix_inv * res_matrix
return res_matrix
class Snap3DUtility(SnapUtilityBase):
@staticmethod
def grid_step(flags, v3d):
f = 1.0 / v3d.grid_subdivisions
s = v3d.grid_scale
if flags[0]: s *= f
if flags[1]: s *= f*f
return s
cube_verts = [Vector((i, j, k))
for i in (-1, 1)
for j in (-1, 1)
for k in (-1, 1)]
def __init__(self, scene, shade):
SnapUtilityBase.__init__(self)
convert_types = {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}
self.cache = MeshCache(scene, convert_types)
# ? seems that dict is enough
self.bbox_cache = {}#collections.OrderedDict()
self.sys_matrix_key = [0.0] * 9
bm = prepare_gridbox_mesh(subdiv=2)
mesh = bpy.data.meshes.new(tmp_name)
bm.to_mesh(mesh)
mesh.update(calc_tessface=True)
#mesh.calc_tessface()
self.bbox_obj = self.cache._make_obj(mesh, None)
self.bbox_obj.hide = True
self.bbox_obj.draw_type = 'WIRE'
self.bbox_obj.name = "BoundBoxSnap"
self.shade_bbox = (shade == 'BOUNDBOX')
def update_targets(self, to_include, to_exclude):
settings = find_settings()
tfm_opts = settings.transform_options
only_solid = tfm_opts.snap_only_to_solid
# Ensure this is a set and not some other
# type of collection
to_exclude = set(to_exclude)
for target in to_include:
if only_solid and ((target.draw_type == 'BOUNDS') \
or (target.draw_type == 'WIRE')):
to_exclude.add(target)
SnapUtilityBase.update_targets(self, to_include, to_exclude)
def dispose(self):
self.hide_bbox(True)
mesh = self.bbox_obj.data
bpy.data.objects.remove(self.bbox_obj)
bpy.data.meshes.remove(mesh)
self.cache.clear()
def hide_bbox(self, hide):
if self.bbox_obj.hide == hide:
return
self.bbox_obj.hide = hide
# We need to unlink bbox until required to show it,
# because otherwise outliner will blink each
# time cursor is clicked
if hide:
self.cache.scene.objects.unlink(self.bbox_obj)
else:
self.cache.scene.objects.link(self.bbox_obj)
def get_bbox_obj(self, obj, sys_matrix, sys_matrix_inv, is_local):
if is_local:
bbox = None
else:
bbox = self.bbox_cache.get(obj, None)
if bbox is None:
m = obj.matrix_world
if is_local:
sys_matrix = m.copy()
try:
sys_matrix_inv = sys_matrix.inverted()
except Exception:
# this is some degenerate system
sys_matrix_inv = Matrix()
m_combined = sys_matrix_inv * m
bbox = [None, None]
variant = ('RAW' if (self.editmode and
(obj.type == 'MESH') and (obj.mode == 'EDIT'))
else 'PREVIEW')
mesh_obj = self.cache.get(obj, variant, reuse=False)
if (mesh_obj is None) or self.shade_bbox or \
(obj.draw_type == 'BOUNDS'):
if is_local:
bbox = [(-1, -1, -1), (1, 1, 1)]
else:
for p in self.cube_verts:
extend_bbox(bbox, m_combined * p.copy())
elif is_local:
bbox = [mesh_obj.bound_box[0], mesh_obj.bound_box[6]]
else:
for v in mesh_obj.data.vertices:
extend_bbox(bbox, m_combined * v.co.copy())
bbox = (Vector(bbox[0]), Vector(bbox[1]))
if not is_local:
self.bbox_cache[obj] = bbox
half = (bbox[1] - bbox[0]) * 0.5
m = MatrixCompose(half[0], half[1], half[2])
m = sys_matrix.to_3x3() * m
m.resize_4x4()
m.translation = sys_matrix * (bbox[0] + half)
self.bbox_obj.matrix_world = m
return self.bbox_obj
# TODO: ?
# - Sort snap targets according to raycasted distance?
# - Ignore targets if their bounding sphere is further
# than already picked position?
# Perhaps these "optimizations" aren't worth the overhead.
def raycast(self, a, b, clip, view_dir, is_bbox, \
sys_matrix, sys_matrix_inv, is_local, x_ray):
# If we need to interpolate normals or snap to
# vertices/edges, we must convert mesh.
#force = (self.interpolation != 'NEVER') or \
# (self.snap_type in {'VERTEX', 'EDGE'})
# Actually, we have to always convert, since
# we need to get face at least to find tangential.
force = True
edit = self.editmode
res = None
L = None
for obj in self.targets:
orig_obj = obj
if obj.name == self.bbox_obj.name:
# is there a better check?
# ("a is b" doesn't work here)
continue
if obj.show_x_ray != x_ray:
continue
if is_bbox:
obj = self.get_bbox_obj(obj, \
sys_matrix, sys_matrix_inv, is_local)
elif obj.draw_type == 'BOUNDS':
# Outside of BBox, there is no meaningful visual snapping
# for such display mode
continue
m = obj.matrix_world.copy()
try:
mi = m.inverted()
except:
# this is some degenerate object
continue
la = mi * a
lb = mi * b
# Bounding sphere check (to avoid unnecesary conversions
# and to make ray 'infinite')
bb_min = Vector(obj.bound_box[0])
bb_max = Vector(obj.bound_box[6])
c = (bb_min + bb_max) * 0.5
r = (bb_max - bb_min).length * 0.5
sec = intersect_line_sphere(la, lb, c, r, False)
if sec[0] is None:
continue # no intersection with the bounding sphere
if not is_bbox:
# Ensure we work with raycastable object.
variant = ('RAW' if (edit and
(obj.type == 'MESH') and (obj.mode == 'EDIT'))
else 'PREVIEW')
obj = self.cache.get(obj, variant, reuse=(not force))
if (obj is None) or (not obj.data.polygons):
continue # the object has no raycastable geometry
# If ray must be infinite, ensure that
# endpoints are outside of bounding volume
if not clip:
# Seems that intersect_line_sphere()
# returns points in flipped order
lb, la = sec
def ray_cast(obj, la, lb):
if bpy.app.version < (2, 77, 0):
# Object.ray_cast(start, end)
# returns (location, normal, index)
res = obj.ray_cast(la, lb)
return ((res[-1] >= 0), res[0], res[1], res[2])
else:
# Object.ray_cast(origin, direction, [distance])
# returns (result, location, normal, index)
ld = lb - la
return obj.ray_cast(la, ld, ld.magnitude)
# Does ray actually intersect something?
try:
success, lp, ln, face_id = ray_cast(obj, la, lb)
except Exception as e:
# Somewhy this seems to happen when snapping cursor
# in Local View mode at least since r55223:
# <