gp_toolbox/OP_cursor_snap_canvas.py

225 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## snap 3D cursor on active grease pencil object canvas surfaces
import bpy
import mathutils
from bpy_extras import view3d_utils
from bpy.app.handlers import persistent
from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position
## override all sursor snap shortcut with this in keymap
class GPTB_OT_cusor_snap(bpy.types.Operator):
bl_idname = "view3d.cusor_snap"
bl_label = "Snap cursor to GP"
bl_description = "Snap 3d cursor to active GP object canvas (else use normal place)"
bl_options = {"REGISTER"}#, "INTERNAL"
# @classmethod
# def poll(cls, context):
# return context.object and context.object.type == 'GREASEPENCIL'
def invoke(self, context, event):
#print('-!SNAP!-')
self.mouse_co = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
# print('self.mouse_co: ', self.mouse_co)
self.execute(context)
return {"FINISHED"}
def execute(self, context):
if not context.object or context.object.type != 'GREASEPENCIL':
self.report({'INFO'}, 'Not GP, Cursor surface project')
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
return {"FINISHED"}
if context.region_data.view_perspective == 'ORTHO':
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
self.report({'WARNING'}, 'Ortholinear ! not snaped to GP plane Cursor surface project)')
return {"FINISHED"}
self.report({'INFO'}, 'Using GP picking')
settings = context.scene.tool_settings
orient = settings.gpencil_sculpt.lock_axis#'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR'
loc = settings.gpencil_stroke_placement_view3d#'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE'
warning = []
if not "AXIS" in orient:
warning.append(f'Orientation is {orient}, no depth picking')
if loc != "ORIGIN":
warning.append(f"Location is '{loc}' not object 'ORIGIN'")
if warning:
self.report({'WARNING'}, ', '.join(warning))
plane_co, plane_no = get_gp_draw_plane()
if not plane_co:#default to object location
plane_co = context.object.matrix_world.to_translation()#context.object.location
if not plane_no:# use view depth (region_to_location instead of )
coord = region_to_location(self.mouse_co, plane_co)
else:
#projected on given plane from view (intersect on plane with a vector from view origin)
origin = get_view_origin_position()#get view origin
region = bpy.context.region
rv3d = bpy.context.region_data
coord = mathutils.geometry.intersect_line_plane(origin, origin - view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse_co), plane_co, plane_no)
# If no plane is crossed, intersect_line_plane return None which naturally goes to traceback...
if not coord:
self.report({'WARNING'}, 'Ortholinear view, used basic cursor snap (no depth picking)')
context.scene.cursor.location = coord
return {"FINISHED"}
#TODO auto-cursor (attach cursor to object)
''' cursor native snap
https://docs.blender.org/api/current/bpy.ops.view3d.html#bpy.ops.view3d.cursor3d
bpy.ops.view3d.cursor3d(use_depth=True, orientation='VIEW')
Set the location of the 3D cursor
Parameters
use_depth (boolean, (optional)) Surface Project, Project onto the surface
orientation (enum in ['NONE', 'VIEW', 'XFORM', 'GEOM'], (optional))
Orientation, Preset viewpoint to use
NONE None, Leave orientation unchanged.
VIEW View, Orient to the viewport.
XFORM Transform, Orient to the current transform setting.
GEOM Geometry, Match the surface normal.
'''
def swap_keymap_by_id(org_idname, new_idname):
'''Replace id operator by another in user keymap'''
wm = bpy.context.window_manager
for cat, keymap in wm.keyconfigs.user.keymaps.items():#wm.keyconfigs.addon.keymaps.items():
for k in keymap.keymap_items:
if k.idname != org_idname:
continue
## Print changes
mods = ' + '.join([m for m in ('ctrl','shift','alt') if getattr(k, m)])
val = f' ({k.value.lower()})' if k.value != 'PRESS' else ''
# ({keymap.space_type}) #VIEW_3D
print(f"Hotswap: {cat} - {k.name}: {mods + ' ' if mods else ''}{k.type}{val} : {k.idname} --> {new_idname}")
k.idname = new_idname
prev_matrix = None
# @call_once(bpy.app.handlers.frame_change_post)
## used in properties file to register in boolprop update
def cursor_follow_update(self, context):
'''append or remove cursor_follow handler according a boolean'''
ob = bpy.context.object
if bpy.context.scene.gptoolprops.cursor_follow_target:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
global prev_matrix
# imported in properties to register in boolprop update
if self.cursor_follow:#True
if ob:
# out of below condition to be called when setting target as well
prev_matrix = ob.matrix_world.copy()
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
bpy.app.handlers.frame_change_post.append(cursor_follow)
else:#False
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
prev_matrix = None
bpy.app.handlers.frame_change_post.remove(cursor_follow)
def cursor_follow(scene):
'''Handler to make the cursor follow active object matrix changes on frame change'''
ob = bpy.context.object
if bpy.context.scene.gptoolprops.cursor_follow_target:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
if not ob:
return
global prev_matrix
current_matrix = ob.matrix_world
if not prev_matrix:
prev_matrix = current_matrix.copy()
return
# debug prints : HANDLER CALLED TWICE in time line when clic (clic press, and clic release)!!!
# print(scene.frame_current)
# print('prev: ', [[f'{j:.2f}' for j in i] for i in prev_matrix[:2] ])
# print('curr: ', [[f'{j:.2f}' for j in i] for i in current_matrix[:2] ])
## translation only
# scene.cursor.location += (current_matrix - prev_matrix).to_translation()
## full
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
# store for next use
prev_matrix = current_matrix.copy()
prev_active_obj = None
## Add check for object selection change
def selection_changed():
"""Callback function for selection changes"""
if not bpy.context.scene.gptoolprops.cursor_follow:
return
if bpy.context.scene.gptoolprops.cursor_follow_target:
# we are following a target, nothing to update on selection change
return
global prev_matrix, prev_active_obj
if prev_active_obj != bpy.context.object:
## Set stored matrix to active object
prev_matrix = bpy.context.object.matrix_world.copy()
prev_active_obj = bpy.context.object
## Note: Same owner as layer manager (will be removed as well)
def subscribe_object_change():
subscribe_to = (bpy.types.LayerObjects, 'active')
bpy.msgbus.subscribe_rna(
key=subscribe_to,
# owner of msgbus subcribe (for clearing later)
owner=bpy.types.GreasePencilv3, # <-- attach to ID during it's lifetime.
args=(),
notify=selection_changed,
options={'PERSISTENT'},
)
@persistent
def subscribe_object_change_handler(dummy):
subscribe_object_change()
classes = (
GPTB_OT_cusor_snap,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
## Follow cursor matrix update on object change
bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change
# ## Directly set msgbus to work at first addon activation # select_change
bpy.app.timers.register(subscribe_object_change, first_interval=1) # select_change
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
def unregister():
bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
# force remove handler if it's there at unregister
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
bpy.app.handlers.frame_change_post.remove(cursor_follow)
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)