1566 lines
51 KiB
Python
1566 lines
51 KiB
Python
import bpy, os
|
||
import numpy as np
|
||
import bmesh
|
||
import mathutils
|
||
import math
|
||
import subprocess
|
||
|
||
from time import time
|
||
from math import sqrt
|
||
from mathutils import Vector
|
||
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
|
||
|
||
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),
|
||
}
|
||
|
||
|
||
def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin):
|
||
return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
|
||
|
||
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 :
|
||
for point in stroke.points :
|
||
point.position = mat @ point.position
|
||
|
||
# get view vector location (the 2 methods work fine)
|
||
def get_view_origin_position():
|
||
## method 1
|
||
# 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
|
||
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
|
||
# -----------------
|
||
|
||
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):
|
||
'''
|
||
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()
|
||
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]
|
||
# dest_point.position = pt
|
||
return stroke
|
||
|
||
## OLD - need update
|
||
def draw_gp_stroke(loop_info, frame, palette, width = 2) :
|
||
stroke = frame.drawing.strokes.new(palette)
|
||
|
||
stroke.line_width = width
|
||
stroke.display_mode = '3DSPACE'# old -> draw_mode
|
||
|
||
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
|
||
# -----------------
|
||
|
||
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
|
||
|
||
|
||
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}°)'
|
||
|
||
|
||
## 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'''
|
||
if not bpy.context.active_object or bpy.context.active_object.type != 'GREASEPENCIL':
|
||
print('No active GP object')
|
||
return []
|
||
|
||
active = bpy.context.active_object
|
||
if selection:
|
||
selection = [o for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
|
||
if not active in selection:
|
||
selection += [active]
|
||
return selection
|
||
|
||
if bpy.context.active_object and bpy.context.active_object.type == 'GREASEPENCIL':
|
||
return [active]
|
||
return []
|
||
|
||
def get_gp_datas(selection=True):
|
||
'''return selected objects or only the active one'''
|
||
if not bpy.context.active_object or bpy.context.active_object.type != 'GREASEPENCIL':
|
||
print('No active GP object')
|
||
return []
|
||
|
||
active_data = bpy.context.active_object.data
|
||
if selection:
|
||
selected = []
|
||
for o in bpy.context.selected_objects:
|
||
if o.type == 'GREASEPENCIL':
|
||
if o.data not in selected:
|
||
selected.append(o.data)
|
||
# selected = [o.data for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
|
||
if not active_data in selected:
|
||
selected += [active_data]
|
||
return selected
|
||
|
||
if bpy.context.active_object and bpy.context.active_object.type == 'GREASEPENCIL':
|
||
return [active_data]
|
||
|
||
print('EOL. No active GP object')
|
||
return []
|
||
|
||
def get_gp_layer(gp_data_block, name) :
|
||
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) :
|
||
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()
|
||
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()
|
||
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'''
|
||
return [location_to_region(p.position) for p in stroke.points]
|
||
|
||
'''#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]
|
||
## direct
|
||
#return[[location_to_region(p.position) for p in s.points] for s in frame.drawing.strokes]
|
||
|
||
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):
|
||
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))
|
||
|
||
return ns
|
||
|
||
"""## 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'''
|
||
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()
|
||
|
||
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
|
||
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
|
||
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
|
||
|
||
# frame_copy_end = time() # time_dbg
|
||
# print(f"frame copy execution time: {frame_copy_end - frame_copy_start} seconds") # time_dbg
|
||
|
||
# -----------------
|
||
### 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]
|
||
|
||
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)])
|
||
|
||
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
|
||
##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
|
||
|
||
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')
|
||
|
||
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()
|
||
|
||
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 sublist have 2 element:
|
||
considered a label [text,icon]
|
||
if sublist have 3 element:
|
||
considered as an operator [ops_id_name, text, icon]
|
||
'''
|
||
|
||
def draw(self, context):
|
||
for l in _message:
|
||
if isinstance(l, str):
|
||
self.layout.label(text=l)
|
||
else:
|
||
if len(l) == 2: # label with icon
|
||
self.layout.label(text=l[0], icon=l[1])
|
||
elif len(l) == 3: # ops
|
||
self.layout.operator_context = "INVOKE_DEFAULT"
|
||
self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
|
||
|
||
## offset pnale when using row...
|
||
# row = self.layout.row()
|
||
# row.label(text=l[1])
|
||
# row.operator(l[0], icon=l[2])
|
||
|
||
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
|
||
|
||
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
|
||
|
||
# -----------------
|
||
### 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):
|
||
'''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
|
||
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
|
||
# return curve, const
|
||
|
||
|
||
# -----------------
|
||
### Object
|
||
# -----------------
|
||
|
||
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
|
||
|
||
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:
|
||
if o.type != 'GREASEPENCIL':
|
||
continue
|
||
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
|
||
|
||
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
|
||
|
||
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:
|
||
if o.type != 'GREASEPENCIL':
|
||
continue
|
||
## Skip hided object
|
||
if o.hide_get() and o.hide_render:
|
||
continue
|
||
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') |