2021-09-30 18:51:49 +02:00
from typing import Coroutine
2021-09-07 23:11:42 +02:00
import bpy
import re
from mathutils import Vector
from pathlib import Path
2021-09-17 18:36:15 +02:00
from math import isclose
2021-09-07 23:11:42 +02:00
from collections import defaultdict
2021-09-30 18:51:49 +02:00
from time import time
import json
2021-09-07 23:11:42 +02:00
def create_node ( type , tree = None , * * kargs ) :
''' Get a type, a tree to add in, and optionnaly multiple attribute to set
return created node
'''
tree = tree or bpy . context . scene . node_tree
node = tree . nodes . new ( type )
for k , v in kargs . items ( ) :
setattr ( node , k , v )
return node
2021-09-18 18:07:44 +02:00
def new_aa_node ( tree , * * kargs ) :
2021-09-07 23:11:42 +02:00
''' create AA node '''
aa = create_node ( ' CompositorNodeAntiAliasing ' , tree ) # type = ANTIALIASING
2021-09-18 18:07:44 +02:00
aa . threshold = 1.0 # 0.5
aa . contrast_limit = 0.25 # 0.5
2021-09-07 23:11:42 +02:00
aa . corner_rounding = 0.25
aa . hide = True
2021-09-18 18:07:44 +02:00
for k , v in kargs . items ( ) :
setattr ( aa , k , v )
2021-09-07 23:11:42 +02:00
return aa
2021-09-18 18:07:44 +02:00
def create_aa_nodegroup ( tree ) :
ngroup = bpy . data . node_groups . get ( ' AA ' )
if not ngroup :
ngroup = bpy . data . node_groups . new ( ' AA ' , ' CompositorNodeTree ' )
ng_in = create_node ( ' NodeGroupInput ' , tree = ngroup , location = ( - 600 , 0 ) )
ng_out = create_node ( ' NodeGroupOutput ' , tree = ngroup , location = ( 600 , 0 ) )
2021-09-21 18:23:25 +02:00
sep = create_node ( ' CompositorNodeSepRGBA ' , tree = ngroup , location = ( - 150 , 0 ) )
comb = create_node ( ' CompositorNodeCombRGBA ' , tree = ngroup , location = ( 350 , 25 ) )
# in AA
# ngroup.links.new(comb.outputs[0], ng_out.inputs[0]) # <- connect without out AA
aa = new_aa_node ( ngroup , location = ( - 400 , 0 ) )
# ngroup.links.new(ng_in.outputs[0], sep.inputs[0])
ngroup . links . new ( ng_in . outputs [ 0 ] , aa . inputs [ 0 ] )
ngroup . links . new ( aa . outputs [ 0 ] , sep . inputs [ 0 ] )
# ngroup.links.new(ng_in.outputs[0], sep.inputs[0])
2021-09-18 18:07:44 +02:00
for i in range ( 3 ) :
ngroup . links . new ( sep . outputs [ i ] , comb . inputs [ i ] )
# alpha AA
2021-09-21 18:23:25 +02:00
alpha_aa = new_aa_node ( ngroup , location = ( 100 , - 150 ) )
2021-09-18 18:07:44 +02:00
ngroup . links . new ( sep . outputs [ 3 ] , alpha_aa . inputs [ 0 ] )
ngroup . links . new ( alpha_aa . outputs [ 0 ] , comb . inputs [ 3 ] )
2021-09-21 18:23:25 +02:00
ngroup . links . new ( comb . outputs [ 0 ] , ng_out . inputs [ 0 ] )
2021-09-18 18:07:44 +02:00
ng = create_node ( ' CompositorNodeGroup ' , tree = tree )
ng . node_tree = ngroup
ng . name = ngroup . name
ng . hide = True
return ng
2021-09-07 23:11:42 +02:00
2021-09-15 18:28:16 +02:00
def copy_settings ( obj_a , obj_b ) :
exclusion = [ ' bl_rna ' , ' id_data ' , ' identifier ' , ' name_property ' , ' rna_type ' , ' properties ' , ' stamp_note_text ' , ' use_stamp_note ' ,
' settingsFilePath ' , ' settingsStamp ' , ' select ' , ' matrix_local ' , ' matrix_parent_inverse ' ,
' matrix_basis ' , ' location ' , ' rotation_euler ' , ' rotation_quaternion ' , ' rotation_axis_angle ' , ' scale ' ]
for attr in dir ( obj_a ) :
if attr . startswith ( ' __ ' ) :
continue
if attr in exclusion :
continue
# print('attr: ', attr)
# if obj_a.is_property_readonly(attr): # block when things aren't attribute
# continue
try :
val = getattr ( obj_a , attr )
except AttributeError :
2021-09-15 19:36:06 +02:00
# print(f'cant get {attr}')
pass
2021-09-15 18:28:16 +02:00
try :
setattr ( obj_b , attr , val )
except :
2021-09-15 19:36:06 +02:00
# print(f"can't set {attr}")
pass
2021-09-15 18:28:16 +02:00
2021-09-21 18:23:25 +02:00
def set_file_output_format ( fo ) :
2021-09-23 15:13:30 +02:00
fo . format . file_format = ' OPEN_EXR '
2021-09-21 18:23:25 +02:00
fo . format . color_mode = ' RGBA '
2021-09-23 15:13:30 +02:00
fo . format . color_depth = ' 16 '
fo . format . exr_codec = ' ZIP '
# fo.format.exr_codec = 'RLE'
# fo.format.file_format = 'PNG'
# fo.format.color_mode = 'RGBA'
# fo.format.color_depth = '8'
# fo.format.compression = 15
2021-09-21 18:23:25 +02:00
2021-10-25 16:02:11 +02:00
def set_scene_aa_settings ( scene = None , aa = True ) :
''' aa == using native AA, else disable scene AA '''
if not scene :
scene = bpy . context . scene
if aa :
scene . eevee . taa_render_samples = 32
scene . grease_pencil_settings . antialias_threshold = 1
else :
scene . eevee . taa_render_samples = 1
scene . grease_pencil_settings . antialias_threshold = 0
def set_settings ( scene = None , aa = True ) :
''' aa == using native AA, else disable scene AA '''
2021-09-07 23:11:42 +02:00
if not scene :
scene = bpy . context . scene
2021-10-25 16:02:11 +02:00
2021-09-07 23:11:42 +02:00
# specify scene settings for these kind of render
2021-10-25 16:02:11 +02:00
set_scene_aa_settings ( scene = scene , aa = aa )
2021-09-10 18:32:50 +02:00
scene . render . film_transparent = True
2021-09-30 18:51:49 +02:00
scene . render . use_compositing = True
scene . render . use_sequencer = False
2021-09-10 18:32:50 +02:00
scene . view_settings . view_transform = ' Standard '
2021-09-23 19:14:48 +02:00
scene . render . resolution_percentage = 100
2021-09-10 18:32:50 +02:00
2021-09-23 15:13:30 +02:00
# output (fast write settings since this is just to delete afterwards...)
2021-10-25 16:02:11 +02:00
scene . render . filepath = f ' //render/preview/ { scene . name } /preview_ '
2021-09-23 15:13:30 +02:00
scene . render . image_settings . file_format = ' JPEG '
2021-09-30 18:51:49 +02:00
scene . render . image_settings . color_mode = ' RGB '
2021-09-23 15:13:30 +02:00
scene . render . image_settings . quality = 0
2021-10-25 16:02:11 +02:00
def scene_aa ( scene = None , toggle = True ) :
''' Change scene AA settings and commute AA nodes according to toggle '''
if not scene :
scene = bpy . context . scene
# enable/disable native anti-alias on active scene
set_scene_aa_settings ( scene = scene , aa = toggle )
# mute/unmute AA nodegroups
for n in scene . node_tree . nodes :
if n . type == ' GROUP ' and n . name . startswith ( ' NG_ ' ) :
# n.mute = False # mute whole nodegroup ?
for gn in n . node_tree . nodes :
if gn . type == ' GROUP ' and gn . node_tree . name == ' AA ' :
gn . mute = toggle
2021-09-23 15:13:30 +02:00
2021-09-30 18:51:49 +02:00
def new_scene_from ( name , src_scn = None , regen = True , crop = True , link_cam = True , link_light = True ) :
''' Get / Create a scene from name and source scene to get settings from '''
scn = bpy . data . scenes . get ( name )
if scn and not regen :
return scn
elif scn and regen :
bpy . data . scenes . remove ( scn )
src_scn = src_scn or bpy . context . scene # given scene, or active scene
scn = bpy . data . scenes . new ( name )
## copy original settings over to new scene
# copy_settings(current, scn) # BAD
for attr in [ ' frame_start ' , ' frame_end ' , ' frame_current ' , ' camera ' , ' world ' ] :
setattr ( scn , attr , getattr ( src_scn , attr ) )
copy_settings ( src_scn . render , scn . render )
## link cameras (and lights ?)
for ob in src_scn . objects :
if link_cam and ob . type == ' CAMERA ' :
scn . collection . objects . link ( ob )
if link_light and ob . type == ' LIGHT ' :
scn . collection . objects . link ( ob )
2021-10-25 20:11:59 +02:00
# set adapted render settings
2021-09-30 18:51:49 +02:00
set_settings ( scn )
if crop :
scn . render . use_border = True
scn . render . use_crop_to_border = True
scn . use_nodes = True
return scn
2021-09-07 23:11:42 +02:00
def get_render_scene ( ) :
''' Get / Create a scene named Render '''
2021-09-18 18:07:44 +02:00
render_scn = bpy . data . scenes . get ( ' Render ' )
2021-10-18 12:09:52 +02:00
if render_scn :
return render_scn
current = bpy . context . scene
render_scn = bpy . data . scenes . new ( ' Render ' )
## copy original settings over to new scene
# copy_settings(current, render_scn) # BAD
for attr in [ ' frame_start ' , ' frame_end ' , ' frame_current ' , ' camera ' , ' world ' ] :
setattr ( render_scn , attr , getattr ( current , attr ) )
copy_settings ( current . render , render_scn . render )
## link cameras (and lights ?)
for ob in bpy . context . scene . objects :
if ob . type in ( ' CAMERA ' , ' LIGHT ' ) :
render_scn . collection . objects . link ( ob )
2021-09-08 18:29:10 +02:00
2021-09-18 18:07:44 +02:00
render_scn . use_nodes = True
2021-10-25 16:02:11 +02:00
2021-10-18 12:09:52 +02:00
# TODO Clear node tree (initial view layer stuff)
2021-10-25 20:11:59 +02:00
set_settings ( render_scn )
render_scn [ ' use_aa ' ] = True
2021-09-18 18:07:44 +02:00
return render_scn
2021-09-07 23:11:42 +02:00
def get_view_layer ( name , scene = None ) :
''' get viewlayer name
return existing / created viewlayer
'''
if not scene :
scene = get_render_scene ( )
### pass double letter prefix as suffix
## pass_name = re.sub(r'^([A-Z]{2})(_)(.*)', r'\3\2\1', 'name')
## pass_name = f'{name}_{passe}'
pass_vl = scene . view_layers . get ( name )
if not pass_vl :
pass_vl = scene . view_layers . new ( name )
return pass_vl
### node location tweaks
def real_loc ( n ) :
if not n . parent :
return n . location
return n . location + real_loc ( n . parent )
def get_frame_transform ( f , node_tree = None ) :
''' Return real transform location of a frame node
only works with one level of nesting ( not recursive )
'''
if not node_tree :
node_tree = f . id_data
if f . type != ' FRAME ' :
return
# return real_loc(f), f.dimensions
childs = [ n for n in node_tree . nodes if n . parent == f ]
# real_locs = [f.location + n.location for n in childs]
xs = [ n . location . x for n in childs ] + [ n . location . x + n . dimensions . x for n in childs ]
ys = [ n . location . y for n in childs ] + [ n . location . y - n . dimensions . y for n in childs ]
xs . sort ( key = lambda loc : loc ) # x val : ascending
ys . sort ( key = lambda loc : loc ) # ascending # , reversed=True) # y val : descending
loc = Vector ( ( min ( xs ) , max ( ys ) ) )
dim = Vector ( ( max ( xs ) - min ( xs ) + 60 , max ( ys ) - min ( ys ) + 60 ) )
return loc , dim
## get all frames with their real transform.
def bbox ( f , frames ) :
xs = [ ]
ys = [ ]
for n in frames [ f ] : # nodes of passed frame
# Better as Vectors ?
if n . type == ' FRAME ' :
if n not in frames . keys ( ) :
# print(f'frame {n.name} not in frame list')
continue
all_xs , all_ys = bbox ( n , frames ) # frames[n]
xs + = all_xs
ys + = all_ys
else :
loc = real_loc ( n )
xs + = [ loc . x , loc . x + n . dimensions . x ] # + (n.dimensions.x/get_dpi_factor())
ys + = [ loc . y , loc . y - n . dimensions . y ] # - (n.dimensions.y/get_dpi_factor())
# margin ~= 30
# return xs and ys
return [ min ( xs ) - 30 , max ( xs ) + 30 ] , [ min ( ys ) - 30 , max ( ys ) + 30 ]
def get_frames_bbox ( node_tree ) :
''' Return a dic with all frames
ex : { frame_node : ( location , dimension ) , . . . }
'''
# create dic of frame object with his direct child nodes nodes
frames = defaultdict ( list )
frames_bbox = { }
for n in node_tree . nodes :
if not n . parent :
continue
# also contains frames
frames [ n . parent ] . append ( n )
# Dic for bbox coord
for f , nodes in frames . items ( ) :
if f . parent :
continue
xs , ys = bbox ( f , frames )
# xs, ys = bbox(nodes, frames)
## returning: list of corner coords
# coords = [
# Vector((xs[0], ys[1])),
# Vector((xs[1], ys[1])),
# Vector((xs[1], ys[0])),
# Vector((xs[0], ys[0])),
# ]
# frames_bbox[f] = coords
## returning: (loc vector, dimensions vector)
frames_bbox [ f ] = Vector ( ( xs [ 0 ] , ys [ 1 ] ) ) , Vector ( ( xs [ 1 ] - xs [ 0 ] , ys [ 1 ] - ys [ 0 ] ) )
return frames_bbox
## nodes helper functions
def clear_nodegroup ( name , full_clear = False ) :
''' remove duplication of a nodegroup (.???)
also remove the base one if full_clear True
'''
for ng in reversed ( bpy . data . node_groups ) :
pattern = name + r ' \ . \ d {3} '
2021-09-10 18:32:50 +02:00
if not full_clear and ng . users :
continue
2021-09-07 23:11:42 +02:00
if re . search ( pattern , ng . name ) :
bpy . data . node_groups . remove ( ng )
if full_clear and ng . name == name :
# if full clear
bpy . data . node_groups . remove ( ng )
def rearrange_frames ( node_tree ) :
frame_d = get_frames_bbox ( node_tree ) # dic : {frame_node:(loc vector, dimensions vector), ...}
if not frame_d :
print ( ' no frame found ' )
return
2021-09-15 19:36:06 +02:00
# print([f.name for f in frame_d.keys()])
2021-09-07 23:11:42 +02:00
## order the dict by frame.y location
frame_d = { key : value for key , value in sorted ( frame_d . items ( ) , key = lambda pair : pair [ 1 ] [ 0 ] . y - pair [ 1 ] [ 1 ] . y , reverse = True ) }
frames = [ [ f , v [ 0 ] , v [ 1 ] . y ] for f , v in frame_d . items ( ) ] # [frame_node, real_loc, real dimensions]
2021-09-16 00:19:57 +02:00
2021-09-07 23:11:42 +02:00
top = frames [ 0 ] [ 1 ] . y # upper node location.y
2021-09-16 00:19:57 +02:00
# top = 0 #always start a 0
2021-09-07 23:11:42 +02:00
offset = 0
for f in frames :
## f[1] : real loc Vector
## f[0] : frame
## move frame by offset needed (delta between real_loc and "fake" loc , minus offset)
2021-09-16 00:19:57 +02:00
f [ 0 ] . location . y = ( f [ 1 ] . y - f [ 0 ] . location . y ) - offset # avoid offset when recalculating from 0 top
2021-09-07 23:11:42 +02:00
# f[0].location.y = f[1].y - top - offset
2021-09-16 00:19:57 +02:00
offset + = f [ 2 ] + 200 # gap
2021-09-07 23:11:42 +02:00
f [ 0 ] . update ( )
def reorder_inputs ( ng ) :
rl_nodes = [ s . links [ 0 ] . from_node for s in ng . inputs if s . is_linked and s . links and s . links [ 0 ] . from_node . type == ' R_LAYERS ' ]
rl_nodes . sort ( key = lambda x : x . location . y , reverse = True )
names = [ n . layer for n in rl_nodes ]
inputs_names = [ s . name for s in ng . inputs ]
filtered_names = [ n for n in names if n in inputs_names ]
for dest , name in enumerate ( filtered_names ) :
## rebuild list at each iteration so index are good
inputs_names = [ s . name for s in ng . inputs ]
src = inputs_names . index ( name )
# reorder on node_tree not directly on node!
ng . node_tree . inputs . move ( src , dest )
def reorder_outputs ( ng ) :
ordered_out_name = [ nis . name for nis in ng . inputs if nis . name in [ o . name for o in ng . outputs ] ]
for s_name in ordered_out_name :
all_outnames = [ o . name for o in ng . outputs ]
# reorder on nodetree, not on node !
ng . node_tree . outputs . move ( all_outnames . index ( s_name ) , ordered_out_name . index ( s_name ) )
def clear_disconnected ( fo ) :
for inp in reversed ( fo . inputs ) :
if not inp . is_linked :
print ( f ' Deleting unlinked fileout slot: { inp . name } ' )
fo . inputs . remove ( inp )
def reorder_fileout ( fo , ng = None ) :
if not ng : # get connected nodegroup
for s in fo . inputs :
if s . is_linked and s . links and s . links [ 0 ] . from_node . type == ' GROUP ' :
ng = s . links [ 0 ] . from_node
break
if not ng :
print ( f ' No nodegroup to refer to filter { fo . name } ' )
return
ordered = [ o . links [ 0 ] . to_socket . name for o in ng . outputs if o . is_linked and o . is_linked and o . links [ 0 ] . to_node == fo ]
for s_name in ordered :
all_outnames = [ s . name for s in fo . inputs ] # same as [fs.path for fs in fo.file_slots]
fo . inputs . move ( all_outnames . index ( s_name ) , ordered . index ( s_name ) )
2021-09-10 18:32:50 +02:00
def reorganise_NG_nodegroup ( ng ) :
''' refit node content to avoid overlap '''
ngroup = ng . node_tree
ng_in = ngroup . nodes . get ( ' Group Input ' )
offset = 35
y = 0
for s in ng_in . outputs :
if s . is_linked :
s . links [ 0 ] . to_node . location . y = y
y - = offset
2021-09-07 23:11:42 +02:00
def connect_to_group_output ( n ) :
for o in n . outputs :
if o . is_linked :
if o . links [ 0 ] . to_node . type == ' GROUP_OUTPUT ' :
return o . links [ 0 ] . to_socket
val = connect_to_group_output ( o . links [ 0 ] . to_node )
if val :
return val
return False
def connect_to_group_input ( n ) :
for i in n . inputs :
if i . is_linked :
if i . links [ 0 ] . from_node . type == ' GROUP_INPUT ' :
return i . links [ 0 ] . from_socket
val = connect_to_group_input ( i . links [ 0 ] . from_node )
if val :
return val
2021-09-08 18:29:10 +02:00
return False
2021-09-23 19:14:48 +02:00
def all_connected_forward ( n , nlist = [ ] ) :
''' return list of all forward connected nodes recursively (include passed nodes) '''
for o in n . outputs :
if o . is_linked :
for lnk in o . links :
if lnk . to_node . type == ' GROUP_OUTPUT ' :
if n not in nlist :
return nlist + [ n ]
else :
return nlist
else :
nlist = all_connected_forward ( lnk . to_node , nlist )
if n in nlist :
return nlist
return nlist + [ n ]
2021-09-10 18:32:50 +02:00
def clear_nodegroup_content_if_disconnected ( ngroup ) :
''' Get a nodegroup.node_tree
delete orphan nodes that are not connected from group input node
'''
for n in reversed ( ngroup . nodes ) :
if n . type in ( ' GROUP_INPUT ' , ' GROUP_OUTPUT ' ) :
continue
if not connect_to_group_input ( n ) and not connect_to_group_output ( n ) : # is disconnected from both side
ngroup . nodes . remove ( n )
2021-09-15 18:28:16 +02:00
def clean_nodegroup_inputs ( ng , skip_existing_pass = True ) :
2021-09-10 18:32:50 +02:00
''' Clear inputs to output of passed nodegroup if not connected '''
ngroup = ng . node_tree
2021-09-15 18:28:16 +02:00
rl_nodes = [ n . layer for n in ng . id_data . nodes if n . type == ' R_LAYERS ' ]
2021-09-10 18:32:50 +02:00
for i in range ( len ( ng . inputs ) ) [ : : - 1 ] :
2021-09-15 18:28:16 +02:00
if not ng . inputs [ i ] . is_linked :
if skip_existing_pass and any ( ng . inputs [ i ] . name == x for x in rl_nodes ) :
# a render layer of this name still exists
continue
2021-09-10 18:32:50 +02:00
ngroup . inputs . remove ( ngroup . inputs [ i ] )
# clear_nodegroup_content_if_disconnected(ngroup)
2021-10-01 12:42:21 +02:00
def bridge_reconnect_nodegroup ( ng , socket_name = None ) :
'''
Reconnect group_in and group out that have been disconnected
: socket : only use this specific socket type
'''
ngroup = ng . node_tree
ng_in = ngroup . nodes . get ( ' Group Input ' )
ng_out = ngroup . nodes . get ( ' Group Output ' )
for sockin in ng_in . outputs :
if socket_name and sockin . name != socket_name :
continue
if not sockin . name : # last empty output is listed
continue
sockout = ng_out . inputs . get ( sockin . name )
if not sockout :
continue
if len ( sockin . links ) and connect_to_group_output ( sockin . links [ 0 ] . to_node ) :
continue
## need reconnect
aa = create_aa_nodegroup ( ngroup )
ngroup . links . new ( sockin , aa . inputs [ 0 ] )
ngroup . links . new ( aa . outputs [ 0 ] , sockout )
print ( f ' { ng . name } : Bridged { sockin . name } ' )
2021-09-08 18:29:10 +02:00
def random_color ( alpha = False ) :
import random
if alpha :
return ( random . uniform ( 0 , 1 ) , random . uniform ( 0 , 1 ) , random . uniform ( 0 , 1 ) , 1 )
2021-09-10 18:32:50 +02:00
return ( random . uniform ( 0 , 1 ) , random . uniform ( 0 , 1 ) , random . uniform ( 0 , 1 ) )
2021-09-10 22:59:52 +02:00
def nodegroup_merge_inputs ( ngroup ) :
''' Get a nodegroup
merge every group inputs with alpha over
then connect to antialias and a new output
'''
ng_in = ngroup . nodes . get ( ' Group Input ' )
ng_out = ngroup . nodes . get ( ' Group Output ' )
x , y = ng_in . location . x + 200 , 0
offset_x , offset_y = 150 , - 100
# merge all inputs in alphaover nodes
prev = None
for i in range ( len ( ng_in . outputs ) - 1 ) : # skip waiting point
inp = ng_in . outputs [ i ]
if not prev :
prev = ng_in
continue
# live connect
ao = create_node ( ' CompositorNodeAlphaOver ' , tree = ngroup , location = ( x , y ) , hide = True )
ngroup . links . new ( prev . outputs [ 0 ] , ao . inputs [ 1 ] )
ngroup . links . new ( inp , ao . inputs [ 2 ] )
x + = offset_x
y + = offset_y
prev = ao
## create a merged name as output ??
2021-09-21 18:23:25 +02:00
aa = create_aa_nodegroup ( ngroup ) # new_aa_node(ngroup)
2021-09-10 22:59:52 +02:00
aa . location = ( ao . location . x + 200 , ao . location . y )
ngroup . links . new ( ao . outputs [ 0 ] , aa . inputs [ 0 ] ) # node_tree
# create one input and link
out = ngroup . outputs . new ( ' NodeSocketColor ' , ngroup . inputs [ 0 ] . name )
ngroup . links . new ( aa . outputs [ 0 ] , ng_out . inputs [ 0 ] )
## --- renumbering funcs ---
2021-09-10 18:32:50 +02:00
def get_numbered_output ( out , slot_name ) :
''' Return output slot name without looking for numbering ???_
'''
pattern = r ' ^(?: \ d {3} _)? ' # optional non capture group of 3 digits + _
pattern = f ' { pattern } { slot_name } '
for inp in out . inputs :
if re . match ( pattern , inp . name ) :
return inp
def add_fileslot_number ( fs , number ) :
elems = fs . path . split ( ' / ' )
for i , e in enumerate ( elems ) :
if re . match ( r ' ^ \ d {3} _ ' , e ) :
elems [ i ] = re . sub ( r ' ^( \ d {3} ) ' , lambda x : str ( number ) . zfill ( 3 ) , e )
else :
elems [ i ] = f ' { str ( number ) . zfill ( 3 ) } _ { e } '
new = ' / ' . join ( elems )
fs . path = new
return new
def renumber ( fo , offset = 10 ) :
''' Force renumber all the slots with a 3 '''
if fo . type != ' OUTPUT_FILE ' : return
2021-09-10 22:59:52 +02:00
ct = 10 # start at 10
2021-09-10 18:32:50 +02:00
for fs in fo . file_slots :
add_fileslot_number ( fs , ct )
ct + = offset
2021-09-10 22:59:52 +02:00
def get_num ( string ) - > int :
''' get a tring or a file_slot object
return leading number or None
'''
if not isinstance ( string , str ) :
string = string . path
2021-09-10 18:32:50 +02:00
num = re . search ( r ' ^( \ d {3} )_ ' , string )
if num :
return int ( num . group ( 1 ) )
2021-09-10 22:59:52 +02:00
def delete_numbering ( fo ) : # padding=3
''' Delete prefix numbering on all slots on passed file output '''
2021-09-10 18:32:50 +02:00
if fo . type != ' OUTPUT_FILE ' : return
2021-09-10 22:59:52 +02:00
for fs in fo . file_slots :
elems = fs . path . split ( ' / ' )
for i , e in enumerate ( elems ) :
elems [ i ] = re . sub ( r ' ^ \ d {3} _ ' , ' ' , e )
new = ' / ' . join ( elems )
fs . path = new
2021-09-10 18:32:50 +02:00
2021-09-21 19:14:15 +02:00
def reverse_fileout_inputs ( fo ) :
count = len ( fo . inputs )
for i in range ( count ) :
fo . inputs . move ( count - 1 , i )
def renumber_keep_existing ( fo , offset = 10 , invert = True ) :
2021-09-10 22:59:52 +02:00
''' Renumber by keeping existing numbers and inserting new one whenever possible
Big and ugly function that do the trick nonetheless . . .
'''
if fo . type != ' OUTPUT_FILE ' : return
ct = 10
2021-09-21 19:14:15 +02:00
if invert :
reverse_fileout_inputs ( fo )
2021-09-10 22:59:52 +02:00
fsl = fo . file_slots
2021-09-10 18:32:50 +02:00
2021-09-10 22:59:52 +02:00
last_idx = len ( fsl ) - 1
2021-09-10 18:32:50 +02:00
prev = None
prev_num = None
2021-09-10 22:59:52 +02:00
for idx , fs in enumerate ( fsl ) :
2021-09-15 19:36:06 +02:00
# print('-->', idx, fs.path)
2021-09-10 22:59:52 +02:00
2021-09-10 18:32:50 +02:00
if idx == last_idx : # handle last
2021-09-21 19:14:15 +02:00
if get_num ( fs ) is not None :
break
2021-09-10 18:32:50 +02:00
if idx > 0 :
2021-09-10 22:59:52 +02:00
prev = fsl [ idx - 1 ]
num = get_num ( prev )
if num is not None :
2021-09-15 10:34:35 +02:00
add_fileslot_number ( fs , num + offset )
2021-09-10 18:32:50 +02:00
else :
2021-09-10 22:59:52 +02:00
add_fileslot_number ( fs , ct )
2021-09-10 18:32:50 +02:00
else :
2021-09-10 22:59:52 +02:00
add_fileslot_number ( fs , 10 ) # there is only one slot (maybe don't number ?)
2021-09-10 18:32:50 +02:00
break
# update the ct with the current taken number if any
2021-09-10 22:59:52 +02:00
number = get_num ( fs )
if number is not None :
2021-09-10 18:32:50 +02:00
prev = fs
ct = number + offset
continue # skip already numbered
# analyse all next slots until there is numbered
2021-09-10 22:59:52 +02:00
divider = 0
2021-09-15 19:36:06 +02:00
# print(f'range(1, {len(fsl) - idx}')
2021-09-10 22:59:52 +02:00
for i in range ( 1 , len ( fsl ) - idx ) :
next_num = get_num ( fsl [ idx + i ] )
if next_num is not None :
divider = i + 1
2021-09-10 18:32:50 +02:00
break
2021-09-10 22:59:52 +02:00
if idx == 0 : # handle first
2021-09-10 18:32:50 +02:00
prev_num = 0
prev = None
2021-09-10 22:59:52 +02:00
if next_num is None :
add_fileslot_number ( fs , 0 )
elif next_num == 0 :
print ( f ' Cannot insert value before 0 to { fsl . path } ' )
continue
else :
add_fileslot_number ( fs , int ( next_num / 2 ) )
2021-09-10 18:32:50 +02:00
else :
2021-09-10 22:59:52 +02:00
prev = fsl [ idx - 1 ]
test_prev = get_num ( prev )
if test_prev is not None :
prev_num = test_prev
if not divider :
if prev_num is not None :
add_fileslot_number ( fs , prev_num + offset )
else :
add_fileslot_number ( fs , ct )
2021-09-10 18:32:50 +02:00
2021-09-10 22:59:52 +02:00
else :
if prev_num is not None :
# iterate rename
gap_inc = int ( ( next_num - prev_num ) / divider )
if gap_inc < 1 : # same values !
print ( f ' cannot insert a median value at { fs . path } between { prev_num } and { next_num } ' )
continue
ct = prev_num
for temp_id in range ( idx , idx + i ) :
ct + = gap_inc
add_fileslot_number ( fsl [ temp_id ] , ct )
else :
print ( " what ' s going on ? \n " )
2021-09-10 18:32:50 +02:00
# first check if it has a number (if not bas)
prev = fs
2021-09-17 16:31:26 +02:00
ct + = offset
2021-09-21 19:14:15 +02:00
if invert :
reverse_fileout_inputs ( fo )
2021-09-17 16:31:26 +02:00
2021-09-17 18:36:15 +02:00
def has_channel_color ( layer ) :
''' Return True if gp_layer.channel_color is different than the default (0.2, 0.2, 0.2) '''
if not any ( isclose ( i , 0.2 , abs_tol = 0.001 ) for i in layer . channel_color ) :
return True
2021-09-22 12:06:40 +02:00
def normalize ( text ) :
return text . lower ( ) . replace ( ' - ' , ' _ ' )
PATTERN = r ' ^(?P<grp>- \ s)?(?P<tag>[A-Z] {2} _)?(?P<name>.*?)(?P<sfix>_[A-Z] {2} )?(?P<inc> \ . \ d {3} )?$ ' # numering
def normalize_layer_name ( layer , prefix = ' ' , desc = ' ' , suffix = ' ' , lower = True , dash_to_underscore = True ) :
''' GET a layer and argument to build and assign name '''
import re
name = layer . info
pattern = PATTERN
sep = ' _ '
res = re . search ( pattern , name . strip ( ) )
grp = ' ' if res . group ( ' grp ' ) is None else res . group ( ' grp ' )
tag = ' ' if res . group ( ' tag ' ) is None else res . group ( ' tag ' )
# tag2 = '' if res.group('tag2') is None else res.group('tag2')
name = ' ' if res . group ( ' name ' ) is None else res . group ( ' name ' )
sfix = ' ' if res . group ( ' sfix ' ) is None else res . group ( ' sfix ' )
inc = ' ' if res . group ( ' inc ' ) is None else res . group ( ' inc ' )
if grp :
grp = ' ' + grp # name is strip(), so grp first spaces are gones.
if prefix :
if prefix == ' prefixkillcode ' :
tag = ' '
else :
tag = prefix . upper ( ) . strip ( ) + sep
# if prefix2:
# tag2 = prefix2.upper().strip() + sep
if desc :
name = desc
if suffix :
if suffix == ' suffixkillcode ' :
sfix = ' '
else :
sfix = sep + suffix . upper ( ) . strip ( )
# check if name is available without the increment ending
if lower :
name = name . lower ( )
if dash_to_underscore :
name = name . replace ( ' - ' , ' _ ' )
new = f ' { grp } { tag } { name } { sfix } ' # lower suffix ?
if new != layer . info :
2021-10-01 12:58:46 +02:00
old = layer . info
print ( f ' { old } >> { new } ' )
2021-09-22 12:06:40 +02:00
layer . info = new
2021-10-01 12:58:46 +02:00
# Also change name string in modifier target !
for ob in [ o for o in bpy . data . objects if o . type == ' GPENCIL ' and o . data == layer . id_data ] :
for m in ob . grease_pencil_modifiers :
if hasattr ( m , ' layer ' ) and m . layer :
if m . layer == old :
print ( f ' - updated in { ob . name } modifier { m . name } ' )
m . layer = new
2021-09-22 12:06:40 +02:00
2021-09-17 16:31:26 +02:00
## confirm pop-up message:
def show_message_box ( _message = " " , _title = " Message Box " , _icon = ' INFO ' ) :
def draw ( self , context ) :
for l in _message :
if isinstance ( l , str ) :
self . layout . label ( text = l )
else :
self . layout . label ( text = l [ 0 ] , icon = l [ 1 ] )
if isinstance ( _message , str ) :
_message = [ _message ]
2021-09-30 18:51:49 +02:00
bpy . context . window_manager . popup_menu ( draw , title = _title , icon = _icon )
def get_bbox_3d ( ob ) :
bbox_coords = ob . bound_box
return [ ob . matrix_world @ Vector ( b ) for b in bbox_coords ]
def get_crop_pixel_coord ( scn ) :
# width height probably not needed. might need
px_width = ( scn . render . border_max_x - scn . render . border_min_x ) * scn . render . resolution_x
px_height = ( scn . render . border_max_y - scn . render . border_min_y ) * scn . render . resolution_y
pos_x = ( scn . render . border_min_x + ( ( scn . render . border_max_x - scn . render . border_min_x ) / 2 ) ) * scn . render . resolution_x
## coord y > image center coord from bottom-left (Blender)
# pos_y = (scn.render.border_min_y + ((scn.render.border_max_y - scn.render.border_min_y) / 2)) * scn.render.resolution_y,
## image center coord from top-left (AE)
pos_y = ( ( 1 - scn . render . border_max_y ) + ( ( scn . render . border_max_y - scn . render . border_min_y ) / 2 ) ) * scn . render . resolution_y
coord = {
' position_x ' : round ( pos_x ) ,
' position_y ' : round ( pos_y ) ,
' width ' : round ( px_width ) ,
' height ' : round ( px_height ) ,
}
return coord
def export_crop_to_json ( ) :
''' Export crop to json coords for AE
'''
blend = Path ( bpy . data . filepath )
json_path = blend . parent / ' render ' / f ' { blend . stem } .json ' #f'{ob.name}.json'
2021-11-05 11:00:22 +01:00
2021-09-30 18:51:49 +02:00
## per scene : json_path = Path(bpy.data.filepath).parent / 'render' / f'{scn.name}.json'
# json_path = Path(bpy.data.filepath).parent / 'render' / f'{scn.name}.json' #f'{ob.name}.json'
coord_dic = { }
for scn in bpy . data . scenes :
# if scn.name in {'Scene', 'Render'}:
2021-11-11 14:36:57 +01:00
# if scn.name == 'Scene':
# continue
2021-09-30 18:51:49 +02:00
if scn . render . use_border :
scn_border = get_crop_pixel_coord ( scn )
2021-11-11 14:36:57 +01:00
## use scn name
coord_dic [ scn . name ] = scn_border
## use name of first found GP :
# gps = [o for o in scn.objects if o.type == 'GPENCIL']
# if gps:
# for ob in gps:
# coord_dic[ob.name] = scn_border
# else:
# coord_dic[scn.name] = scn_border
2021-09-30 18:51:49 +02:00
2021-10-29 16:42:35 +02:00
if coord_dic :
2021-11-05 11:00:22 +01:00
json_path . parent . mkdir ( parents = False , exist_ok = True )
2021-10-29 16:42:35 +02:00
# save bbox
with json_path . open ( ' w ' ) as fd :
json . dump ( coord_dic , fd , indent = ' \t ' )
2021-09-30 18:51:49 +02:00
2021-10-29 16:42:35 +02:00
print ( f ' coord saved at: { json_path } ' )
2021-09-30 18:51:49 +02:00
return coord_dic
def set_border_region_from_coord ( coords , scn = None , margin = 30 , export_json = True ) :
2021-10-13 19:00:15 +02:00
''' Get a list of point coord in worldcamera view space (0 to 1) on each axis
set border ( of passed scene : scn : ) with given coordinate
return the coords list as pixel coordinate
'''
2021-09-30 18:51:49 +02:00
scn = scn or bpy . context . scene
coords2d_x = sorted ( [ c [ 0 ] for c in coords ] )
coords2d_y = sorted ( [ c [ 1 ] for c in coords ] )
margin_width = margin / scn . render . resolution_x
margin_height = margin / scn . render . resolution_y
# set crop
scn . render . border_min_x = coords2d_x [ 0 ] - margin_width
scn . render . border_max_x = coords2d_x [ - 1 ] + margin_width
scn . render . border_min_y = coords2d_y [ 0 ] - margin_height
scn . render . border_max_y = coords2d_y [ - 1 ] + margin_height
## get clamped relative value
# relative_bbox2d_coords = [
# (scn.render.border_min_x, scn.render.border_min_y),
# (scn.render.border_min_x, scn.render.border_max_y),
# (scn.render.border_max_x, scn.render.border_max_y),
# (scn.render.border_max_x, scn.render.border_min_y),
# ]
pixel_bbox2d_coords = [
( scn . render . border_min_x * scn . render . resolution_x , scn . render . border_min_y * scn . render . resolution_y ) ,
( scn . render . border_min_x * scn . render . resolution_x , scn . render . border_max_y * scn . render . resolution_y ) ,
( scn . render . border_max_x * scn . render . resolution_x , scn . render . border_max_y * scn . render . resolution_y ) ,
( scn . render . border_max_x * scn . render . resolution_x , scn . render . border_min_y * scn . render . resolution_y ) ,
]
# if export_json:
# export_crop_to_json(scn)
return pixel_bbox2d_coords
def get_gp_box_all_frame ( ob , cam = None ) :
''' set crop to object bounding box considering whole animation. Cam should not be animated (render in bg_cam)
return 2 d bbox in pixels
'''
from bpy_extras . object_utils import world_to_camera_view
coords_cam_list = [ ]
scn = bpy . context . scene
cam = cam or scn . camera
start = time ( )
if ob . animation_data and ob . animation_data . action : # use frame set on all frames
print ( f ' { ob . name } has anim ' )
# frame_nums = sorted(list(set([f.frame_number for l in ob.data.layers if len(l.frames) for f in l.frames if len(f.strokes) and scn.frame_start <= f.frame_number <= scn.frame_end])))
for num in range ( scn . frame_start , scn . frame_end + 1 ) :
scn . frame_set ( num )
for l in ob . data . layers :
if l . hide or l . opacity == 0.0 :
continue
if l . active_frame :
for s in l . active_frame . strokes :
if len ( s . points ) == 1 : # skip isolated points
continue
coords_cam_list + = [ world_to_camera_view ( scn , cam , ob . matrix_world @ p . co ) for p in s . points ]
else :
# if object is not animated no need to frame_set to update object position
print ( f ' { ob . name } no anim ' )
for l in ob . data . layers :
if l . hide or l . opacity == 0.0 :
continue
for f in l . frames :
if not ( scn . frame_start < = f . frame_number < = scn . frame_end ) :
continue
for s in f . strokes :
if len ( s . points ) == 1 : # skip isolated points
continue
coords_cam_list + = [ world_to_camera_view ( scn , cam , ob . matrix_world @ p . co ) for p in s . points ]
print ( time ( ) - start ) # Dbg-time
return coords_cam_list
def has_anim ( ob ) :
# TODO make a better check (check if there is only one key in each channel, count as not animated)
return ob . animation_data and ob . animation_data . action
2021-11-04 22:20:21 +01:00
def has_keyframe ( ob , attr ) :
anim = ob . animation_data
if anim is not None and anim . action is not None :
for fcu in anim . action . fcurves :
if fcu . data_path == attr :
return len ( fcu . keyframe_points ) > 0
return False
2021-10-13 19:00:15 +02:00
def get_gp_box_all_frame_selection ( oblist = None , scn = None , cam = None , timeout = 40 ) :
2021-09-30 18:51:49 +02:00
'''
get points of all selection
return 2 d bbox in pixels
2021-10-13 19:00:15 +02:00
return None if timeout ( too long to process , better to do it visually )
2021-09-30 18:51:49 +02:00
'''
from bpy_extras . object_utils import world_to_camera_view
2021-10-13 19:00:15 +02:00
t0 = time ( )
2021-09-30 18:51:49 +02:00
coords_cam_list = [ ]
scn = scn or bpy . context . scene
oblist = oblist or [ o for o in scn . objects if o . select_get ( ) ]
cam = cam or scn . camera
start = time ( )
if any ( has_anim ( ob ) for ob in oblist ) :
print ( f ' at least one is animated: { oblist } ' )
for num in range ( scn . frame_start , scn . frame_end + 1 ) :
scn . frame_set ( num )
for ob in oblist :
for l in ob . data . layers :
if l . hide or l . opacity == 0.0 :
continue
if not l . active_frame :
continue
for s in l . active_frame . strokes :
if len ( s . points ) == 1 : # skip isolated points
continue
coords_cam_list + = [ world_to_camera_view ( scn , cam , ob . matrix_world @ p . co ) for p in s . points ]
2021-10-13 19:00:15 +02:00
if time ( ) - t0 > timeout :
print ( f ' timeout (more than { timeout } s to calculate) evaluating frame position of objects { oblist } ' )
return
2021-09-30 18:51:49 +02:00
else :
print ( f ' No anim ' )
for ob in oblist :
# if object is not animated no need to frame_set to update object position
for l in ob . data . layers :
if l . hide or l . opacity == 0.0 :
continue
for f in l . frames :
2021-10-13 19:00:15 +02:00
if time ( ) - t0 > timeout :
print ( f ' timeout (more than { timeout } s to calculate) evaluating frame position of objects { oblist } ' )
return
2021-09-30 18:51:49 +02:00
if not ( scn . frame_start < = f . frame_number < = scn . frame_end ) :
continue
for s in f . strokes :
if len ( s . points ) == 1 : # skip isolated points
continue
coords_cam_list + = [ world_to_camera_view ( scn , cam , ob . matrix_world @ p . co ) for p in s . points ]
2021-10-13 19:00:15 +02:00
2021-09-30 18:51:49 +02:00
print ( f ' { len ( coords_cam_list ) } gp points listed { time ( ) - start : .1f } s ' )
return coords_cam_list
def get_bbox_2d ( ob , cam = None ) :
from bpy_extras . object_utils import world_to_camera_view
scn = bpy . context . scene
cam = cam or scn . camera
coords2d = [ world_to_camera_view ( scn , cam , p ) for p in get_bbox_3d ( ob ) ]
coords2d_x = sorted ( [ c [ 0 ] for c in coords2d ] )
coords2d_y = sorted ( [ c [ 1 ] for c in coords2d ] )
bbox2d_coords = [
( coords2d_x [ 0 ] , coords2d_y [ 0 ] ) ,
( coords2d_x [ 0 ] , coords2d_y [ - 1 ] ) ,
( coords2d_x [ - 1 ] , coords2d_y [ - 1 ] ) ,
( coords2d_x [ - 1 ] , coords2d_y [ 0 ] ) ,
]
return [ Vector ( b ) for b in bbox2d_coords ]
def set_box_from_selected_objects ( scn = None , cam = None , export_json = False ) :
scn = scn or bpy . context . scene
cam = cam or scn . camera
selection = [ o for o in scn . objects if o . select_get ( ) ] # selected_objects
coords = get_gp_box_all_frame_selection ( oblist = selection , scn = scn , cam = cam )
2021-10-13 19:00:15 +02:00
if not coords :
return f ' Border not set: Timeout during analysis of { len ( selection ) } objects '
2021-09-30 18:51:49 +02:00
_bbox_px = set_border_region_from_coord ( coords , margin = 30 , scn = scn , export_json = export_json )
def get_collection_childs_recursive ( col , cols = [ ] ) :
''' return a list of all the sub-collections in passed col '''
for sub in col . children :
if sub not in cols :
cols . append ( sub )
if len ( sub . children ) :
cols = get_collection_childs_recursive ( sub , cols )
return cols
def unlink_objects_from_scene ( oblist , scn ) :
all_col = [ scn . collection ]
all_col + = get_collection_childs_recursive ( scn . collection )
for col in all_col :
for ob in reversed ( col . objects ) :
if ob in oblist :
col . objects . unlink ( ob )
def remove_scene_nodes_by_obj_names ( scn , name_list , negative = False ) :
for n in reversed ( scn . node_tree . nodes ) :
if negative :
if ( n . parent and n . parent . label not in name_list ) or ( n . type == ' FRAME ' and n . label not in name_list ) :
scn . node_tree . nodes . remove ( n )
else :
if ( n . parent and n . parent . label in name_list ) or ( n . type == ' FRAME ' and n . label in name_list ) :
scn . node_tree . nodes . remove ( n )
def split_object_to_scene ( ) :
''' Create a new scene from object selection '''
active = bpy . context . object
scene_name = active . name
objs = [ o for o in bpy . context . selected_objects ]
if bpy . data . scenes . get ( scene_name ) :
print ( f ' Scene " { scene_name } " Already Exists ' )
raise Exception ( f ' Scene " { scene_name } " Already Exists ' )
src = bpy . context . scene
bpy . ops . scene . new ( type = ' LINK_COPY ' )
new = bpy . context . scene
new . name = scene_name
2021-11-11 14:48:53 +01:00
## OPT
## set individual output for composite or go in /tmp ? (might not be needed)
# new.render.filepath = f'//render/preview/{bpy.path.clean_name(new.name.lower())}/preview_'
# new.render.filepath = f'/tmp/'
2021-09-30 18:51:49 +02:00
## unlink unwanted objects from collection
all_col = [ new . collection ]
all_col + = get_collection_childs_recursive ( new . collection )
for col in all_col :
for sob in reversed ( col . objects ) :
if sob . type in ( ' CAMERA ' , ' LIGHT ' ) :
continue
if sob not in objs :
col . objects . unlink ( sob )
frame_names = [ n . label for n in new . node_tree . nodes if n . type == ' FRAME ' if new . objects . get ( n . label ) ]
remove_scene_nodes_by_obj_names ( new , frame_names , negative = True )
bpy . ops . gp . clean_compo_tree ( )
# add crop
new . render . use_border = True
new . render . use_crop_to_border = True
new . render . use_compositing = True
new . render . use_sequencer = False
## remove asset from original scene
#src_frame_names = [n.label for n in src.node_tree.nodes if n.type == 'FRAME' and n.label in [o.name for o in objs]]
#remove_scene_nodes_by_obj_names(src, src_frame_names)
remove_scene_nodes_by_obj_names ( src , frame_names , negative = False )
# unlink objects ?
unlink_objects_from_scene ( objs , src )
# border to GP objects of the scene
gp_objs = [ o for o in new . objects if o . type == ' GPENCIL ' ]
coords = get_gp_box_all_frame_selection ( oblist = gp_objs , scn = new , cam = new . camera )
2021-10-13 19:00:15 +02:00
if not coords :
return f ' Scene " { scene_name } " created. But Border was not set (Timeout during GP analysis), should be done by hand if needed then use export crop to json '
2021-10-25 16:02:11 +02:00
2021-09-30 18:51:49 +02:00
set_border_region_from_coord ( coords , margin = 30 , scn = new , export_json = True )
export_crop_to_json ( )
2021-10-11 16:47:22 +02:00
def clear_frame_out_of_range ( o , verbose = False ) :
''' get a GP object
delete frame out of active scene range in all layers
return number of deleted frame
'''
scn = bpy . context . scene
ct = 0
if o . type != ' GPENCIL ' :
print ( f ' { o . name } not a Gpencil ' )
return 0
for l in o . data . layers :
first = True
for f in reversed ( l . frames ) :
# after
if f . frame_number > scn . frame_end + 1 :
if verbose :
print ( f ' del: obj { o . name } > layer { l . info } > frame { f . frame_number } ' )
l . frames . remove ( f )
ct + = 1
# before
elif f . frame_number < scn . frame_start - 1 :
if first :
first = False
continue
if verbose :
print ( f ' del: obj { o . name } > layer { l . info } > frame { f . frame_number } ' )
l . frames . remove ( f )
ct + = 1
# print('INFO', f'{ct} frames deleted')
return ct
## not used
def clear_frame_out_of_range_all_object ( ) :
scene = bpy . context . scene
ct = 0
for o in scene . objects :
if o . type == ' GPENCIL ' :
nct = clear_frame_out_of_range ( o , verbose = False )
print ( f ' { o . name } : { nct } frames deleted ' )
ct + = nct
print ( f ' { ct } gp frames deleted ' )
return ct