Newer
Older
# ***** 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 <http://www.gnu.org/licenses/>.
#
# ***** END GPL LICENSE BLOCK *****
# <pep8-80 compliant>
bl_info = {
"name": "Enhanced 3D Cursor",
"description": "Cursor history and bookmarks; drag/snap cursor.",
"author": "dairin0d",
"version": (3, 0, 7),
"location": "View3D > Action mouse; F10; Properties panel",
"warning": "",
"wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
"tracker_url": "https://github.com/dairin0d/enhanced-3d-cursor/issues",
Dima Glib
committed
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.
Dima Glib
committed
strip trailing space? (one of campbellbarton's commits did that)
CoDEmanX
committed
Dima Glib
committed
- 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?
Dima Glib
committed
- user preferences? (stored in a file)
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
- 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
Dima Glib
committed
import bmesh
from mathutils import Vector, Matrix, Quaternion, Euler
from mathutils.geometry import (intersect_line_sphere,
intersect_ray_tri,
barycentric_transform,
Dima Glib
committed
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"
CoDEmanX
committed
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':"*",
}
CoDEmanX
committed
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",
}
CoDEmanX
committed
key_pivot_map = {
'H':'ACTIVE',
'U':'CURSOR',
'I':'INDIVIDUAL',
'O':'CENTER',
'P':'MEDIAN',
}
CoDEmanX
committed
key_snap_map = {
'C':'INCREMENT',
'V':'VERTEX',
'E':'EDGE',
'F':'FACE',
}
CoDEmanX
committed
key_tfm_mode_map = {
'G':'MOVE',
'R':'ROTATE',
'S':'SCALE',
}
CoDEmanX
committed
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'},
}
CoDEmanX
committed
CoDEmanX
committed
angle_grid_steps = {True:1.0, False:5.0}
scale_grid_steps = {True:0.01, False:0.1}
CoDEmanX
committed
# ====== 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))
CoDEmanX
committed
def modal(self, context, event):
context.area.tag_redraw()
return self.try_process_input(context, event)
CoDEmanX
committed
def invoke(self, context, event):
# Attempt to launch the monitor
if bpy.ops.view3d.cursor3d_monitor.poll():
bpy.ops.view3d.cursor3d_monitor()
CoDEmanX
committed
# 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'}
CoDEmanX
committed
CursorDynamicSettings.active_transform_operator = self
CoDEmanX
committed
CoDEmanX
committed
settings = find_settings()
tfm_opts = settings.transform_options
CoDEmanX
committed
settings_scene = context.scene.cursor_3d_tools_settings
CoDEmanX
committed
Dima Glib
committed
self.setup_keymaps(context, event)
CoDEmanX
committed
# Coordinate System Utility
self.particles, self.csu = gather_particles(context=context)
self.particles = [View3D_Cursor(context)]
CoDEmanX
committed
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()
CoDEmanX
committed
# View3D Utility
self.vu = ViewUtility(context.region, context.space_data,
context.region_data)
CoDEmanX
committed
# Snap Utility
self.su = SnapUtility(context)
CoDEmanX
committed
# 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({})
CoDEmanX
committed
# 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)
CoDEmanX
committed
self.click_start = time.time()
if not self.initiated_by_mouse:
self.click_start -= self.click_period
CoDEmanX
committed
self.stick_obj_name = settings_scene.stick_obj_name
self.stick_obj_pos = settings_scene.stick_obj_pos
CoDEmanX
committed
# Initial run
self.try_process_input(context, event, True)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
CoDEmanX
committed
def cancel(self, context):
for particle in self.particles:
particle.revert()
CoDEmanX
committed
set_stick_obj(context.scene, self.stick_obj_name, self.stick_obj_pos)
CoDEmanX
committed
CoDEmanX
committed
# ====== CLEANUP/FINALIZE ====== #
def finalize(self, context):
# restore view locking
self.vu.set_locks(self.view_locks)
self.vu.set_position(self.view_pos, True)
CoDEmanX
committed
CoDEmanX
committed
# 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
CoDEmanX
committed
# Ensure there are no leftovers from draw_callback
context.area.tag_redraw()
CoDEmanX
committed
CoDEmanX
committed
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
CoDEmanX
committed
CursorDynamicSettings.active_transform_operator = None
CoDEmanX
committed
Dima Glib
committed
def setup_keymaps(self, context, event=None):
CoDEmanX
committed
# There is no such event as 'ACTIONMOUSE',
# it's always 'LEFTMOUSE' or 'RIGHTMOUSE'
Dima Glib
committed
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:
keyconfig = wm.keyconfigs.active
select_mouse = getattr(keyconfig.preferences, "select_mouse", "LEFT")
Dima Glib
committed
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'}
CoDEmanX
committed
# Use user-defined "free mouse" key, if it exists
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
CoDEmanX
committed
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
CoDEmanX
committed
def process_input(self, context, event, initial_run=False):
wm = context.window_manager
v3d = context.space_data
CoDEmanX
committed
if event.type in self.key_map["confirm"]:
if self.free_mouse:
finished = (event.value == 'PRESS')
else:
finished = (event.value == 'RELEASE')
CoDEmanX
committed
if finished:
return self.finalize(context)
CoDEmanX
committed
dairin0d
committed
self.cancel(context)
return {'CANCELLED'}
CoDEmanX
committed
CoDEmanX
committed
settings = find_settings()
tfm_opts = settings.transform_options
CoDEmanX
committed
make_snapshot = False
tangential_snapshot = False
CoDEmanX
committed
if event.value == 'PRESS':
if event.type in self.key_map["free_mouse"]:
Sebastian Nell
committed
if self.free_mouse and not initial_run:
# confirm if pressed second time
return self.finalize(context)
else:
self.free_mouse = True
CoDEmanX
committed
if event.type in self.key_tfm_mode_map:
new_mode = self.key_tfm_mode_map[event.type]
CoDEmanX
committed
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)
CoDEmanX
committed
CoDEmanX
committed
if event.type in self.key_map["make_normal_snapshot"]:
make_snapshot = True
tangential_snapshot = False
CoDEmanX
committed
if event.type in self.key_map["make_tangential_snapshot"]:
make_snapshot = True
tangential_snapshot = True
CoDEmanX
committed
if event.type in self.key_map["snap_to_raw_mesh"]:
tool_settings.use_snap_self = \
not tool_settings.use_snap_self
CoDEmanX
committed
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]
CoDEmanX
committed
if self.allowed_axes != axis_lock:
self.allowed_axes = axis_lock
else:
self.allowed_axes = [True, True, True]
CoDEmanX
committed
if event.type in self.key_map["use_absolute_coords"]:
tfm_opts.use_relative_coords = \
not tfm_opts.use_relative_coords
CoDEmanX
committed
CoDEmanX
committed
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
CoDEmanX
committed
if incr != 0:
self.current_axis = (self.current_axis + incr) % 3
self.caret_pos = len(self.axes_values[self.current_axis])
CoDEmanX
committed
incr = 0
if event.type in self.key_map["precision_up"]:
incr = 1
elif event.type in self.key_map["precision_down"]:
incr = -1
CoDEmanX
committed
if incr != 0:
settings.free_coord_precision += incr
self.coord_format = "{:." + \
str(settings.free_coord_precision) + "f}"
CoDEmanX
committed
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
CoDEmanX
committed
Dima Glib
committed
jc = (", " if tfm_opts.use_comma_separator else "\t")
Dima Glib
committed
wm.clipboard = jc.join(self.get_axes_text(True))
elif event.type in self.key_map["cut_axes"]:
Dima Glib
committed
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"]:
Dima Glib
committed
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)
CoDEmanX
committed
if event.type in self.key_map["use_object_centers"]:
v3d.use_pivot_point_align = not v3d.use_pivot_point_align
CoDEmanX
committed
if event.type in self.key_pivot_map:
self.csu.set_pivot(self.key_pivot_map[event.type])
CoDEmanX
committed
CoDEmanX
committed
if event.ctrl:
self.snap_to_system_origin(force_pivot=True)
CoDEmanX
committed
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
CoDEmanX
committed
use_snap = (tool_settings.use_snap != event.ctrl)
if use_snap:
snap_type = tool_settings.snap_element
else:
userprefs_input = context.preferences.input
if userprefs_input.use_mouse_depth_cursor:
# Suggested by Lissanro in the forum
use_snap = True
snap_type = 'FACE'
CoDEmanX
committed
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
CoDEmanX
committed
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,
)
CoDEmanX
committed
self.do_raycast = ("MOUSE" in event.type)
self.grid_substep = event.shift
self.modify_surface_orientation = (len(self.particles) == 1)
self.xy = Vector((event.mouse_region_x, event.mouse_region_y))
CoDEmanX
committed
self.use_object_centers = v3d.use_pivot_point_align
CoDEmanX
committed
if event.type == 'MOUSEMOVE':
self.update_transform_mousemove()
CoDEmanX
committed
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
CoDEmanX
committed
for particle in self.particles:
transform_func(particle)
CoDEmanX
committed
self.make_normal_snapshot(context.collection, tangential_snapshot)
CoDEmanX
committed
CoDEmanX
committed
def update_origin_projection(self, context):
r = context.region
rv3d = context.region_data
CoDEmanX
committed
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))
CoDEmanX
committed
self.delta_xy = (self.start_xy - self.origin_xy).to_3d()
self.prev_delta_xy = self.delta_xy
CoDEmanX
committed
def init_xy_angle_distance(self, context, event):
self.start_xy = Vector((event.mouse_region_x, event.mouse_region_y))
CoDEmanX
committed
CoDEmanX
committed
# 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]
CoDEmanX
committed
def update_transform_mousemove(self):
delta_xy = (self.xy - self.origin_xy).to_3d()
CoDEmanX
committed
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])
CoDEmanX
committed
CoDEmanX
committed
i_allowed = 0
for i in range(3):
if self.allowed_axes[i]:
i_allowed = i
CoDEmanX
committed
view_dir = self.vu.get_direction()
if view_dir.dot(sys_matrix[i_allowed][:3]) < 0:
offset = -offset
CoDEmanX
committed
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)
CoDEmanX
committed
if self.grid_substep:
offset *= 0.1
else:
offset *= 0.5
CoDEmanX
committed
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])
CoDEmanX
committed
CoDEmanX
committed
CoDEmanX
committed
try:
view_dir = sys_matrix.inverted().to_3x3() * view_dir
except:
# this is some degenerate system
pass
CoDEmanX
committed
CoDEmanX
committed
matrix = Euler(self.raw_angles, 'XYZ').to_matrix()
matrix.rotate(rot)
CoDEmanX
committed
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])
CoDEmanX
committed
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
CoDEmanX
committed
CoDEmanX
committed
global set_cursor_location__reset_stick
CoDEmanX
committed
src_matrix = particle.get_matrix()
initial_matrix = particle.get_initial_matrix()
CoDEmanX
committed
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)
CoDEmanX
committed
set_cursor_location__reset_stick = False
set_cursor_location__reset_stick = True
CoDEmanX
committed
def rotate_matrix(self, matrix):
sys_matrix = self.csu.get_matrix()
CoDEmanX
committed
try:
matrix = sys_matrix.inverted() * matrix
except:
# this is some degenerate system
pass
CoDEmanX
committed
# Blender's order of rotation [in local axes]
rotation_order = [2, 1, 0]
CoDEmanX
committed
# Seems that 4x4 matrix cannot be rotated using rotate() ?
sys_matrix3 = sys_matrix.to_3x3()
CoDEmanX
committed
for i in range(3):
j = rotation_order[i]
axis = sys_matrix3[j]
angle = self.angles[j]
CoDEmanX
committed
rot = angle_axis_to_quat(angle, axis)
# this seems to be buggy too
#rot = Matrix.Rotation(angle, 3, axis)
CoDEmanX
committed
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)
CoDEmanX
committed
for i in range(3):
sys_matrix[i][:3] = sys_matrix3[i]
CoDEmanX
committed
CoDEmanX
committed
CoDEmanX
committed
def transform_rotate(self, particle):
grid_step = self.angle_grid_steps[self.grid_substep]
grid_step *= (math.pi / 180.0)
CoDEmanX
committed
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)
CoDEmanX
committed
CoDEmanX
committed
if self.su.implementation.snap_type == 'INCREMENT':
for i in range(3):
self.angles[i] = round_step(self.angles[i], grid_step)
CoDEmanX
committed
initial_matrix = particle.get_initial_matrix()
matrix = self.rotate_matrix(initial_matrix)
CoDEmanX
committed
CoDEmanX
committed
def scale_matrix(self, matrix):
sys_matrix = self.csu.get_matrix()
CoDEmanX
committed
try:
matrix = sys_matrix.inverted() * matrix
except:
# this is some degenerate system
pass
CoDEmanX
committed
for i in range(3):
sys_matrix[i] *= self.scales[i]
CoDEmanX
committed
CoDEmanX
committed
CoDEmanX
committed
def transform_scale(self, particle):
grid_step = self.scale_grid_steps[self.grid_substep]
CoDEmanX
committed
for i in range(3):
if self.axes_values[i] and self.axes_eval_success[i]:
self.scales[i] = self.axes_coords[i]
CoDEmanX
committed
if self.su.implementation.snap_type == 'INCREMENT':
for i in range(3):
self.scales[i] = round_step(self.scales[i], grid_step)
CoDEmanX
committed
initial_matrix = particle.get_initial_matrix()
matrix = self.scale_matrix(initial_matrix)
CoDEmanX
committed
CoDEmanX
committed
def set_axis_input(self, axis_id, axis_val):
if axis_val == self.axes_values[axis_id]:
return
CoDEmanX
committed
CoDEmanX
committed
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
CoDEmanX
committed
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")
CoDEmanX
committed
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)
CoDEmanX
committed
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)
CoDEmanX
committed
CoDEmanX
committed
def get_axes_text(self, offset=False):
axes_values = self.get_axes_values(as_string=True)
CoDEmanX
committed
axes_text = []
for i in range(3):
j = i
if offset:
j = (i + self.current_axis) % 3
CoDEmanX
committed
CoDEmanX
committed
CoDEmanX
committed
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(",", ".") # ???
CoDEmanX
committed
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])
CoDEmanX
committed
def process_axis_input(self, event):
axis_id = self.current_axis
axis_val = self.axes_values[axis_id]
CoDEmanX
committed
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
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
CoDEmanX
committed
self.caret_pos = min(max(self.caret_pos, 0), len(axis_val))
CoDEmanX
committed
CoDEmanX
committed
# ====== 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
CoDEmanX
committed
def gizmo_scale(self, pos):
return self.gizmo_distance(pos) * self.gizmo_factor
CoDEmanX
committed
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
CoDEmanX
committed
def draw_3d(self, context):
if self.check_v3d_local(context):
return
CoDEmanX
committed
if time.time() < (self.click_start + self.click_period):
return
CoDEmanX
committed
settings = find_settings()
tfm_opts = settings.transform_options
CoDEmanX
committed
initial_matrix = self.particles[0].get_initial_matrix()
CoDEmanX
committed
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()
CoDEmanX
committed
if self.is_normal_visible():
p0, x, y, z, _x, _z = \
self.get_normal_params(tfm_opts, dest_point)
CoDEmanX
committed
# use theme colors?
#ThemeView3D.normal
#ThemeView3D.vertex_normal
CoDEmanX
committed
CoDEmanX
committed
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)
CoDEmanX
committed
bgl.glEnable(bgl.GL_BLEND)
bgl.glDisable(bgl.GL_DEPTH_TEST)
CoDEmanX
committed
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)
CoDEmanX
committed
if settings.draw_guides:
p0 = dest_point
try:
p00 = sys_matrix.inverted() * p0
except:
# this is some degenerate system
p00 = p0.copy()
CoDEmanX
committed
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)),
]
CoDEmanX
committed
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)
CoDEmanX
committed
# line from origin to cursor
p0 = sys_origin
p1 = dest_point
CoDEmanX
committed
bgl.glEnable(bgl.GL_LINE_STIPPLE)
bgl.glColor4f(1, 1, 0, 1)
CoDEmanX
committed
draw_line_hidden_depth(p0, p1, (1, 1, 0), 1.0, 0.5, True, True)
CoDEmanX
committed
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)
CoDEmanX
committed
bgl.glEnable(bgl.GL_BLEND)
bgl.glDisable(bgl.GL_DEPTH_TEST)
CoDEmanX
committed
bgl.glLineWidth(2)
bgl.glColor4f(0, 0, 1, 0.5)
CoDEmanX
committed
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)
CoDEmanX
committed
CoDEmanX
committed
Dima Glib
committed
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()
CoDEmanX
committed
if self.check_v3d_local(context):
return
CoDEmanX
committed
r = context.region
rv3d = context.region_data
CoDEmanX
committed
CoDEmanX
committed
if settings.draw_snap_elements:
sui = self.su.implementation
CoDEmanX
committed
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)
CoDEmanX
committed
if snap_points:
bgl.glEnable(bgl.GL_BLEND)
CoDEmanX
committed
bgl.glPointSize(5)
bgl.glColor4f(1, 0, 0, 0.5)
CoDEmanX
committed
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()
CoDEmanX
committed
CoDEmanX
committed
if self.transform_mode == 'MOVE':
return
CoDEmanX
committed
CoDEmanX
committed
CoDEmanX
committed
bgl.glColor4f(0, 0, 0, 1)
draw_line_2d(self.origin_xy, self.xy)
CoDEmanX
committed
CoDEmanX
committed
line_width = 3
bgl.glLineWidth(line_width)
CoDEmanX
committed
L = 12.0
arrow_len = 6.0
arrow_width = 8.0
arrow_space = 5.0
CoDEmanX
committed
Lmax = arrow_space * 2 + L * 2 + line_width
CoDEmanX
committed
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]))
CoDEmanX
committed
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)
CoDEmanX
committed
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)
CoDEmanX
committed
CoDEmanX
committed
def draw_axes_coords(self, context, header_size):
if self.check_v3d_local(context):
return
CoDEmanX
committed
if time.time() < (self.click_start + self.click_period):
return
CoDEmanX
committed
CoDEmanX
committed
userprefs_view = context.preferences.view
CoDEmanX
committed
CoDEmanX
committed
settings = find_settings()
tfm_opts = settings.transform_options
CoDEmanX
committed
localmat = CursorDynamicSettings.local_matrix
CoDEmanX
committed
CoDEmanX
committed
font_size = 11
blf.size(font_id, font_size, 72) # font, point size, dpi
CoDEmanX
committed
tet = context.preferences.themes[0].text_editor
CoDEmanX
committed
# 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"]
CoDEmanX
committed
axis_cells = []
coord_cells = []
#caret_cell = TextCell("_", tet.cursor)
caret_cell = TextCell("|", tet.cursor)
CoDEmanX
committed
CoDEmanX
committed
Dima Glib
committed
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))
CoDEmanX
committed
if self.axes_values[i]:
if self.axes_eval_success[i]:
color = tet.syntax_numbers
else:
color = tet.syntax_string
else:
Dima Glib
committed
color = tet.space.text
text = axes_text[i]
coord_cells.append(TextCell(text, color))
except Exception as e:
print(repr(e))
CoDEmanX
committed
CoDEmanX
committed
try:
snap_type = self.su.implementation.snap_type
if snap_type is None:
Dima Glib
committed
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))
CoDEmanX
committed
Dima Glib
committed
color = tet.space.text
else:
color = tet.syntax_builtin
text = self.csu.tou.get_title()
mode_cells.append(TextCell(text, color))
CoDEmanX
committed
Dima Glib
committed
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))
CoDEmanX
committed
CoDEmanX
committed
try:
xyz_x_start_min = 12
xyz_x_start = xyz_x_start_min
mode_x_start = 6
CoDEmanX
committed
mode_margin = 4
xyz_margin = 16
blend_margin = 32
CoDEmanX
committed
Dima Glib
committed
color = tet.space.back
bgl.glColor4f(color[0], color[1], color[2], 1.0)
draw_rect(0, 0, hdr_w, hdr_h)
CoDEmanX
committed
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)
CoDEmanX
committed
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)
CoDEmanX
committed
curr_axis_x_start = 0
curr_axis_x_end = 0
caret_x = 0
CoDEmanX
committed
xyz_width = 0
for i in range(3):
if i == self.current_axis:
CoDEmanX
committed
CoDEmanX
committed
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
CoDEmanX
committed
CoDEmanX
committed
CoDEmanX
committed
CoDEmanX
committed
xyz_width = int(xyz_width)
xyz_width_ext = xyz_width + blend_margin
CoDEmanX
committed
offset = (xyz_x_start + curr_axis_x_end) - hdr_w
if offset > 0:
xyz_x_start -= offset
CoDEmanX
committed
offset = xyz_x_start_min - (xyz_x_start + curr_axis_x_start)
if offset > 0:
xyz_x_start += offset
CoDEmanX
committed
offset = (xyz_x_start + caret_x) - hdr_w
if offset > 0:
xyz_x_start -= offset
CoDEmanX
committed
# 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)
Dima Glib
committed
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()
CoDEmanX
committed
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
CoDEmanX
committed
cell = coord_cells[i]
cell.draw(x, y, (0, 0.5))
x += (cell.w + xyz_margin)
CoDEmanX
committed
caret_x -= blf.dimensions(font_id, caret_cell.text)[0] * 0.5
caret_cell.draw(xyz_x_start + caret_x, y, (0, 0.5))
CoDEmanX
committed
bgl.glEnable(bgl.GL_BLEND)
bgl.glShadeModel(bgl.GL_SMOOTH)
gl_enable(bgl.GL_SMOOTH, True)
Dima Glib
committed
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()
CoDEmanX
committed
print(repr(e))
CoDEmanX
committed
CoDEmanX
committed
# ====== NORMAL SNAPSHOT ====== #
def is_normal_visible(self):
if self.csu.tou.get() == "Surface":
return True
CoDEmanX
committed
if self.use_object_centers:
return False
CoDEmanX
committed
return self.su.implementation.snap_type \
not in {None, 'INCREMENT', 'VOLUME'}
CoDEmanX
committed
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()
CoDEmanX
committed
m3 = surf_matrix.to_3x3()
p0 = surf_origin
scl = self.gizmo_scale(p0)
CoDEmanX
committed
# 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()
CoDEmanX
committed
CoDEmanX
committed
return p0, x * scl, y * scl, z * scl, _x * scl, _z * scl
CoDEmanX
committed
def make_normal_snapshot(self, collection, tangential=False):
settings = find_settings()
tfm_opts = settings.transform_options
CoDEmanX
committed
dest_point = self.particles[0].get_location()
CoDEmanX
committed
if self.is_normal_visible():
p0, x, y, z, _x, _z = \
self.get_normal_params(tfm_opts, dest_point)
CoDEmanX
committed
snapshot = bpy.data.objects.new("normal_snapshot", None)
CoDEmanX
committed
Dima Glib
committed
snapshot.matrix_world = m
CoDEmanX
committed
snapshot.empty_display_type = 'SINGLE_ARROW'
#snapshot.empty_display_type = 'ARROWS'
collection.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)
CoDEmanX
committed
def revert(self):
self.set_location(self.initial_pos)
CoDEmanX
committed
return get_cursor_location(v3d=self.v3d)
CoDEmanX
committed
set_cursor_location(Vector(value), v3d=self.v3d)
CoDEmanX
committed
def get_rotation(self):
return Quaternion()
CoDEmanX
committed
CoDEmanX
committed
def get_scale(self):
return Vector((1.0, 1.0, 1.0))
CoDEmanX
committed
CoDEmanX
committed
def get_matrix(self):
return Matrix.Translation(self.get_location())
CoDEmanX
committed
def set_matrix(self, value):
self.set_location(value.to_translation())
CoDEmanX
committed
def get_initial_matrix(self):
return self.initial_matrix
class View3D_Object(Particle):
def __init__(self, obj):
self.obj = obj
CoDEmanX
committed
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
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)
CoDEmanX
committed
area_type = kwargs.get("area_type", context.area.type)
CoDEmanX
committed
view_layer = kwargs.get("view_layer", context.view_layer)
CoDEmanX
committed
space_data = kwargs.get("space_data", context.space_data)
region_data = kwargs.get("region_data", context.region_data)
CoDEmanX
committed
particles = []
pivots = {}
normal_system = None
CoDEmanX
committed
active_element = None
cursor_pos = None
median = None
CoDEmanX
committed
if area_type == 'VIEW_3D':
context_mode = kwargs.get("context_mode", context.mode)
CoDEmanX
committed
selected_objects = kwargs.get("selected_objects",
context.selected_objects)
CoDEmanX
committed
active_object = kwargs.get("active_object",
context.active_object)
CoDEmanX
committed
if context_mode == 'OBJECT':
for obj in selected_objects:
particle = View3D_Object(obj)
particles.append(particle)
CoDEmanX
committed
if active_object:
active_element = active_object.\
matrix_world.to_translation()
CoDEmanX
committed
# 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'}):
CoDEmanX
committed
CoDEmanX
committed
positions = []
normal = Vector((0, 0, 0))
CoDEmanX
committed
bm = bmesh.from_edit_mesh(active_object.data)
CoDEmanX
committed
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)
CoDEmanX
committed
for v in bm.verts:
if v.select:
positions.append(v.co)
normal += v.normal
CoDEmanX
committed
# 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
CoDEmanX
committed
# 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
'''
CoDEmanX
committed
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
CoDEmanX
committed
# consider only topmost parents
bones = set()
for bone in active_object.data.bones:
if bone.select:
bones.add(bone)
CoDEmanX
committed
parents = set()
for bone in bones:
if not set(bone.parent_recursive).intersection(bones):
parents.add(bone)
CoDEmanX
committed
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)
CoDEmanX
committed
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
CoDEmanX
committed
if n is not None:
if n.length_squared < epsilon:
n = -nL
normal += n.normalized()
CoDEmanX
committed
for point in spline.points:
if point.select:
positions.append(point.co)
CoDEmanX
committed
if len(positions) != 0:
if normal.length_squared < epsilon:
normal = Vector((0, 0, 1))
normal.rotate(m)
normal.normalize()
CoDEmanX
committed
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)
CoDEmanX
committed
median, bbox_center = calc_median_bbox_pivots(positions)
median = m * median
bbox_center = m * bbox_center
CoDEmanX
committed
# 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()
CoDEmanX
committed
median = active_element
bbox_center = active_element
CoDEmanX
committed
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)
CoDEmanX
committed
if active_object:
active_element = active_object.\
matrix_world.to_translation()
CoDEmanX
committed
cursor_pos = get_cursor_location(v3d=space_data)
CoDEmanX
committed
#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
CoDEmanX
committed
#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':
CoDEmanX
committed
else:
print("gather_particles() not implemented for '{}'".\
format(area_type))
return None, None
CoDEmanX
committed
CoDEmanX
committed
if cursor_pos:
pivots['CURSOR'] = cursor_pos.copy()
CoDEmanX
committed
if active_element:
# in v3d: ACTIVE_ELEMENT
pivots['ACTIVE'] = active_element.copy()
CoDEmanX
committed
if (len(particles) != 0) and (median is None):
positions = (p.get_location() for p in particles)
median, bbox_center = calc_median_bbox_pivots(positions)
CoDEmanX
committed
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()
CoDEmanX
committed
csu = CoordinateSystemUtility(scene, space_data, region_data, \
pivots, normal_system, view_layer)
CoDEmanX
committed
return particles, csu
def calc_median_bbox_pivots(positions):
median = None # pos can be 3D or 2D
bbox = [None, None]
CoDEmanX
committed
n = 0
for pos in positions:
extend_bbox(bbox, pos)
try:
median += pos
except:
median = pos.copy()
n += 1
CoDEmanX
committed
median = median / n
bbox_center = (Vector(bbox[0]) + Vector(bbox[1])) * 0.5
CoDEmanX
committed
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',
CoDEmanX
committed
'CURSOR':'CURSOR',
'INDIVIDUAL_ORIGINS':'INDIVIDUAL',
'ACTIVE_ELEMENT':'ACTIVE',
'WORLD':'WORLD',
'SURFACE':'SURFACE', # ?
'BOOKMARK':'BOOKMARK',
}
pivot_v3d_map = {
'CENTER':'BOUNDING_BOX_CENTER',
'MEDIAN':'MEDIAN_POINT',
CoDEmanX
committed
'CURSOR':'CURSOR',
'INDIVIDUAL':'INDIVIDUAL_ORIGINS',
'ACTIVE':'ACTIVE_ELEMENT',
}
CoDEmanX
committed
def __init__(self, scene, space_data, region_data, \
pivots, normal_system, view_layer):
self.space_data = space_data
self.region_data = region_data
CoDEmanX
committed
if space_data.type == 'VIEW_3D':
self.pivot_map_inv = self.pivot_v3d_map
CoDEmanX
committed
scene, space_data, region_data, view_layer)
CoDEmanX
committed
CoDEmanX
committed
# Assigned by caller (for cursor or selection)
self.source_pos = None
self.source_rot = None
self.source_scale = None
CoDEmanX
committed
def set_orientation(self, name):
self.tou.set(name)
CoDEmanX
committed
def set_pivot(self, pivot):
self.space_data.pivot_point = self.pivot_map_inv[pivot]
CoDEmanX
committed
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
CoDEmanX
committed
CoDEmanX
committed
if relative is None:
settings = find_settings()
tfm_opts = settings.transform_options
relative = tfm_opts.use_relative_coords
CoDEmanX
committed
if relative:
pivot = "RELATIVE"
elif (name == 'GLOBAL') or (pivot == 'WORLD'):
pivot = 'WORLD'
elif (name == "Surface") or (pivot == 'SURFACE'):
pivot = "SURFACE"
CoDEmanX
committed
CoDEmanX
committed
def get_origin(self, name=None, relative=None, pivot=None):
if not pivot:
pivot = self.get_pivot_name(name, relative)
CoDEmanX
committed
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'
CoDEmanX
committed
#if pivot == 'ACTIVE':
# print(self.pivots)
CoDEmanX
committed
try:
return self.pivots[pivot]
except:
return Vector()
CoDEmanX
committed
def get_matrix(self, name=None, relative=None, pivot=None):
if not name:
name = self.tou.get()
CoDEmanX
committed
CoDEmanX
committed
if isinstance(pivot, Vector):
pos = pivot
else:
pos = self.get_origin(name, relative, pivot)
CoDEmanX
committed
return to_matrix4x4(matrix, pos)
# ====== TRANSFORM ORIENTATION UTILITIES ====== #
class TransformOrientationUtility:
special_systems = {"Surface", "Scaled"}
predefined_systems = {
'GLOBAL', 'LOCAL', 'VIEW', 'NORMAL', 'GIMBAL',
"Scaled", "Surface",
}
CoDEmanX
committed
def __init__(self, scene, v3d, rv3d, vwly):
self.scene = scene
self.v3d = v3d
self.rv3d = rv3d
CoDEmanX
committed
self.custom_systems = [item for item in scene.orientations \
if item.name not in self.special_systems]
CoDEmanX
committed
self.is_custom = False
self.custom_id = -1
CoDEmanX
committed
# This is calculated elsewhere
self.normal_system = None
CoDEmanX
committed
CoDEmanX
committed
def get(self):
return self.transform_orientation
CoDEmanX
committed
def get_title(self):
if self.is_custom:
return self.transform_orientation
CoDEmanX
committed
name = self.transform_orientation
return name[:1].upper() + name[1:].lower()
CoDEmanX
committed
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
CoDEmanX
committed
CoDEmanX
committed
if self.is_custom:
# If already custom, switch to next custom system
self.custom_id = (self.custom_id + increment) % n
CoDEmanX
committed
CoDEmanX
committed
name = self.custom_systems[self.custom_id].name
else:
self.is_custom = name not in self.predefined_systems
CoDEmanX
committed
if self.is_custom:
self.custom_id = next((i for i, v in \
enumerate(self.custom_systems) if v.name == name), -1)
CoDEmanX
committed
if name in self.special_systems:
# Ensure such system exists
self.get_custom(name)
CoDEmanX
committed
CoDEmanX
committed
if set_v3d:
self.v3d.transform_orientation = name
CoDEmanX
committed
active_obj = self.view_layer.objects.active
CoDEmanX
committed
if not name:
name = self.transform_orientation
CoDEmanX
committed
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()
CoDEmanX
committed
CoDEmanX
committed
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 = view_layer.objects.active
CoDEmanX
committed
if active_obj:
prev_mode = active_obj.mode
bpy.ops.object.mode_set(mode='OBJECT')
else:
bpy.ops.object.add()
CoDEmanX
committed
# ATTENTION! This uses context's scene
bpy.ops.transform.create_orientation()
CoDEmanX
committed
CoDEmanX
committed
basename = name
i = 1
while name in scene.orientations:
name = "%s.%03i" % (basename, i)
i += 1
CoDEmanX
committed
if matrix:
tfm_orient.matrix = matrix.to_3x3()
CoDEmanX
committed
if active_obj:
bpy.ops.object.mode_set(mode=prev_mode)
else:
bpy.ops.object.delete()
CoDEmanX
committed
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),
)
CoDEmanX
committed
def __init__(self, region, space_data, region_data):
self.region = region
self.space_data = space_data
self.region_data = region_data
CoDEmanX
committed
if space_data.type == 'VIEW_3D':
self.implementation = View3DUtility(
region, space_data, region_data)
else:
self.implementation = None
CoDEmanX
committed
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:
Sebastian Nell
committed
lock_types = {"lock_cursor": False, "lock_object": None, "lock_bone": ""}
CoDEmanX
committed
# ====== INITIALIZATION / CLEANUP ====== #
def __init__(self, region, space_data, region_data):
self.region = region
self.space_data = space_data
self.region_data = region_data
CoDEmanX
committed
# ====== 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}
CoDEmanX
committed
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]))
CoDEmanX
committed
def _get_lock_obj_bone(self):
v3d = self.space_data
CoDEmanX
committed
obj = v3d.lock_object
if not obj:
return None, None
CoDEmanX
committed
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
CoDEmanX
committed
CoDEmanX
committed
# 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
CoDEmanX
committed
if no_locks:
return rv3d.view_location.copy()
CoDEmanX
committed
# rv3d.perspective_matrix and rv3d.view_matrix
# seem to have some weird translation components %)
CoDEmanX
committed
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()
CoDEmanX
committed
def set_position(self, pos, no_locks=False):
v3d = self.space_data
rv3d = self.region_data
CoDEmanX
committed
CoDEmanX
committed
if no_locks:
rv3d.view_location = pos
return
CoDEmanX
committed
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:
Loading
Loading full blame…