2021-01-10 16:47:17 +01:00
|
|
|
|
## snap 3D cursor on active grease pencil object canvas surfaces
|
|
|
|
|
import bpy
|
|
|
|
|
import mathutils
|
|
|
|
|
from bpy_extras import view3d_utils
|
2024-11-27 16:43:41 +01:00
|
|
|
|
from bpy.app.handlers import persistent
|
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
|
# return context.object and context.object.type == 'GREASEPENCIL'
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
|
if not context.object or context.object.type != 'GREASEPENCIL':
|
2021-01-10 16:47:17 +01:00
|
|
|
|
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))
|
|
|
|
|
|
2024-01-18 19:32:05 +01:00
|
|
|
|
plane_co, plane_no = get_gp_draw_plane()
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2024-11-27 16:43:41 +01:00
|
|
|
|
## used in properties file to register in boolprop update
|
|
|
|
|
def cursor_follow_update(self, context):
|
2021-01-10 16:47:17 +01:00
|
|
|
|
'''append or remove cursor_follow handler according a boolean'''
|
2024-12-03 16:37:42 +01:00
|
|
|
|
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
|
2021-01-10 16:47:17 +01:00
|
|
|
|
global prev_matrix
|
|
|
|
|
# imported in properties to register in boolprop update
|
|
|
|
|
if self.cursor_follow:#True
|
2024-12-03 16:37:42 +01:00
|
|
|
|
if ob:
|
|
|
|
|
# out of below condition to be called when setting target as well
|
|
|
|
|
prev_matrix = ob.matrix_world.copy()
|
2021-01-10 16:47:17 +01:00
|
|
|
|
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'''
|
2024-12-03 16:37:42 +01:00
|
|
|
|
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:
|
2021-01-10 16:47:17 +01:00
|
|
|
|
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()
|
2024-12-03 16:10:30 +01:00
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
|
## full
|
|
|
|
|
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
|
|
|
|
|
|
|
|
|
|
# store for next use
|
|
|
|
|
prev_matrix = current_matrix.copy()
|
|
|
|
|
|
2024-11-27 16:43:41 +01:00
|
|
|
|
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
|
2024-12-03 16:37:42 +01:00
|
|
|
|
if bpy.context.scene.gptoolprops.cursor_follow_target:
|
|
|
|
|
# we are following a target, nothing to update on selection change
|
|
|
|
|
return
|
2024-11-27 16:43:41 +01:00
|
|
|
|
global prev_matrix, prev_active_obj
|
|
|
|
|
if prev_active_obj != bpy.context.object:
|
|
|
|
|
## Set stored matrix to active object
|
2024-12-03 16:10:30 +01:00
|
|
|
|
prev_matrix = bpy.context.object.matrix_world.copy()
|
2024-11-27 16:43:41 +01:00
|
|
|
|
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()
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
|
|
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
|
2024-11-27 16:43:41 +01:00
|
|
|
|
|
|
|
|
|
## Follow cursor matrix update on object change
|
2024-12-03 16:10:30 +01:00
|
|
|
|
bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change
|
2024-11-27 16:43:41 +01:00
|
|
|
|
# ## Directly set msgbus to work at first addon activation # select_change
|
2024-12-03 16:10:30 +01:00
|
|
|
|
bpy.app.timers.register(subscribe_object_change, first_interval=1) # select_change
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
2024-11-27 16:43:41 +01:00
|
|
|
|
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def unregister():
|
2024-12-03 16:10:30 +01:00
|
|
|
|
bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
|
|
# 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]:
|
2024-11-27 16:43:41 +01:00
|
|
|
|
bpy.app.handlers.frame_change_post.remove(cursor_follow)
|
|
|
|
|
|
|
|
|
|
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)
|