## 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)