gp_toolbox/utils.py

1576 lines
52 KiB
Python
Raw Normal View History

2021-01-10 16:47:17 +01:00
import bpy, os
import numpy as np
import bmesh
import mathutils
import math
import subprocess
2024-11-21 15:11:06 +01:00
from time import time
2021-01-10 16:47:17 +01:00
from math import sqrt
from mathutils import Vector
2021-01-10 16:47:17 +01:00
from sys import platform
## constants values
## Default stroke and points attributes
stroke_attr = [
'start_cap',
'end_cap',
'softness',
'material_index',
'fill_opacity',
'fill_color',
'cyclic',
'aspect_ratio',
'time_start',
# 'curve_type', # read-only
]
point_attr = [
'position',
'radius',
'rotation',
'opacity',
'vertex_color',
'delta_time',
# 'select',
]
### Attribute value, types and shape
2021-01-10 16:47:17 +01:00
attribute_value_string = {
'FLOAT': "value",
'INT': "value",
'FLOAT_VECTOR': "vector",
'FLOAT_COLOR': "color",
'BYTE_COLOR': "color",
'STRING': "value",
'BOOLEAN': "value",
'FLOAT2': "value",
'INT8': "value",
'INT32_2D': "value",
'QUATERNION': "value",
'FLOAT4X4': "value",
}
attribute_value_dtype = {
'FLOAT': np.float32,
'INT': np.dtype('int'),
'FLOAT_VECTOR': np.float32,
'FLOAT_COLOR': np.float32,
'BYTE_COLOR': np.int8,
'STRING': np.dtype('str'),
'BOOLEAN': np.dtype('bool'),
'FLOAT2': np.float32,
'INT8': np.int8,
'INT32_2D': np.dtype('int'),
'QUATERNION': np.float32,
'FLOAT4X4': np.float32,
}
attribute_value_shape = {
'FLOAT': (),
'INT': (),
'FLOAT_VECTOR': (3,),
'FLOAT_COLOR': (4,),
'BYTE_COLOR': (4,),
'STRING': (),
'BOOLEAN': (),
'FLOAT2':(2,),
'INT8': (),
'INT32_2D': (2,),
'QUATERNION': (4,),
'FLOAT4X4': (4,4),
}
2021-01-10 16:47:17 +01:00
def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin):
return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
2021-01-10 16:47:17 +01:00
def get_matrix(ob) :
'''return a copy of the world_matrix, applied object matrix if its a bone'''
if isinstance(ob, bpy.types.PoseBone) :
return ob.id_data.matrix_world @ ob.matrix.copy()# * ?
else :
return ob.matrix_world.copy()
def set_matrix(gp_frame,mat):
for stroke in gp_frame.drawing.strokes :
2021-01-10 16:47:17 +01:00
for point in stroke.points :
2024-11-11 16:23:11 +01:00
point.position = mat @ point.position
2021-01-10 16:47:17 +01:00
# get view vector location (the 2 methods work fine)
def get_view_origin_position():
## method 1
2021-01-10 16:47:17 +01:00
# from bpy_extras import view3d_utils
# region = bpy.context.region
# rv3d = bpy.context.region_data
# view_loc = view3d_utils.region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
## method 2
2021-01-10 16:47:17 +01:00
r3d = bpy.context.space_data.region_3d
view_loc2 = r3d.view_matrix.inverted().translation
return view_loc2
def location_to_region(worldcoords):
from bpy_extras import view3d_utils
return view3d_utils.location_3d_to_region_2d(bpy.context.region, bpy.context.space_data.region_3d, worldcoords)
def region_to_location(viewcoords, depthcoords):
from bpy_extras import view3d_utils
return view3d_utils.region_2d_to_location_3d(bpy.context.region, bpy.context.space_data.region_3d, viewcoords, depthcoords)
def vector_len_from_coord(a, b):
'''get either two points or world coordinates and return length'''
from mathutils import Vector
if type(a) is Vector:
return (a - b).length
else:
return (a.co - b.co).length
def transfer_value(Value, OldMin, OldMax, NewMin, NewMax):
'''map a value from a range to another (transfer/translate value)'''
return (((Value - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
def object_derived_get(ob, scene):
if ob.dupli_type != 'NONE' :
ob.dupli_list_create(scene)
ob_matrix_pairs = [(dob.object, dob.matrix.copy()) for dob in ob.dupli_list]
ob.dupli_list_clear()
else:
ob_matrix_pairs = [(ob, ob.matrix_world.copy())]
return ob_matrix_pairs
# -----------------
### Bmesh
# -----------------
2021-01-10 16:47:17 +01:00
def link_vert(v,ordered_vert) :
for e in v.link_edges :
other_vert = e.other_vert(v)
if other_vert not in ordered_vert :
ordered_vert.append(other_vert)
link_vert(other_vert,ordered_vert)
return ordered_vert
def find_loops(bm) :
verts = []
loops = []
print([v for v in bm.verts if len(v.link_edges)==1])
for v in [v for v in bm.verts if len(v.link_edges)==1] :
if v not in verts :
loop = link_vert(v,[v])
loops.append(loop)
for vert in loop :
verts.append(vert)
return loops
def get_perimeter(points) :
perimeter = 0
print('pointlen',len(points))
for i,point in enumerate(points) :
if i != 0 :
perimeter += (Vector(point) -Vector(points[i-1])).length
return perimeter
def points_to_bm_face(points,depth=0) :
bm = bmesh.new()
for point in points :
bm.verts.new((point[0],point[1],depth))
bm.faces.new(bm.verts)
bm.faces.ensure_lookup_table()
return bm
def gp_stroke_to_bmesh(strokes):
strokes_info = []
for stroke in strokes :
info = {'stroke' : stroke ,'color':stroke.colorname,'line_width':stroke.line_width}
bm = bmesh.new()
strength = bm.verts.layers.float.new('strength')
pressure = bm.verts.layers.float.new('pressure')
select = bm.verts.layers.int.new('select')
verts = []
for i,point in enumerate(stroke.points) :
v = bm.verts.new(point.co)
v[strength] = point.strength
v[pressure] = point.pressure
v[select] = point.select
verts.append(v)
if i > 0 :
e = bm.edges.new([verts[-1],verts[-2]])
info['bmesh']= bm
strokes_info.append(info)
return strokes_info
# -----------------
### GP Drawing
# -----------------
def layer_active_index(gpl):
'''Get layer list and return index of active layer
Can return None if no active layer found (active item can be a group)
'''
return next((i for i, l in enumerate(gpl) if l == gpl.active), None)
def get_top_layer_from_group(gp, group):
upper_layer = None
for layer in gp.layers:
if layer.parent_group == group:
upper_layer = layer
return upper_layer
def get_closest_active_layer(gp):
'''Get active layer from GP object, getting upper layer if in group
if a group is active, return the top layer of this group
if group is active but no layer in it, return None
'''
if gp.layers.active:
return gp.layers.active
## No active layer, return active from group (can be None !)
return get_top_layer_from_group(gp, gp.layer_groups.active)
def closest_layer_active_index(gp, fallback_index=0):
'''Get active layer index from GP object, getting upper layer if in group
if a group is active, return index at the top layer of this group
if group is active but no layer in it, return fallback_index (0 by default, stack bottom)'''
closest_active_layer = get_closest_active_layer(gp)
if closest_active_layer:
return next((i for i, l in enumerate(gp.layers) if l == closest_active_layer), fallback_index)
return fallback_index
## Check for nested lock
def is_locked(stack_item):
'''Check if passed stack item (layer or group) is locked
either itself or by parent groups'''
if stack_item.lock:
return True
if stack_item.parent_group:
return is_locked(stack_item.parent_group)
return False
def is_parent_locked(stack_item):
'''Check if passed stack item (layer or group) is locked by parent groups'''
if stack_item.parent_group:
return is_locked(stack_item.parent_group)
return False
## Check for nested hide
def is_hidden(stack_item):
'''Check if passed stack item (layer or group) is hidden
either itself or by parent groups'''
if stack_item.hide:
return True
if stack_item.parent_group:
return is_hidden(stack_item.parent_group)
return False
def is_parent_hidden(stack_item):
'''Check if passed stack item (layer or group) is hidden by parent groups'''
if stack_item.parent_group:
return is_hidden(stack_item.parent_group)
return False
def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0):
2021-01-10 16:47:17 +01:00
'''
draw basic stroke by passing list of point 3D coordinate
the frame to draw on and optional width parameter (default = 2)
'''
stroke = frame.drawing.strokes.new()
2021-01-10 16:47:17 +01:00
stroke.line_width = width
stroke.display_mode = '3DSPACE'
stroke.material_index = mat_id
# readonly -> stroke.is_nofill_stroke# boundary_stroke
stroke.points.add(len(pts))
seq = [i for vec in pts for i in vec]## foreach_set flatlist for speed
stroke.points.foreach_set('co', seq)
## one by one
# for i, pt in enumerate(pts):
# stroke.points.add()
# dest_point = stroke.points[i]
2024-11-11 16:23:11 +01:00
# dest_point.position = pt
2021-01-10 16:47:17 +01:00
return stroke
## OLD - need update
def draw_gp_stroke(loop_info, frame, palette, width = 2) :
stroke = frame.drawing.strokes.new(palette)
2021-01-10 16:47:17 +01:00
stroke.line_width = width
stroke.display_mode = '3DSPACE'# old -> draw_mode
2021-01-10 16:47:17 +01:00
for i,info in enumerate(loop_info) :
stroke.points.add()
dest_point = stroke.points[i]
for attr,value in info.items() :
setattr(dest_point,attr,value)
return stroke
def get_camera_frame_info(cam, distance = 1):
'''
return a list with 4 screen corner top-right first rotating CC
4-------1
| |
3-------2
'''
cam_coord = cam.matrix_world.to_translation()
scene = bpy.context.scene
#shift_x = cam.data.shift_x
#shift_y = cam.data.shift_y
#cam.data.shift_x = 0
#cam.data.shift_y = 0
frame = cam.data.view_frame(scene)
frame = [cam.matrix_world * corner for corner in frame]
#frame = [corner+(corner-cam_coord).normalized()*distance for corner in frame]
#cam.data.shift_x = shift_x
#cam.data.shift_y = shift_y
# bpy.context.scene.cursor_location = frame[0]# test by placing cursor
return frame
def id_convert(fimg,id,operation = 'EQUAL',border = True):
new_img = fimg.copy()
width = len(new_img[0])
if operation == 'EQUAL' :
thresh_mask = new_img[...] == id
elif operation == 'GREATER' :
thresh_mask = new_img[...] > id
elif operation == 'LOWER' :
thresh_mask = new_img[...] < id
else :
return
new_img[:] = 1.0
new_img[thresh_mask] = 0.0
if border :
# Adding black around the image
new_img = np.concatenate((new_img,[[1]*width]))
new_img = np.concatenate(([[1]*width],new_img))
new_img = np.insert(new_img,width,1,axis = 1)
new_img = np.insert(new_img,0,1,axis = 1)
return new_img
def remapping(value, leftMin, leftMax, rightMin, rightMax):
# Figure out how 'wide' each range is
leftSpan = leftMax - leftMin
rightSpan = rightMax - rightMin
# Convert the left range into a 0-1 range (float)
valueScaled = float(value - leftMin) / float(leftSpan)
# Convert the 0-1 range into a value in the right range.
return rightMin + (valueScaled * rightSpan)
# -----------------
### GP funcs
# -----------------
2021-01-10 16:47:17 +01:00
def get_gp_draw_plane(obj=None, orient=None):
''' return tuple with plane coordinate and normal
of the curent drawing according to geometry'''
if obj is None:
obj = bpy.context.object
settings = bpy.context.scene.tool_settings
if orient is None:
orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR'
loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE'
mat = obj.matrix_world
plane_no = Vector((0.0, 0.0, 1.0))
plane_co = mat.to_translation()
# -> orientation
if orient == 'VIEW':
mat = bpy.context.scene.camera.matrix_world
# -> placement
if loc == "CURSOR":
plane_co = bpy.context.scene.cursor.location
mat = bpy.context.scene.cursor.matrix
elif orient in ('AXIS_Y', 'FRONT'): # front (X-Z)
plane_no = Vector((0,1,0))
elif orient in ('AXIS_X', 'SIDE'): # side (Y-Z)
plane_no = Vector((1,0,0))
elif orient in ('AXIS_Z', 'TOP'): # top (X-Y)
plane_no = Vector((0,0,1))
plane_no.rotate(mat)
return plane_co, plane_no
2021-01-10 16:47:17 +01:00
def check_angle_from_view(obj=None, plane_no=None, context=None):
'''Return angle to obj according to chosen drawing axis'''
import math
from bpy_extras import view3d_utils
if not context:
context = bpy.context
if not plane_no:
_plane_co, plane_no = get_gp_draw_plane(obj=obj)
view_direction = view3d_utils.region_2d_to_vector_3d(context.region, context.region_data, (context.region.width/2.0, context.region.height/2.0))
angle = math.degrees(view_direction.angle(plane_no))
# correct angle value when painting from other side (seems a bit off...)
if angle > 90:
angle = abs(90 - (angle - 90))
# over angle warning
if angle > 75:
return angle, 'ERROR', f"Canvas not aligned at all! (angle: {angle:.2f}°), use Realign GP or change draw axis"
if angle > 6:
return angle, 'ERROR', f'Canvas not aligned! angle: {angle:.2f}°, use Realign GP'
if angle > 3: # below 3 is reasonable (camera seem to view induce error !)
return angle, 'INFO', f'Canvas almost aligned (angle: {angle:.2f}°)'
if angle == 0:
return angle, 'INFO', f'Canvas perfectly aligned (angle: {angle:.2f}°)'
# below 1 consider aligned
return angle, 'INFO', f'Canvas alignement seem ok (angle: {angle:.2f}°)'
2021-01-10 16:47:17 +01:00
## need big update
def create_gp_palette(gp_data_block,info) :
palette = gp_data_block.palettes.active
name = info["name"]
if palette.colors.get(name) :
return palette.colors.get(name)
else :
p = palette.colors.new()
for attr,value in info.items() :
setattr(p,attr,value)
return p
""" def get_gp_data_block() :
scene = bpy.context.scene
if scene.tool_settings.grease_pencil_source == 'OBJECT' and ob and ob.grease_pencil:
gp_data_block = ob.grease_pencil
elif scene.grease_pencil :
gp_data_block = scene.grease_pencil
else :
gp_data_block =bpy.data.grease_pencil.new('GPencil')
scene.grease_pencil = gp_data_block
palette = gp_data_block.palettes.active
if not palette :
palette = gp_data_block.palettes.new("palette")
return gp_data_block,palette """
def get_gp_objects(selection=True):
'''return selected objects or only the active one'''
2024-11-11 15:35:39 +01:00
if not bpy.context.active_object or bpy.context.active_object.type != 'GREASEPENCIL':
2021-01-10 16:47:17 +01:00
print('No active GP object')
return []
active = bpy.context.active_object
if selection:
2024-11-11 15:35:39 +01:00
selection = [o for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
2021-01-10 16:47:17 +01:00
if not active in selection:
selection += [active]
return selection
2024-11-11 15:35:39 +01:00
if bpy.context.active_object and bpy.context.active_object.type == 'GREASEPENCIL':
2021-01-10 16:47:17 +01:00
return [active]
return []
def get_gp_datas(selection=True):
'''return selected objects or only the active one'''
2024-11-11 15:35:39 +01:00
if not bpy.context.active_object or bpy.context.active_object.type != 'GREASEPENCIL':
2021-01-10 16:47:17 +01:00
print('No active GP object')
return []
active_data = bpy.context.active_object.data
if selection:
selected = []
for o in bpy.context.selected_objects:
2024-11-11 15:35:39 +01:00
if o.type == 'GREASEPENCIL':
2021-01-10 16:47:17 +01:00
if o.data not in selected:
selected.append(o.data)
2024-11-11 15:35:39 +01:00
# selected = [o.data for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
2021-01-10 16:47:17 +01:00
if not active_data in selected:
selected += [active_data]
return selected
2024-11-11 15:35:39 +01:00
if bpy.context.active_object and bpy.context.active_object.type == 'GREASEPENCIL':
2021-01-10 16:47:17 +01:00
return [active_data]
print('EOL. No active GP object')
return []
def get_gp_layer(gp_data_block, name) :
2021-01-10 16:47:17 +01:00
gp_layer = gp_data_block.layers.get(name)
if not gp_layer :
gp_layer = gp_data_block.layers.new(name)
return gp_layer
def get_gp_frame(layer, frame_nb=None) :
2021-01-10 16:47:17 +01:00
scene = bpy.context.scene
if not frame_nb :
frame_nb = scene.frame_current
frames={}
for i,f in enumerate(layer.frames) :
frames[f.frame_number]=i
if not scene.frame_current in frames.keys():
dest_frame = layer.frames.new(frame_nb)
else :
dest_frame = layer.frames[frames[frame_nb]]
dest_frame.clear()
return dest_frame
def get_active_frame(layer_name=None):
'''Return active frame of active layer or from layer name passed'''
if layer_name:
lay = bpy.context.scene.grease_pencil.layers.get(layer_name)
if lay:
frame = lay.current_frame()
2021-01-10 16:47:17 +01:00
if frame:
return frame
else:
print ('no active frame for layer', layer_name)
else:
print('no layers named', layer_name, 'in scene layers')
else:#active layer
frame = bpy.context.scene.grease_pencil.layers.active.current_frame()
2021-01-10 16:47:17 +01:00
if frame:
return frame
else:
print ('no active frame on active layer')
def get_stroke_2D_coords(stroke):
'''return a list containing points 2D coordinates of passed gp stroke object'''
2024-11-11 16:23:11 +01:00
return [location_to_region(p.position) for p in stroke.points]
2021-01-10 16:47:17 +01:00
'''#foreach method for retreiving multiple other attribute quickly and stack them
point_nb = len(stroke.points)
seq = [0]*(point_nb*3)
stroke.points.foreach_get("co",seq)
print("raw_list", seq)#Dbg
import numpy as np
#can use np.stack to add points infos on same index (on different line/dimension)
#https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.stack.html
'''
def get_all_stroke_2D_coords(frame):
'''return a list of lists with all strokes's points 2D location'''
## using modification from get_stroke_2D_coords func'
return [get_stroke_2D_coords(s) for s in frame.drawing.strokes]
2021-01-10 16:47:17 +01:00
## direct
#return[[location_to_region(p.position) for p in s.points] for s in frame.drawing.strokes]
2021-01-10 16:47:17 +01:00
def selected_strokes(frame):
'''return all stroke having a point selected as a list of strokes objects'''
stlist = []
for i, s in enumerate(frame.drawing.strokes):
2021-01-10 16:47:17 +01:00
if any(pt.select for pt in s.points):
stlist.append(s)
return stlist
## Copy stroke to a frame
def copy_stroke_to_frame(s, frame, select=True):
'''Copy stroke to given frame
return created stroke
'''
frame.drawing.add_strokes([len(s.points)])
ns = frame.drawing.strokes[-1]
# print(len(s.points), 'new:', len(ns.points))
#ns.material_index
## replicate attributes (simple loop)
## TODO : might need to create atribute domain if does not exists in destination
for attr in stroke_attr:
setattr(ns, attr, getattr(s, attr))
for src_p, dest_p in zip(s.points, ns.points):
for attr in point_attr:
setattr(dest_p, attr, getattr(src_p, attr))
## Define selection
dest_p.select=select
## Direcly iterate over attribute ?
# src_start = src_dr.curve_offsets[0].value
# src_end = src_start + data_size
# dst_start = dst_dr.curve_offsets[0].value
# dst_end = dst_start + data_size
# for src_idx, dest_idx in zip(range(src_start, src_end),range(dst_start, dst_end)):
# setattr(dest_attr.data[dest_idx], val_type, getattr(source_attr.data[src_idx], val_type))
2021-01-10 16:47:17 +01:00
return ns
2021-01-10 16:47:17 +01:00
"""## Works, but do not copy all attributes type (probably ok for GP though)
def bulk_frame_copy_attributes(source_attr, target_attr):
'''Get and apply data as flat numpy array based on attribute type'''
2024-11-21 15:11:06 +01:00
if source_attr.data_type == 'INT':
data = np.empty(len(source_attr.data), dtype=np.int32)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'INT8':
data = np.empty(len(source_attr.data), dtype=np.int8)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'FLOAT':
data = np.empty(len(source_attr.data), dtype=np.float32)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'FLOAT_VECTOR':
data = np.empty(len(source_attr.data) * 3, dtype=np.float32)
source_attr.data.foreach_get('vector', data)
target_attr.data.foreach_set('vector', data)
elif source_attr.data_type == 'FLOAT_COLOR':
data = np.empty(len(source_attr.data) * 4, dtype=np.float32)
source_attr.data.foreach_get('color', data)
target_attr.data.foreach_set('color', data)
elif source_attr.data_type == 'BOOLEAN':
data = np.empty(len(source_attr.data), dtype=bool)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
## works in slowmotion (keep as reference for testing)
# def copy_attribute_values(src_dr, dst_dr, source_attr, dest_attr, data_size):
# ## Zip method to copy one by one
# val_type = {'FLOAT_COLOR': 'color','FLOAT_VECTOR': 'vector'}.get(source_attr.data_type, 'value')
# src_start = src_dr.curve_offsets[0].value
# src_end = src_start + data_size
# dst_start = dst_dr.curve_offsets[0].value
# dst_end = dst_start + data_size
# for src_idx, dest_idx in zip(range(src_start, src_end),range(dst_start, dst_end)):
# setattr(dest_attr.data[dest_idx], val_type, getattr(source_attr.data[src_idx], val_type))
"""
def bulk_copy_attributes(source_attr, target_attr):
'''Get and apply data as flat numpy array based on attribute type'''
value_string = attribute_value_string[source_attr.data_type]
dtype = attribute_value_dtype[source_attr.data_type]
shape = attribute_value_shape[source_attr.data_type]
domain_size = len(source_attr.data)
## Need to pass attributes to get domain size
# domain_size = attributes.domain_size(source_attr.domain)
# start = time()
data = np.empty((domain_size, *shape), dtype=dtype).ravel()
source_attr.data.foreach_get(value_string, data)
target_attr.data.foreach_set(value_string, data)
# end = time()
# np_empty = end - start
## np.prod (works, supposedly faster but tested slower)
# data = np.empty(int(domain_size * np.prod(shape)), dtype=dtype)
# source_attr.data.foreach_get(value_string, data)
# target_attr.data.foreach_set(value_string, data)
## np.zeros (works, sometimes faster on big set of attributes)
# start = time()
# data = np.zeros((domain_size, *shape), dtype=dtype)
# source_attr.data.foreach_get(value_string, np.ravel(data))
# target_attr.data.foreach_set(value_string, np.ravel(data))
# end = time()
# np_zero = end - start
# print('np EMPTY faster' if np_empty < np_zero else 'np ZERO faster', source_attr.domain, source_attr.data_type, domain_size)
# print('np_zero', np_zero)
# print('np_empty', np_empty)
# print()
2024-11-21 15:11:06 +01:00
def copy_frame_at(source_frame, layer, frame_number):
'''Copy a frame (source_frame) to a layer at given frame_number'''
source_drawing = source_frame.drawing
# frame_copy_start = time() # time_dbg
2024-11-21 15:11:06 +01:00
frame = layer.frames.new(frame_number)
dr = frame.drawing
dr.add_strokes([len(s.points) for s in source_drawing.strokes])
for attr_name in source_drawing.attributes.keys():
source_attr = source_drawing.attributes[attr_name]
if attr_name not in dr.attributes:
dr.attributes.new(
name=attr_name, type=source_attr.data_type, domain=source_attr.domain)
target_attr = dr.attributes[attr_name]
# start_time = time() # time_dbg-per-attrib
# bulk_frame_copy_attributes(source_attr, target_attr) # only some attributes
2024-11-21 15:11:06 +01:00
bulk_copy_attributes(source_attr, target_attr)
# copy_attribute_values(source_drawing, dr, source_attr, target_attr, source_drawing.attributes.domain_size(source_attr.domain)) # super slow
# end_time = time() # time_dbg-per-attrib
# print(f"copy_attribute '{attr_name}' execution time: {end_time - start_time} seconds") # time_dbg-per-attrib
2024-11-21 15:11:06 +01:00
# frame_copy_end = time() # time_dbg
# print(f"frame copy execution time: {frame_copy_end - frame_copy_start} seconds") # time_dbg
2024-11-21 15:11:06 +01:00
2021-01-10 16:47:17 +01:00
# -----------------
### Vector utils 3d
# -----------------
def matrix_transform(coords, matrix):
coords_4d = np.column_stack((coords, np.ones(len(coords), dtype='float64')))
return np.einsum('ij,aj->ai', matrix, coords_4d)[:, :-1]
2021-01-10 16:47:17 +01:00
def single_vector_length(v):
return sqrt((v[0] * v[0]) + (v[1] * v[1]) + (v[2] * v[2]))
def vector_length(A,B):
''''take two Vector3 and return length'''
return sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2 + (A[2] - B[2])**2)
def vector_length_coeff(size, A, B):
'''
Calculate the vector lenght
return the coefficient to multiply this vector
to obtain a vector of the size given in paramerter
'''
Vlength = sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2 + (A[2] - B[2])**2)
if Vlength == 0:
print('problem Vector lenght == 0 !')
return (1)
return (size / Vlength)
def cross_vector_coord(foo, bar, size):
'''Return the coord in space of a cross vector between the two point with specified size'''
between = foo - bar
#create a generic Up vector (on Y or Z)
up = Vector([1.0,0,0])
new = Vector.cross(up, between)#the cross product return a 90 degree Vector
if new == Vector([0.0000, 0.0000, 0.0000]):
#new == 0 if up vector and between are aligned ! (so change up vector)
up = Vector([0,-1.0,0])
new = Vector.cross(up, between)#the cross product return a 90 degree Vector
perpendicular = foo + new
coeff = vector_length_coeff(size, foo, perpendicular)
#position the point in space by adding the new vector multiplied by coeff value to get wanted lenght
return (foo + (new * coeff))
def midpoint(p1, p2):
'''middle location between 2 vector is calculated by adding the two vector and divide by two'''
##mid = (foo + bar) / 2
return (Vector([(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2, (p1[2] + p2[2]) / 2]))
def extrapolate_points_by_length(a,b, length):
'''
Return a third point C from by continuing in AB direction
Length define BC distance. both vector2 and vector3
'''
# return b + ((b - a).normalized() * length)# one shot
ab = b - a
if not ab: return None
return b + (ab.normalized() * length)
# -----------------
### Vector utils 2d
# -----------------
def is_vector_close(a, b, rel_tol=1e-03):
'''compare Vector or sequence of value
by default tolerance is set on 1e-03 (0.001)
'''
return all([math.isclose(i, j, rel_tol=rel_tol) for i, j in zip(a,b)])
2021-01-10 16:47:17 +01:00
def single_vector_length_2d(v):
return sqrt((v[0] * v[0]) + (v[1] * v[1]))
def vector_length_2d(A,B):
''''take two Vector and return length'''
return sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2)
def vector_length_coeff_2d(size, A, B):
'''
Calculate the vector lenght
return the coefficient to multiply this vector
to obtain a vector of the size given in paramerter
'''
Vlength = sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2)
if Vlength == 0:
print('problem Vector lenght == 0 !')
return (1)
return (size / Vlength)
def cross_vector_coord_2d(foo, bar, size):
'''Return the coord in space of a cross vector between the two point with specified size'''
##middle location between 2 vector is calculated by adding the two vector and divide by two
2021-01-10 16:47:17 +01:00
##mid = (foo + bar) / 2
between = foo - bar
#create a generic Up vector (on Y or Z)
up = Vector([0,1.0])
new = Vector.cross(up, between)#the cross product return a 90 degree Vector
if new == Vector([0.0000, 0.0000]):
#new == 0 if up vector and between are aligned ! (so change up vector)
up = Vector([0,-1.0,0])
new = Vector.cross(up, between)#the cross product return a 90 degree Vector
perpendicular = foo + new
coeff = vector_length_coeff(size, foo, perpendicular)
#position the point in space by adding the new vector multiplied by coeff value to get wanted lenght
return (foo + (new * coeff))
def midpoint_2d(p1, p2):
return (Vector([(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2]))
# -----------------
### Collection management
# -----------------
def set_collection(ob, collection, unlink=True) :
''' link an object in a collection and create it if necessary, if unlink object is removed from other collections'''
scn = bpy.context.scene
col = None
visible = False
linked = False
# check if collection exist or create it
for c in bpy.data.collections :
if c.name == collection : col = c
if not col : col = bpy.data.collections.new(name=collection)
# link the collection to the scene's collection if necessary
for c in scn.collection.children :
if c.name == col.name : visible = True
if not visible : scn.collection.children.link(col)
# check if the object is already in the collection and link it if necessary
for o in col.objects :
if o == ob : linked = True
if not linked : col.objects.link(ob)
# remove object from scene's collection
for o in scn.collection.objects :
if o == ob : scn.collection.objects.unlink(ob)
# if unlink flag we remove the object from other collections
if unlink :
for c in ob.users_collection :
if c.name != collection : c.objects.unlink(ob)
# -----------------
### Path utils
# -----------------
def get_addon_prefs():
return bpy.context.preferences.addons[__package__].preferences
2021-01-10 16:47:17 +01:00
def open_addon_prefs():
'''Open addon prefs windows with focus on current addon'''
from .__init__ import bl_info
wm = bpy.context.window_manager
wm.addon_filter = 'All'
if not 'COMMUNITY' in wm.addon_support: # reactivate community
wm.addon_support = set([i for i in wm.addon_support] + ['COMMUNITY'])
wm.addon_search = bl_info['name']
bpy.context.preferences.active_section = 'ADDONS'
bpy.ops.preferences.addon_expand(module=__package__)
bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
2021-01-10 16:47:17 +01:00
def open_file(file_path) :
'''Open filepath with default browser'''
if platform.lower() == 'darwin':
subprocess.call(('open', file_path))
elif platform.lower().startswith('win'):
os.startfile(file_path)
# subprocess.call(('start', file_path))
else:#linux
subprocess.call(('xdg-open', file_path))
def open_folder(folderpath):
'''Open the folder at given path with default browser'''
myOS = platform
if myOS.startswith('linux') or myOS.startswith('freebsd'):
cmd = 'xdg-open'
elif myOS.startswith('win'):
cmd = 'explorer'
if not folderpath:
return('/')
else:#elif myOS == "darwin":
cmd = 'open'
if not folderpath:
return('//')
folderpath = os.path.normpath(folderpath)# to prevent bad path string
fullcmd = [cmd, folderpath]
print(fullcmd)
# subprocess.call(fullcmd)
subprocess.Popen(fullcmd)
return ' '.join(fullcmd)#back to string to return and print
def detect_OS():
"""return str of os name : Linux, Windows, Mac (None if undetected)"""
myOS = platform
if myOS.startswith('linux') or myOS.startswith('freebsd'):# linux
# print("operating system : Linux")
return ("Linux")
elif myOS.startswith('win'):# Windows
# print("operating system : Windows")
return ("Windows")
elif myOS == "darwin":# OS X
# print("operating system : Mac")
return ('Mac')
else:# undetected
print("Cannot detect OS, python 'sys.platform' give :", myOS)
return None
def fuzzy_match(s1, s2, tol=0.8, case_sensitive=False):
'''Tell if two strings are similar using a similarity ratio (0 to 1) value passed as third arg'''
from difflib import SequenceMatcher
# can also use difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6)
if case_sensitive:
similarity = SequenceMatcher(None, s1, s2)
else:
similarity = SequenceMatcher(None, s1.lower(), s2.lower())
return similarity.ratio() > tol
def fuzzy_match_ratio(s1, s2, case_sensitive=False):
'''Tell how much two passed strings are similar 1.0 being exactly similar'''
from difflib import SequenceMatcher
if case_sensitive:
similarity = SequenceMatcher(None, s1, s2)
else:
similarity = SequenceMatcher(None, s1.lower(), s2.lower())
return similarity.ratio()
2021-01-10 16:47:17 +01:00
def convert_attr(Attr):
'''Convert given value to a Json serializable format'''
if isinstance(Attr, (mathutils.Vector,mathutils.Color)):
return Attr[:]
elif isinstance(Attr, mathutils.Matrix):
return [v[:] for v in Attr]
elif isinstance(Attr,bpy.types.bpy_prop_array):
return [Attr[i] for i in range(0,len(Attr))]
else:
return(Attr)
## confirm pop-up message:
def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
'''Show message box with element passed as string or list
if _message if a list of lists:
if first element is "OPERATOR":
List format: ["OPERATOR", operator_id, text, icon, {prop_name: value, ...}]
if sublist have 2 element:
considered a label [text, icon]
if sublist have 3 element:
considered as an operator [ops_id_name, text, icon]
if sublist have 4 element:
considered as a property [object, propname, text, icon]
'''
2021-01-10 16:47:17 +01:00
def draw(self, context):
layout = self.layout
2021-01-10 16:47:17 +01:00
for l in _message:
if isinstance(l, str):
layout.label(text=l)
elif l[0] == "OPERATOR": # Special operator case with properties
layout.operator_context = "INVOKE_DEFAULT"
op = layout.operator(l[1], text=l[2], icon=l[3], emboss=False)
if len(l) > 4 and isinstance(l[4], dict):
for prop_name, value in l[4].items():
setattr(op, prop_name, value)
elif len(l) == 2: # label with icon
layout.label(text=l[0], icon=l[1])
elif len(l) == 3: # ops
layout.operator_context = "INVOKE_DEFAULT"
layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
elif len(l) == 4: # prop
row = layout.row(align=True)
row.label(text=l[2], icon=l[3])
row.prop(l[0], l[1], text='')
2021-01-10 16:47:17 +01:00
if isinstance(_message, str):
_message = [_message]
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
# -----------------
### UI utils
# -----------------
def refresh_areas():
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
area.tag_redraw()
# for area in bpy.context.screen.areas:
# area.tag_redraw()
## kmi draw for addon without delete button
def draw_kmi(km, kmi, layout):
map_type = kmi.map_type
## col = _indented_layout(layout)
# layout.label(text=f'{km.name} - {km.space_type} - {km.region_type}') # debug
# layout.label(text=f'{km.name}')
col = layout.column()
if kmi.show_expanded:
col = col.column(align=True)
box = col.box()
else:
box = col.column()
split = box.split()
# header bar
row = split.row(align=True)
row.prop(kmi, "show_expanded", text="", emboss=False)
row.prop(kmi, "active", text="", emboss=False)
if km.is_modal:
row.separator()
row.prop(kmi, "propvalue", text="")
else:
row.label(text=kmi.name)
row = split.row()
row.prop(kmi, "map_type", text="")
if map_type == 'KEYBOARD':
row.prop(kmi, "type", text="", full_event=True)
elif map_type == 'MOUSE':
row.prop(kmi, "type", text="", full_event=True)
elif map_type == 'NDOF':
row.prop(kmi, "type", text="", full_event=True)
elif map_type == 'TWEAK':
subrow = row.row()
subrow.prop(kmi, "type", text="")
subrow.prop(kmi, "value", text="")
elif map_type == 'TIMER':
row.prop(kmi, "type", text="")
else:
row.label()
### / Hided delete button
if (not kmi.is_user_defined) and kmi.is_user_modified:
row.operator("preferences.keyitem_restore", text="", icon='BACK').item_id = kmi.id
else:
pass ### NO REMOVE
# row.operator(
# "preferences.keyitem_remove",
# text="",
# # Abusing the tracking icon, but it works pretty well here.
# icon=('TRACKING_CLEAR_BACKWARDS' if kmi.is_user_defined else 'X')
# ).item_id = kmi.id
### Hided delete button /
# Expanded, additional event settings
if kmi.show_expanded:
box = col.box()
split = box.split(factor=0.4)
sub = split.row()
if km.is_modal:
sub.prop(kmi, "propvalue", text="")
else:
# One day...
# sub.prop_search(kmi, "idname", bpy.context.window_manager, "operators_all", text="")
sub.prop(kmi, "idname", text="")
if map_type not in {'TEXTINPUT', 'TIMER'}:
sub = split.column()
subrow = sub.row(align=True)
if map_type == 'KEYBOARD':
subrow.prop(kmi, "type", text="", event=True)
subrow.prop(kmi, "value", text="")
subrow_repeat = subrow.row(align=True)
subrow_repeat.active = kmi.value in {'ANY', 'PRESS'}
subrow_repeat.prop(kmi, "repeat", text="Repeat")
elif map_type in {'MOUSE', 'NDOF'}:
subrow.prop(kmi, "type", text="")
subrow.prop(kmi, "value", text="")
subrow = sub.row()
subrow.scale_x = 0.75
subrow.prop(kmi, "any", toggle=True)
subrow.prop(kmi, "shift", toggle=True)
subrow.prop(kmi, "ctrl", toggle=True)
subrow.prop(kmi, "alt", toggle=True)
subrow.prop(kmi, "oskey", text="Cmd", toggle=True)
subrow.prop(kmi, "key_modifier", text="", event=True)
# Operator properties
box.template_keymap_item_properties(kmi)
## Modal key maps attached to this operator
# if not km.is_modal:
# kmm = kc.keymaps.find_modal(kmi.idname)
# if kmm:
# draw_km(display_keymaps, kc, kmm, None, layout + 1)
# layout.context_pointer_set("keymap", km)
# -----------------
### linking utility
# -----------------
def link_objects_in_blend(filepath, obj_name_list, link=True):
'''Link an object by name from a file, if link is False, append instead of linking'''
if isinstance(obj_name_list, str):
obj_name_list = [obj_name_list]
with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to):
data_to.objects = [o for o in data_from.objects if o in obj_name_list]
return data_to.objects
def check_materials_in_blend(filepath):
'''Return a list of all material in remote blend file'''
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
l = [m for m in data_from.materials]
return l
def check_objects_in_blend(filepath, avoid_camera=True):
'''Return a list of object name in remote blend file'''
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
if avoid_camera:
l = [o for o in data_from.objects if not any(x in o.lower() for x in ('camera', 'draw_cam', 'obj_cam'))]
else:
l = [o for o in data_from.objects]
return l
# -----------------
### props handling
# -----------------
def iterate_selector(zone, attr, state, info_attr = None, active_access='active'):
'''Iterate with given attribute'''
item_number = len(zone)
if item_number <= 1:
return
if getattr(zone, attr) == None:
print('no', attr, 'in', zone)
return
if state: # swap
info = None
bottom = None
new_index = getattr(zone, attr) + state
setattr(zone, attr, new_index % item_number)
if new_index == item_number:
bottom = 1 # bottom reached, cycle to first
elif new_index < 0:
bottom = -1 # up reached, cycle to last
if info_attr:
active_item = getattr(zone, active_access) # active by default
if active_item:
info = getattr(active_item, info_attr)
return info, bottom
2022-11-04 18:45:37 +01:00
def iterate_active_layer(gpd, state):
'''Iterate active GP layer in stack
gpd: Grease Pencil Data
'''
layers = gpd.layers
l_count = len(layers)
if state: # swap
# info = None
# bottom = None
## Get active layer index
active_index = closest_layer_active_index(gpd, fallback_index=None)
if active_index == None:
## fallback to first layer if nothing found
gpd.layers.active = layers[0]
return
target_index = active_index + state
new_index = target_index % l_count
## set active layer
gpd.layers.active = layers[new_index]
if target_index == l_count:
bottom = 1 # bottom reached, cycle to first
elif target_index < 0:
bottom = -1 # up reached, cycle to last
# info = gpd.layers.active.name
# return info, bottom
2022-11-04 18:45:37 +01:00
# -----------------
### Curve handle
# -----------------
def create_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None):
'''Create curve at provided location and direction vector'''
## option to create nurbs instaed of bezier ?
context = context or bpy.context
## using ops (dirty)
# bpy.ops.curve.primitive_bezier_curve_add(radius=1, enter_editmode=enter_edit, align='WORLD', location=location, scale=(1, 1, 1))
# curve = context.object
# curve.name = 'curve_path'
# # fast straighten
# bpy.ops.curve.handle_type_set(type='VECTOR')
# bpy.ops.curve.handle_type_set(type='ALIGNED')
# bpy.ops.transform.translate(value=(1, 0, 0), orient_type='LOCAL',
# orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='LOCAL',
# constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False)
## using data
curve_data = bpy.data.curves.new(name, 'CURVE') # ('CURVE', 'SURFACE', 'FONT')
curve_data.dimensions = '3D'
curve_data.use_path = True
curve = bpy.data.objects.new(name, curve_data)
spl = curve_data.splines.new('BEZIER') # ('POLY', 'BEZIER', 'NURBS')
spl.bezier_points.add(1) # One point already exists
for i in range(2):
spl.bezier_points[i].handle_left_type = 'VECTOR' # ('FREE', 'VECTOR', 'ALIGNED', 'AUTO')
spl.bezier_points[i].handle_right_type = 'VECTOR'
spl.bezier_points[1].co = direction
# Back to aligned mode
for i in range(2):
spl.bezier_points[i].handle_right_type = spl.bezier_points[i].handle_left_type = 'ALIGNED'
# Select second point
spl.bezier_points[1].select_control_point = True
spl.bezier_points[1].select_left_handle = True
spl.bezier_points[1].select_right_handle = True
# link
context.scene.collection.objects.link(curve)
# curve object settings
curve.location = location
curve.show_in_front = True
# enter edit
if enter_edit and context.mode == 'OBJECT':
curve.select_set(True)
context.view_layer.objects.active = curve
bpy.ops.object.mode_set(mode='EDIT', toggle=False) # EDIT_CURVE
## set viewport overlay visibility for better view
if context.space_data.type == 'VIEW_3D':
context.space_data.overlay.show_curve_normals = True
context.space_data.overlay.normals_length = 0.2
return curve
def get_direction_vector_from_enum(string) -> Vector:
orient_vectors = {
'FORWARD_X' : Vector((1,0,0)),
'FORWARD_Y' : Vector((0,1,0)),
'FORWARD_Z' : Vector((0,0,1)),
'TRACK_NEGATIVE_X' : Vector((-1,0,0)),
'TRACK_NEGATIVE_Y' : Vector((0,-1,0)),
'TRACK_NEGATIVE_Z' : Vector((0,0,-1))
}
return orient_vectors[string]
def orentation_track_from_vector(input_vector) -> str:
'''return closest world track orientation name from passed vector direction'''
orient_vectors = {
'FORWARD_X' : Vector((1,0,0)),
'FORWARD_Y' : Vector((0,1,0)),
'FORWARD_Z' : Vector((0,0,1)),
'TRACK_NEGATIVE_X' : Vector((-1,0,0)),
'TRACK_NEGATIVE_Y' : Vector((0,-1,0)),
'TRACK_NEGATIVE_Z' : Vector((0,0,-1))
}
orient = None
min_angle = 10000
for track, v in orient_vectors.items():
angle = input_vector.angle(v)
if angle < min_angle:
min_angle = angle
orient = track
return orient
def create_follow_path_constraint(ob, curve, follow_curve=False, use_fixed_location=True):
2022-11-04 18:45:37 +01:00
'''return create constraint'''
# # Clear bone follow path constraint
exiting_fp_constraints = [c for c in ob.constraints if c.type == 'FOLLOW_PATH']
for c in exiting_fp_constraints:
ob.constraints.remove(c)
# loc = ob.matrix_world @ ob.matrix.to_translation()
if ob.location != (0,0,0):
old_loc = ob.location
ob.location = (0,0,0)
print(f'ob moved from {old_loc} to (0,0,0) to counter follow curve offset')
const = ob.constraints.new('FOLLOW_PATH')
const.target = curve
if follow_curve:
const.use_curve_follow = True
if use_fixed_location:
const.use_fixed_location = True
2022-11-04 18:45:37 +01:00
return const
## on_bones:
# prefs = get_addon_prefs()
# root_name = prefs.tgt_bone
# root = ob.pose.bones.get(root_name)
# if not root:
# return ('ERROR', f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name')
# # Clear bone follow path constraint
# exiting_fp_constraints = [c for c in root.constraints if c.type == 'FOLLOW_PATH']
# for c in exiting_fp_constraints:
# root.constraints.remove(c)
# # loc = ob.matrix_world @ root.matrix.to_translation()
# if root.name == ('world', 'root') and root.location != (0,0,0):
# old_loc = root.location
# root.location = (0,0,0)
# print(f'root moved from {old_loc} to (0,0,0) to counter follow curve offset')
# const = root.constraints.new('FOLLOW_PATH')
# const.target = curve
# # axis only in this case, should be in addon to prefs
# ## determine which axis to use... maybe found orientation in world space from matrix_basis ?
# root_world_base_direction = root.bone.matrix_local @ get_direction_vector_from_enum(bpy.context.scene.anim_cycle_settings.forward_axis)
# const.forward_axis = orentation_track_from_vector(root_world_base_direction) # 'TRACK_NEGATIVE_Y' # bpy.context.scene.anim_cycle_settings.forward_axis # 'FORWARD_X'
# print('const.forward_axis: ', const.forward_axis)
# const.use_curve_follow = True
2022-11-10 18:51:00 +01:00
# return curve, const
# -----------------
### Object
# -----------------
2022-11-10 18:51:00 +01:00
def go_edit_mode(ob, context=None):
'''set mode to object, set passed obhject as active and go Edit'''
context = context or bpy.context
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
ob.select_set(True)
context.view_layer.objects.active = ob
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
## Not used anymore
def all_anim_enabled(objects) -> bool:
'''return False if at least one fcurve/group has disabled animation in passed object anim'''
for o in objects:
if o.animation_data and o.animation_data.action:
## Skip hided object ?
# if o.hide_get():
# continue
## Check if groups are muted
for grp in o.animation_data.action.groups:
if grp.mute:
return False
## Check if fcurves are muted
for fcu in o.animation_data.action.fcurves:
if fcu.mute:
return False
2024-11-11 15:35:39 +01:00
if o.type in ('GREASEPENCIL', 'CAMERA'):
if o.data.animation_data and o.data.animation_data.action:
## Check if object data attributes fcurves are muted
for fcu in o.animation_data.action.fcurves:
if fcu.mute:
return False
return True
## Not used anymore
def all_object_modifier_enabled(objects) -> bool:
'''Return False if one modifier of one object has GP modifier disabled in viewport but enabled in render'''
for o in objects:
2024-11-11 15:35:39 +01:00
if o.type != 'GREASEPENCIL':
continue
2024-11-27 16:40:31 +01:00
for m in o.modifiers:
if m.show_render and not m.show_viewport:
return False
## return False when viewport but no render ?
# if not m.show_render and m.show_viewport:
# return False
return True
## Only used in anim_status in 'per GP object' mode
def has_fully_enabled_anim(o):
if o.animation_data and o.animation_data.action:
## Skip hided object ? (missmatch)
# if o.hide_get():
# continue
## Check if groups are muted
for grp in o.animation_data.action.groups:
if grp.mute:
return False
## Check if fcurves are muted
for fcu in o.animation_data.action.fcurves:
if fcu.mute:
return False
2024-11-11 15:35:39 +01:00
if o.type in ('GREASEPENCIL', 'CAMERA'):
if o.data.animation_data and o.data.animation_data.action:
## Check if object data attributes fcurves are muted
for fcu in o.animation_data.action.fcurves:
if fcu.mute:
return False
return True
def anim_status(objects) -> tuple((str, str)):
'''Return a tutple of icon string status in ('ALL_ON', 'MIXED', 'ALL_OFF', 'NONE')'''
on_count = off_count = count = 0
for o in objects:
## Skip hidden objects
if o.hide_get() and o.hide_render:
continue
### Per Object
# count += 1
# if has_fully_enabled_anim(o):
# on_count += 1
# else:
# off_count += 1
### Consider All channels individually
if o.animation_data and o.animation_data.action:
for grp in o.animation_data.action.groups:
## Check if groups are muted
if grp.mute:
off_count += 1
else:
on_count += 1
count += 1
for fcu in o.animation_data.action.fcurves:
## Check if fcurves are muted
if fcu.mute:
off_count += 1
else:
on_count += 1
count += 1
2024-11-11 15:35:39 +01:00
if o.type in ('GREASEPENCIL', 'CAMERA'):
datablock = o.data
if datablock.animation_data is None:
continue
if not datablock.animation_data.action:
continue
## Check if object data attributes fcurves are muted
for fcu in datablock.animation_data.action.fcurves:
if fcu.mute:
off_count += 1
else:
on_count += 1
count += 1
if not on_count and not off_count:
return ('BLANK1', 'BLANK1') # 'NONE'
elif on_count == count:
return ('LAYER_ACTIVE', 'BLANK1') # 'ALL_ON'
elif off_count == count:
return ('BLANK1', 'LAYER_ACTIVE') # 'ALL_OFF'
else:
return ('LAYER_USED', 'LAYER_USED') # 'MIXED'
def gp_modifier_status(objects) -> tuple((str, str)):
'''return icons on/off tuple'''
on_count = off_count = count = 0
for o in objects:
2024-11-11 15:35:39 +01:00
if o.type != 'GREASEPENCIL':
continue
## Skip hided object
if o.hide_get() and o.hide_render:
continue
2024-11-27 16:40:31 +01:00
for m in o.modifiers:
if m.show_render and not m.show_viewport:
off_count += 1
else:
on_count += 1
count += 1
## return False when viewport but no render ?
# if not m.show_render and m.show_viewport:
# return False
if not on_count and not off_count:
return ('BLANK1', 'BLANK1') # 'NONE'
elif on_count == count:
return ('LAYER_ACTIVE', 'BLANK1') # 'ALL_ON'
elif off_count == count:
return ('BLANK1', 'LAYER_ACTIVE') # 'ALL_OFF'
else:
return ('LAYER_USED', 'LAYER_USED')