gp_toolbox/utils.py

744 lines
23 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
from mathutils import Vector
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))
# print("view_loc1", view_loc)#Dbg
#method 2
r3d = bpy.context.space_data.region_3d
view_loc2 = r3d.view_matrix.inverted().translation
# print("view_loc2", view_loc2)#Dbg
# if view_loc != view_loc2: print('Might be an error when finding view coordinate')
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
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
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):
2021-01-10 16:47:17 +01:00
''' 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
2021-01-10 16:47:17 +01:00
# -> placement
if loc == "CURSOR":
plane_co = context.scene.cursor.location
else: # ORIGIN (also on origin if set to 'SURFACE', 'STROKE')
2021-01-10 16:47:17 +01:00
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}°)'
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'''
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 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_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 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]
'''
2021-01-10 16:47:17 +01:00
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])
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)