955 lines
30 KiB
Python
955 lines
30 KiB
Python
import bpy, os
|
||
import numpy as np
|
||
import bmesh
|
||
import mathutils
|
||
from mathutils import Vector
|
||
import math
|
||
from math import sqrt
|
||
from sys import platform
|
||
import subprocess
|
||
|
||
|
||
|
||
""" def get_gp_parent(layer) :
|
||
if layer.parent_type == "BONE" and layer.parent_bone :
|
||
return layer.parent.pose.bones.get(layer.parent_bone)
|
||
else :
|
||
return layer.parent
|
||
"""
|
||
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.strokes :
|
||
for point in stroke.points :
|
||
point.co = mat @ point.co
|
||
|
||
# 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 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.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.co = pt
|
||
return stroke
|
||
|
||
## OLD - need update
|
||
def draw_gp_stroke(loop_info, frame, palette, width = 2) :
|
||
stroke = frame.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(context, obj=None):
|
||
''' return tuple with plane coordinate and normal
|
||
of the curent drawing accordign to geometry'''
|
||
|
||
settings = context.scene.tool_settings
|
||
orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR'
|
||
loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE'
|
||
if obj:
|
||
mat = obj.matrix_world
|
||
else:
|
||
mat = context.object.matrix_world if context.object else None
|
||
|
||
# -> placement
|
||
if loc == "CURSOR":
|
||
plane_co = context.scene.cursor.location
|
||
|
||
else: # ORIGIN (also on origin if set to 'SURFACE', 'STROKE')
|
||
if not context.object:
|
||
plane_co = None
|
||
else:
|
||
plane_co = context.object.matrix_world.to_translation()# context.object.location
|
||
|
||
# -> orientation
|
||
if orient == 'VIEW':
|
||
#only depth is important, no need to get view vector
|
||
plane_no = None
|
||
|
||
elif orient == 'AXIS_Y':#front (X-Z)
|
||
plane_no = Vector((0,1,0))
|
||
plane_no.rotate(mat)
|
||
|
||
elif orient == 'AXIS_X':#side (Y-Z)
|
||
plane_no = Vector((1,0,0))
|
||
plane_no.rotate(mat)
|
||
|
||
elif orient == 'AXIS_Z':#top (X-Y)
|
||
plane_no = Vector((0,0,1))
|
||
plane_no.rotate(mat)
|
||
|
||
elif orient == 'CURSOR':
|
||
plane_no = Vector((0,0,1))
|
||
plane_no.rotate(context.scene.cursor.matrix)
|
||
|
||
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(context, 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 != 'GPENCIL':
|
||
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 == 'GPENCIL']
|
||
if not active in selection:
|
||
selection += [active]
|
||
return selection
|
||
|
||
if bpy.context.active_object and bpy.context.active_object.type == 'GPENCIL':
|
||
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 != 'GPENCIL':
|
||
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 == 'GPENCIL':
|
||
if o.data not in selected:
|
||
selected.append(o.data)
|
||
# selected = [o.data for o in bpy.context.selected_objects if o.type == 'GPENCIL']
|
||
if not active_data in selected:
|
||
selected += [active_data]
|
||
return selected
|
||
|
||
if bpy.context.active_object and bpy.context.active_object.type == 'GPENCIL':
|
||
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.active_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.active_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.co) 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.strokes]
|
||
## direct
|
||
#return[[location_to_region(p.co) for p in s.points] for s in frame.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.strokes):
|
||
if any(pt.select for pt in s.points):
|
||
stlist.append(s)
|
||
return stlist
|
||
|
||
from math import sqrt
|
||
from mathutils import Vector
|
||
|
||
|
||
# -----------------
|
||
### Vector utils 3d
|
||
# -----------------
|
||
|
||
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():
|
||
'''
|
||
function to read current addon preferences properties
|
||
|
||
access a prop like this :
|
||
prefs = get_addon_prefs()
|
||
option_state = prefs.super_special_option
|
||
|
||
oneliner : get_addon_prefs().super_special_option
|
||
'''
|
||
import os
|
||
addon_name = os.path.splitext(__name__)[0]
|
||
preferences = bpy.context.preferences
|
||
addon_prefs = preferences.addons[addon_name].preferences
|
||
return (addon_prefs)
|
||
|
||
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
|
||
# -----------------
|
||
|
||
## 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
|