gp_toolbox/OP_helpers.py

546 lines
20 KiB
Python
Raw Normal View History

2021-01-10 16:47:17 +01:00
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)