2021-01-10 16:47:17 +01:00
|
|
|
import bpy
|
2021-11-25 17:00:09 +01:00
|
|
|
import mathutils
|
|
|
|
import math
|
2024-01-24 17:54:14 +01:00
|
|
|
|
|
|
|
from time import ctime
|
|
|
|
from mathutils import Vector #, Matrix
|
|
|
|
from pathlib import Path
|
2021-01-10 16:47:17 +01:00
|
|
|
from math import radians
|
2024-01-24 17:54:14 +01:00
|
|
|
from bpy.types import Operator
|
|
|
|
|
2023-02-14 18:49:48 +01:00
|
|
|
from .view3d_utils import View3D
|
2021-09-15 01:35:18 +02:00
|
|
|
from . import utils
|
2021-01-10 16:47:17 +01:00
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_copy_text(Operator):
|
2021-01-10 16:47:17 +01:00
|
|
|
bl_idname = "wm.copytext"
|
2022-10-07 02:20:28 +02:00
|
|
|
bl_label = "Copy To Clipboard"
|
2021-01-10 16:47:17 +01:00
|
|
|
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"}
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_flipx_view(Operator):
|
2022-10-07 02:20:28 +02:00
|
|
|
bl_idname = "view3d.camera_mirror_flipx"
|
|
|
|
bl_label = "Cam Mirror Flipx"
|
2021-01-10 16:47:17 +01:00
|
|
|
bl_description = "Invert X scale on camera to flip image horizontally"
|
|
|
|
bl_options = {"REGISTER"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
2022-10-16 20:13:52 +02:00
|
|
|
return context.area.type == 'VIEW_3D' and \
|
|
|
|
context.region_data.view_perspective == 'CAMERA'
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
context.scene.camera.scale.x *= -1
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_view_camera_frame_fit(Operator):
|
2023-02-14 18:49:48 +01:00
|
|
|
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"}
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_rename_data_from_obj(Operator):
|
2021-01-10 16:47:17 +01:00
|
|
|
bl_idname = "gp.rename_data_from_obj"
|
2022-10-07 02:20:28 +02:00
|
|
|
bl_label = "Rename GP From Object"
|
2021-01-10 16:47:17 +01:00
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
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:
|
2024-11-11 15:35:39 +01:00
|
|
|
if o.type == 'GREASEPENCIL':
|
2021-01-10 16:47:17 +01:00
|
|
|
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)
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_draw_cam(Operator):
|
2021-01-10 16:47:17 +01:00
|
|
|
bl_idname = "gp.draw_cam_switch"
|
2022-10-07 02:20:28 +02:00
|
|
|
bl_label = "Draw Cam Switch"
|
2021-01-10 16:47:17 +01:00
|
|
|
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
|
2021-06-05 01:20:35 +02:00
|
|
|
maincam.data.show_passepartout = context.scene.gptoolprops.drawcam_passepartout
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
else:
|
|
|
|
maincam = None
|
2021-06-05 01:20:35 +02:00
|
|
|
main_name = drawcam.get('maincam_name')# Custom prop with previous active cam.
|
2021-01-10 16:47:17 +01:00
|
|
|
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:
|
2021-09-15 01:35:18 +02:00
|
|
|
utils.set_collection(drawcam, camcol_name)
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
# 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)
|
2021-09-15 01:35:18 +02:00
|
|
|
utils.set_collection(drawcam, 'manip_cams')
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
if dcam_name == 'draw_cam':
|
|
|
|
drawcam.parent = maincam
|
2021-06-05 01:20:35 +02:00
|
|
|
if created: # set to main at creation time
|
2021-01-10 16:47:17 +01:00
|
|
|
drawcam.matrix_world = maincam.matrix_world
|
|
|
|
drawcam.lock_location = (True,True,True)
|
2021-06-05 01:20:35 +02:00
|
|
|
# drawcam.hide_viewport = True
|
|
|
|
context.scene.gptoolprops.drawcam_passepartout = maincam.data.show_passepartout
|
2023-02-23 19:15:55 +01:00
|
|
|
drawcam.data = maincam.data # get data from parent
|
2021-06-05 01:20:35 +02:00
|
|
|
|
2023-02-23 19:15:55 +01:00
|
|
|
# Hide the other passepartout to let only the custom OpenGL one
|
|
|
|
maincam.data.show_passepartout = False
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
else:
|
2023-02-23 19:15:55 +01:00
|
|
|
# object cam
|
2021-01-10 16:47:17 +01:00
|
|
|
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))
|
|
|
|
|
2024-11-11 15:35:39 +01:00
|
|
|
if act.type == 'GREASEPENCIL':
|
2021-01-10 16:47:17 +01:00
|
|
|
#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
|
|
|
|
|
2021-06-05 01:20:35 +02:00
|
|
|
if created and drawcam.name == 'obj_cam': # Go in camera view
|
2021-01-10 16:47:17 +01:00
|
|
|
context.region_data.view_perspective = 'CAMERA'
|
|
|
|
# ## make active
|
|
|
|
# bpy.context.view_layer.objects.active = ob
|
|
|
|
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_set_view_as_cam(Operator):
|
2021-01-10 16:47:17 +01:00
|
|
|
bl_idname = "gp.set_view_as_cam"
|
2022-10-07 02:20:28 +02:00
|
|
|
bl_label = "Cam At View"
|
2021-01-10 16:47:17 +01:00
|
|
|
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):
|
2022-10-16 20:13:52 +02:00
|
|
|
return context.area.type == 'VIEW_3D' and \
|
|
|
|
context.region_data.view_perspective != 'CAMERA'# need to be out of camera
|
2021-01-10 16:47:17 +01:00
|
|
|
# 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"}
|
|
|
|
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_reset_cam_rot(Operator):
|
2021-11-25 17:00:09 +01:00
|
|
|
bl_idname = "gp.reset_cam_rot"
|
2022-10-07 02:20:28 +02:00
|
|
|
bl_label = "Reset Rotation"
|
2021-11-25 17:00:09 +01:00
|
|
|
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"}
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_toggle_mute_animation(Operator):
|
2021-01-10 16:47:17 +01:00
|
|
|
bl_idname = "gp.toggle_mute_animation"
|
2021-10-20 21:17:37 +02:00
|
|
|
bl_label = "Toggle Animation Mute"
|
2021-01-10 16:47:17 +01:00
|
|
|
bl_description = "Enable/Disable animation evaluation\n(shift+clic to affect selection only)"
|
|
|
|
bl_options = {"REGISTER"}
|
|
|
|
|
|
|
|
|
|
|
|
mute : bpy.props.BoolProperty(default=False)
|
2022-06-23 15:43:25 +02:00
|
|
|
mode : bpy.props.StringProperty(default='OBJECT') # GPENCIL, CAMERA, OBJECT, ALL
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
def invoke(self, context, event):
|
|
|
|
self.selection = event.shift
|
|
|
|
return self.execute(context)
|
|
|
|
|
2022-03-05 15:36:01 +01:00
|
|
|
def set_action_mute(self, act):
|
2022-03-04 02:18:41 +01:00
|
|
|
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
|
2023-03-09 14:52:58 +01:00
|
|
|
for g in act.groups:
|
|
|
|
g.mute = self.mute
|
2022-03-04 02:18:41 +01:00
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
def execute(self, context):
|
|
|
|
if self.selection:
|
|
|
|
pool = context.selected_objects
|
|
|
|
else:
|
|
|
|
pool = context.scene.objects
|
|
|
|
|
|
|
|
for o in pool:
|
2024-11-11 15:35:39 +01:00
|
|
|
if self.mode == 'GREASEPENCIL' and o.type != 'GREASEPENCIL':
|
2021-01-10 16:47:17 +01:00
|
|
|
continue
|
2024-11-11 15:35:39 +01:00
|
|
|
if self.mode == 'OBJECT' and o.type in ('GREASEPENCIL', 'CAMERA'):
|
2022-06-23 15:43:25 +02:00
|
|
|
continue
|
|
|
|
if self.mode == 'CAMERA' and o.type != 'CAMERA':
|
2021-01-10 16:47:17 +01:00
|
|
|
continue
|
2022-03-05 15:36:01 +01:00
|
|
|
|
2022-11-09 18:59:22 +01:00
|
|
|
# mute attribute animation for GP and cameras
|
2024-11-11 15:35:39 +01:00
|
|
|
if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data:
|
2022-03-05 15:36:01 +01:00
|
|
|
gp_act = o.data.animation_data.action
|
|
|
|
if gp_act:
|
|
|
|
print(f'\n---{o.name} data:')
|
|
|
|
self.set_action_mute(gp_act)
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
if not o.animation_data:
|
|
|
|
continue
|
|
|
|
act = o.animation_data.action
|
|
|
|
if not act:
|
|
|
|
continue
|
|
|
|
|
2022-03-05 15:36:01 +01:00
|
|
|
print(f'\n---{o.name}:')
|
|
|
|
self.set_action_mute(act)
|
2022-03-04 02:18:41 +01:00
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_toggle_hide_gp_modifier(Operator):
|
2021-10-20 21:17:37 +02:00
|
|
|
bl_idname = "gp.toggle_hide_gp_modifier"
|
|
|
|
bl_label = "Toggle Modifier Hide"
|
2023-03-09 14:52:58 +01:00
|
|
|
bl_description = "Show/Hide viewport on GP objects modifier\
|
|
|
|
\nOnly touch modifier that are showed in render\
|
|
|
|
\nShift + click to affect selection only"
|
2021-10-20 21:17:37 +02:00
|
|
|
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:
|
2024-11-11 15:35:39 +01:00
|
|
|
if o.type != 'GREASEPENCIL':
|
2021-10-20 21:17:37 +02:00
|
|
|
continue
|
2024-11-27 16:40:31 +01:00
|
|
|
for m in o.modifiers:
|
2021-10-20 21:17:37 +02:00
|
|
|
# skip modifier that are not visible in render
|
|
|
|
if not m.show_render:
|
|
|
|
continue
|
|
|
|
m.show_viewport = self.show
|
|
|
|
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_list_disabled_anims(Operator):
|
2021-01-10 16:47:17 +01:00
|
|
|
bl_idname = "gp.list_disabled_anims"
|
2021-10-20 20:54:59 +02:00
|
|
|
bl_label = "List Disabled Anims"
|
2021-01-10 16:47:17 +01:00
|
|
|
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:
|
2024-11-11 15:35:39 +01:00
|
|
|
# if self.skip_gp and o.type == 'GREASEPENCIL':
|
2021-01-10 16:47:17 +01:00
|
|
|
# continue
|
2024-11-11 15:35:39 +01:00
|
|
|
# if self.skip_obj and o.type != 'GREASEPENCIL':
|
2021-01-10 16:47:17 +01:00
|
|
|
# continue
|
2022-03-05 15:36:01 +01:00
|
|
|
|
2024-11-11 15:35:39 +01:00
|
|
|
if o.type == 'GREASEPENCIL':
|
2022-03-05 15:36:01 +01:00
|
|
|
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}')
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
if not o.animation_data:
|
|
|
|
continue
|
|
|
|
act = o.animation_data.action
|
|
|
|
if not act:
|
|
|
|
continue
|
2023-03-09 14:52:58 +01:00
|
|
|
|
|
|
|
for g in act.groups:
|
|
|
|
if g.mute:
|
|
|
|
li.append(f'{o.name} - group: {g.name}')
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
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)
|
2022-03-05 15:36:01 +01:00
|
|
|
li.append(f'{o.name}:')
|
|
|
|
li.append(f' - {fcu.data_path} {fcu.array_index}')
|
2021-01-10 16:47:17 +01:00
|
|
|
if li:
|
2021-09-15 01:35:18 +02:00
|
|
|
utils.show_message_box(li)
|
2021-01-10 16:47:17 +01:00
|
|
|
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 ?)
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_overlay_presets(Operator):
|
2021-01-10 16:47:17 +01:00
|
|
|
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'}
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_clear_active_frame(Operator):
|
2021-03-31 01:38:17 +02:00
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
2021-03-31 01:38:17 +02:00
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
obj = context.object
|
|
|
|
l = obj.data.layers.active
|
|
|
|
if not l:
|
|
|
|
self.report({'ERROR'}, 'No layers')
|
|
|
|
return {'CANCELLED'}
|
2024-11-11 17:30:33 +01:00
|
|
|
f = l.current_frame()
|
2021-03-31 01:38:17 +02:00
|
|
|
if not f:
|
|
|
|
self.report({'ERROR'}, 'No active frame')
|
|
|
|
return {'CANCELLED'}
|
|
|
|
|
2024-11-11 17:48:11 +01:00
|
|
|
ct = len(f.drawing.strokes)
|
2021-03-31 01:38:17 +02:00
|
|
|
if not ct:
|
|
|
|
self.report({'ERROR'}, 'Active frame already empty')
|
|
|
|
return {'CANCELLED'}
|
|
|
|
|
2024-11-11 17:48:11 +01:00
|
|
|
for s in reversed(f.drawing.strokes):
|
|
|
|
f.drawing.strokes.remove(s)
|
2021-03-31 01:38:17 +02:00
|
|
|
self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)')
|
|
|
|
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
class GPTB_OT_check_canvas_alignement(Operator):
|
2021-09-15 01:35:18 +02:00
|
|
|
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
|
2024-11-11 15:35:39 +01:00
|
|
|
return context.object and context.object.type == 'GREASEPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW'
|
2021-09-15 01:35:18 +02:00
|
|
|
|
|
|
|
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'}
|
|
|
|
|
2024-01-24 17:54:14 +01:00
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
2024-01-24 17:54:14 +01:00
|
|
|
|
|
|
|
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):
|
2021-12-04 13:57:32 +01:00
|
|
|
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'}
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
classes = (
|
|
|
|
GPTB_OT_copy_text,
|
|
|
|
GPTB_OT_flipx_view,
|
2023-02-14 18:49:48 +01:00
|
|
|
GPTB_OT_view_camera_frame_fit,
|
2021-01-10 16:47:17 +01:00
|
|
|
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,
|
2021-10-20 21:17:37 +02:00
|
|
|
GPTB_OT_toggle_hide_gp_modifier,
|
2021-01-10 16:47:17 +01:00
|
|
|
GPTB_OT_list_disabled_anims,
|
2021-03-31 01:38:17 +02:00
|
|
|
GPTB_OT_clear_active_frame,
|
2021-09-15 01:35:18 +02:00
|
|
|
GPTB_OT_check_canvas_alignement,
|
2024-01-24 17:54:14 +01:00
|
|
|
GPTB_OT_step_select_frames,
|
2021-12-04 13:57:32 +01:00
|
|
|
GPTB_OT_open_addon_prefs,
|
2021-01-10 16:47:17 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
def register():
|
|
|
|
for cls in classes:
|
|
|
|
bpy.utils.register_class(cls)
|
|
|
|
|
|
|
|
def unregister():
|
|
|
|
for cls in reversed(classes):
|
|
|
|
bpy.utils.unregister_class(cls)
|