import bpy import mathutils import math from time import ctime from mathutils import Vector #, Matrix from pathlib import Path from math import radians from bpy.types import Operator from .view3d_utils import View3D from . import utils class GPTB_OT_copy_text(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(Operator): bl_idname = "view3d.camera_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.area.type == 'VIEW_3D' and \ context.region_data.view_perspective == 'CAMERA' def execute(self, context): context.scene.camera.scale.x *= -1 return {"FINISHED"} class GPTB_OT_view_camera_frame_fit(Operator): bl_idname = "view3d.view_camera_frame_fit" bl_label = "View Fit" bl_description = "Fit the camera in view (view 1:1)" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.area.type == 'VIEW_3D' and \ context.region_data.view_perspective == 'CAMERA' def zoom_from_fac(self, zoomfac): from math import sqrt return (sqrt(4 * zoomfac) - sqrt(2)) * 50.0 def execute(self, context): # Calculate zoom level to fit in view considering upper and side panel (Not done by native view 1:1) # context.space_data.region_3d.view_camera_zoom = 0 # (value range: -30, - 600) view3d = View3D() view3d.fit_camera_view() ## re-center # context.space_data.region_3d.view_camera_offset = (0,0) # With a margin # Calculate pan to fit view in viewport return {"FINISHED"} class GPTB_OT_rename_data_from_obj(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 == 'GREASEPENCIL' 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 == 'GREASEPENCIL': 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(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 maincam.data.show_passepartout = context.scene.gptoolprops.drawcam_passepartout else: maincam = None main_name = drawcam.get('maincam_name')# Custom prop with previous active 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: utils.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) utils.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) # drawcam.hide_viewport = True context.scene.gptoolprops.drawcam_passepartout = maincam.data.show_passepartout drawcam.data = maincam.data # get data from parent # Hide the other passepartout to let only the custom OpenGL one maincam.data.show_passepartout = False else: # object cam 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 == 'GREASEPENCIL': #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(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.area.type == 'VIEW_3D' and \ 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(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 get_center_view(self, context, cam): from bpy_extras.view3d_utils import location_3d_to_region_2d frame = cam.data.view_frame() mat = cam.matrix_world frame = [mat @ v for v in frame] frame_px = [location_3d_to_region_2d(context.region, context.space_data.region_3d, v) for v in frame] center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2 center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2 return mathutils.Vector((center_x, center_y)) def get_ui_ratio(self, context): '''correct ui overlap from header/toolbars''' regs = context.area.regions if context.preferences.system.use_region_overlap: w = context.area.width # minus tool header h = context.area.height - regs[0].height else: # minus tool leftbar + sidebar right w = context.area.width - regs[2].width - regs[3].width # minus tool header + header h = context.area.height - regs[0].height - regs[1].height self.ratio = h / w self.ratio_inv = w / h def execute(self, context): cam = context.scene.camera if not cam.parent or cam.parent.type != 'CAMERA': self.report({'ERROR'}, "No parents to refer to for rotation reset") return {"CANCELLED"} # store original rotation mode org_rotation_mode = cam.rotation_mode # set to euler to works with quaternions, restored at finish cam.rotation_mode = 'XYZ' # store camera matrix world org_cam_matrix = cam.matrix_world.copy() org_cam_z = cam.rotation_euler.z ## initialize current view_offset in camera view_cam_offset = mathutils.Vector(context.space_data.region_3d.view_camera_offset) # Do the reset to parent transforms cam.matrix_world = cam.parent.matrix_world # wrong, get the parent rotation offset # Get diff angle angle = cam.rotation_euler.z - org_cam_z # create rotation matrix with negative angle (we want to counter the move) neg = -angle rot_mat2d = mathutils.Matrix([[math.cos(neg), -math.sin(neg)], [math.sin(neg), math.cos(neg)]]) # restore original rotation mode cam.rotation_mode = org_rotation_mode self.get_ui_ratio(context) # apply rotation matrix new_cam_offset = view_cam_offset.copy() new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio)) # apply screen ratio new_cam_offset.rotate(rot_mat2d) new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio_inv)) # restore screen ratio context.space_data.region_3d.view_camera_offset = new_cam_offset return {"FINISHED"} class GPTB_OT_toggle_mute_animation(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) mode : bpy.props.StringProperty(default='OBJECT') # GPENCIL, CAMERA, OBJECT, ALL def invoke(self, context, event): self.selection = event.shift return self.execute(context) def set_action_mute(self, act): for i, fcu in enumerate(act.fcurves): print(i, fcu.data_path, fcu.array_index) # fcu.group don't have mute attribute in api. fcu.mute = self.mute for g in act.groups: g.mute = self.mute def execute(self, context): if self.selection: pool = context.selected_objects else: pool = context.scene.objects for o in pool: if self.mode == 'GREASEPENCIL' and o.type != 'GREASEPENCIL': continue if self.mode == 'OBJECT' and o.type in ('GREASEPENCIL', 'CAMERA'): continue if self.mode == 'CAMERA' and o.type != 'CAMERA': continue # mute attribute animation for GP and cameras if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data: gp_act = o.data.animation_data.action if gp_act: print(f'\n---{o.name} data:') self.set_action_mute(gp_act) if not o.animation_data: continue act = o.animation_data.action if not act: continue print(f'\n---{o.name}:') self.set_action_mute(act) return {'FINISHED'} class GPTB_OT_toggle_hide_gp_modifier(Operator): bl_idname = "gp.toggle_hide_gp_modifier" bl_label = "Toggle Modifier Hide" bl_description = "Show/Hide viewport on GP objects modifier\ \nOnly touch modifier that are showed in render\ \nShift + click to affect selection only" bl_options = {"REGISTER"} show : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'}) 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 o.type != 'GREASEPENCIL': continue for m in o.modifiers: # skip modifier that are not visible in render if not m.show_render: continue m.show_viewport = self.show return {'FINISHED'} class GPTB_OT_list_disabled_anims(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 == 'GREASEPENCIL': # continue # if self.skip_obj and o.type != 'GREASEPENCIL': # continue if o.type == 'GREASEPENCIL': if o.data.animation_data: gp_act = o.data.animation_data.action if gp_act: for i, fcu in enumerate(gp_act.fcurves): if fcu.mute: if o not in oblist: oblist.append(o) li.append(f'{o.name}:') li.append(f' - {fcu.data_path} {fcu.array_index}') if not o.animation_data: continue act = o.animation_data.action if not act: continue for g in act.groups: if g.mute: li.append(f'{o.name} - group: {g.name}') 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}:') li.append(f' - {fcu.data_path} {fcu.array_index}') if li: utils.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(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'} class GPTB_OT_clear_active_frame(Operator): bl_idname = "gp.clear_active_frame" bl_label = "Clear Active Frame" bl_description = "Delete all strokes in active frames" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' def execute(self, context): obj = context.object l = obj.data.layers.active if not l: self.report({'ERROR'}, 'No layers') return {'CANCELLED'} f = l.current_frame() if not f: self.report({'ERROR'}, 'No active frame') return {'CANCELLED'} ct = len(f.drawing.strokes) if not ct: self.report({'ERROR'}, 'Active frame already empty') return {'CANCELLED'} for s in reversed(f.drawing.strokes): f.drawing.strokes.remove(s) self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)') return {'FINISHED'} class GPTB_OT_check_canvas_alignement(Operator): bl_idname = "gp.check_canvas_alignement" bl_label = "Check Canvas Alignement" bl_description = "Check if view is aligned to canvas\nWarn if the drawing angle to surface is too high\nThere can be some error margin" bl_options = {"REGISTER"} @classmethod def poll(cls, context): # if lock_axis is 'VIEW' then the draw axis is always aligned return context.object and context.object.type == 'GREASEPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW' def execute(self, context): if context.scene.tool_settings.gpencil_sculpt.lock_axis == 'VIEW': self.report({'INFO'}, 'Drawing plane use "View" (always aligned)') return {'FINISHED'} _angle, ret, message = utils.check_angle_from_view(obj=context.object, context=context) if not ret or not message: self.report({'ERROR'}, 'Could not get view angle infos') return {'CANCELLED'} title = 'Aligned \o/' if ret == 'INFO' else "Not aligned !" if context.region_data.view_perspective != 'CAMERA': title = title + ' ( not in camera view)' utils.show_message_box(_message=message, _title=title, _icon=ret) # self.report({ret}, message) return {'FINISHED'} class GPTB_OT_step_select_frames(Operator): bl_idname = "gptb.step_select_frames" bl_label = "Step Select Frame" bl_description = "Select frames by a step frame value" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' start : bpy.props.IntProperty(name='Start Frame', description='Start frame of the step animation', default=100) step : bpy.props.IntProperty(name='Step', description='Step of the frame, value of 2 select one frame on two', default=2, min=2) strict : bpy.props.BoolProperty(name='Strict', description='Strictly select step frame from start to scene end range\ \nElse reset step when a gap exsits already', default=False) # TODO: add option to start at cursor (True default) def invoke(self, context, execute): ## list frame to keep return context.window_manager.invoke_props_dialog(self, width=450) # return self.execute(context) def draw(self, context): layout = self.layout col = layout.column() col.prop(self, 'step') col.prop(self, 'strict') if self.strict: col.prop(self, 'start') ## helper (need more work) # col.separator() # range_string = f"{', '.join(numbers[:3])} ... {', '.join(numbers[-3:])}" # col.label(text=f'Will keep {range_string}') if not self.strict: col.label(text=f'Each gap will be considered a new step start', icon='INFO') def execute(self, context): numbers = [i for i in range(self.start, context.scene.frame_end + 1, self.step)] self.to_select = numbers ## Negative switch : list frames to remove self.to_select = [i for i in range(self.start, context.scene.frame_end + 1) if i not in numbers] gp = context.object.data ## Get frame summary (reset start after each existing gaps) key_summary = list(set([f.frame_number for l in gp.layers for f in l.frames])) key_summary.sort() print('key summary: ', key_summary) start = key_summary[0] if self.strict: to_select = self.to_select else: to_select = [] prev = None for k in key_summary: print(k, prev) if prev is not None and k != prev + 1: ## this is a gap ! new start prev = start = k # print('new start', start) continue new_range = [i for i in range(start, key_summary[-1] + 1, self.step)] # print('new_range: ', new_range) if k not in new_range: to_select.append(k) prev = k ## deselect all for l in gp.layers: for f in l.frames: f.select = False print('To select:', to_select) gct = 0 for i in to_select: ct = 0 for l in gp.layers: frame = next((f for f in l.frames if f.frame_number == i), None) if not frame: continue ## Select instead of remove frame.select = True ## Optionnally remove frames ? # l.frames.remove(frame) ct += 1 # print(f'{i}: Selected {ct} frame(s)') gct += ct self.report({'INFO'}, f'Selected {gct} frames') return {"FINISHED"} class GPTB_OT_open_addon_prefs(Operator): bl_idname = "gptb.open_addon_prefs" bl_label = "Open Addon Prefs" bl_description = "Open user preferences window in addon tab and prefill the search with addon name" bl_options = {"REGISTER", "INTERNAL"} def execute(self, context): utils.open_addon_prefs() return {'FINISHED'} classes = ( GPTB_OT_copy_text, GPTB_OT_flipx_view, GPTB_OT_view_camera_frame_fit, 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_toggle_hide_gp_modifier, GPTB_OT_list_disabled_anims, GPTB_OT_clear_active_frame, GPTB_OT_check_canvas_alignement, GPTB_OT_step_select_frames, GPTB_OT_open_addon_prefs, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)