gp_toolbox/OP_helpers.py

630 lines
23 KiB
Python

from time import ctime
import bpy
import mathutils
from mathutils import Vector#, Matrix
from pathlib import Path
import math
from math import radians
from . import utils
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_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
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
maincam.data.show_passepartout = False
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 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(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_toggle_hide_gp_modifier(bpy.types.Operator):
bl_idname = "gp.toggle_hide_gp_modifier"
bl_label = "Toggle Modifier Hide"
bl_description = "Show/Hide gp objects modifier (only touch modifier that are showed in render)\n(shift+clic 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 != 'GPENCIL':
continue
for m in o.grease_pencil_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(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:
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(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'}
class GPTB_OT_clear_active_frame(bpy.types.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 == 'GPENCIL'
def execute(self, context):
obj = context.object
l = obj.data.layers.active
if not l:
self.report({'ERROR'}, 'No layers')
return {'CANCELLED'}
f = l.active_frame
if not f:
self.report({'ERROR'}, 'No active frame')
return {'CANCELLED'}
ct = len(f.strokes)
if not ct:
self.report({'ERROR'}, 'Active frame already empty')
return {'CANCELLED'}
for s in reversed(f.strokes):
f.strokes.remove(s)
self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)')
return {'FINISHED'}
class GPTB_OT_check_canvas_alignement(bpy.types.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 == 'GPENCIL'# 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_open_addon_prefs(bpy.types.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_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_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)