import bpy from mathutils import Vector#, Matrix from pathlib import Path from math import radians from .utils import get_gp_objects, set_collection, show_message_box class GPTB_OT_copy_text(bpy.types.Operator): bl_idname = "wm.copytext" bl_label = "Copy to clipboard" bl_description = "Insert passed text to clipboard" bl_options = {"REGISTER", "INTERNAL"} text : bpy.props.StringProperty(name="cliptext", description="text to clip", default="") def execute(self, context): context.window_manager.clipboard = self.text mess = f'Clipboard: {context.window_manager.clipboard}' self.report({'INFO'}, mess) return {"FINISHED"} class GPTB_OT_flipx_view(bpy.types.Operator): bl_idname = "gp.mirror_flipx" bl_label = "cam mirror flipx" bl_description = "Invert X scale on camera to flip image horizontally" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.region_data.view_perspective == 'CAMERA' def execute(self, context): context.scene.camera.scale.x *= -1 return {"FINISHED"} class GPTB_OT_jump_gp_keyframe(bpy.types.Operator): bl_idname = "screen.gp_keyframe_jump" bl_label = "Jump to GPencil keyframe" bl_description = "Jump to prev/next keyframe on active and selected layers of active grease pencil object" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.object and context.object.type == 'GPENCIL' next : bpy.props.BoolProperty( name="Next GP keyframe", description="Go to next active GP keyframe", default=True) target : bpy.props.EnumProperty( name="Target layer", description="Choose wich layer to evaluate for keyframe change", default='ACTIVE',# options={'ANIMATABLE'}, update=None, get=None, set=None, items=( ('ACTIVE', 'Active and selected', 'jump in keyframes of active and other selected layers ', 0), ('VISIBLE', 'Visibles layers', 'jump in keyframes of visibles layers', 1), ('ACCESSIBLE', 'Visible and unlocked layers', 'jump in keyframe of all layers', 2), )) #(key, label, descr, id[, icon]) def execute(self, context): if not context.object.data.layers.active: self.report({'ERROR'}, 'No active layer on current GPencil object') return {"CANCELLED"} layer = [] if self.target == 'ACTIVE': gpl = [l for l in context.object.data.layers if l.select and not l.hide] if not context.object.data.layers.active in gpl: gpl.append(context.object.data.layers.active) elif self.target == 'VISIBLE': gpl = [l for l in context.object.data.layers if not l.hide] elif self.target == 'ACCESSIBLE': gpl = [l for l in context.object.data.layers if not l.hide and not l.lock] current = context.scene.frame_current p = n = None mins = [] maxs = [] for l in gpl: for f in l.frames: if f.frame_number < current: p = f.frame_number if f.frame_number > current: n = f.frame_number break mins.append(p) maxs.append(n) p = n = None mins = [i for i in mins if i is not None] maxs = [i for i in maxs if i is not None] if mins: p = max(mins) if maxs: n = min(maxs) if self.next and n is not None: context.scene.frame_set(n) elif not self.next and p is not None: context.scene.frame_set(p) else: self.report({'INFO'}, 'No keyframe in this direction') return {"CANCELLED"} return {"FINISHED"} class GPTB_OT_rename_data_from_obj(bpy.types.Operator): bl_idname = "gp.rename_data_from_obj" bl_label = "Rename GP from object" bl_description = "Rename the GP datablock with the same name as the object" bl_options = {"REGISTER"} rename_all : bpy.props.BoolProperty(default=False) @classmethod def poll(cls, context): return context.object and context.object.type == 'GPENCIL' def execute(self, context): if not self.rename_all: obj = context.object if obj.name == obj.data.name: self.report({'WARNING'}, 'Nothing to rename') return {"FINISHED"} old = obj.data.name obj.data.name = obj.name self.report({'INFO'}, f'GP data renamed: {old} -> {obj.data.name}') else: oblist = [] for o in context.scene.objects: if o.type == 'GPENCIL': if o.name == o.data.name: continue oblist.append(f'{o.data.name} -> {o.name}') o.data.name = o.name print('\nrenamed GP datablock:') for i in oblist: print(i) self.report({'INFO'}, f'{len(oblist)} data renamed (see console for detail)') return {"FINISHED"} # TODO make secondary cam # 2 solution : # - parenting to main cam except for roll axis (drivers or simple parents) # - Facing current "peg" (object) and parented to it to keep distance # --> reset roll means aligning to object again (to main camera if one) or maybe align to global Z (as possible). # other solution, button to disable all object Fcu evaluation (fix object movement while moving in timeline) # 1 ops to enter in manip/draw Cam (create if not exists) # 1 ops to reset rotation # 1 ops to swap between cam follow or object follow (toggle or two button), maybe accessible only when drawcam is active # hide camera that isn't used (playblast should always get main camera) def get_gp_alignement_vector(context): #SETTINGS 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' ### CHOOSE HOW TO PROJECT """ # -> placement if loc == "CURSOR": plane_co = scn.cursor.location else:#ORIGIN (also on origin if set to 'SURFACE', 'STROKE') plane_co = obj.location """ # -> orientation if orient == 'VIEW': #only depth is important, no need to get view vector return None elif orient == 'AXIS_Y':#front (X-Z) return Vector((0,1,0)) elif orient == 'AXIS_X':#side (Y-Z) return Vector((1,0,0)) elif orient == 'AXIS_Z':#top (X-Y) return Vector((0,0,1)) elif orient == 'CURSOR': return Vector((0,0,1))#.rotate(context.scene.cursor.matrix) class GPTB_OT_draw_cam(bpy.types.Operator): bl_idname = "gp.draw_cam_switch" bl_label = "Draw cam switch" bl_description = "switch between main camera and draw (manipulate) camera" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.scene.camera # return context.region_data.view_perspective == 'CAMERA'# check if in camera cam_mode : bpy.props.StringProperty() def execute(self, context): created=False if self.cam_mode == 'draw': dcam_name = 'draw_cam' else: dcam_name = 'obj_cam' act = context.object if not act: self.report({'ERROR'}, "No active object to lock on") return {"CANCELLED"} if context.region_data.view_perspective == 'ORTHO': self.report({'ERROR'}, "Can't be set in othographic view, swith to persp (numpad 5)") return {"CANCELLED"} camcol_name = 'manip_cams' if not context.scene.camera: self.report({'ERROR'}, "No camera to return to") return {"CANCELLED"} ## if already in draw_cam BACK to main camera if context.scene.camera.name in ('draw_cam', 'obj_cam'): drawcam = context.scene.camera # get main cam and error if not available if drawcam.name == 'draw_cam': maincam = drawcam.parent else: maincam = None main_name = drawcam.get('maincam_name')# Custom prop with previous avtive cam. if main_name: maincam = context.scene.objects.get(main_name) if not maincam: cams = [ob for ob in context.scene.objects if ob.type == 'CAMERA' and not ob.name in ("draw_cam", "obj_cam")] if not cams: self.report({'ERROR'}, "Can't find any other camera to switch to...") return {"CANCELLED"} maincam = cams[0] # dcam_col = bpy.data.collections.get(camcol_name) # if not dcam_col: set_collection(drawcam, camcol_name) # Swap to it, unhide if necessary and hide previous context.scene.camera = maincam ## hide cam object drawcam.hide_viewport = True maincam.hide_viewport = False ## if in main camera GO to drawcam elif context.scene.camera.name not in ('draw_cam', 'obj_cam'): # use current cam as main cam (more flexible than naming convention) maincam = context.scene.camera drawcam = context.scene.objects.get(dcam_name) if not drawcam: created=True drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data) drawcam.show_name = True set_collection(drawcam, 'manip_cams') if dcam_name == 'draw_cam': drawcam.parent = maincam if created:#set to main at creation time drawcam.matrix_world = maincam.matrix_world drawcam.lock_location = (True,True,True) else: if created: drawcam['maincam_name'] = context.scene.camera.name drawcam.parent = act drawcam.matrix_world = context.space_data.region_3d.view_matrix.inverted() # Place cam from current view ''' drawcam.parent = act vec = Vector((0,1,0)) if act.type == 'GPENCIL': #change vector according to alignement vec = get_gp_alignement_vector(context) vec = None #!# FORCE creation of cam at current viewpoint if vec: # Place drawcam at distance at standard distance from the object facing it drawcam.location = act.matrix_world @ (vec * -6) drawcam.rotation_euler = act.rotation_euler drawcam.rotation_euler.x -= radians(-90) else: #Create cam at view point drawcam.matrix_world = context.space_data.region_3d.view_matrix.inverted() ''' ## hide cam object context.scene.camera = drawcam drawcam.hide_viewport = False maincam.hide_viewport = True if created and drawcam.name == 'obj_cam':#Go in camera view context.region_data.view_perspective = 'CAMERA' # ## make active # bpy.context.view_layer.objects.active = ob return {"FINISHED"} class GPTB_OT_set_view_as_cam(bpy.types.Operator): bl_idname = "gp.set_view_as_cam" bl_label = "Cam at view" bl_description = "Place the active camera at current viewpoint, parent to active object. (need to be out of camera)" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.region_data.view_perspective != 'CAMERA'# need to be out of camera # return context.scene.camera and not context.scene.camera.name.startswith('Cam') def execute(self, context): if context.region_data.view_perspective == 'ORTHO': self.report({'ERROR'}, "Can't be set in othographic view") return {"CANCELLED"} ## switching to persp work in 2 times, but need update before... #context.area.tag_redraw() #context.region_data.view_perspective = 'PERSP' cam = context.scene.camera if not cam: self.report({'ERROR'}, "No camera to set") return {"CANCELLED"} obj = context.object if obj and obj.type != 'CAMERA':# parent to object cam.parent = obj if not cam.parent: self.report({'WARNING'}, "No parents...") # set view cam.matrix_world = context.space_data.region_3d.view_matrix.inverted() # Enter in cam view #https://blender.stackexchange.com/questions/30643/how-to-toggle-to-camera-view-via-python context.region_data.view_perspective = 'CAMERA' return {"FINISHED"} class GPTB_OT_reset_cam_rot(bpy.types.Operator): bl_idname = "gp.reset_cam_rot" bl_label = "Reset rotation" bl_description = "Reset rotation of the draw manipulation camera" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.scene.camera and not context.scene.camera.name.startswith('Cam') # return context.region_data.view_perspective == 'CAMERA'# check if in camera def execute(self, context): # dcam_name = 'draw_cam' # camcol_name = 'manip_cams' drawcam = context.scene.camera if drawcam.parent.type == 'CAMERA': ## align to parent camera drawcam.matrix_world = drawcam.parent.matrix_world#wrong, get the parent rotation offset # drawcam.rotation_euler = drawcam.parent.rotation_euler#wrong, get the parent rotation offset elif drawcam.parent: ## there is a parent, so align the Y of the camera to object's Z # drawcam.rotation_euler.rotate(drawcam.parent.matrix_world)# wrong pass else: self.report({'ERROR'}, "No parents to refer to for rotation reset") return {"CANCELLED"} return {"FINISHED"} class GPTB_OT_toggle_mute_animation(bpy.types.Operator): bl_idname = "gp.toggle_mute_animation" bl_label = "Toggle animation mute" bl_description = "Enable/Disable animation evaluation\n(shift+clic to affect selection only)" bl_options = {"REGISTER"} mute : bpy.props.BoolProperty(default=False) skip_gp : bpy.props.BoolProperty(default=False) skip_obj : bpy.props.BoolProperty(default=False) def invoke(self, context, event): self.selection = event.shift return self.execute(context) def execute(self, context): if self.selection: pool = context.selected_objects else: pool = context.scene.objects for o in pool: if self.skip_gp and o.type == 'GPENCIL': continue if self.skip_obj and o.type != 'GPENCIL': continue if not o.animation_data: continue act = o.animation_data.action if not act: continue for i, fcu in enumerate(act.fcurves): print(i, fcu.data_path, fcu.array_index) fcu.mute = self.mute return {'FINISHED'} class GPTB_OT_list_disabled_anims(bpy.types.Operator): bl_idname = "gp.list_disabled_anims" bl_label = "List disabled anims" bl_description = "List disabled animations channels in scene. (shit+clic to list only on seleciton)" bl_options = {"REGISTER"} mute : bpy.props.BoolProperty(default=False) # skip_gp : bpy.props.BoolProperty(default=False) # skip_obj : bpy.props.BoolProperty(default=False) def invoke(self, context, event): self.selection = event.shift return self.execute(context) def execute(self, context): li = [] oblist = [] if self.selection: pool = context.selected_objects else: pool = context.scene.objects for o in pool: # if self.skip_gp and o.type == 'GPENCIL': # continue # if self.skip_obj and o.type != 'GPENCIL': # continue if not o.animation_data: continue act = o.animation_data.action if not act: continue for i, fcu in enumerate(act.fcurves): # print(i, fcu.data_path, fcu.array_index) if fcu.mute: if o not in oblist: oblist.append(o) li.append(f'{o.name} : {fcu.data_path} {fcu.array_index}') else: li.append(f'{" "*len(o.name)} - {fcu.data_path} {fcu.array_index}') if li: show_message_box(li) else: self.report({'INFO'}, f"No animation disabled on {'selection' if self.selection else 'scene'}") return {'FINISHED'} ## TODO presets are still not used... need to make a custom preset save/remove/quickload manager to be efficient (UIlist ?) class GPTB_OT_overlay_presets(bpy.types.Operator): bl_idname = "gp.overlay_presets" bl_label = "Overlay presets" bl_description = "Overlay save/load presets for showing only whats needed" bl_options = {"REGISTER"} # @classmethod # def poll(cls, context): # return context.region_data.view_perspective == 'CAMERA' val_dic = {} def execute(self, context): self.zones = [bpy.context.space_data.overlay] exclude = ( ### add lines here to exclude specific attribute 'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic ) if not self.val_dic: ## store attribute of data_path in self.zones list. for data_path in self.zones: self.val_dic[data_path] = {} for attr in dir(data_path):#iterate in attribute of given datapath if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr): self.val_dic[data_path][attr] = getattr(data_path, attr) # Do tomething with the dic (backup to a json ?) else: ## restore attribute from self.zones list for data_path, prop_dic in self.val_dic.items(): for attr, val in prop_dic.items(): try: setattr(data_path, attr, val) except Exception as e: print(f"/!\ Impossible to re-assign: {attr} = {val}") print(e) ''' overlay = context.space_data.overlay # still need ref overlay.show_extras = not val overlay.show_outline_selected = val overlay.show_object_origins = val overlay.show_motion_paths = val overlay.show_relationship_lines = val overlay.show_bones = val overlay.show_annotation = val overlay.show_text = val overlay.show_cursor = val overlay.show_floor = val overlay.show_axis_y = val overlay.show_axis_x = val ''' return {'FINISHED'} classes = ( GPTB_OT_copy_text, GPTB_OT_flipx_view, GPTB_OT_jump_gp_keyframe, GPTB_OT_rename_data_from_obj, GPTB_OT_draw_cam, GPTB_OT_set_view_as_cam, GPTB_OT_reset_cam_rot, GPTB_OT_toggle_mute_animation, GPTB_OT_list_disabled_anims, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)