first public version commit

gpv2
Pullusb 2021-01-10 16:47:17 +01:00
commit 98c6739003
30 changed files with 11931 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__
*.pyc
gp_toolbox_updater
gp_toolbox_updater/GP_toolbox_updater_status.json

View File

@ -0,0 +1,78 @@
## Can be renamed and used as standalone __init__.py file
bl_info = {
"name": "GP guided colorize",
"description": "Blender <> G'MIC bridge for auto color",
"author": "Samuel Bernou",
"version": (0, 1, 0),
"blender": (2, 82, 0),
"location": "3D view > Gpencil > Colorize",
"warning": "WIP",
"doc_url": "",#2.8 > 2.82 : "wiki_url":"",
"category": "3D View",
}
import bpy
from . import OP_gmic
# from .OP_gmic import (GMICOLOR_OT_propagate_spots,
# GMICOLOR_OT_clear_cam_bg_images,
# GMICOLOR_OT_open_gmic_tool_folder,
# GMICOLOR_PT_auto_color_panel,)
from . import OP_line_closer
from . import OP_create_empty_frames
# from .OP_line_closer import (GPSTK_OT_extend_lines,
# GPSTK_PT_line_closer_panel,
# GPSTK_OT_comma_finder,)
## Colorize properties
class GPCOLOR_PG_settings(bpy.types.PropertyGroup) :
res_percentage: bpy.props.IntProperty(
name="Gmic out resolution", description="Overrides resolution percentage for playblast", default = 50, min=1, max=200, soft_min=10, soft_max=100, subtype='PERCENTAGE')#, precision=0
# extend_layers: bpy.props.BoolProperty(name='Extend layers' default=True, description="Work on selected layers, else only active")
extend_layer_tgt : bpy.props.EnumProperty(
name="Extend layers", description="Choose which layer to target",
default='ACTIVE',
items=(
('ACTIVE', 'Active only', 'Target active layer only', 0),#include icon name in fourth position
('SELECTED', 'Selected', 'Target selected layers in GP dopesheet', 1),
('ALL_VISIBLE', 'All visible', 'target all visible layers', 2),
('ALL', 'All', 'All (even locked and hided layer)', 2),
))
extend_selected: bpy.props.BoolProperty(name='Extend selected', default=False, description="Work on selected stroke only if True, else All stroke")
extend_length: bpy.props.FloatProperty(name='Extend length', default=0.01, precision=3, step=0.01, description="Length for extending strokes boundary")
deviation_tolerance : bpy.props.FloatProperty(
name="Deviation angle", description="Deviation angle tolerance of last point(s) to be considered accidental trace",
default=1.22, min=0.017, max=3.124, soft_min=0.017, soft_max=1.92, step=3, precision=2, unit='ROTATION')#, subtype='ANGLE')
classes = (
GPCOLOR_PG_settings,
# GMICOLOR_OT_propagate_spots,
# GMICOLOR_OT_clear_cam_bg_images,
# GMICOLOR_OT_open_gmic_tool_folder,
# GMICOLOR_PT_auto_color_panel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
OP_create_empty_frames.register()
OP_line_closer.register()
OP_gmic.register()
bpy.types.Scene.gpcolor_props = bpy.props.PointerProperty(type = GPCOLOR_PG_settings)
def unregister():
OP_gmic.unregister()
OP_line_closer.unregister()
OP_create_empty_frames.unregister()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.gpcolor_props
if __name__ == "__main__":
register()

View File

@ -0,0 +1,58 @@
## Create empty keyframe where keyframe exists in layers above.
import bpy
class GP_OT_create_empty_frames(bpy.types.Operator):
bl_idname = "gp.create_empty_frames"
bl_label = "Create empty frames"
bl_description = "Create new empty frames on active layer where there is a frame in layer above\n(usefull in color layers to match line frames)"
bl_options = {'REGISTER','UNDO'}
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
def execute(self, context):
obj = context.object
gpl = obj.data.layers
gpl.active_index
## Only possible on 'fill' layer ??
# if not 'fill' in gpl.active.info.lower():
# self.report({'ERROR'}, f"There must be 'fill' text in layer name")
# return {'CANCELLED'}
frame_id_list = []
for i, l in enumerate(gpl):
# don't list layer below
if i <= gpl.active_index:
continue
# print(l.info, "index:", i)
for f in l.frames:
frame_id_list.append(f.frame_number)
frame_id_list = list(set(frame_id_list))
frame_id_list.sort()
current_frames = [f.frame_number for f in gpl.active.frames]
fct = 0
for num in frame_id_list:
if num in current_frames:
continue
#Create empty frame
gpl.active.frames.new(num, active=False)
fct += 1
if fct:
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
else:
self.report({'WARNING'}, f"No frames to create !")
return {'FINISHED'}
def register():
bpy.utils.register_class(GP_OT_create_empty_frames)
def unregister():
bpy.utils.unregister_class(GP_OT_create_empty_frames)

View File

@ -0,0 +1,424 @@
from .func_gmic import *
from ..utils import get_addon_prefs, open_folder
import bpy
from os.path import join, basename, exists, dirname, abspath, splitext
'''#decorator mod
def with_renderfile(filepath):
def with_renderfile_decorator(func):
def decorator(*args, **kwargs):
r = bpy.context.scene.render
old_filepath, r.filepath = r.filepath, filepath
try:
func(*args, **kwargs)
finally:
r.filepath = old_filepath
return decorator
return with_renderfile_decorator
@with_renderfile("//myfile")
'''
# self with implementation
def render_filepath(filepath):
class RenderFileRestorer:
def __enter__(self):
bpy.context.scene.render.film_transparent = True
bpy.context.scene.render.filepath = filepath
bpy.context.scene.render.resolution_percentage = bpy.context.scene.gpcolor_props.res_percentage
def __exit__(self, type, value, traceback):
bpy.context.scene.render.filepath = old_filepath
bpy.context.scene.render.film_transparent = transparent
bpy.context.scene.render.resolution_percentage = old_res
transparent = bpy.context.scene.render.film_transparent
old_filepath = bpy.context.scene.render.filepath
old_res = bpy.context.scene.render.resolution_percentage
return RenderFileRestorer()
def layer_state(gp_data):
class LayerStateRestorer:
def __enter__(self):
# mask/restore other GP object ?
self.layers_state = {l:l.hide for l in gp_data.layers}
def __exit__(self, type, value, traceback):
for k, v in self.layers_state.items():
k.hide = v
return LayerStateRestorer()
def cursor_state():
class CursorStateRestorer:
def __enter__(self):
...
def __exit__(self, type, value, traceback):
bpy.context.window_manager.progress_end()
return CursorStateRestorer()
def imtool_fp(dest='tool',name=None):
if name:
return join(bpy.path.abspath('//'), dest, name)
return join(bpy.path.abspath('//'), dest)
def generate_seq_placeholder():
with cursor_state():
wm = bpy.context.window_manager
wm.progress_begin(bpy.context.scene.frame_start, bpy.context.scene.frame_end)
for i in range(bpy.context.scene.frame_start,bpy.context.scene.frame_end+1):
fp = imtool_fp(name = f'colotmp_{str(i).zfill(4)}.png')
wm.progress_update(i)#remap from frame range to 0-1000 with transfer_value(Value, OldMin, OldMax, NewMin, NewMax)
if not exists(fp):
generate_empty_image(fp)
def set_bg_img_settings(bgimg):
bgimg.display_depth = 'FRONT' if bpy.data.version[1] < 83 else 'BACK'
bgimg.alpha = 0.6
bgimg.frame_method = 'FIT'#'STRETCH'
def load_bg_image(colo_fp):
# load new image in camera background
cam = bpy.context.scene.camera.data
cam.show_background_images = True
colo = basename(colo_fp)
colo_img = bpy.data.images.get(colo)
# load as images
if colo_img:
colo_img.reload()
else:
if not exists(colo_fp):return
colo_img = bpy.data.images.load(colo_fp)
bgimg = None
for bg in cam.background_images:
if bg.image == colo_img:
bgimg = bg
break
if not bgimg:
bgimg = cam.background_images.new()
bgimg.image = colo_img
set_bg_img_settings(bgimg)
return bgimg
def load_bg_movieclip():
# load new image in camera background
cam = bpy.context.scene.camera.data
cam.show_background_images = True
## load first image
colo = f'colotmp_{str(bpy.context.scene.frame_start).zfill(4)}.png'
colo_fp = join(imtool_fp(), colo)
if not exists(colo_fp):
try:
generate_empty_image(colo_fp)
pass
# TODO generate empty alpha image with GMIC or fast lib (numpy ?)
except Exception as identifier:
print('In load_bg_movieclip')
print(f'NOT FOUND : {colo_fp}')
return
return
# load as movie clip
colo_img = bpy.data.movieclips.get(colo)
if colo_img:
pass# colo_img.reload()# video has no reload prop
#TODO find a way to trigger refresh automagically !!
else:
if not exists(colo_fp):return
colo_img = bpy.data.movieclips.load(colo_fp)
bgimg = None
for bg in cam.background_images:
if bg.clip == colo_img:
bgimg = bg
break
if not bgimg:
bgimg = cam.background_images.new()
bgimg.source = 'MOVIE_CLIP'
bgimg.clip = colo_img
set_bg_img_settings(bgimg)
return bgimg
def guide_color(anim=False):
'''render lines and spots separately > gmic > feeback into cam background'''
scene = bpy.context.scene
#### other solution
### how about conerting polylines to stroke under the hood with opencv in camera space according to layer
### then feed generated array to gmic, might be faster (but dont take stroke thickness into account... problem or feature ?)
# Generate temporary (local for speed) folder or render in /tmp if not specified ?
if not bpy.context.object or bpy.context.object.type != 'GPENCIL': return 1
gp = bpy.context.object.data
frame = str(scene.frame_current).zfill(4)
line = f"line_{frame}.png"
spot = f"spot_{frame}.png"
colo = f"colotmp_{frame}.png"
with layer_state(gp):
# show/hide layers to render lines/spots only
for l in gp.layers:
l.hide = any(x in l.info for x in ('spot', 'colo'))#keep only lines
# better hide by material namespace ?
with render_filepath(f"//tool/{line}"):
bpy.ops.render.render(animation = anim, write_still=True)
# show/hide layers to render spots only
for l in gp.layers:
l.hide = 'spot' not in l.info#keep only spots
with render_filepath(f"//tool/{spot}"):
bpy.ops.render.render(animation = anim, write_still=True)
line_fp = join(imtool_fp(), line)
spot_fp = join(imtool_fp(), spot)
colo_fp = join(imtool_fp(), colo)
propagate_color(line_fp, spot_fp, colo_fp)
## ~4.6sec
# now try and check if openCV or gmic can smooth vectorize the stuff
## ! surely possible to avoid writting the file like giving it back as a numpy array and keep it within blender !
## >> just feed numpy array to gmic and get output ?
## Again maybe possible to avoid writing to disk but more complicated to send to gmic (use slot 8 and 9 ?)
# clear line/spot render if necessary...
class GMICOLOR_OT_propagate_spots(bpy.types.Operator):
"""
Propagate the spots with a gmic call
use shift+clic to force reload after operation.
"""
bl_idname = "bgmic.propagate_color"
bl_label = "Gmic propagate color"
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
anim : bpy.props.BoolProperty(
name="animation", description="render and propagate color for whole animation", default=False, subtype='NONE', options={'ANIMATABLE'})#HIDDEN
## TODO set preview mode (with low percentage)
# subtype (string) Enumerator in ['FILE_PATH', 'DIR_PATH', 'FILE_NAME', 'BYTE_STRING', 'PASSWORD', 'NONE'].
# options (set) Enumerator in ['HIDDEN', 'SKIP_SAVE', 'ANIMATABLE', 'LIBRARY_EDITABLE', 'PROPORTIONAL','TEXTEDIT_UPDATE'].
mode : bpy.props.StringProperty(
name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'})
load : bpy.props.BoolProperty(default=False)
def execute(self, context):
## TODO make a warning if animation mode is on and there is a consequent number of frame with big resolutions...
frame = str(context.scene.frame_current).zfill(4)
colo = f'colotmp_{frame}.png'
colo_fp = join(imtool_fp(), colo)
bgimg = None
## render one image or full-anim
if self.mode == 'render':
## Or maybe use the technique of BG rendering... with subprocess call
bgimg = guide_color(self.anim)
## re-load
if self.mode == 'load' or self.load:
""" if not exists(colo_fp):
print(f'/!\\ {frame}: color was not generated')
return {'CANCELLED'} """
bpy.ops.bgmic.clear_cam_bg_images(real_clear=False)#clear before load
if self.anim:
bgimg = load_bg_movieclip()
generate_seq_placeholder()#generate all placeholders
else:
bgimg = load_bg_image(colo_fp)
if not bgimg:
mess = f"Not Found, Loading {'anim' if self.anim else 'image'} has failed at {dirname(colo_fp)}"
self.report({'ERROR'}, mess)#WARNING, ERROR
return {'CANCELLED'}
return {'FINISHED'}
def invoke(self, context, event):
self.load = event.shift#if shift is pressed, force inclusion of load
return self.execute(context)
class GMICOLOR_OT_clear_cam_bg_images(bpy.types.Operator):
"""
Disable and clear background images from scene camera (mask non spot imgs)
Shift+clic for a real clear (delete all refs images from camera bg images)
"""
bl_idname = "bgmic.clear_cam_bg_images"
bl_label = "Clear camera background color images"
@classmethod
def poll(cls, context):
return context.scene.camera
real_clear : bpy.props.BoolProperty(default=False)
def execute(self, context):
# TODO be selective to user cam (filter on name)
# TODO need to delete only "spot" name instead of full clear
if self.real_clear:
context.scene.camera.data.background_images.clear()
else:
to_del = []
for bgimg in context.scene.camera.data.background_images:
if bgimg.source == 'MOVIE_CLIP':
if not bgimg.clip or 'spot' in bgimg.clip.name or 'colotmp' in bgimg.clip.name:
to_del.append(bgimg)
continue
else:#just hide
bgimg.show_background_image = False
else:
if not bgimg.image or 'spot' in bgimg.image.name or 'colotmp' in bgimg.image.name:
to_del.append(bgimg)
continue
else:#just hide
bgimg.show_background_image = False
for bimg in reversed(to_del):
context.scene.camera.data.background_images.remove(bimg)
context.scene.camera.data.show_background_images = False#toggle off then on to force refresh
# context.scene.camera.data.show_background_images = True
return {'FINISHED'}
def invoke(self, context, event):
self.real_clear = event.shift#if shift is pressed, force inclusion of load
return self.execute(context)
class GMICOLOR_OT_open_gmic_tool_folder(bpy.types.Operator):
"""
Disable and clear background images from scene camera (mask non spot imgs)
Shift+clic for a real clear (delete all refs images from camera bg images)
"""
bl_idname = "bgmic.open_gmic_tool_folder"
bl_label = "Open spot-image-tool Folder"
bl_options = {'REGISTER', 'INTERNAL'}
def execute(self, context):
fp = join(bpy.path.abspath('//'), 'tool')
if not exists(fp):
mess = f'{fp} not found'
self.report({'WARNING'}, mess)
return {'CANCELLED'}
open_folder(fp)
self.report({'INFO'}, 'Gmic tool folder opened')
return {'FINISHED'}
## base panel
class GMICOLOR_PT_auto_color_panel(bpy.types.Panel):
bl_label = "GP Colorize"# title
# bl_parent_id # If set, the panel becomes a sub-panel
## bl_options = {'DEFAULT_CLOSED', 'HIDE_HEADER' }# closed by default, collapse the panel and the label
## is_popover = False # if ommited
## bl_space_type = ['EMPTY', 'VIEW_3D', 'IMAGE_EDITOR', 'NODE_EDITOR', 'SEQUENCE_EDITOR', 'CLIP_EDITOR', 'DOPESHEET_EDITOR', 'GRAPH_EDITOR', 'NLA_EDITOR', 'TEXT_EDITOR', 'CONSOLE', 'INFO', 'TOPBAR', 'STATUSBAR', 'OUTLINER', 'PROPERTIES', 'FILE_BROWSER', 'PREFERENCES'], default 'EMPTY'
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"#name of the tab
# activating on some context only
## bl_context : object, objectmode, mesh_edit, curve_edit, surface_edit, text_edit, armature_edit, mball_edit, lattice_edit, pose_mode, imagepaint, weightpaint, vertexpaint, particlemode
#bl_context = "objectmode"#render
#need to be in object mode
@classmethod
def poll(cls, context):
return get_addon_prefs().use_color_tools#(context.object is not None and context.object.type == 'GPENCIL')
## draw stuff inside the header (place before main label)
# def draw_header(self, context):
# layout = self.layout
# layout.label(text="More text in header")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
prefs = get_addon_prefs()
if not prefs.gmic_path:
layout.label(text='Gmic path missing in addon prefs', icon='ERROR')
return
if not [l for l in context.object.data.layers if 'spot' in l.info]:
layout.label(text='Need at least one spots layer !', icon='ERROR')
layout.label(text='("spot" in name to identify)')
return
# row = layout.row()
layout.prop(context.scene.gpcolor_props, 'res_percentage')
## render and load frame
ops = layout.operator("bgmic.propagate_color", icon = 'FILE_IMAGE')
ops.mode = 'render'
ops.anim = False
## render and load anim
ops = layout.operator("bgmic.propagate_color", text='Gmic propagate animation', icon = 'FILE_MOVIE')
ops.mode = 'render'
ops.anim = True
layout.separator()
## Load frame
ops = layout.operator("bgmic.propagate_color", text='load frame', icon = 'FILE_IMAGE')
ops.mode = 'load'
ops.anim = False
## Load anim
ops = layout.operator("bgmic.propagate_color", text='Load animation', icon = 'FILE_MOVIE')
ops.mode = 'load'
ops.anim = True
layout.separator()
layout.operator("bgmic.clear_cam_bg_images")
## Open gmic tool location
layout.operator("bgmic.open_gmic_tool_folder", icon = 'FILE_FOLDER')#, text='Open img tool folder'
## TODO : Add an operator to generate empty alpha image to avoid the pink flashes...
## todo : choose to overwrite or not
## Add button to delete current "frame" (delete image on disk...)
classes = (
GMICOLOR_OT_propagate_spots,
GMICOLOR_OT_clear_cam_bg_images,
GMICOLOR_OT_open_gmic_tool_folder,
GMICOLOR_PT_auto_color_panel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -0,0 +1,444 @@
from .func_gmic import *
from ..utils import (location_to_region,
region_to_location,
vector_length_2d,
vector_length,
draw_gp_stroke,
extrapolate_points_by_length,
simple_draw_gp_stroke)
import bpy
from math import degrees
from mathutils import Vector
# from os.path import join, basename, exists, dirname, abspath, splitext
# iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level
def get_closeline_mat(ob):
# ob = C.object
gp = ob.data
# get material
closeline = bpy.data.materials.get('closeline')
if not closeline:
print('Creating line closing material in material database')
closeline = bpy.data.materials.new('closeline')
bpy.data.materials.create_gpencil_data(closeline)#make it GP
closeline.grease_pencil.color = [0.012318, 0.211757, 0.607766, 1.000000]#blue - [0.778229, 0.759283, 0.000000, 1.000000]# yellow urgh
if not closeline.name in gp.materials:
gp.materials.append(closeline)
# get index in list
index = None
for i, ms in enumerate(ob.material_slots):
if ms.material == closeline:
index = i
break
if not index:
print(f'could not find material {closeline.name} in material list')
return index
def create_gap_stroke(f, ob, tol=10, mat_id=None):
'''Take a frame, original object, an optional tolerance value and material ID
Get all extremity points
for each one, analyse if he is close to another in screen space
- if it's the case mark this point as used by the other in the list (avoid to redo the other way)
dic encounter[point:[list of points already encountered]]
'''
from collections import defaultdict
encounter = defaultdict(list)
plist = []
matrix = ob.matrix_world
for s in f.strokes:#add first and last
smat = ob.material_slots[s.material_index].material
if not smat:continue#no material on line
if smat.grease_pencil.show_fill:continue# skip fill lines -> #smat.grease_pencil.show_stroke
if len(s.points) < 2:continue#avoid 0 or 1 points
plist.append(s.points[0])
plist.append(s.points[-1])
# plist.extend([s.points[0], s.points[-1])# is extend faster ?
# pnum = len(plist)
ctl = 0
passed = []
for i, p in enumerate(plist):
# print(f'{i+1}/{pnum}')
for op in plist:#other points
if p == op:# print('same point')
continue
gap2d = vector_length_2d(location_to_region(matrix @ p.co), location_to_region(matrix @ op.co))
# print('gap2d: ', gap2d)
if gap2d > tol:
continue
if gap2d < 1:#less than one pixel no need
continue
# print('create_boundary_stroke')
## dont evaluate a point twice (skip if > 1 intersection)
passed.append(op)
if p in passed:
# print('op in passed')
continue
## Filter to avoid same stroke to be recreated switched
pairlist = encounter.get(op)
if pairlist:
# print('is in dic')#Dbg
if p in pairlist:
# print('found it')#Dbg
#already encountered, skip
continue
# from pprint import pprint#Dbg
# pprint(encounter)#Dbg
# print('new line', p, op)
# not met before, mark as encountered and create line.
encounter[p].append(op)
simple_draw_gp_stroke([p.co, op.co], f, width = 2, mat_id = mat_id)
ctl += 1
print(f'{ctl} line created')
##test_call: #create_gap_stroke(C.object.data.layers.active.active_frame, C.object, mat_id=C.object.active_material_index)
def create_closing_line(tolerance=0.2):
for ob in bpy.context.selected_objects:
if ob.type != 'GPENCIL':
continue
mat_id = get_closeline_mat(ob)# get a the closing material
if not mat_id:
print(f'object {ob.name} has no line closing material and could not create one !')
continue
# can do something to delete all line already there using this material
gp = ob.data
gpl = gp.layers
if not gpl:continue#print(f'obj {ob.name} has no layers')
for l in gpl:
## filter on selected
if not l.select:continue# comment this line for all
# for f in l.frames:#not all for now
f = l.active_frame
## create gap stroke
create_gap_stroke(f, ob, tol=tolerance, mat_id=mat_id)
def is_deviating_by(s, deviation=0.75):
'''get a stroke and a deviation angle (radians, 0.75~=42 degrees)
return true if end points angle pass the threshold'''
if len(s.points) < 3:
return
pa = s.points[-1]
pb = s.points[-2]
pc = s.points[-3]
a = location_to_region(pa.co)
b = location_to_region(pb.co)
c = location_to_region(pc.co)
#cb-> compare angle with ba->
angle = (b-c).angle(a-b)
print('angle: ', degrees(angle))
pa.select = angle > deviation
return angle > deviation
def extend_stroke_tips(s,f,ob,length, mat_id):
'''extend line boundary by given length'''
for id_pair in [ [1,0], [-2,-1] ]:# start and end pair
## 2D mode
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].co)
# b_loc = ob.matrix_world @ s.points[id_pair[1]].co
# b = location_to_region(b_loc)
# c = extrapolate_points_by_length(a,b,length)#print(vector_length_2d(b,c))
# c_loc = region_to_location(c, b_loc)
# simple_draw_gp_stroke([ob.matrix_world.inverted() @ b_loc, ob.matrix_world.inverted() @ c_loc], f, width=2, mat_id=mat_id)
## 3D
a = s.points[id_pair[0]].co# ob.matrix_world @
b = s.points[id_pair[1]].co# ob.matrix_world @
c = extrapolate_points_by_length(a,b,length)#print(vector_length(b,c))
simple_draw_gp_stroke([b,c], f, width=2, mat_id=mat_id)
def change_extension_length(ob, strokelist, length, selected=False):
mat_id = get_closeline_mat(ob)
if not mat_id:
print('could not get/set closeline mat')
return
ct = 0
for s in strokelist:
if s.material_index != mat_id:#is NOT a closeline
continue
if len(s.points) < 2:#not enough point to evaluate
continue
if selected and not s.select:# select filter
continue
## Change length of current length to designated
# Vector point A to point B (direction), push point B in this direction
a = s.points[-2].co
bp = s.points[-1]#end-point
b = bp.co
ab = b - a
if not ab:
continue
# new pos of B is A + new length in the AB direction
newb = a + (ab.normalized() * length)
bp.co = newb
ct += 1
return ct
def extend_all_strokes_tips(ob, frame, length=10, selected=False):
'''extend all strokes boundary by calling extend_stroke_tips'''
# ob = bpy.context.object
mat_id = get_closeline_mat(ob)
if not mat_id:
print('could not get/set closeline mat')
return
# TODO need custom filters or go in GP refine strokes...
# frame = ob.data.layers.active.active_frame
if not frame: return
ct = 0
#TODO need to delete previous closing lines on frame before launching
# iterate in a copy of stroke list to avoid growing frame.strokes as we loop in !
for s in list(frame.strokes):
if s.material_index == mat_id:#is a closeline
continue
if len(s.points) < 2:#not enough point to evaluate
continue
if selected and not s.select:#filter by selection
continue
extend_stroke_tips(s, frame, ob, length, mat_id=mat_id)
ct += 1
return ct
class GPSTK_OT_extend_lines(bpy.types.Operator):
"""
Extend lines on stroke boundarys
"""
bl_idname = "gp.extend_close_lines"
bl_label = "Gpencil extend closing lines"
bl_options = {'REGISTER','UNDO'}
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
# mode : bpy.props.StringProperty(
# name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'})
layer_tgt : bpy.props.EnumProperty(
name="Extend layers", description="Choose which layer to target",
default='ACTIVE',
items=(
('ACTIVE', 'Active only', 'Target active layer only', 0),#include icon name in fourth position
('SELECTED', 'Selected', 'Target selected layers in GP dopesheet (only visible)', 1),
('ALL_VISIBLE', 'All visible', 'target all visible layers', 2),
('ALL', 'All', 'All (even locked and hided layer)', 3),
))
selected : bpy.props.BoolProperty(name='Selected', default=False, description="Work on selected stroke only if True, else All stroke")
length : bpy.props.FloatProperty(name='Length', default=0.2, precision=3, step=0.01, description="length of the extended strokes")
def invoke(self, context, event):
# self.selected = event.shift#if shift is pressed, force inclusion of load
## get init value from scene prop settings
self.selected = context.scene.gpcolor_props.extend_selected
self.length = context.scene.gpcolor_props.extend_length
self.layer_tgt = context.scene.gpcolor_props.extend_layer_tgt
return self.execute(context)
def execute(self, context):
ob = context.object
if self.layer_tgt == 'ACTIVE':
lays = [ob.data.layers.active]
elif self.layer_tgt == 'SELECTED':
lays = [l for l in ob.data.layers if l.select and not l.hide]
elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not l.hide]
else:
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
fct = 0
for l in lays:
if not l.active_frame:
print(f'{l.info} has no active frame')
continue
fct += extend_all_strokes_tips(ob, l.active_frame, length = self.length, selected = self.selected)
if not fct:
mess = "No strokes extended... see console"
self.report({'WARNING'}, mess)#WARNING, ERROR
return {'CANCELLED'}
mess = f"{fct} strokes extended with closing lines"
self.report({'INFO'}, mess)#WARNING, ERROR
return {'FINISHED'}
class GPSTK_OT_change_closeline_length(bpy.types.Operator):
"""
Change extended lines length
"""
bl_idname = "gp.change_close_lines_extension"
bl_label = "Change closeline length (use F9 to call redo panel)"
bl_options = {'REGISTER','UNDO'}
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
layer_tgt : bpy.props.EnumProperty(
name="Extend layers", description="Choose which layer to target",
default='ACTIVE',
items=(
('ACTIVE', 'Active only', 'Target active layer only', 0),#include icon name in fourth position
('SELECTED', 'Selected', 'Target selected layers in GP dopesheet (only visible)', 1),
('ALL_VISIBLE', 'All visible', 'target all visible layers', 2),
('ALL', 'All', 'All (even locked and hided layer)', 3),
))
selected : bpy.props.BoolProperty(name='Selected', default=False, description="Work on selected stroke only if True, else All stroke")
length : bpy.props.FloatProperty(name='Length', default=0.2, precision=3, step=0.01, description="length of the extended strokes")#step=0.00,
def invoke(self, context, event):
## get init value from scene prop settings
self.selected = context.scene.gpcolor_props.extend_selected
self.length = context.scene.gpcolor_props.extend_length
self.layer_tgt = context.scene.gpcolor_props.extend_layer_tgt
return self.execute(context)
def execute(self, context):
ob = context.object
if self.layer_tgt == 'ACTIVE':
lays = [ob.data.layers.active]
elif self.layer_tgt == 'SELECTED':
lays = [l for l in ob.data.layers if l.select and not l.hide]
elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not l.hide]
else:
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
fct = 0
for l in lays:
if not l.active_frame:
print(f'{l.info} has no active frame')
continue
fct += change_extension_length(ob, [s for s in l.active_frame.strokes], length = self.length, selected = self.selected)
if not fct:
mess = "No extension modified... see console"
self.report({'WARNING'}, mess)
return {'CANCELLED'}
mess = f"{fct} extension tweaked"
self.report({'INFO'}, mess)
return {'FINISHED'}
class GPSTK_OT_comma_finder(bpy.types.Operator):
"""
Tester to identify accidental comma trace
"""
bl_idname = "gp.comma_finder"
bl_label = "Strokes comma finder"
bl_options = {'REGISTER','UNDO'}
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
def execute(self, context):
ct = 0
ob = context.object
lays = [l for l in ob.data.layers if not l.hide and not l.lock]
for l in lays:
if not l.active_frame:continue
for s in l.active_frame.strokes:
if is_deviating_by(s, context.scene.gpcolor_props.deviation_tolerance):
ct+=1
self.report({'INFO'}, f'{ct} endpoint found')#WARNING, ERROR
return {'FINISHED'}
class GPSTK_PT_line_closer_panel(bpy.types.Panel):
bl_label = "GP line stopper"# title
## bl_options = {'DEFAULT_CLOSED', 'HIDE_HEADER' }# closed by default, collapse the panel and the label
## is_popover = False # if ommited
## bl_space_type = ['EMPTY', 'VIEW_3D', 'IMAGE_EDITOR', 'NODE_EDITOR', 'SEQUENCE_EDITOR', 'CLIP_EDITOR', 'DOPESHEET_EDITOR', 'GRAPH_EDITOR', 'NLA_EDITOR', 'TEXT_EDITOR', 'CONSOLE', 'INFO', 'TOPBAR', 'STATUSBAR', 'OUTLINER', 'PROPERTIES', 'FILE_BROWSER', 'PREFERENCES'], default 'EMPTY'
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"#name of the tab
#attach in gmic colorize ? (bad idea since gmicolor not in init and may be disable)
# bl_parent_id = "GMICOLOR_PT_auto_color_panel"
@classmethod
def poll(cls, context):
return (context.object is not None)# and context.object.type == 'GPENCIL'
## draw stuff inside the header (place before main label)
# def draw_header(self, context):
# layout = self.layout
# layout.label(text="More text in header")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
# prefs = get_addon_prefs()
layout.prop(context.scene.gpcolor_props, 'extend_layer_tgt')
layout.prop(context.scene.gpcolor_props, 'extend_selected')
layout.prop(context.scene.gpcolor_props, 'extend_length')
layout.operator("gp.extend_close_lines", icon = 'SNAP_MIDPOINT')
#diplay closeline visibility
if context.object.type == 'GPENCIL' and context.object.data.materials.get('closeline'):
row=layout.row()
row.prop(context.object.data.materials['closeline'].grease_pencil, 'hide', text='Stop lines')
row.operator("gp.change_close_lines_extension", text='Length', icon = 'DRIVER_DISTANCE')
else:
layout.label(text='-no stop lines-')
layout.separator()
layout.prop(context.scene.gpcolor_props, 'deviation_tolerance')
layout.operator("gp.comma_finder", icon = 'INDIRECT_ONLY_OFF')
#TODO change length (on selection, on all)
#TODO remove all line (in unlocked frame, on all)
classes = (
GPSTK_OT_extend_lines,
GPSTK_OT_change_closeline_length,
GPSTK_OT_comma_finder,
GPSTK_PT_line_closer_panel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -0,0 +1,184 @@
import os, re
from os.path import join, basename, exists, dirname, abspath, splitext
import subprocess
import time, datetime
from ..utils import get_addon_prefs, transfer_value
def get_gmic():
prefs = get_addon_prefs()
return prefs.gmic_path
## globals
image_exts = ('.png', '.jpg', '.tiff', '.tga', '.jpeg',)
Rnum = re.compile(r'(\d+)(?!.*\d)')
def is_img(fp):
if splitext(basename(fp))[1].lower() in image_exts:
return True
def is_img_folder(d):
ct = 0
for f in os.listdir(d):
ct += 1
#if os.path.isfile(d)
if not is_img(f):
#print("not an image:", f)#Dbg
return 0
if not ct:#return false if nothing found
#print('nothing in folder')
return -1
###all files has been evaluated as images
return 1
def auto_colo(line, colo, out):
gmic = get_gmic()
opt = r'fx_colorize_lineart_smart 2,96,0,0,0,1,24,197,0,90.4,1,34.05,22.27,48.96,0.57,6.4,1,0,20,50.18,7.5,0.5,0'
if not exists(line) or not exists(colo):
print('one source directory not exists')
return
line_l = [join(line, i) for i in os.listdir(line) if i.endswith(('.png', '.jpg'))]
colo_l = [join(colo, i) for i in os.listdir(colo) if i.endswith(('.png', '.jpg'))]
line_l.sort()
colo_l.sort()
#print(line_l)
#print(colo_l)
if len(line_l) != len(colo_l):
print('lists of line and colo files have not the same lenght')
return
outfolder_name = basename(dirname(line)) + '_autocolo'
outfolder = join(out, outfolder_name)
if not exists(outfolder):
os.mkdir(outfolder)
ct = 0
for i, f in enumerate(line_l):
ct += 1
filename = outfolder_name + '_'+ str(i+101).zfill(4) +'.png'
outfile = join(outfolder, filename)
#print('-', f,outfile)
cmd = '{} {} {} {} -o[1] {}'.format(gmic, line_l[i], colo_l[i], opt, outfile)
#note on -o[1]
#this filter output 2 img, original line and the new color, here specify only color (keep your name clean)
print(cmd)
os.system(cmd)
# if ct > 3:return#limiter
print('Done')
def random_fill(line, out):
'''gmic command to convert line file to pseudo color out file'''
gmic = get_gmic()
#Todo, handle png compression, will be deleted so can be uncompressed
start_time = time.time()#timer
opt = r'fx_colorize_lineart_smart 0,100,0,0,0,80,184,0,90.5,2,0,0,0,0,4,1,2'
cmd = [gmic, line] + opt.split(' ') + ['-o[1]', out]# + ['to_rgba']
print(cmd)
subprocess.call(cmd)
print("elapsed", time.time() - start_time)#timer
def random_fill_folder(src, dest):
'''gmic command to convert a line folder to pseudo color output in another folder'''
# opt = r'fx_colorize_lineart_smart 0,97.2,0,0,0,0,210,213,0,86.5,1,49.41,28.67,37.26,0.29,12.7,0,0,0,0,0,0,0'
if not exists(src):
print('source directory not exists')
return
line_l = [join(src, i) for i in os.listdir(src) if is_img(i)]
line_l.sort()
'''
outfolder_name = basename(dirname(src)) + '_randomcolo'
outfolder = join(dest, outfolder_name)
if not exists(outfolder):
os.mkdir(outfolder)
'''
#ct = 0
for i, f in enumerate(line_l):
outfile = join(dest, 'random_fill_' + str(i+101).zfill(4) +'.png')
random_fill(f, outfile)
print('Done')
def generate_empty_image(fp):
'''
Generate an empty 1x1 pixel full transparent
width,height,depth,spectrum(channels)
doc: https://gmic.eu/tutorial/_input.shtml
generate a file of ~200byte
'''
gmic = get_gmic()
cmd = [gmic, '1,1,1,4', '-o', fp]
# cmd = [gmic, '16,9,1,4', '-o', fp] #classic ratio... influence nothing
subprocess.call(cmd)
def propagate_color(line, spot, out):
gmic = get_gmic()
start_time = time.time()#timer
## colorize
opt = ['fx_colorize_lineart', '1,3,0,0.02']
## antialias
aa = ['fx_smooth_antialias', '10,10.1,1.22,0,50,50']# smooth aa
# aa = ['gcd_anti_alias', '60,0.1,0,0']# basic aa
## BG remove
# vert pur 0,255,0,255 for BG color to delete, rose pale : [234,139,147,255] : [0.822786, 0.258183, 0.291771, 1.000000] #EA8B93
# 2 first value tolerance(1~3),smoothness(need0)
del_bg = ['to_rgba', 'replace_color', '3,0,1,255,1,255,255,255,255,0']
print('gmic: ', gmic)
# gmic = f'"{gmic}"'
cmd = [gmic, line, spot] + opt + del_bg + ['-o[1]', out]# + aa
print('cmd: ', cmd)
subprocess.call(cmd)
print("elapsed", time.time() - start_time)#timer
def propagate_color_folder(line_fp, spot_fp, outfolder):
print('Starting', datetime.datetime.now())# print full current date
start_time = time.time()# get start time
# [gmic_krita_qt]./apply/ v -99 fx_colorize_lineart 1,3,0,0.02
if not exists(line_fp) or not exists(spot_fp) or not exists(outfolder):
print(f'''some directories not exists:
{exists(line_fp)}: {line_fp}
{exists(spot_fp)}: {spot_fp}
{exists(outfolder)}: {outfolder}''')
return 1
lines = sorted([f.path for f in os.scandir(line_fp) if is_img(f.name) and Rnum.search(f.name)])
spots = sorted([f.path for f in os.scandir(spot_fp) if is_img(f.name) and Rnum.search(f.name)])
# if len(lines) != len(spots):#file number test...valid but disabled for tests
# print('lists of line and colo files have not the same lenght')
# return
for l, s in zip(lines, spots):
lframe = int(Rnum.search(l).group(1))
sframe = int(Rnum.search(s).group(1))
if lframe != sframe:
print(f'line img has not the same number as spot img {lframe} != {sframe}')
continue
out = join(outfolder, f'colo_{str(lframe).zfill(3)}.png')
print(f'frame {lframe}')
propagate_color(l,s,out)
elapsed_time = time.time() - start_time# seconds
full_time = str(datetime.timedelta(seconds=elapsed_time))# hh:mm:ss format
print("elapsed time", elapsed_time)
print(full_time)

674
LICENSE.txt Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

579
OP_box_deform.py Normal file
View File

@ -0,0 +1,579 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
'''
Based on Box_deform addon
! Standalone file ! Stripped preference, and set best default auto transform)
'''
import bpy
import numpy as np
""" def get_addon_prefs():
import os
addon_name = os.path.splitext(__name__)[0]
preferences = bpy.context.preferences
addon_prefs = preferences.addons[addon_name].preferences
return (addon_prefs) """
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 assign_vg(obj, vg_name):
## create vertex group
vg = obj.vertex_groups.get(vg_name)
if vg:
# remove to start clean
obj.vertex_groups.remove(vg)
vg = obj.vertex_groups.new(name=vg_name)
bpy.ops.gpencil.vertex_group_assign()
return vg
def view_cage(obj):
lattice_interp = 'KEY_LINEAR'#get_addon_prefs().default_deform_type
gp = obj.data
gpl = gp.layers
coords = []
initial_mode = bpy.context.mode
## get points
if bpy.context.mode == 'EDIT_GPENCIL':
for l in gpl:
if l.lock or l.hide or not l.active_frame:#or len(l.frames)
continue
if gp.use_multiedit:
target_frames = [f for f in l.frames if f.select]
else:
target_frames = [l.active_frame]
for f in target_frames:
for s in f.strokes:
if not s.select:
continue
for p in s.points:
if p.select:
# get real location
coords.append(obj.matrix_world @ p.co)
elif bpy.context.mode == 'OBJECT':#object mode -> all points
for l in gpl:# if l.hide:continue# only visible ? (might break things)
if not len(l.frames):
continue#skip frameless layer
for s in l.active_frame.strokes:
for p in s.points:
coords.append(obj.matrix_world @ p.co)
elif bpy.context.mode == 'PAINT_GPENCIL':
# get last stroke points coordinated
if not gpl.active or not gpl.active.active_frame:
return 'No frame to deform'
if not len(gpl.active.active_frame.strokes):
return 'No stroke found to deform'
paint_id = -1
if bpy.context.scene.tool_settings.use_gpencil_draw_onback:
paint_id = 0
coords = [obj.matrix_world @ p.co for p in gpl.active.active_frame.strokes[paint_id].points]
else:
return 'Wrong mode!'
if not coords:
## maybe silent return instead (need special str code to manage errorless return)
return 'No points found!'
if bpy.context.mode in ('EDIT_GPENCIL', 'PAINT_GPENCIL') and len(coords) < 2:
# Dont block object mod
return 'Less than two point selected'
vg_name = 'lattice_cage_deform_group'
if bpy.context.mode == 'EDIT_GPENCIL':
vg = assign_vg(obj, vg_name)
if bpy.context.mode == 'PAINT_GPENCIL':
# points cannot be assign to API yet(ugly and slow workaround but only way)
# -> https://developer.blender.org/T56280 so, hop'in'ops !
# store selection and deselect all
plist = []
for s in gpl.active.active_frame.strokes:
for p in s.points:
plist.append([p, p.select])
p.select = False
# select
## foreach_set does not update
# gpl.active.active_frame.strokes[paint_id].points.foreach_set('select', [True]*len(gpl.active.active_frame.strokes[paint_id].points))
for p in gpl.active.active_frame.strokes[paint_id].points:
p.select = True
# assign
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
vg = assign_vg(obj, vg_name)
# restore
for pl in plist:
pl[0].select = pl[1]
## View axis Mode ---
## get view coordinate of all points
coords2D = [location_to_region(co) for co in coords]
# find centroid for depth (or more economic, use obj origin...)
centroid = np.mean(coords, axis=0)
# not a mean ! a mean of extreme ! centroid2d = np.mean(coords2D, axis=0)
all_x, all_y = np.array(coords2D)[:, 0], np.array(coords2D)[:, 1]
min_x, min_y = np.min(all_x), np.min(all_y)
max_x, max_y = np.max(all_x), np.max(all_y)
width = (max_x - min_x)
height = (max_y - min_y)
center_x = min_x + (width/2)
center_y = min_y + (height/2)
centroid2d = (center_x,center_y)
center = region_to_location(centroid2d, centroid)
# bpy.context.scene.cursor.location = center#Dbg
#corner Bottom-left to Bottom-right
x0 = region_to_location((min_x, min_y), centroid)
x1 = region_to_location((max_x, min_y), centroid)
x_worldsize = (x0 - x1).length
#corner Bottom-left to top-left
y0 = region_to_location((min_x, min_y), centroid)
y1 = region_to_location((min_x, max_y), centroid)
y_worldsize = (y0 - y1).length
## in case of 3
lattice_name = 'lattice_cage_deform'
# cleaning
cage = bpy.data.objects.get(lattice_name)
if cage:
bpy.data.objects.remove(cage)
lattice = bpy.data.lattices.get(lattice_name)
if lattice:
bpy.data.lattices.remove(lattice)
# create lattice object
lattice = bpy.data.lattices.new(lattice_name)
cage = bpy.data.objects.new(lattice_name, lattice)
cage.show_in_front = True
## Master (root) collection
bpy.context.scene.collection.objects.link(cage)
# spawn cage and align it to view (Again ! align something to a vector !!! argg)
r3d = bpy.context.space_data.region_3d
viewmat = r3d.view_matrix
cage.matrix_world = viewmat.inverted()
cage.scale = (x_worldsize, y_worldsize, 1)
## Z aligned in view direction (need minus X 90 degree to be aligned FRONT)
# cage.rotation_euler.x -= radians(90)
# cage.scale = (x_worldsize, 1, y_worldsize)
cage.location = center
lattice.points_u = 2
lattice.points_v = 2
lattice.points_w = 1
lattice.interpolation_type_u = lattice_interp#'KEY_LINEAR'-'KEY_BSPLINE'
lattice.interpolation_type_v = lattice_interp#'KEY_LINEAR'-'KEY_BSPLINE'
lattice.interpolation_type_w = lattice_interp#'KEY_LINEAR'-'KEY_BSPLINE'
mod = obj.grease_pencil_modifiers.new('tmp_lattice', 'GP_LATTICE')
# move to top if modifiers exists
for _ in range(len(obj.grease_pencil_modifiers)):
bpy.ops.object.gpencil_modifier_move_up(modifier='tmp_lattice')
mod.object = cage
if initial_mode == 'PAINT_GPENCIL':
mod.layer = gpl.active.info
# note : if initial was Paint, changed to Edit
# so vertex attribution is valid even for paint
if bpy.context.mode == 'EDIT_GPENCIL':
mod.vertex_group = vg.name
#Go in object mode if not already
if bpy.context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# Store name of deformed object in case of 'revive modal'
cage.vertex_groups.new(name=obj.name)
## select and make cage active
# cage.select_set(True)
bpy.context.view_layer.objects.active = cage
obj.select_set(False)#deselect GP object
bpy.ops.object.mode_set(mode='EDIT')# go in lattice edit mode
bpy.ops.lattice.select_all(action='SELECT')# select all points
## Eventually change tool mode to tweak for direct point editing (reset after before leaving)
bpy.ops.wm.tool_set_by_id(name="builtin.select")# Tweaktoolcode
return cage
def back_to_obj(obj, gp_mode, org_lattice_toolset, context):
if context.mode == 'EDIT_LATTICE' and org_lattice_toolset:# Tweaktoolcode - restore the active tool used by lattice edit..
bpy.ops.wm.tool_set_by_id(name = org_lattice_toolset)# Tweaktoolcode
# gp object active and selected
bpy.ops.object.mode_set(mode='OBJECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
def delete_cage(cage):
lattice = cage.data
bpy.data.objects.remove(cage)
bpy.data.lattices.remove(lattice)
def apply_cage(gp_obj, cage):
mod = gp_obj.grease_pencil_modifiers.get('tmp_lattice')
if mod:
bpy.ops.object.gpencil_modifier_apply(apply_as='DATA', modifier=mod.name)
else:
print('tmp_lattice modifier not found to apply...')
delete_cage(cage)
def cancel_cage(gp_obj, cage):
#remove modifier
mod = gp_obj.grease_pencil_modifiers.get('tmp_lattice')
if mod:
gp_obj.grease_pencil_modifiers.remove(mod)
else:
print('tmp_lattice modifier not found to remove...')
delete_cage(cage)
class GP_OT_latticeGpDeform(bpy.types.Operator):
"""Create a lattice to use as quad corner transform"""
bl_idname = "gp.latticedeform"
bl_label = "Box deform"
bl_description = "Use lattice for free box transforms on grease pencil points (Ctrl+T)"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object is not None and context.object.type in ('GPENCIL','LATTICE')
# local variable
tab_press_ct = 0
def modal(self, context, event):
display_text = f"Deform Cage size: {self.lat.points_u}x{self.lat.points_v} (1-9 or ctrl + ←→↑↓]) | \
mode (M) : {'Linear' if self.lat.interpolation_type_u == 'KEY_LINEAR' else 'Spline'} | \
valid:Spacebar/Enter/Tab, cancel:Del/Backspace"
context.area.header_text_set(display_text)
## Handle ctrl+Z
if event.type in {'Z'} and event.value == 'PRESS' and event.ctrl:
## Disable (capture key)
return {"RUNNING_MODAL"}
## Not found how possible to find modal start point in undo stack to
# print('ops list', context.window_manager.operators.keys())
# if context.window_manager.operators:#can be empty
# print('\nlast name', context.window_manager.operators[-1].name)
# Auto interpo check
if self.auto_interp:
if event.type in {'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO',} and event.value == 'PRESS':
self.set_lattice_interp('KEY_BSPLINE')
if event.type in {'DOWN_ARROW', "UP_ARROW", "RIGHT_ARROW", "LEFT_ARROW"} and event.value == 'PRESS' and event.ctrl:
self.set_lattice_interp('KEY_BSPLINE')
if event.type in {'ONE'} and event.value == 'PRESS':
self.set_lattice_interp('KEY_LINEAR')
# Single keys
if event.type in {'H'} and event.value == 'PRESS':
# self.report({'INFO'}, "Can't hide")
return {"RUNNING_MODAL"}
if event.type in {'ONE'} and event.value == 'PRESS':# , 'NUMPAD_1'
self.lat.points_u = self.lat.points_v = 2
return {"RUNNING_MODAL"}
if event.type in {'TWO'} and event.value == 'PRESS':# , 'NUMPAD_2'
self.lat.points_u = self.lat.points_v = 3
return {"RUNNING_MODAL"}
if event.type in {'THREE'} and event.value == 'PRESS':# , 'NUMPAD_3'
self.lat.points_u = self.lat.points_v = 4
return {"RUNNING_MODAL"}
if event.type in {'FOUR'} and event.value == 'PRESS':# , 'NUMPAD_4'
self.lat.points_u = self.lat.points_v = 5
return {"RUNNING_MODAL"}
if event.type in {'FIVE'} and event.value == 'PRESS':# , 'NUMPAD_5'
self.lat.points_u = self.lat.points_v = 6
return {"RUNNING_MODAL"}
if event.type in {'SIX'} and event.value == 'PRESS':# , 'NUMPAD_6'
self.lat.points_u = self.lat.points_v = 7
return {"RUNNING_MODAL"}
if event.type in {'SEVEN'} and event.value == 'PRESS':# , 'NUMPAD_7'
self.lat.points_u = self.lat.points_v = 8
return {"RUNNING_MODAL"}
if event.type in {'EIGHT'} and event.value == 'PRESS':# , 'NUMPAD_8'
self.lat.points_u = self.lat.points_v = 9
return {"RUNNING_MODAL"}
if event.type in {'NINE'} and event.value == 'PRESS':# , 'NUMPAD_9'
self.lat.points_u = self.lat.points_v = 10
return {"RUNNING_MODAL"}
if event.type in {'ZERO'} and event.value == 'PRESS':# , 'NUMPAD_0'
self.lat.points_u = 2
self.lat.points_v = 1
return {"RUNNING_MODAL"}
if event.type in {'RIGHT_ARROW'} and event.value == 'PRESS' and event.ctrl:
if self.lat.points_u < 20:
self.lat.points_u += 1
return {"RUNNING_MODAL"}
if event.type in {'LEFT_ARROW'} and event.value == 'PRESS' and event.ctrl:
if self.lat.points_u > 1:
self.lat.points_u -= 1
return {"RUNNING_MODAL"}
if event.type in {'UP_ARROW'} and event.value == 'PRESS' and event.ctrl:
if self.lat.points_v < 20:
self.lat.points_v += 1
return {"RUNNING_MODAL"}
if event.type in {'DOWN_ARROW'} and event.value == 'PRESS' and event.ctrl:
if self.lat.points_v > 1:
self.lat.points_v -= 1
return {"RUNNING_MODAL"}
# change modes
if event.type in {'M'} and event.value == 'PRESS':
self.auto_interp = False
interp = 'KEY_BSPLINE' if self.lat.interpolation_type_u == 'KEY_LINEAR' else 'KEY_LINEAR'
self.set_lattice_interp(interp)
return {"RUNNING_MODAL"}
# Valid
if event.type in {'RET', 'SPACE'}:
if event.value == 'PRESS':
#bpy.ops.ed.flush_edits()# TODO: find a way to get rid of undo-registered lattices tweaks
self.restore_prefs(context)
back_to_obj(self.gp_obj, self.gp_mode, self.org_lattice_toolset, context)
apply_cage(self.gp_obj, self.cage)#must be in object mode
# back to original mode
if self.gp_mode != 'OBJECT':
bpy.ops.object.mode_set(mode=self.gp_mode)
context.area.header_text_set(None)#reset header
return {'FINISHED'}
# Abort ---
# One Warning for Tab cancellation.
if event.type == 'TAB' and event.value == 'PRESS':
self.tab_press_ct += 1
if self.tab_press_ct < 2:
self.report({'WARNING'}, "Pressing TAB again will Cancel")
return {"RUNNING_MODAL"}
if event.type in {'T'} and event.value == 'PRESS' and event.ctrl:# Retyped same shortcut
self.cancel(context)
return {'CANCELLED'}
if event.type in {'DEL', 'BACK_SPACE'} or self.tab_press_ct >= 2:#'ESC',
self.cancel(context)
return {'CANCELLED'}
return {'PASS_THROUGH'}
def set_lattice_interp(self, interp):
self.lat.interpolation_type_u = self.lat.interpolation_type_v = self.lat.interpolation_type_w = interp
def cancel(self, context):
self.restore_prefs(context)
back_to_obj(self.gp_obj, self.gp_mode, self.org_lattice_toolset, context)
cancel_cage(self.gp_obj, self.cage)
context.area.header_text_set(None)
if self.gp_mode != 'OBJECT':
bpy.ops.object.mode_set(mode=self.gp_mode)
def store_prefs(self, context):
# store_valierables <-< preferences
self.use_drag_immediately = context.preferences.inputs.use_drag_immediately
self.drag_threshold_mouse = context.preferences.inputs.drag_threshold_mouse
self.drag_threshold_tablet = context.preferences.inputs.drag_threshold_tablet
self.use_overlays = context.space_data.overlay.show_overlays
def restore_prefs(self, context):
# preferences <-< store_valierables
context.preferences.inputs.use_drag_immediately = self.use_drag_immediately
context.preferences.inputs.drag_threshold_mouse = self.drag_threshold_mouse
context.preferences.inputs.drag_threshold_tablet = self.drag_threshold_tablet
context.space_data.overlay.show_overlays = self.use_overlays
def set_prefs(self, context):
context.preferences.inputs.use_drag_immediately = True
context.preferences.inputs.drag_threshold_mouse = 1
context.preferences.inputs.drag_threshold_tablet = 3
context.space_data.overlay.show_overlays = True
def invoke(self, context, event):
## Restrict to 3D view
if context.area.type != 'VIEW_3D':
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
if not context.object:#do it in poll ?
self.report({'ERROR'}, "No active objects found")
return {'CANCELLED'}
# self.prefs = get_addon_prefs()#get_prefs
self.org_lattice_toolset = None
self.gp_mode = 'EDIT_GPENCIL'
# --- special Case of lattice revive modal, just after ctrl+Z back into lattice with modal stopped
if context.mode == 'EDIT_LATTICE' and context.object.name == 'lattice_cage_deform' and len(context.object.vertex_groups):
self.gp_obj = context.scene.objects.get(context.object.vertex_groups[0].name)
if not self.gp_obj:
self.report({'ERROR'}, "/!\\ Box Deform : Cannot find object to target")
return {'CANCELLED'}
if not self.gp_obj.grease_pencil_modifiers.get('tmp_lattice'):
self.report({'ERROR'}, "/!\\ No 'tmp_lattice' modifiers on GP object")
return {'CANCELLED'}
self.cage = context.object
self.lat = self.cage.data
self.set_prefs(context)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
if context.object.type != 'GPENCIL':
# self.report({'ERROR'}, "Works only on gpencil objects")
## silent return
return {'CANCELLED'}
#paint need VG workaround. object need good shortcut
if context.mode not in ('EDIT_GPENCIL', 'OBJECT', 'PAINT_GPENCIL'):
# self.report({'WARNING'}, "Works only in following GPencil modes: edit")# ERROR
## silent return
return {'CANCELLED'}
# bpy.ops.ed.undo_push(message="Box deform step")#don't work as expected (+ might be obsolete)
# https://developer.blender.org/D6147 <- undo forget
self.gp_obj = context.object
# Clean potential failed previous job (delete tmp lattice)
mod = self.gp_obj.grease_pencil_modifiers.get('tmp_lattice')
if mod:
print('Deleted remaining lattice modifiers')
self.gp_obj.grease_pencil_modifiers.remove(mod)
phantom_obj = context.scene.objects.get('lattice_cage_deform')
if phantom_obj:
print('Deleted remaining lattice object')
delete_cage(phantom_obj)
if [m for m in self.gp_obj.grease_pencil_modifiers if m.type == 'GP_LATTICE']:
self.report({'ERROR'}, "Grease pencil object already has a lattice modifier (can only have one)")
return {'CANCELLED'}
self.gp_mode = context.mode#store mode for restore
# All good, create lattice and start modal
# Create lattice (and switch to lattice edit) ----
self.cage = view_cage(self.gp_obj)
if isinstance(self.cage, str):#error, cage not created, display error
self.report({'ERROR'}, self.cage)
return {'CANCELLED'}
self.lat = self.cage.data
## usability toggles
## pref for clic drag -> if self.prefs.use_clic_drag:#Store the active tool since we will change it
self.org_lattice_toolset = bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname# Tweaktoolcode
self.auto_interp = True#self.prefs.auto_swap_deform_type
#store (scene properties needed in case of ctrlZ revival)
self.store_prefs(context)
self.set_prefs(context)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
## --- KEYMAP
addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new("gp.latticedeform", type ='T', value = "PRESS", ctrl = True)
kmi.repeat = False
addon_keymaps.append((km, kmi))
def unregister_keymaps():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
### --- REGISTER ---
def register():
if bpy.app.background:
return
bpy.utils.register_class(GP_OT_latticeGpDeform)
register_keymaps()
def unregister():
if bpy.app.background:
return
unregister_keymaps()
bpy.utils.unregister_class(GP_OT_latticeGpDeform)

339
OP_breakdowner.py Normal file
View File

@ -0,0 +1,339 @@
#Breakdowner object mode V1
import bpy
import re
from mathutils import Vector, Matrix
from math import radians, degrees
# exemple for future improve: https://justinsbarrett.com/tweenmachine/
def get_surrounding_points(fc, frame):
'''Take an Fcurve and a frame and return previous and next frames'''
if not frame: frame = bpy.context.scene.frame_current
p_pt = n_pt = None
mins = []
maxs = []
for pt in fc.keyframe_points:
if pt.co[0] < frame:
p_pt = pt
if pt.co[0] > frame:
n_pt = pt
break
return p_pt, n_pt
## unused direct breackdown func
def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale'), axe=(0,1,2)):
cf = bpy.context.scene.frame_current# use operator context (may be unsynced timeline)
axes_name = ('x', 'y', 'z')
obj = bpy.context.object# better use self.context
if not obj:
print('no active object')
return
anim_data = obj.animation_data
if not anim_data:
print(f'no animation data on obj: {obj.name}')
return
action = anim_data.action
if not action:
print(f'no action on animation data of obj: {obj.name}')
return
skipping = []
for fc in action.fcurves:
# if fc.data_path.split('"')[1] in bone_names_filter:# bones
# if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones
if fc.data_path in channels and fc.array_index in axe:# .split('.')[-1]
fc_name = f'{fc.data_path}.{axes_name[fc.array_index]}'
print(fc_name)
pkf, nkf = get_surrounding_points(fc, frame=cf)
# check previous, next keyframe (if one or both is missing, skip)
if pkf is None or nkf is None:
skipping.append(fc_name)
continue
prv, nxt = pkf.co[1], nkf.co[1]
if prv == nxt:
nval = prv
else:
nval = ((percentage * (nxt - prv)) / 100) + prv#intermediate val
print('value:', nval)
fc.keyframe_points.add(1)
fc.keyframe_points[-1].co[0] = cf
fc.keyframe_points[-1].co[1] = nval
fc.keyframe_points[-1].type = pkf.type# make same type ?
fc.keyframe_points[-1].interpolation = pkf.interpolation
fc.update()
# obj.keyframe_insert(fc.data_path, index=fc.array_index, )
### breakdown_keys(channels=('location', 'rotation_euler', 'scale'))
class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
"""Breakdown percentage between two keyframes like bone pose mode"""
bl_idname = "object.breakdown_anim"
bl_label = "breakdown object keyframe"
bl_description = "Percentage value between previous dans next keyframes, "
bl_options = {"REGISTER", "UNDO"}
pressed_ctrl = False
pressed_shift = False
# pressed_alt = False
str_val = ''
step = 5
@classmethod
def poll(cls, context):
return context.mode == 'OBJECT'and context.object
def percentage(self):
return (self.xmouse - self.xmin) / self.width * 100
def assign_transforms(self, percentage):
for obj, path_dic in self.obdic.items():
for data_path, index_dic in path_dic.items():
for index, vals in index_dic.items():# prv, nxt = vals
# exec(f'bpy.data.objects["{obj.name}"].{data_path}[{index}] = {((self.percentage() * (vals[1] - vals[0])) / 100) + vals[0]}')
getattr(obj, data_path)[index] = ((percentage * (vals[1] - vals[0])) / 100) + vals[0]
def modal(self, context, event):
context.area.tag_redraw()
refresh = False
### /TESTER - keycode printer (flood console but usefull to know a keycode name)
# if event.type not in {'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE'}:#avoid flood of mouse move.
# print('key:', event.type, 'value:', event.value)
### TESTER/
## Handle modifier keys state
if event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}: self.pressed_shift = event.value == 'PRESS'
if event.type in {'LEFT_CTRL', 'RIGHT_CTRL'}: self.pressed_ctrl = event.value == 'PRESS'
# if event.type in {'LEFT_ALT', 'RIGHT_ALT'}: self.pressed_alt = event.value == 'PRESS'
### KEYBOARD SINGLE PRESS
if event.value == 'PRESS':
refresh=True
if event.type in {'NUMPAD_MINUS'}:#, 'LEFT_BRACKET', 'WHEELDOWNMOUSE'
if self.str_val.startswith('-'):
self.str_val = self.str_val.strip('-')
else:
self.str_val = '-' + self.str_val#.strip('-')
## number
if event.type in {'ZERO', 'NUMPAD_0'}: self.str_val += '0'
if event.type in {'ONE', 'NUMPAD_1'}: self.str_val += '1'
if event.type in {'TWO', 'NUMPAD_2'}: self.str_val += '2'
if event.type in {'THREE', 'NUMPAD_3'}: self.str_val += '3'
if event.type in {'FOUR', 'NUMPAD_4'}: self.str_val += '4'
if event.type in {'FIVE', 'NUMPAD_5'}: self.str_val += '5'
if event.type in {'SIX', 'NUMPAD_6'}: self.str_val += '6'
if event.type in {'SEVEN', 'NUMPAD_7'}: self.str_val += '7'
if event.type in {'EIGHT', 'NUMPAD_8'}: self.str_val += '8'
if event.type in {'NINE', 'NUMPAD_9'}: self.str_val += '9'
if event.type in {'NUMPAD_PERIOD', 'COMMA'}:
if not '.' in self.str_val: self.str_val += '.'
# remove end chars
if event.type in {'DEL', 'BACK_SPACE'}: self.str_val = self.str_val[:-1]
# TODO lock transforms
# if event.type in {'G'}:pass# grab translate only
# if event.type in {'R'}:pass# rotation only
# if event.type in {'S'}:pass# scale only
## TODO need to check if self.str_val is valid and if not : display warning and return running modal
if re.search(r'\d', self.str_val):
use_num = True
percentage = float(self.str_val)
display_percentage = f'{percentage:.1f}' if '.' in self.str_val else f'{percentage:.0f}'
display_text = f'Breakdown: [{display_percentage}]% | manual type, erase for mouse control'
else:# use mouse
use_num = False
percentage = self.percentage()
if self.pressed_ctrl:# round
percentage = int(percentage)
if self.pressed_shift:# by step of 5
modulo = percentage % self.step
if modulo < self.step/2.0:
percentage = int( percentage - modulo )
else:
percentage = int( percentage + (self.step - modulo) )
display_percentage = f'{percentage:.1f}' if isinstance(percentage, float) else str(percentage)
display_text = f'Breakdown: {display_percentage}% | MODES ctrl: round - shift: 5 steps'
context.area.header_text_set(display_text)
## Get mouse move
if event.type in {'MOUSEMOVE'}:# , 'INBETWEEN_MOUSEMOVE'
if not use_num:#avoid compute on mouse move when manual type on
refresh = True
## percentage of xmouse in screen
self.xmouse = event.mouse_region_x
## assign stuff
if refresh:
self.assign_transforms(percentage)
# Valid
if event.type in {'RET', 'SPACE', 'LEFTMOUSE'}:
## 'INSERTKEY_AVAILABLE' ? ? filter
context.area.header_text_set(None)
context.window.cursor_set("DEFAULT")
if context.scene.tool_settings.use_keyframe_insert_auto:# auto key OK
if context.scene.tool_settings.use_keyframe_insert_keyingset and context.scene.keying_sets_all.active:
bpy.ops.anim.keyframe_insert('INVOKE_DEFAULT')#type='DEFAULT'
else:
bpy.ops.anim.keyframe_insert('INVOKE_DEFAULT', type='Available')
# "DEFAULT" not found in ('Available', 'Location', 'Rotation', 'Scaling', 'BUILTIN_KSI_LocRot', 'LocRotScale', 'BUILTIN_KSI_LocScale', 'BUILTIN_KSI_RotScale', 'BUILTIN_KSI_DeltaLocation', 'BUILTIN_KSI_DeltaRotation', 'BUILTIN_KSI_DeltaScale', 'BUILTIN_KSI_VisualLoc', 'BUILTIN_KSI_VisualRot', 'BUILTIN_KSI_VisualScaling', 'BUILTIN_KSI_VisualLocRot', 'BUILTIN_KSI_VisualLocRotScale', 'BUILTIN_KSI_VisualLocScale', 'BUILTIN_KSI_VisualRotScale')
return {'FINISHED'}
# Abort
if event.type in {'RIGHTMOUSE', 'ESC'}:
## Remove draw handler (if there was any)
# bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
context.scene.frame_set(self.cf)# reset object pos (update scene to re-evaluate anim)
context.area.header_text_set(None)#reset header
context.window.cursor_set("DEFAULT")
# print('Breakdown Cancelled')#Dbg
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
## cursors
## 'DEFAULT', 'NONE', 'WAIT', 'CROSSHAIR', 'MOVE_X', 'MOVE_Y', 'KNIFE', 'TEXT', 'PAINT_BRUSH', 'PAINT_CROSS', 'DOT', 'ERASER', 'HAND', 'SCROLL_X', 'SCROLL_Y', 'SCROLL_XY', 'EYEDROPPER'
## start checks
message = None
if context.area.type != 'VIEW_3D':message = 'View3D not found, cannot run operator'
obj = bpy.context.object# better use self.context
if not obj:message = 'no active object'
anim_data = obj.animation_data
if not anim_data:message = f'no animation data on obj: {obj.name}'
action = anim_data.action
if not action:message = f'no action on animation data of obj: {obj.name}'
if message:
self.report({'WARNING'}, message)# ERROR
return {'CANCELLED'}
## initiate variable to use
self.width = context.area.width# include sidebar...
## with exclude sidebar >>> C.screen.areas[3].regions[5].width
self.xmin = context.area.x
self.xmouse = event.mouse_region_x
self.pressed_alt = event.alt
self.pressed_ctrl = event.ctrl
self.pressed_shift = event.shift
self.cf = context.scene.frame_current
self.channels = ('location', 'rotation_euler', 'rotation_quaternion', 'scale')
skipping = []
found = 0
same = 0
self.obdic = {}
## TODO for ob in context.selected objects, need to reduce list with upper filters...
for fc in action.fcurves:
# if fc.data_path.split('"')[1] in bone_names_filter:# bones
# if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones
if fc.data_path in self.channels:# .split('.')[-1]# and fc.array_index in axe
fc_name = f'{fc.data_path}.{fc.array_index}'
pkf, nkf = get_surrounding_points(fc, frame = self.cf)
if pkf is None or nkf is None:
# check previous, next keyframe (if one or both is missing, skip)
skipping.append(fc_name)
continue
found +=1
prv, nxt = pkf.co[1], nkf.co[1]
if not obj in self.obdic:
self.obdic[obj] = {}
if not fc.data_path in self.obdic[obj]:
self.obdic[obj][fc.data_path] = {}
self.obdic[obj][fc.data_path][fc.array_index] = [prv, nxt]
if prv == nxt:
same += 1
else:
# exec(f'bpy.data.objects["{obj.name}"].{fc.data_path}[{fc.array_index}] = {((self.percentage() * (nxt - prv)) / 100) + prv}')
getattr(obj, fc.data_path)[fc.array_index] = ((self.percentage() * (nxt - prv)) / 100) + prv
'''# debug print value dic
import pprint
print('\nDIC print: ')
pprint.pprint(self.obdic)
'''
if not found:
self.report({'ERROR'}, "No key pairs to breakdown found ! need to be between a key pair")#
return {'CANCELLED'}
if found == same:
self.report({'ERROR'}, "All Key pairs found have same values")#
return {'CANCELLED'}
## Starts the modal
context.window.cursor_set("SCROLL_X")
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
### --- KEYMAP ---
breakdowner_addon_keymaps = []
def register_keymaps():
# pref = get_addon_prefs()
# if not pref.breakdowner_use_shortcut:
# return
addon = bpy.context.window_manager.keyconfigs.addon
try:
km = bpy.context.window_manager.keyconfigs.addon.keymaps["3D View"]
except Exception as e:
km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")
pass
ops_id = 'object.breakdown_anim'
if ops_id not in km.keymap_items:
km = addon.keymaps.new(name='3D View', space_type='VIEW_3D')#EMPTY
kmi = km.keymap_items.new(ops_id, type="E", value="PRESS", shift=True)
breakdowner_addon_keymaps.append((km, kmi))
def unregister_keymaps():
for km, kmi in breakdowner_addon_keymaps:
km.keymap_items.remove(kmi)
breakdowner_addon_keymaps.clear()
# del breakdowner_addon_keymaps[:]
### --- REGISTER ---
def register():
if not bpy.app.background:
bpy.utils.register_class(OBJ_OT_breakdown_obj_anim)
register_keymaps()
def unregister():
if not bpy.app.background:
unregister_keymaps()
bpy.utils.unregister_class(OBJ_OT_breakdown_obj_anim)
if __name__ == "__main__":
register()

284
OP_canvas_rotate.py Normal file
View File

@ -0,0 +1,284 @@
from .utils import get_addon_prefs
## known issue: auto-perspective mess up when triggered out after rotation
import bpy
import math
import mathutils
from bpy_extras.view3d_utils import location_3d_to_region_2d
## draw utils
import gpu
import bgl
import blf
from gpu_extras.batch import batch_for_shader
from gpu_extras.presets import draw_circle_2d
"""
Notes:
Samuel.B:
OpenGL drawing can be disabled by passing self.hud to False in invoke (mainly used for debugging)
Base script by Jum, simplified and modified to work in both view and camera with rotate axis method suggested by Christophe Seux
Jum:
Script base. Thanks to bigLarry and Jum
https://blender.stackexchange.com/questions/136183/rotating-camera-view-in-grease-pencil-draw-mode-in-blender-2-8
"""
def draw_callback_px(self, context):
# 50% alpha, 2 pixel width line
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
bgl.glEnable(bgl.GL_BLEND)
bgl.glLineWidth(2)
# init
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": [self.center, self.initial_pos]})#self.vector_initial
shader.bind()
shader.uniform_float("color", (0.5, 0.5, 0.8, 0.6))
batch.draw(shader)
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": [self.center, self.pos_current]})
shader.bind()
shader.uniform_float("color", (0.3, 0.7, 0.2, 0.5))
batch.draw(shader)
## vector init vector current (substracted by center)
# batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": [self.vector_initial, self.vector_current]})
# shader.bind()
# shader.uniform_float("color", (0.5, 0.5, 0.5, 0.5))
# batch.draw(shader)
# restore opengl defaults
bgl.glLineWidth(1)
bgl.glDisable(bgl.GL_BLEND)
## text
font_id = 0
## draw text debug infos
blf.position(font_id, 15, 30, 0)
blf.size(font_id, 20, 72)
blf.draw(font_id, f'angle: {math.degrees(self.angle):.1f}')
class RC_OT_RotateCanvas(bpy.types.Operator):
bl_idname = 'view3d.rotate_canvas'
bl_label = 'Rotate Canvas'
bl_options = {"REGISTER", "UNDO"}
# @classmethod
# def poll(cls, context):
# return context.region_data.view_perspective == 'CAMERA'
"""
def get_center_view(self, area, cam):
'''
https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border
Thanks to ideasman42
'''
region_3d = area.spaces[0].region_3d
for region in area.regions:
if region.type == 'WINDOW':
frame = cam.data.view_frame()
# if cam.parent:
# mat = cam.matrix_parent_inverse @ cam.matrix_world
# # mat = cam.parent.matrix_world @ cam.matrix_world# not inverse from parent
# else:
# mat = cam.matrix_world
mat = cam.matrix_world
frame = [mat @ v for v in frame]
## bpy.context.scene.cursor.location = frame[1]#DEBUG
frame_px = [location_3d_to_region_2d(region, region_3d, v) for v in frame]
center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2
center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2
return mathutils.Vector((center_x, center_y))
return None """
def get_center_view(self, context, cam):
'''
https://blender.stackexchange.com/questions/6377/coordinates-of-corners-of-camera-view-border
Thanks to ideasman42
'''
frame = cam.data.view_frame()
mat = cam.matrix_world
frame = [mat @ v for v in frame]
frame_px = [location_3d_to_region_2d(context.region, context.space_data.region_3d, v) for v in frame]
center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2
center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2
return mathutils.Vector((center_x, center_y))
def execute(self, context):
if self.hud:
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
context.area.tag_redraw()
if self.in_cam:
self.cam.rotation_mode = self.org_rotation_mode
return {'FINISHED'}
def modal(self, context, event):
if event.type in {'MOUSEMOVE','INBETWEEN_MOUSEMOVE'}:
# Get current mouse coordination (region)
self.pos_current = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
# Get current vector
self.vector_current = (self.pos_current - self.center).normalized()
# Calculates the angle between initial and current vectors
self.angle = self.vector_initial.angle_signed(self.vector_current)#radian
# print (math.degrees(self.angle), self.vector_initial, self.vector_current)
if self.in_cam:
self.cam.matrix_world = self.cam_matrix
# self.cam.rotation_euler = self.cam_init_euler
self.cam.rotation_euler.rotate_axis("Z", self.angle)
else:#free view
context.space_data.region_3d.view_matrix = self.view_matrix
rot = context.space_data.region_3d.view_rotation
rot = rot.to_euler()
rot.rotate_axis("Z", self.angle)
context.space_data.region_3d.view_rotation = rot.to_quaternion()
if event.type in {'RIGHTMOUSE', 'LEFTMOUSE', 'MIDDLEMOUSE'} and event.value == 'RELEASE':
self.execute(context)
return {'FINISHED'}
if event.type == 'ESC':#Cancel
self.execute(context)
if self.in_cam:
self.cam.matrix_world = self.cam_matrix
else:
context.space_data.region_3d.view_matrix = self.view_matrix
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
self.hud = False
self.angle = 0.0# for draw degub, else not needed
self.in_cam = context.region_data.view_perspective == 'CAMERA'
if self.in_cam:
# Get camera from scene
self.cam = bpy.context.scene.camera
## avoid manipulating real cam or locked cams
if not 'manip_cams' in [c.name for c in self.cam.users_collection]:
self.report({'WARNING'}, 'Not in manipulation cam (draw/obj cam)')
return {'CANCELLED'}
if self.cam.lock_rotation[:] != (False, False, False):
self.report({'WARNING'}, 'Camera rotation is locked')
return {'CANCELLED'}
self.center = self.get_center_view(context, self.cam)
# store original rotation mode
self.org_rotation_mode = self.cam.rotation_mode
# set to euler to works with quaternions, restored at finish
self.cam.rotation_mode = 'XYZ'
# store camera matrix world
self.cam_matrix = self.cam.matrix_world.copy()
# self.cam_init_euler = self.cam.rotation_euler.copy()
else:
self.center = mathutils.Vector((context.area.width/2, context.area.height/2))
# store current view matrix
self.view_matrix = context.space_data.region_3d.view_matrix.copy()
# Get current mouse coordination
self.pos_current = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
self.initial_pos = self.pos_current# for draw debug, else no need
# Calculate inital vector
self.vector_initial = self.pos_current - self.center
self.vector_initial.normalize()
# Initializes the current vector with the same initial vector.
self.vector_current = self.vector_initial.copy()
args = (self, context)
if self.hud:
self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
class PREFS_OT_rebind(bpy.types.Operator):
"""Rebind shortcuts canvas rotate shortcuts"""
bl_idname = "prefs.rebind_shortcut"
bl_label = "Rebind canvas rotate shortcut"
bl_options = {'REGISTER', 'INTERNAL'}
def execute(self, context):
unregister_keymaps()
register_keymaps()
return{'FINISHED'}
addon_keymaps = []
def register_keymaps():
pref = get_addon_prefs()
if not pref.canvas_use_shortcut:
return
addon = bpy.context.window_manager.keyconfigs.addon
""" ## NATIVE FREENAV BIND (left to right)
km = bpy.context.window_manager.keyconfigs.addon.keymaps.get("3D View")
if not km:
km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")
# print('BINDING CANVAS ROTATE KEYMAPS')#Dbg
if 'view3d.view_roll' not in km.keymap_items:
# print('creating view3d.view_roll')#Dbg
# kmi = km.keymap_items.new("view3d.view_roll", type = 'MIDDLEMOUSE', value = "PRESS", ctrl=True, shift=False, alt=True)#PRESS#CLICK_DRAG
kmi = km.keymap_items.new("view3d.view_roll", type=pref.mouse_click, value = "PRESS", alt=pref.use_alt, ctrl=pref.use_ctrl, shift=pref.use_shift, any=False)#PRESS#CLICK_DRAG
kmi.properties.type = 'ANGLE'
addon_keymaps.append(km)
"""
km = bpy.context.window_manager.keyconfigs.addon.keymaps.get("3D View")
if not km:
km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")
if 'view3d.rotate_canvas' not in km.keymap_items:
# print('creating view3d.rotate_canvas')#Dbg
## keymap to operator cam space (in grease pencil mode only ?)
km = addon.keymaps.new(name='3D View', space_type='VIEW_3D')#EMPTY #Grease Pencil
# kmi = km.keymap_items.new('view3d.rotate_canvas', 'MIDDLEMOUSE', 'PRESS', ctrl=True, shift=False, alt=True)
kmi = km.keymap_items.new('view3d.rotate_canvas', type=pref.mouse_click, value="PRESS", alt=pref.use_alt, ctrl=pref.use_ctrl, shift=pref.use_shift, any=False)
addon_keymaps.append((km, kmi))
# print(addon_keymaps)
def unregister_keymaps():
# print('UNBIND CANVAS ROTATE KEYMAPS')#Dbg
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
# del addon_keymaps[:]
canvas_classes = (
PREFS_OT_rebind,
RC_OT_RotateCanvas,
# RC_OT_RotateCanvasFreeNav
)
def register():
if not bpy.app.background:
for cls in canvas_classes:
bpy.utils.register_class(cls)
register_keymaps()
# wm = bpy.context.window_manager
# km = wm.keyconfigs.addon.keymaps.new(name='Grease Pencil', space_type='EMPTY')
# kmi = km.keymap_items.new('view3d.rotate_canvas', 'MIDDLEMOUSE', 'PRESS', ctrl=True, shift=False, alt=True)
# addon_keymaps.append(km)
def unregister():
if not bpy.app.background:
unregister_keymaps()
for cls in reversed(canvas_classes):
bpy.utils.unregister_class(cls)
# if __name__ == "__main__":
# register()

709
OP_copy_paste.py Normal file
View File

@ -0,0 +1,709 @@
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
## basec on GPclipboard 1.3.1 (without addon prefs)
bl_info = {
"name": "GP clipboard",
"description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends",
"author": "Samuel Bernou",
"version": (1, 3, 1),
"blender": (2, 83, 0),
"location": "View3D > Toolbar > Gpencil > GP clipboard",
"warning": "",
"doc_url": "https://github.com/Pullusb/GP_clipboard",
"category": "Object" }
import bpy
import os
import mathutils
from mathutils import Vector
import json
from time import time
from operator import itemgetter
from itertools import groupby
# from pprint import pprint
def convertAttr(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)
def getMatrix (layer) :
matrix = mathutils.Matrix.Identity(4)
if layer.is_parented:
if layer.parent_type == 'BONE':
object = layer.parent
bone = object.pose.bones[layer.parent_bone]
matrix = bone.matrix @ object.matrix_world
matrix = matrix.copy() @ layer.matrix_inverse
else :
matrix = layer.parent.matrix_world @ layer.matrix_inverse
return matrix.copy()
def dump_gp_point(p, l, obj):
'''add properties of a given points to a dic and return it'''
pdic = {}
#point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type'
#for att in point_attr_list:
# pdic[att] = convertAttr(getattr(p, att))
if l.parent:
mat = getMatrix(l)
pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co'))
else:
pdic['co'] = convertAttr(obj.matrix_world @ getattr(p,'co'))
pdic['pressure'] = convertAttr(getattr(p,'pressure'))
# pdic['select'] = convertAttr(getattr(p,'select'))# need selection ?
pdic['strength'] = convertAttr(getattr(p,'strength'))
## get vertex color (long...)
if p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
pdic['vertex_color'] = convertAttr(getattr(p,'vertex_color'))
return pdic
def dump_gp_stroke_range(s, sid, l, obj):
'''Get a grease pencil stroke and return a dic with attribute
(points attribute being a dic of dics to store points and their attributes)
'''
sdic = {}
stroke_attr_list = ('line_width',) #'select'#read-only: 'triangles'
for att in stroke_attr_list:
sdic[att] = getattr(s, att)
## Dump following these value only if they are non default
if s.material_index != 0:
sdic['material_index'] = s.material_index
if s.draw_cyclic:
sdic['draw_cyclic'] = s.draw_cyclic
if s.uv_scale != 1.0:
sdic['uv_scale'] = s.uv_scale
if s.uv_rotation != 0.0:
sdic['uv_rotation'] = s.uv_rotation
if s.hardness != 1.0:
sdic['hardness'] = s.hardness
if s.uv_translation != Vector((0.0, 0.0)):
sdic['uv_translation'] = convertAttr(s.uv_translation)
if s.vertex_color_fill[:] != (0,0,0,0):
sdic['vertex_color_fill'] = convertAttr(s.vertex_color_fill)
points = []
if sid is None:#no ids, just full points...
for p in s.points:
points.append(dump_gp_point(p,l,obj))
else:
for pid in sid:
points.append(dump_gp_point(s.points[pid],l,obj))
sdic['points'] = points
return sdic
def copycut_strokes(layers=None, copy=True, keep_empty=True):# (mayber allow filter)
'''
copy all visibles selected strokes on active frame
layers can be None, a single layer object or list of layer object as filter
if keep_empty is False the frame is deleted when all strokes are cutted
'''
t0 = time()
### must iterate in all layers ! (since all layers are selectable / visible !)
obj = bpy.context.object
gp = obj.data
gpl = gp.layers
# if not color:#get active color name
# color = gp.palettes.active.colors.active.name
if not layers:
#by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock]#[]
if not isinstance(layers, list):
#if a single layer object is send put in a list
layers = [layers]
stroke_list = []#one stroke list for all layers.
for l in layers:
f = l.active_frame
if f:#active frame can be None
if not copy:
staylist = []#init part of strokes that must survive on this layer
for s in f.strokes:
if s.select:
# separate in multiple stroke if parts of the strokes a selected.
sel = [i for i, p in enumerate(s.points) if p.select]
substrokes = []# list of list containing isolated selection
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):# continuity stroke have same substract result between point index and enumerator
group = list(map(itemgetter(1), g))
substrokes.append(group)
for ss in substrokes:
if len(ss) > 1:#avoid copy isolated points
stroke_list.append(dump_gp_stroke_range(s,ss,l,obj))
#Cutting operation
if not copy:
maxindex = len(s.points)-1
if len(substrokes) == maxindex+1:#si un seul substroke, c'est le stroke entier
f.strokes.remove(s)
else:
neg = [i for i, p in enumerate(s.points) if not p.select]
staying = []
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
group = list(map(itemgetter(1), g))
#extend group to avoid gap when cut, a bit dirty
if group[0] > 0:
group.insert(0,group[0]-1)
if group[-1] < maxindex:
group.append(group[-1]+1)
staying.append(group)
for ns in staying:
if len(ns) > 1:
staylist.append(dump_gp_stroke_range(s,ns,l,obj))
#make a negative list containing all last index
'''#full stroke version
# if s.colorname == color: #line for future filters
stroke_list.append(dump_gp_stroke(s,l))
#delete stroke on the fly
if not copy:
f.strokes.remove(s)
'''
if not copy:
# delete all selected strokes...
for s in f.strokes:
if s.select:
f.strokes.remove(s)
# ...recreate these uncutted ones
#pprint(staylist)
if staylist:
add_multiple_strokes(staylist, l)
#for ns in staylist:#weirdly recreate the stroke twice !
# add_stroke(ns, f, l)
#if nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
if not copy and not keep_empty:#
if not len(f.strokes):
l.frames.remove(f)
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
#print(stroke_list)
return stroke_list
"""# Unused
def copy_all_strokes(layers=None):
'''
copy all stroke, not affected by selection on active frame
layers can be None, a single layer object or list of layer object as filter
if keep_empty is False the frame is deleted when all strokes are cutted
'''
t0 = time()
scene = bpy.context.scene
obj = bpy.context.object
gp = obj.data
gpl = gp.layers
if not layers:
# by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
if not isinstance(layers, list):
# if a single layer object is send put in a list
layers = [layers]
stroke_list = []# one stroke list for all layers.
for l in layers:
f = l.active_frame
if not f:
continue# active frame can be None
for s in f.strokes:
## full stroke version
# if s.select:
stroke_list.append(dump_gp_stroke_range(s, None, l, obj))
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
#print(stroke_list)
return stroke_list
"""
def copy_all_strokes_in_frame(frame=None, layers=None, obj=None):
'''
copy all stroke, not affected by selection on active frame
layers can be None, a single layer object or list of layer object as filter
if keep_empty is False the frame is deleted when all strokes are cutted
'''
t0 = time()
scene = bpy.context.scene
obj = bpy.context.object
gp = obj.data
gpl = gp.layers
if not frame or not obj:
return
if not layers:
# by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
if not isinstance(layers, list):
# if a single layer object is send put in a list
layers = [layers]
stroke_list = []
for l in layers:
f = l.active_frame
if not f:
continue# active frame can be None
for s in f.strokes:
## full stroke version
# if s.select:
# send index of all points to get the whole stroke with "range"
stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj) )
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
#print(stroke_list)
return stroke_list
def add_stroke(s, frame, layer, obj):
'''add stroke on a given frame, (layer is for parentage setting)'''
# print(3*'-',s)
ns = frame.strokes.new()
for att, val in s.items():
if att not in ('points'):
setattr(ns, att, val)
pts_to_add = len(s['points'])
# print(pts_to_add, 'points')#dbg
ns.points.add(pts_to_add)
ob_mat_inv = obj.matrix_world.inverted()
## patch pressure 1
# pressure_flat_list = [di['pressure'] for di in s['points']] #get all pressure flatened
if layer.is_parented:
mat = getMatrix(layer).inverted()
for i, pt in enumerate(s['points']):
for k, v in pt.items():
if k == 'co':
setattr(ns.points[i], k, v)
ns.points[i].co = ob_mat_inv @ mat @ ns.points[i].co# invert of object * invert of layer * coordinate
else:
setattr(ns.points[i], k, v)
else:
for i, pt in enumerate(s['points']):
for k, v in pt.items():
if k == 'co':
setattr(ns.points[i], k, v)
ns.points[i].co = ob_mat_inv @ ns.points[i].co# invert of object * coordinate
else:
setattr(ns.points[i], k, v)
## patch pressure 2
# ns.points.foreach_set('pressure', pressure_flat_list)
def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True):
'''
add a list of strokes to active frame of given layer
if no layer specified, active layer is used
if use_current_frame is True, a new frame will be created only if needed
'''
scene = bpy.context.scene
obj = bpy.context.object
gp = obj.data
gpl = gp.layers
#default: active
if not layer:
layer = gpl.active
fnum = scene.frame_current
target_frame = False
act = layer.active_frame
for s in stroke_list:
if act:
if use_current_frame or act.frame_number == fnum:
#work on current frame if exists
# use current frame anyway if one key exist at this scene.frame
target_frame = act
if not target_frame:
#no active frame
#or active exists but not aligned scene.current with use_current_frame disabled
target_frame = layer.frames.new(fnum)
add_stroke(s, target_frame, layer, obj)
'''
for s in stroke_data:
add_stroke(s, target_frame)
'''
print(len(stroke_list), 'strokes pasted')
### OPERATORS
class GPCLIP_OT_copy_strokes(bpy.types.Operator):
bl_idname = "gp.copy_strokes"
bl_label = "GP Copy strokes"
bl_description = "Copy strokes to str in paperclip"
bl_options = {"REGISTER"}
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def execute(self, context):
# if not context.object or not context.object.type == 'GPENCIL':
# self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"}
t0 = time()
#ct = check_pressure()
strokelist = copycut_strokes(copy=True, keep_empty=True)
if not strokelist:
self.report({'ERROR'},'rien a copier')
return {"CANCELLED"}
bpy.context.window_manager.clipboard = json.dumps(strokelist)#copy=self.copy
#if ct:
# self.report({'ERROR'}, "Copie OK\n{} points ont une épaisseur supérieure a 1.0 (max = {:.2f})\nCes épaisseurs seront plafonnées à 1 au 'coller'".format(ct[0], ct[1]))
self.report({'INFO'}, f'Copied (time : {time() - t0:.4f})')
# print('copy total time:', time() - t0)
return {"FINISHED"}
class GPCLIP_OT_cut_strokes(bpy.types.Operator):
bl_idname = "gp.cut_strokes"
bl_label = "GP Cut strokes"
bl_description = "Cut strokes to str in paperclip"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def execute(self, context):
# if not context.object or not context.object.type == 'GPENCIL':
# self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"}
t0 = time()
strokelist = copycut_strokes(copy=False, keep_empty=True)#ct = check_pressure()
if not strokelist:
self.report({'ERROR'},'Nothing to cut')
return {"CANCELLED"}
bpy.context.window_manager.clipboard = json.dumps(strokelist)
self.report({'INFO'}, f'Cutted (time : {time() - t0:.4f})')
return {"FINISHED"}
class GPCLIP_OT_paste_strokes(bpy.types.Operator):
bl_idname = "gp.paste_strokes"
bl_label = "GP Paste strokes"
bl_description = "paste stroke from paperclip"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def execute(self, context):
# if not context.object or not context.object.type == 'GPENCIL':
# self.report({'ERROR'},'No GP object selected to paste on')
# return {"CANCELLED"}
t0 = time()
#add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
try:
data = json.loads(bpy.context.window_manager.clipboard)
except:
mess = 'Clipboard does not contain drawing data (load error)'
self.report({'ERROR'}, mess)
return {"CANCELLED"}
print('data loaded', time() - t0)
add_multiple_strokes(data, use_current_frame=True)
print('total_time', time() - t0)
return {"FINISHED"}
### --- multi copy
class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
bl_idname = "gp.copy_multi_strokes"
bl_label = "GP Copy multi strokes"
bl_description = "Copy multiple layers>frames>strokes (unlocked and unhided ones) to str in paperclip"
bl_options = {"REGISTER"}
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def execute(self, context):
bake_moves = True
skip_empty_frame = False
org_frame = context.scene.frame_current
obj = context.object
gpl = obj.data.layers
t0 = time()
#ct = check_pressure()
layerdic = {}
layerpool = [l for l in gpl if not l.hide and l.select]# and not l.lock
if not layerpool:
self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
return {"CANCELLED"}
if not bake_moves:# copy only drawed frames as is.
for l in layerpool:
if not l.frames:
continue# skip empty layers
frame_dic = {}
for f in l.frames:
if skip_empty_frame and not len(f.strokes):
continue
context.scene.frame_set(f.frame_number)#use matrix of this frame
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj)
frame_dic[f.frame_number] = strokelist
layerdic[l.info] = frame_dic
else:# bake position: copy frame where object as moved even if frame is unchanged
for l in layerpool:
if not l.frames:
continue# skip empty layers
frame_dic = {}
fnums_dic = {f.frame_number: f for f in l.frames}
context.scene.frame_set(context.scene.frame_start)
curmat = prevmat = obj.matrix_world.copy()
for i in range(context.scene.frame_start, context.scene.frame_end):
context.scene.frame_set(i)#use matrix of this frame
curmat = obj.matrix_world.copy()
# if object has moved or current time is on a draw key
if prevmat != curmat or i in fnums_dic.keys():
# get the current used frame
for j in fnums_dic.keys():
if j >= i:
f = fnums_dic[j]
break
## skip empty frame if specified
if skip_empty_frame and not len(f.strokes):
continue
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj)
frame_dic[i] = strokelist
prevmat = curmat
layerdic[l.info] = frame_dic
## All to clipboard manager
bpy.context.window_manager.clipboard = json.dumps(layerdic)
# reset original frame.
context.scene.frame_set(org_frame)
self.report({'INFO'}, f'Copied layers (time : {time() - t0:.4f})')
# print('copy total time:', time() - t0)
return {"FINISHED"}
class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
bl_idname = "gp.paste_multi_strokes"
bl_label = "GP paste multi strokes"
bl_description = "Paste multiple layers>frames>strokes from paperclip"
bl_options = {"REGISTER"}
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def execute(self, context):
org_frame = context.scene.frame_current
obj = context.object
gpl = obj.data.layers
t0 = time()
#add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
try:
data = json.loads(bpy.context.window_manager.clipboard)
except:
mess = 'Clipboard does not contain drawing data (load error)'
self.report({'ERROR'}, mess)
return {"CANCELLED"}
print('data loaded', time() - t0)
# add layers (or merge with existing names ?)
### structure
# {layername :
# {1: [strokelist of frame 1], 3: [strokelist of frame 3]}
# }
for layname, allframes in data.items():
layer = gpl.get(layname)
if not layer:
layer = gpl.new(layname)
for fnum, fstrokes in allframes.items():
context.scene.frame_set(int(fnum))#use matrix of this frame for copying (maybe just evaluate depsgraph for object
add_multiple_strokes(fstrokes, use_current_frame=False)#create a new frame at each encoutered
print('total_time', time() - t0)
# reset original frame.
context.scene.frame_set(org_frame)
self.report({'INFO'}, f'Copied layers (time : {time() - t0:.4f})')
# print('copy total time:', time() - t0)
return {"FINISHED"}
##--PANEL
class GPCLIP_PT_clipboard_ui(bpy.types.Panel):
# bl_idname = "gp_clipboard_panel"
bl_label = "GP Clipboard"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.operator('gp.copy_strokes', text='Copy strokes', icon='COPYDOWN')
row.operator('gp.cut_strokes', text='Cut strokes', icon='PASTEFLIPUP')
layout.operator('gp.paste_strokes', text='Paste strokes', icon='PASTEDOWN')
layout.separator()
layout.operator('gp.copy_multi_strokes', text='Copy layers', icon='COPYDOWN')
layout.operator('gp.paste_multi_strokes', text='Paste layers', icon='PASTEDOWN')
###---TEST zone
"""
##main defs
def copy_strokes_to_paperclip():
bpy.context.window_manager.clipboard = json.dumps(copycut_strokes(copy=True, keep_empty=True))#default layers are visible one
def cut_strokes_to_paperclip():
bpy.context.window_manager.clipboard = json.dumps(copycut_strokes(copy=False, keep_empty=True))
def paste_strokes_from_paperclip():
#add condition to detect if clipboard contains loadable values
add_multiple_strokes(json.loads(bpy.context.window_manager.clipboard), use_current_frame=True)#layer= layers.active
#copy_strokes_to_paperclip()
#paste_strokes_from_paperclip()
#test direct
#li = copycut_strokes(copy=True)
#add_multiple_strokes(li, bpy.context.scene.grease_pencil.layers['correct'])
"""
#use directly operator idname in shortcut settings :
# gp.copy_strokes
# gp.cut_strokes
# gp.paste_strokes
# gp.copy_multi_strokes
# gp.paste_multi_strokes
###---REGISTER + copy cut paste keymapping
addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY", region_type='WINDOW')# in Grease context
# km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")# in 3D context
# km = addon.keymaps.new(name = "Window", space_type = "EMPTY")# from everywhere
kmi = km.keymap_items.new("gp.copy_strokes", type = "C", value = "PRESS", ctrl=True, shift=True)
kmi.repeat = False
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("gp.cut_strokes", type = "X", value = "PRESS", ctrl=True, shift=True)
kmi.repeat = False
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("gp.paste_strokes", type = "V", value = "PRESS", ctrl=True, shift=True)
kmi.repeat = False
addon_keymaps.append((km, kmi))
def unregister_keymaps():
# wm = bpy.context.window_manager
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
# wm.keyconfigs.addon.keymaps.remove(km)
addon_keymaps.clear()
classes = (
GPCLIP_OT_copy_strokes,
GPCLIP_OT_cut_strokes,
GPCLIP_OT_paste_strokes,
GPCLIP_OT_copy_multi_strokes,
GPCLIP_OT_paste_multi_strokes,
GPCLIP_PT_clipboard_ui,
)
def register():
for cl in classes:
bpy.utils.register_class(cl)
## make scene property for empty key preservation and bake movement for layers...
register_keymaps()
def unregister():
unregister_keymaps()
for cl in reversed(classes):
bpy.utils.unregister_class(cl)
if __name__ == "__main__":
register()

182
OP_cursor_snap_canvas.py Normal file
View File

@ -0,0 +1,182 @@
## snap 3D cursor on active grease pencil object canvas surfaces
import bpy
import mathutils
from bpy_extras import view3d_utils
from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position
## override all sursor snap shortcut with this in keymap
class GPTB_OT_cusor_snap(bpy.types.Operator):
bl_idname = "view3d.cusor_snap"
bl_label = "Snap cursor to GP"
bl_description = "Snap 3d cursor to active GP object canvas (else use normal place)"
bl_options = {"REGISTER"}#, "INTERNAL"
# @classmethod
# def poll(cls, context):
# return context.object and context.object.type == 'GPENCIL'
def invoke(self, context, event):
#print('-!SNAP!-')
self.mouse_co = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
# print('self.mouse_co: ', self.mouse_co)
self.execute(context)
return {"FINISHED"}
def execute(self, context):
if not context.object or context.object.type != 'GPENCIL':
self.report({'INFO'}, 'Not GP, Cursor surface project')
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
return {"FINISHED"}
if context.region_data.view_perspective == 'ORTHO':
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
self.report({'WARNING'}, 'Ortholinear ! not snaped to GP plane Cursor surface project)')
return {"FINISHED"}
self.report({'INFO'}, 'Using GP picking')
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'
warning = []
if not "AXIS" in orient:
warning.append(f'Orientation is {orient}, no depth picking')
if loc != "ORIGIN":
warning.append(f"Location is '{loc}' not object 'ORIGIN'")
if warning:
self.report({'WARNING'}, ', '.join(warning))
plane_co, plane_no = get_gp_draw_plane(context)
if not plane_co:#default to object location
plane_co = context.object.matrix_world.to_translation()#context.object.location
if not plane_no:# use view depth (region_to_location instead of )
coord = region_to_location(self.mouse_co, plane_co)
else:
#projected on given plane from view (intersect on plane with a vector from view origin)
origin = get_view_origin_position()#get view origin
region = bpy.context.region
rv3d = bpy.context.region_data
coord = mathutils.geometry.intersect_line_plane(origin, origin - view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse_co), plane_co, plane_no)
# If no plane is crossed, intersect_line_plane return None which naturally goes to traceback...
if not coord:
self.report({'WARNING'}, 'Ortholinear view, used basic cursor snap (no depth picking)')
context.scene.cursor.location = coord
return {"FINISHED"}
#TODO auto-cursor (attach cursor to object)
''' cursor native snap
https://docs.blender.org/api/current/bpy.ops.view3d.html#bpy.ops.view3d.cursor3d
bpy.ops.view3d.cursor3d(use_depth=True, orientation='VIEW')
Set the location of the 3D cursor
Parameters
use_depth (boolean, (optional)) Surface Project, Project onto the surface
orientation (enum in ['NONE', 'VIEW', 'XFORM', 'GEOM'], (optional))
Orientation, Preset viewpoint to use
NONE None, Leave orientation unchanged.
VIEW View, Orient to the viewport.
XFORM Transform, Orient to the current transform setting.
GEOM Geometry, Match the surface normal.
'''
def swap_keymap_by_id(org_idname, new_idname):
'''Replace id operator by another in user keymap'''
wm = bpy.context.window_manager
for cat, keymap in wm.keyconfigs.user.keymaps.items():#wm.keyconfigs.addon.keymaps.items():
for k in keymap.keymap_items:
if k.idname != org_idname:
continue
## Print changes
mods = ' + '.join([m for m in ('ctrl','shift','alt') if getattr(k, m)])
val = f' ({k.value.lower()})' if k.value != 'PRESS' else ''
# ({keymap.space_type}) #VIEW_3D
print(f"Hotswap: {cat} - {k.name}: {mods + ' ' if mods else ''}{k.type}{val} : {k.idname} --> {new_idname}")
k.idname = new_idname
# prev_matrix = mathutils.Matrix()
prev_matrix = None
# @call_once(bpy.app.handlers.frame_change_post)
def cursor_follow_update(self,context):
'''append or remove cursor_follow handler according a boolean'''
global prev_matrix
# imported in properties to register in boolprop update
if self.cursor_follow:#True
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
if context.object:
prev_matrix = context.object.matrix_world
bpy.app.handlers.frame_change_post.append(cursor_follow)
else:#False
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
prev_matrix = None
bpy.app.handlers.frame_change_post.remove(cursor_follow)
def cursor_follow(scene):
'''Handler to make the cursor follow active object matrix changes on frame change'''
## TODO update global prev_matrix to equal current_matrix on selection change (need another handler)...
if not bpy.context.object:
return
global prev_matrix
ob = bpy.context.object
current_matrix = ob.matrix_world
if not prev_matrix:
prev_matrix = current_matrix.copy()
return
# debug prints : HANDLER CALLED TWICE in time line when clic (clic press, and clic release)!!!
# print(scene.frame_current)
# print('prev: ', [[f'{j:.2f}' for j in i] for i in prev_matrix[:2] ])
# print('curr: ', [[f'{j:.2f}' for j in i] for i in current_matrix[:2] ])
## translation only
# scene.cursor.location += (current_matrix - prev_matrix).to_translation()
# print('offset:', (current_matrix - prev_matrix).to_translation())
## full
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
# store for next use
prev_matrix = current_matrix.copy()
classes = (
GPTB_OT_cusor_snap,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
# bpy.app.handlers.frame_change_post.append(cursor_follow)
def unregister():
# bpy.app.handlers.frame_change_post.remove(cursor_follow)
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
# force remove handler if it's there at unregister
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
bpy.app.handlers.frame_change_post.remove(cursor_follow)

300
OP_file_checker.py Normal file
View File

@ -0,0 +1,300 @@
import bpy
import os
from pathlib import Path
from .utils import show_message_box, get_addon_prefs
class GPTB_OT_file_checker(bpy.types.Operator):
bl_idname = "gp.file_checker"
bl_label = "File check"
bl_description = "Check / correct some aspect of the file, properties and such and report"
bl_options = {"REGISTER"}
# @classmethod
# def poll(cls, context):
# return context.region_data.view_perspective == 'CAMERA'
## list of action :
# Lock main cam:
# set scene res
# set scene percentage at 100:
# set show slider and sync range
# set fps
# set cursor type
# GP use additive drawing (else creating a frame in dopesheet makes it blank...)
# GP stroke placement/projection check
# Disabled animation
# Set onion skin filter to 'All type'
def execute(self, context):
prefs = get_addon_prefs()
problems = []
## Lock main cam:
if not 'layout' in Path(bpy.data.filepath).stem:#dont touch layout cameras
if context.scene.camera:
cam = context.scene.camera
if cam.name == 'draw_cam' and cam.parent:
if cam.parent.type == 'CAMERA':
cam = cam.parent
else:
cam = None
if cam:
triple = (True,True,True)
if cam.lock_location[:] != triple or cam.lock_rotation[:] != triple:
problems.append('Lock main camera')
cam.lock_location = cam.lock_rotation = triple
## set scene res at pref res according to addon pref
rx, ry = prefs.render_res_x, prefs.render_res_y
if context.scene.render.resolution_x != rx or context.scene.render.resolution_y != ry:
problems.append(f'Resolution {context.scene.render.resolution_x}x{context.scene.render.resolution_y} >> {rx}x{ry}')
context.scene.render.resolution_x, context.scene.render.resolution_y = rx, ry
## set scene percentage at 100:
if context.scene.render.resolution_percentage != 100:
problems.append('Resolution output to 100%')
context.scene.render.resolution_percentage = 100
## set show slider and sync range
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'DOPESHEET_EDITOR':
if hasattr(area.spaces[0], 'show_sliders'):
setattr(area.spaces[0], 'show_sliders', True)
if hasattr(area.spaces[0], 'show_locked_time'):
setattr(area.spaces[0], 'show_locked_time', True)
## set fps according to preferences settings
if context.scene.render.fps != prefs.fps:
problems.append( (f"framerate corrected {context.scene.render.fps} >> {prefs.fps}", 'ERROR') )
context.scene.render.fps = prefs.fps
## set cursor type (according to prefs ?)
if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
tool = prefs.select_active_tool
if tool != 'none':
if bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname != tool:
bpy.ops.wm.tool_set_by_id(name=tool)# Tweaktoolcode
problems.append(f'tool changed to {tool.split(".")[1]}')
## GP use additive drawing (else creating a frame in dopesheet makes it blank...)
if not context.scene.tool_settings.use_gpencil_draw_additive:
problems.append(f'Activated Gp additive drawing mode (snowflake)')
context.scene.tool_settings.use_gpencil_draw_additive = True
## GP stroke placement/projection check
if context.scene.tool_settings.gpencil_sculpt.lock_axis != 'AXIS_Y':
problems.append('/!\\ Draw axis not "Front" (Need Manual change if not Ok)')
if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN':
problems.append('/!\\ Draw placement not "Origin" (Need Manual change if not Ok)')
## Disabled animation
fcu_ct = 0
for act in bpy.data.actions:
if not act.users:
continue
for fcu in act.fcurves:
if fcu.mute:
fcu_ct += 1
print(f"muted: {act.name} > {fcu.data_path}")
if fcu_ct:
problems.append(f'{fcu_ct} anim channel disabled (details -> console)')
## Set onion skin filter to 'All type'
fix_kf_type = 0
for gp in bpy.data.grease_pencils:#from data
if not gp.is_annotation:
if gp.onion_keyframe_type != 'ALL':
gp.onion_keyframe_type = 'ALL'
fix_kf_type += 1
if fix_kf_type:
problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
# for ob in context.scene.objects:#from object
# if ob.type == 'GPENCIL':
# ob.data.onion_keyframe_type = 'ALL'
#### --- print fix/problems report
if problems:
print('===File check===')
for p in problems:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
show_message_box(problems, _title = "Changed Settings", _icon = 'INFO')
else:
self.report({'INFO'}, 'All good')
return {"FINISHED"}
""" OLD links checker with show_message_box
class GPTB_OT_links_checker(bpy.types.Operator):
bl_idname = "gp.links_checker"
bl_label = "Links check"
bl_description = "Check states of file direct links"
bl_options = {"REGISTER"}
def execute(self, context):
all_lnks = []
has_broken_link = False
## check for broken links
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
lfp = Path(lib)
realib = Path(current)
if not lfp.exists():
has_broken_link = True
all_lnks.append( (f"Broken link: {realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
else:
if realib.as_posix().startswith('//'):
all_lnks.append( (f"Link: {realib.as_posix()}", 'LINKED') )#lfp.as_posix()
else:
all_lnks.append( (f"Link: {realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
all_lnks.sort(key=lambda x: x[1], reverse=True)
if all_lnks:
print('===File check===')
for p in all_lnks:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
show_message_box(all_lnks, _title = "Links", _icon = 'INFO')
return {"FINISHED"} """
class GPTB_OT_links_checker(bpy.types.Operator):
bl_idname = "gp.links_checker"
bl_label = "Links check"
bl_description = "Check states of file direct links"
bl_options = {"REGISTER"}
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.label(text=self.title)
if self.broke_ct:
layout.label(text="You can try to scan for missing files:")
## How to launch directly without filebrowser ?
# in Shot folder
layout.operator('file.find_missing_files', text='in parent hierarchy').directory = Path(bpy.data.filepath).parents[1].as_posix()
if self.proj:
# In Library
layout.operator('file.find_missing_files', text='in library').directory = (Path(self.proj)/'library').as_posix()
# In all project
layout.operator('file.find_missing_files', text='in all project (last resort)').directory = self.proj
layout.separator()
for l in self.all_lnks:
if l[1] == 'LIBRARY_DATA_BROKEN':
layout.label(text=l[0], icon=l[1])
else:
split=layout.split(factor=0.75)
split.label(text=l[0], icon=l[1])
split.operator('wm.path_open', text='Open folder', icon='FILE_FOLDER').filepath = Path(bpy.path.abspath(l[0])).resolve().parent.as_posix()
split.operator('wm.path_open', text='Open file', icon='FILE_TICK').filepath = Path(bpy.path.abspath(l[0])).resolve().as_posix()#os.path.abspath(bpy.path.abspath(dirname(l[0])))
def invoke(self, context, event):
self.all_lnks = []
self.title = ''
self.broke_ct = 0
abs_ct = 0
rel_ct = 0
## check for broken links
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
lfp = Path(lib)
realib = Path(current)
if not lfp.exists():
self.broke_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
else:
if realib.as_posix().startswith('//'):
rel_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )#lfp.as_posix()
else:
abs_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
if not self.all_lnks:
self.report({'INFO'}, 'No external links in files')
return {"FINISHED"}
bct = f"{self.broke_ct} broken " if self.broke_ct else ''
act = f"{abs_ct} absolute " if abs_ct else ''
rct = f"{rel_ct} clean " if rel_ct else ''
self.title = f"{bct}{act}{rct}"
self.all_lnks.sort(key=lambda x: x[1], reverse=True)
if self.all_lnks:
print('===File check===')
for p in self.all_lnks:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
# if broke_ct == 0:
# show_message_box(self.all_lnks, _title = self.title, _icon = 'INFO')# Links
# return {"FINISHED"}
try:
self.proj = context.preferences.addons['pipe_sync'].preferences['local_folder']
except:
self.proj = None
return context.window_manager.invoke_props_dialog(self, width=800)
'''### OLD
class GPTB_OT_check_scene(bpy.types.Operator):
bl_idname = "gp.scene_check"
bl_label = "Check GP scene"
bl_description = "Check and fix scene settings"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
## check scene resolution / 100% / framerate
context.scene.render.resolution_percentage = 100
context.scene.render.resolution_x = 3072# define addon properties to make generic ?
context.scene.render.resolution_y = 1620# define addon properties to make generic ?
context.scene.render.fps = 24# define addon properties to make generic ?
## check GP datas name
gp_os = [o for o in context.scene.objects if o.type == 'GPENCIL' if o.data.users == 1]#no multiple users
for gpo in gp_os:
if gpo.data.name.startswith('Stroke'):# dont touch already renamed group
if gpo.data.name != gpo.name:
print('renaming GP data:', gpo.data.name, '-->', gpo.name)
gpo.data.name = gpo.name
## disable autolock
context.scene.tool_settings.lock_object_mode = False
return {"FINISHED"}
'''
classes = (
# GPTB_OT_check_scene,
GPTB_OT_file_checker,
GPTB_OT_links_checker,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

546
OP_helpers.py Normal file
View File

@ -0,0 +1,546 @@
import bpy
from mathutils import Vector#, Matrix
from pathlib import Path
from math import radians
from .utils import get_gp_objects, set_collection, show_message_box
class GPTB_OT_copy_text(bpy.types.Operator):
bl_idname = "wm.copytext"
bl_label = "Copy to clipboard"
bl_description = "Insert passed text to clipboard"
bl_options = {"REGISTER", "INTERNAL"}
text : bpy.props.StringProperty(name="cliptext", description="text to clip", default="")
def execute(self, context):
context.window_manager.clipboard = self.text
mess = f'Clipboard: {context.window_manager.clipboard}'
self.report({'INFO'}, mess)
return {"FINISHED"}
class GPTB_OT_flipx_view(bpy.types.Operator):
bl_idname = "gp.mirror_flipx"
bl_label = "cam mirror flipx"
bl_description = "Invert X scale on camera to flip image horizontally"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.region_data.view_perspective == 'CAMERA'
def execute(self, context):
context.scene.camera.scale.x *= -1
return {"FINISHED"}
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
bl_idname = "screen.gp_keyframe_jump"
bl_label = "Jump to GPencil keyframe"
bl_description = "Jump to prev/next keyframe on active and selected layers of active grease pencil object"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
next : bpy.props.BoolProperty(
name="Next GP keyframe", description="Go to next active GP keyframe", default=True)
target : bpy.props.EnumProperty(
name="Target layer", description="Choose wich layer to evaluate for keyframe change", default='ACTIVE',# options={'ANIMATABLE'}, update=None, get=None, set=None,
items=(
('ACTIVE', 'Active and selected', 'jump in keyframes of active and other selected layers ', 0),
('VISIBLE', 'Visibles layers', 'jump in keyframes of visibles layers', 1),
('ACCESSIBLE', 'Visible and unlocked layers', 'jump in keyframe of all layers', 2),
))
#(key, label, descr, id[, icon])
def execute(self, context):
if not context.object.data.layers.active:
self.report({'ERROR'}, 'No active layer on current GPencil object')
return {"CANCELLED"}
layer = []
if self.target == 'ACTIVE':
gpl = [l for l in context.object.data.layers if l.select and not l.hide]
if not context.object.data.layers.active in gpl:
gpl.append(context.object.data.layers.active)
elif self.target == 'VISIBLE':
gpl = [l for l in context.object.data.layers if not l.hide]
elif self.target == 'ACCESSIBLE':
gpl = [l for l in context.object.data.layers if not l.hide and not l.lock]
current = context.scene.frame_current
p = n = None
mins = []
maxs = []
for l in gpl:
for f in l.frames:
if f.frame_number < current:
p = f.frame_number
if f.frame_number > current:
n = f.frame_number
break
mins.append(p)
maxs.append(n)
p = n = None
mins = [i for i in mins if i is not None]
maxs = [i for i in maxs if i is not None]
if mins:
p = max(mins)
if maxs:
n = min(maxs)
if self.next and n is not None:
context.scene.frame_set(n)
elif not self.next and p is not None:
context.scene.frame_set(p)
else:
self.report({'INFO'}, 'No keyframe in this direction')
return {"CANCELLED"}
return {"FINISHED"}
class GPTB_OT_rename_data_from_obj(bpy.types.Operator):
bl_idname = "gp.rename_data_from_obj"
bl_label = "Rename GP from object"
bl_description = "Rename the GP datablock with the same name as the object"
bl_options = {"REGISTER"}
rename_all : bpy.props.BoolProperty(default=False)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def execute(self, context):
if not self.rename_all:
obj = context.object
if obj.name == obj.data.name:
self.report({'WARNING'}, 'Nothing to rename')
return {"FINISHED"}
old = obj.data.name
obj.data.name = obj.name
self.report({'INFO'}, f'GP data renamed: {old} -> {obj.data.name}')
else:
oblist = []
for o in context.scene.objects:
if o.type == 'GPENCIL':
if o.name == o.data.name:
continue
oblist.append(f'{o.data.name} -> {o.name}')
o.data.name = o.name
print('\nrenamed GP datablock:')
for i in oblist:
print(i)
self.report({'INFO'}, f'{len(oblist)} data renamed (see console for detail)')
return {"FINISHED"}
# TODO make secondary cam
# 2 solution :
# - parenting to main cam except for roll axis (drivers or simple parents)
# - Facing current "peg" (object) and parented to it to keep distance
# --> reset roll means aligning to object again (to main camera if one) or maybe align to global Z (as possible).
# other solution, button to disable all object Fcu evaluation (fix object movement while moving in timeline)
# 1 ops to enter in manip/draw Cam (create if not exists)
# 1 ops to reset rotation
# 1 ops to swap between cam follow or object follow (toggle or two button), maybe accessible only when drawcam is active
# hide camera that isn't used (playblast should always get main camera)
def get_gp_alignement_vector(context):
#SETTINGS
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'
### CHOOSE HOW TO PROJECT
""" # -> placement
if loc == "CURSOR":
plane_co = scn.cursor.location
else:#ORIGIN (also on origin if set to 'SURFACE', 'STROKE')
plane_co = obj.location """
# -> orientation
if orient == 'VIEW':
#only depth is important, no need to get view vector
return None
elif orient == 'AXIS_Y':#front (X-Z)
return Vector((0,1,0))
elif orient == 'AXIS_X':#side (Y-Z)
return Vector((1,0,0))
elif orient == 'AXIS_Z':#top (X-Y)
return Vector((0,0,1))
elif orient == 'CURSOR':
return Vector((0,0,1))#.rotate(context.scene.cursor.matrix)
class GPTB_OT_draw_cam(bpy.types.Operator):
bl_idname = "gp.draw_cam_switch"
bl_label = "Draw cam switch"
bl_description = "switch between main camera and draw (manipulate) camera"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.scene.camera
# return context.region_data.view_perspective == 'CAMERA'# check if in camera
cam_mode : bpy.props.StringProperty()
def execute(self, context):
created=False
if self.cam_mode == 'draw':
dcam_name = 'draw_cam'
else:
dcam_name = 'obj_cam'
act = context.object
if not act:
self.report({'ERROR'}, "No active object to lock on")
return {"CANCELLED"}
if context.region_data.view_perspective == 'ORTHO':
self.report({'ERROR'}, "Can't be set in othographic view, swith to persp (numpad 5)")
return {"CANCELLED"}
camcol_name = 'manip_cams'
if not context.scene.camera:
self.report({'ERROR'}, "No camera to return to")
return {"CANCELLED"}
## if already in draw_cam BACK to main camera
if context.scene.camera.name in ('draw_cam', 'obj_cam'):
drawcam = context.scene.camera
# get main cam and error if not available
if drawcam.name == 'draw_cam':
maincam = drawcam.parent
else:
maincam = None
main_name = drawcam.get('maincam_name')# Custom prop with previous avtive cam.
if main_name:
maincam = context.scene.objects.get(main_name)
if not maincam:
cams = [ob for ob in context.scene.objects if ob.type == 'CAMERA' and not ob.name in ("draw_cam", "obj_cam")]
if not cams:
self.report({'ERROR'}, "Can't find any other camera to switch to...")
return {"CANCELLED"}
maincam = cams[0]
# dcam_col = bpy.data.collections.get(camcol_name)
# if not dcam_col:
set_collection(drawcam, camcol_name)
# Swap to it, unhide if necessary and hide previous
context.scene.camera = maincam
## hide cam object
drawcam.hide_viewport = True
maincam.hide_viewport = False
## if in main camera GO to drawcam
elif context.scene.camera.name not in ('draw_cam', 'obj_cam'):
# use current cam as main cam (more flexible than naming convention)
maincam = context.scene.camera
drawcam = context.scene.objects.get(dcam_name)
if not drawcam:
created=True
drawcam = bpy.data.objects.new(dcam_name, context.scene.camera.data)
drawcam.show_name = True
set_collection(drawcam, 'manip_cams')
if dcam_name == 'draw_cam':
drawcam.parent = maincam
if created:#set to main at creation time
drawcam.matrix_world = maincam.matrix_world
drawcam.lock_location = (True,True,True)
else:
if created:
drawcam['maincam_name'] = context.scene.camera.name
drawcam.parent = act
drawcam.matrix_world = context.space_data.region_3d.view_matrix.inverted()
# Place cam from current view
'''
drawcam.parent = act
vec = Vector((0,1,0))
if act.type == 'GPENCIL':
#change vector according to alignement
vec = get_gp_alignement_vector(context)
vec = None #!# FORCE creation of cam at current viewpoint
if vec:
# Place drawcam at distance at standard distance from the object facing it
drawcam.location = act.matrix_world @ (vec * -6)
drawcam.rotation_euler = act.rotation_euler
drawcam.rotation_euler.x -= radians(-90)
else:
#Create cam at view point
drawcam.matrix_world = context.space_data.region_3d.view_matrix.inverted()
'''
## hide cam object
context.scene.camera = drawcam
drawcam.hide_viewport = False
maincam.hide_viewport = True
if created and drawcam.name == 'obj_cam':#Go in camera view
context.region_data.view_perspective = 'CAMERA'
# ## make active
# bpy.context.view_layer.objects.active = ob
return {"FINISHED"}
class GPTB_OT_set_view_as_cam(bpy.types.Operator):
bl_idname = "gp.set_view_as_cam"
bl_label = "Cam at view"
bl_description = "Place the active camera at current viewpoint, parent to active object. (need to be out of camera)"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.region_data.view_perspective != 'CAMERA'# need to be out of camera
# return context.scene.camera and not context.scene.camera.name.startswith('Cam')
def execute(self, context):
if context.region_data.view_perspective == 'ORTHO':
self.report({'ERROR'}, "Can't be set in othographic view")
return {"CANCELLED"}
## switching to persp work in 2 times, but need update before...
#context.area.tag_redraw()
#context.region_data.view_perspective = 'PERSP'
cam = context.scene.camera
if not cam:
self.report({'ERROR'}, "No camera to set")
return {"CANCELLED"}
obj = context.object
if obj and obj.type != 'CAMERA':# parent to object
cam.parent = obj
if not cam.parent:
self.report({'WARNING'}, "No parents...")
# set view
cam.matrix_world = context.space_data.region_3d.view_matrix.inverted()
# Enter in cam view
#https://blender.stackexchange.com/questions/30643/how-to-toggle-to-camera-view-via-python
context.region_data.view_perspective = 'CAMERA'
return {"FINISHED"}
class GPTB_OT_reset_cam_rot(bpy.types.Operator):
bl_idname = "gp.reset_cam_rot"
bl_label = "Reset rotation"
bl_description = "Reset rotation of the draw manipulation camera"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.scene.camera and not context.scene.camera.name.startswith('Cam')
# return context.region_data.view_perspective == 'CAMERA'# check if in camera
def execute(self, context):
# dcam_name = 'draw_cam'
# camcol_name = 'manip_cams'
drawcam = context.scene.camera
if drawcam.parent.type == 'CAMERA':
## align to parent camera
drawcam.matrix_world = drawcam.parent.matrix_world#wrong, get the parent rotation offset
# drawcam.rotation_euler = drawcam.parent.rotation_euler#wrong, get the parent rotation offset
elif drawcam.parent:
## there is a parent, so align the Y of the camera to object's Z
# drawcam.rotation_euler.rotate(drawcam.parent.matrix_world)# wrong
pass
else:
self.report({'ERROR'}, "No parents to refer to for rotation reset")
return {"CANCELLED"}
return {"FINISHED"}
class GPTB_OT_toggle_mute_animation(bpy.types.Operator):
bl_idname = "gp.toggle_mute_animation"
bl_label = "Toggle animation mute"
bl_description = "Enable/Disable animation evaluation\n(shift+clic to affect selection only)"
bl_options = {"REGISTER"}
mute : bpy.props.BoolProperty(default=False)
skip_gp : bpy.props.BoolProperty(default=False)
skip_obj : bpy.props.BoolProperty(default=False)
def invoke(self, context, event):
self.selection = event.shift
return self.execute(context)
def execute(self, context):
if self.selection:
pool = context.selected_objects
else:
pool = context.scene.objects
for o in pool:
if self.skip_gp and o.type == 'GPENCIL':
continue
if self.skip_obj and o.type != 'GPENCIL':
continue
if not o.animation_data:
continue
act = o.animation_data.action
if not act:
continue
for i, fcu in enumerate(act.fcurves):
print(i, fcu.data_path, fcu.array_index)
fcu.mute = self.mute
return {'FINISHED'}
class GPTB_OT_list_disabled_anims(bpy.types.Operator):
bl_idname = "gp.list_disabled_anims"
bl_label = "List disabled anims"
bl_description = "List disabled animations channels in scene. (shit+clic to list only on seleciton)"
bl_options = {"REGISTER"}
mute : bpy.props.BoolProperty(default=False)
# skip_gp : bpy.props.BoolProperty(default=False)
# skip_obj : bpy.props.BoolProperty(default=False)
def invoke(self, context, event):
self.selection = event.shift
return self.execute(context)
def execute(self, context):
li = []
oblist = []
if self.selection:
pool = context.selected_objects
else:
pool = context.scene.objects
for o in pool:
# if self.skip_gp and o.type == 'GPENCIL':
# continue
# if self.skip_obj and o.type != 'GPENCIL':
# continue
if not o.animation_data:
continue
act = o.animation_data.action
if not act:
continue
for i, fcu in enumerate(act.fcurves):
# print(i, fcu.data_path, fcu.array_index)
if fcu.mute:
if o not in oblist:
oblist.append(o)
li.append(f'{o.name} : {fcu.data_path} {fcu.array_index}')
else:
li.append(f'{" "*len(o.name)} - {fcu.data_path} {fcu.array_index}')
if li:
show_message_box(li)
else:
self.report({'INFO'}, f"No animation disabled on {'selection' if self.selection else 'scene'}")
return {'FINISHED'}
## TODO presets are still not used... need to make a custom preset save/remove/quickload manager to be efficient (UIlist ?)
class GPTB_OT_overlay_presets(bpy.types.Operator):
bl_idname = "gp.overlay_presets"
bl_label = "Overlay presets"
bl_description = "Overlay save/load presets for showing only whats needed"
bl_options = {"REGISTER"}
# @classmethod
# def poll(cls, context):
# return context.region_data.view_perspective == 'CAMERA'
val_dic = {}
def execute(self, context):
self.zones = [bpy.context.space_data.overlay]
exclude = (
### add lines here to exclude specific attribute
'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic
)
if not self.val_dic:
## store attribute of data_path in self.zones list.
for data_path in self.zones:
self.val_dic[data_path] = {}
for attr in dir(data_path):#iterate in attribute of given datapath
if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr):
self.val_dic[data_path][attr] = getattr(data_path, attr)
# Do tomething with the dic (backup to a json ?)
else:
## restore attribute from self.zones list
for data_path, prop_dic in self.val_dic.items():
for attr, val in prop_dic.items():
try:
setattr(data_path, attr, val)
except Exception as e:
print(f"/!\ Impossible to re-assign: {attr} = {val}")
print(e)
'''
overlay = context.space_data.overlay
# still need ref
overlay.show_extras = not val
overlay.show_outline_selected = val
overlay.show_object_origins = val
overlay.show_motion_paths = val
overlay.show_relationship_lines = val
overlay.show_bones = val
overlay.show_annotation = val
overlay.show_text = val
overlay.show_cursor = val
overlay.show_floor = val
overlay.show_axis_y = val
overlay.show_axis_x = val
'''
return {'FINISHED'}
classes = (
GPTB_OT_copy_text,
GPTB_OT_flipx_view,
GPTB_OT_jump_gp_keyframe,
GPTB_OT_rename_data_from_obj,
GPTB_OT_draw_cam,
GPTB_OT_set_view_as_cam,
GPTB_OT_reset_cam_rot,
GPTB_OT_toggle_mute_animation,
GPTB_OT_list_disabled_anims,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

259
OP_palettes.py Normal file
View File

@ -0,0 +1,259 @@
import bpy
import json
import os
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from .utils import convert_attr, get_addon_prefs
### --- Json serialized material load/save
def load_palette(context, filepath):
with open(filepath, 'r') as fd:
mat_dic = json.load(fd)
# from pprint import pprint
# pprint(mat_dic)
ob = context.object
for mat_name, attrs in mat_dic.items():
curmat = bpy.data.materials.get(mat_name)
if curmat:#exists
if curmat.is_grease_pencil:
if curmat not in ob.data.materials[:]:# add only if it's not already there
ob.data.materials.append(curmat)
continue
else:
mat_name = mat_name+'.01'#rename to avoid conflict
## to create a GP mat (from https://developer.blender.org/T67102)
mat = bpy.data.materials.new(name=mat_name)
bpy.data.materials.create_gpencil_data(mat)#cast to GP mat
ob.data.materials.append(mat)
for attr, value in attrs.items():
setattr(mat.grease_pencil, attr, value)
class GPTB_OT_load_default_palette(bpy.types.Operator):
bl_idname = "gp.load_default_palette"
bl_label = "Load basic palette"
bl_description = "Load a material palette on the current GP object\nif material name already exists in scene it will uses these"
bl_options = {"REGISTER", "INTERNAL"}
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def execute(self, context):
# Start Clean (delete unuesed sh*t)
bpy.ops.object.material_slot_remove_unused()
#Rename default solid stroke if still there
line = context.object.data.materials.get('Black')
if line:
line.name = 'line'
if not line:
line = context.object.data.materials.get('Solid Stroke')
if line:
line.name = 'line'
# load json
pfp = Path(bpy.path.abspath(get_addon_prefs().palette_path))
if not pfp.exists():
self.report({'ERROR'}, f'Palette path not found')
return {"CANCELLED"}
base = pfp / 'base.json'
if not base.exists():
self.report({'ERROR'}, f'base.json palette not found in {pfp.as_posix()}')
return {"CANCELLED"}
load_palette(context, base)
self.report({'INFO'}, f'Loaded base Palette')
return {"FINISHED"}
class GPTB_OT_load_palette(bpy.types.Operator, ImportHelper):
bl_idname = "gp.load_palette"
bl_label = "Load palette"
bl_description = "Load a material palette on the current GP object\nif material name already exists in scene it will uses these"
#bl_options = {"REGISTER", "INTERNAL"}
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
filename_ext = '.json'
filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'} )#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp
filepath : bpy.props.StringProperty(
name="File Path",
description="File path used for import",
maxlen= 1024)
def execute(self, context):
# load json
load_palette(context, self.filepath)
self.report({'INFO'}, f'settings loaded from: {os.path.basename(self.filepath)}')
return {"FINISHED"}
class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
bl_idname = "gp.save_palette"
bl_label = "save palette"
bl_description = "Save a material palette from material on current GP object."
#bl_options = {"REGISTER", "INTERNAL"}
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'})#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp
filename_ext = '.json'
filepath : bpy.props.StringProperty(
name="File Path",
description="File path used for export",
maxlen= 1024)
def execute(self, context):
ob = context.object
exclusions = ('bl_rna', 'rna_type')
# save json
dic = {}
allmat=[]
for mat in ob.data.materials:
if not mat.is_grease_pencil:
continue
if mat in allmat:
continue
allmat.append(mat)
dic[mat.name] = {}
for attr in dir(mat.grease_pencil):
if attr.startswith('__'):
continue
if attr in exclusions:
continue
if mat.grease_pencil.bl_rna.properties[attr].is_readonly:#avoid readonly
continue
dic[mat.name][attr] = convert_attr(getattr(mat.grease_pencil, attr))
if not dic:
self.report({'ERROR'}, f'No materials on this GP object')
return {"CANCELLED"}
# export
with open(self.filepath, 'w') as fd:
json.dump(dic, fd, indent='\t')
self.report({'INFO'}, f'Palette saved: {self.filepath}')#WARNING, ERROR
return {"FINISHED"}
### --- Direct material append/link from blend file
def load_blend_palette(context, filepath):
'''Load materials on current active object from current chosen blend'''
#from pathlib import Path
#palette_fp = C.preferences.addons['GP_toolbox'].preferences['palette_path']
#fp = Path(palette_fp) / 'christina.blend'
print(f'-- import palette from : {filepath} --')
for ob in context.selected_objects:
if ob.type != 'GPENCIL':
print(f'{ob.name} not a GP object')
continue
print('\n', ob.name, ':')
obj_mats = [m.name for m in ob.data.materials if m]# can found Nonetype
scene_mats = [m.name for m in bpy.data.materials]
# Link into the blend file
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
for name in data_from.materials:
if name.lower() in ('bg', 'line', 'dots stroke'):
continue
if name in obj_mats:
print(f"!- {name} already in object materials")
continue
if name in scene_mats:
print(f'- {name} (found in scene)')
ob.data.materials.append(bpy.data.materials[name])
continue
## TODO find a way to Update color !... complex...
data_to.materials.append(name)
if not data_to.materials:
# print('Nothing to link/append from lib palette!')
continue
print('From palette append:')
for mat in data_to.materials:
print(f'- {mat.name}')
ob.data.materials.append(mat)
print(f'-- import Done --')
## list sources in a palette txt data block
palette_txt = bpy.data.texts.get('palettes')
if not palette_txt:
palette_txt = bpy.data.texts.new('palettes')
lines = [l.body for l in palette_txt.lines]
if not os.path.basename(filepath) in lines:
palette_txt.write('\n' + os.path.basename(filepath))
class GPTB_OT_load_blend_palette(bpy.types.Operator, ImportHelper):
bl_idname = "gp.load_blend_palette"
bl_label = "Load colo palette"
bl_description = "Load a material palette from blend file on the current GP object\nif material name already exists in scene it will uses these"
#bl_options = {"REGISTER", "INTERNAL"}
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
filename_ext = '.blend'
filter_glob: bpy.props.StringProperty(default='*.blend', options={'HIDDEN'} )#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp
filepath : bpy.props.StringProperty(
name="File Path",
description="File path used for import",
maxlen= 1024)
def execute(self, context):
# load json
load_blend_palette(context, self.filepath)
self.report({'INFO'}, f'materials loaded from: {os.path.basename(self.filepath)}')
return {"FINISHED"}
classes = (
GPTB_OT_load_palette,
GPTB_OT_save_palette,
GPTB_OT_load_default_palette,
GPTB_OT_load_blend_palette,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

201
OP_playblast.py Normal file
View File

@ -0,0 +1,201 @@
import bpy
import os
from os import listdir, scandir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from pathlib import Path
from time import strftime
C = bpy.context
D = bpy.data
from .utils import open_file, open_folder, get_addon_prefs
exclude = (
### add lines here to exclude specific attribute
'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic
)
"""
rd_keep = [
"resolution_percentage",
"resolution_x",
"resolution_y",
"filepath",
"use_stamp",
"stamp_font_size",
]
im_keep = [
'file_format',
'color_mode',
'quality',
'compression',
]
ff_keep = [
'codec',
'format',
'constant_rate_factor',
'ffmpeg_preset',
'gopsize',
'audio_codec',
'audio_bitrate',
]
"""
def render_with_restore():
class RenderFileRestorer:
rd = bpy.context.scene.render
im = rd.image_settings
ff = rd.ffmpeg
# ffmpeg (ff) need to be before image_settings(im) in list
# otherwise __exit__ may try to restore settings of image mode in video mode !
# ex : "RGBA" not found in ('BW', 'RGB') (will still not stop thx to try block)
zones = [rd, ff, im]
val_dic = {}
cam = bpy.context.scene.camera
def __enter__(self):
## store attribute of data_path in self.zones list.
for data_path in self.zones:
self.val_dic[data_path] = {}
for attr in dir(data_path):#iterate in attribute of given datapath
if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr):
self.val_dic[data_path][attr] = getattr(data_path, attr)
if self.cam and self.cam.name == 'draw_cam':
if self.cam.parent:
bpy.context.scene.camera = self.cam.parent
def __exit__(self, type, value, traceback):
## restore attribute from self.zones list
for data_path, prop_dic in self.val_dic.items():
for attr, val in prop_dic.items():
try:
setattr(data_path, attr, val)
except Exception as e:
print(f"/!\ Impossible to re-assign: {attr} = {val}")
print(e)
if self.cam:
bpy.context.scene.camera = self.cam
return RenderFileRestorer()
def playblast(viewport = False, stamping = True):
scn = bpy.context.scene
res_factor = scn.gptoolprops.resolution_percentage
rd = scn.render
ff = rd.ffmpeg
with render_with_restore():
### can add propeties for personalisation as toolsetting props
rd.resolution_percentage = res_factor
while ( rd.resolution_x * res_factor / 100 ) % 2 != 0:# rd.resolution_percentage
rd.resolution_x = rd.resolution_x + 1
while ( rd.resolution_y * res_factor / 100 ) % 2 != 0:# rd.resolution_percentage
rd.resolution_y = rd.resolution_y + 1
rd.image_settings.file_format = 'FFMPEG'
ff.format = 'MPEG4'
ff.codec = 'H264'
ff.constant_rate_factor = 'HIGH'# MEDIUM
ff.ffmpeg_preset = 'REALTIME'
ff.gopsize = 10
ff.audio_codec = 'AAC'
ff.audio_bitrate = 128
rd.use_sequencer = False
rd.stamp_background = (0.0, 0.0, 0.0, 0.75)# blacker notes BG (default 0.25)
# rd.use_compositing
# rd.filepath = join(dirname(bpy.data.filepath), basename(bpy.data.filepath))
# rd.frame_path(frame=0, preview=0, view="_sauce")## give absolute render filepath with some suffix
# rd.is_movie_format# check if its movie mode
## set filepath
# mode incermental or just use fulldate (cannot create conflict and filter OK but long name)
blend = Path(bpy.data.filepath)
date_format = "%Y-%m-%d_%H-%M-%S"
fp = join(blend.parent, "images", f'playblast_{blend.stem}_{strftime(date_format)}.mp4')
#may need a properties for choosing location : bpy.types.Scene.qrd_savepath = bpy.props.StringProperty(subtype='DIR_PATH', description="Export location, if not specify, create a 'quick_render' directory aside blend location")#(change defaut name in user_prefernece)
rd.filepath = fp
rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast
#stamp options
rd.stamp_font_size = rd.stamp_font_size * res_factor / 100# rd.resolution_percentage
# bpy.ops.render.render_wrap(use_view=viewport)
### render
if viewport:## openGL
bpy.ops.render.opengl(animation=True, view_context=True)# 'INVOKE_DEFAULT',
else:## normal render
bpy.ops.render.render(animation=True)# 'INVOKE_DEFAULT',
# print("Playblast Done :", fp)#Dbg
return fp
class PBLAST_OT_playblast_anim(bpy.types.Operator):
bl_idname = "render.playblast_anim"
bl_label = "Playblast anim"
bl_description = "Launch animation playblast, use resolution percentage (Lock blender during process)"
bl_options = {"REGISTER"}
use_view : bpy.props.BoolProperty(name='use_view', default=False)
def execute(self, context):
if not bpy.data.is_saved:
self.report({'ERROR'}, 'File is not saved, Playblast cancelled')
return {"CANCELLED"}
fp = playblast(viewport = self.use_view, stamping = True)
if fp:
self.report({'INFO'}, f'File saved at: {fp}')
addon_prefs = get_addon_prefs()
if addon_prefs:
if addon_prefs.playblast_auto_play:
open_file(fp)
if addon_prefs.playblast_auto_open_folder:
open_folder(dirname(fp))
return {"FINISHED"}
def register():
bpy.utils.register_class(PBLAST_OT_playblast_anim)
def unregister():
bpy.utils.unregister_class(PBLAST_OT_playblast_anim)
'''
## Potential cancelling method for image sequence rendering.
for cfra in range(start, end+1):
print("Baking frame " + str(cfra))
# update scene to new frame and bake to template image
scene.frame_set(cfra)
ret = bpy.ops.object.bake_image()
if 'CANCELLED' in ret:
return {'CANCELLED'}
'''
"""
class PBLAST_OT_render_wrap(bpy.types.Operator):
bl_idname = "render.render_wrap"
bl_label = "Render wraped"
bl_description = "render"
bl_options = {"REGISTER"}## need hide
use_view : bpy.props.BoolProperty(name='use_view', default=False)
def execute(self, context):
if self.use_view:## openGL
ret = bpy.ops.render.opengl('INVOKE_DEFAULT', animation=True, view_context=True)
else:## normal render
ret = bpy.ops.render.render('INVOKE_DEFAULT', animation=True)
return {"FINISHED"}
"""
""" if __name__ == "__main__":
register() """

384
OP_playblast_bg.py Normal file
View File

@ -0,0 +1,384 @@
import bpy
import os
from os import listdir, scandir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from pathlib import Path
from time import strftime
import subprocess, threading
# viewport draw
import gpu, blf
from gpu_extras.batch import batch_for_shader
from .utils import open_file, open_folder, get_addon_prefs, detect_OS
## based on playblaster
exclude = (
### add lines here to exclude specific attribute
'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic
)
def delete_file(filepath) :
try:
if os.path.isfile(filepath) :
print('removing', filepath)
os.remove(filepath)
return True
except PermissionError:
print(f'impossible to remove {filepath}')
return False
# render function
def render_function(cmd, total_frame, scene) :
debug = bpy.context.window_manager.pblast_debug
# launch rendering
if debug : print(cmd)
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
frame_count = 0
while True :
if not bpy.context.window_manager.pblast_is_rendering :
# print('!!! Not rendering')
if detect_OS() == 'Windows':
subprocess.Popen(f"TASKKILL /F /PID {process.pid} /T")
else:
process.kill()
break
line = process.stdout.readline()
if line != '' :
#debug
if debug : print(line)
if b'Traceback' in line:
print('/!\\ Traceback in line return')
if b"Append frame " in line :
frame_count += 1
try :
# print('frame_count: ', frame_count, 'total_frame: ', total_frame)
bpy.context.window_manager.pblast_completion = frame_count / total_frame * 100
except AttributeError :
#debug
if debug : print("AttributeError avoided")
pass
if b"Blender quit" in line :
break
else:
break
print('ENDED')
# launch threading
def threading_render(arguments) :
render_thread = threading.Thread(target=render_function, args=arguments)
render_thread.start()
# callback for loading bar in 3D view
def draw_callback_px(self, context):
# get color and size of progress bar
# prefs = get_addon_prefs()
color_bar = [1, 1, 1] # prefs.progress_bar_color
background = [0.2, 0.2, 0.2] # prefs.progress_bar_background_color
bar_thickness = 10 # prefs.progress_bar_size
# Progress Bar
width = context.area.width
# context.window_manager.pblast_completion
complete = self.completion / 100# context.window_manager.pblast_completion / 100
size = int(width * complete)
# rectangle background
vertices_2 = (
(0, 0), (width, 0),
(0, bar_thickness + 20), (width, bar_thickness + 20))
indices = (
(0, 1, 2), (2, 1, 3))
shader2 = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
batch2 = batch_for_shader(shader2, 'TRIS', {"pos": vertices_2}, indices=indices)
shader2.bind()
shader2.uniform_float("color", [*background, 1])
batch2.draw(shader2)
# rectangle 1
vertices = (
(0, 0), (size, 0),
(0, bar_thickness), (size, bar_thickness))
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
shader.bind()
shader.uniform_float("color", [*color_bar, 1])
batch.draw(shader)
# Text
# text = f'Playblast in Progress ({complete}%) - Shift + Esc to Cancel'
text = f'Playblast in Progress - Shift + Esc to Cancel'
blf.color(0, *color_bar, 1)
blf.size(0, 12, 72)
blf.position(0, 10, bar_thickness + 5, 0)
blf.draw(0, text)
class BGBLAST_OT_playblast_modal_check(bpy.types.Operator):
'''Modal and external render from Samy Tichadou (Tonton)'''
bl_idname = "render.playblast_modal_check"
bl_label = "Playblast Modal Check"
bl_options = {'INTERNAL'}
_timer = None
blend_pb : bpy.props.StringProperty()
video_pb : bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return context.window_manager.pblast_is_rendering
def modal(self, context, event):
self.completion = context.window_manager.pblast_completion
# redraw area
try:
context.area.tag_redraw()
except AttributeError:
pass
# handle cancelling
if event.type in {'ESC'} and event.shift:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER' :
if self.completion == 100 :
self.finish(context)
return {'FINISHED'}
return {'PASS_THROUGH'}
# def invoke(self, context, event):
def execute(self, context):
context.window_manager.pblast_completion = 0
wm = context.window_manager
args = (self, context)
wm = context.window_manager
self.debug = wm.pblast_debug
self.completion = wm.pblast_completion
self._timer = wm.event_timer_add(0.1, window = context.window)
self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
print('in CANCEL')
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
wm = context.window_manager
wm.event_timer_remove(self._timer)
# turn off is_rendering
wm.pblast_is_rendering = False
wm.pblast_completion = 0
# delete temp file
delete_file(self.blend_pb)
self.blend_pb = ""
# delete temp video
delete_file(self.video_pb)
self.video_pb = ""
self.report({'WARNING'}, "Render Canceled")
def finish(self, context):
print('in FINISH')
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
wm = context.window_manager
wm.event_timer_remove(self._timer)
# turn off is_rendering
wm.pblast_is_rendering = False
wm.pblast_completion = 0
# debug
if self.debug :
print("blend temp : " + self.blend_pb)
print("video temp : " + self.video_pb)
# delete temp file
delete_file(self.blend_pb)
# open video file and/or folder
prefs = get_addon_prefs()
if prefs.playblast_auto_play:
# open_file(self.video_pb)
bpy.ops.wm.path_open(filepath=self.video_pb)
if prefs.playblast_auto_open_folder:
# open_folder(dirname(self.video_pb))
bpy.ops.wm.path_open(filepath=dirname(self.video_pb))
wm.pblast_previous_render = self.video_pb
self.report({'INFO'}, "Render Finished")
### classic sauce
def render_with_restore():
class RenderFileRestorer:
rd = bpy.context.scene.render
im = rd.image_settings
ff = rd.ffmpeg
# ffmpeg (ff) need to be before image_settings(im) in list
# otherwise __exit__ may try to restore settings of image mode in video mode !
# ex : "RGBA" not found in ('BW', 'RGB') (will still not stop thx to try block)
zones = [rd, ff, im]
val_dic = {}
def __enter__(self):
## store attribute of data_path in self.zones list.
for data_path in self.zones:
self.val_dic[data_path] = {}
for attr in dir(data_path):#iterate in attribute of given datapath
if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr):
self.val_dic[data_path][attr] = getattr(data_path, attr)
def __exit__(self, type, value, traceback):
## restore attribute from self.zones list
for data_path, prop_dic in self.val_dic.items():
for attr, val in prop_dic.items():
try:
setattr(data_path, attr, val)
except Exception as e:
print(f"/!\ Impossible to re-assign: {attr} = {val}")
print(e)
return RenderFileRestorer()
def playblast(context, viewport = False, stamping = True):
scn = bpy.context.scene
res_factor = scn.gptoolprops.resolution_percentage
rd = scn.render
ff = rd.ffmpeg
prefix = 'tempblast_'
# delete old playblast and blend files
folder = dirname(bpy.data.filepath)
for f in os.listdir(folder):
if f.startswith(prefix):
delete_file(join(folder, f))
pblast_folder = join(folder, 'playblast')
if exists(pblast_folder):
for f in os.listdir(pblast_folder):
if f.startswith(prefix):
delete_file(join(pblast_folder, f))
tempblend = str( Path(folder) / f'{prefix}{basename(bpy.data.filepath)}' )
with render_with_restore():
### can add propeties for personalisation as toolsetting props
rd.resolution_percentage = res_factor
while ( rd.resolution_x * res_factor / 100 ) % 2 != 0:# rd.resolution_percentage
rd.resolution_x = rd.resolution_x + 1
while ( rd.resolution_y * res_factor / 100 ) % 2 != 0:# rd.resolution_percentage
rd.resolution_y = rd.resolution_y + 1
backup_img_settings = rd.image_settings.file_format
rd.image_settings.file_format = 'FFMPEG'
ff.format = 'MPEG4'
ff.codec = 'H264'
ff.constant_rate_factor = 'HIGH'# MEDIUM
ff.ffmpeg_preset = 'REALTIME'
ff.gopsize = 10
ff.audio_codec = 'AAC'
ff.audio_bitrate = 128
rd.use_sequencer = False
# rd.use_compositing
# rd.filepath = join(dirname(bpy.data.filepath), basename(bpy.data.filepath))
# rd.frame_path(frame=0, preview=0, view="_sauce")## give absolute render filepath with some suffix
# rd.is_movie_format# check if its movie mode
## set filepath
# mode incermental or just use fulldate (cannot create conflict and filter OK but long name)
blend = Path(bpy.data.filepath)
date_format = "%Y-%m-%d_%H-%M-%S"
fp = join(blend.parent, "playblast", f'{prefix}{blend.stem}_{strftime(date_format)}.mp4')
#may need a properties for choosing location : bpy.types.Scene.qrd_savepath = bpy.props.StringProperty(subtype='DIR_PATH', description="Export location, if not specify, create a 'quick_render' directory aside blend location")#(change defaut name in user_prefernece)
rd.filepath = fp
rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast
#stamp options
rd.stamp_font_size = rd.stamp_font_size * res_factor / 100# rd.resolution_percentage
# get total number of frames
total_frame = context.scene.frame_end - context.scene.frame_start + 1
# bpy.ops.render.render_wrap(use_view=viewport)
bpy.ops.wm.save_as_mainfile(filepath = tempblend, copy=True)
cmd = f'"{bpy.app.binary_path}" -b "{tempblend}" -a'
# cmd = '"' + bpy.app.binary_path + '"' + " -b " + '"' + new_blend_filepath + '"' + " -E " + render_engine + " -a"
threading_render([cmd, total_frame, context.scene])
rd.image_settings.file_format = backup_img_settings#seems it tries to set some properties before the man imgs settings
# lauch BG render modal (ovrerride ?)
context.window_manager.pblast_completion = 0
context.window_manager.pblast_is_rendering = True
bpy.ops.render.playblast_modal_check(blend_pb=tempblend,video_pb=fp)
# print("Playblast Done :", fp)#Dbg
return fp
class BGBLAST_OT_playblast_anim(bpy.types.Operator):
bl_idname = "render.thread_playblast"
bl_label = "Playblast anim"
bl_description = "Launch animation playblast in a thread (non-blocking)"
bl_options = {"REGISTER"}
def execute(self, context):
if not bpy.data.is_saved:
self.report({'ERROR'}, 'File is not saved, Playblast cancelled')
return {"CANCELLED"}
playblast(context, viewport = False, stamping = True)
return {"FINISHED"}
def register():
bpy.types.WindowManager.pblast_is_rendering = bpy.props.BoolProperty()
bpy.types.WindowManager.pblast_completion = bpy.props.IntProperty(min = 0, max = 100)
bpy.types.WindowManager.pblast_previous_render = bpy.props.StringProperty()
bpy.types.WindowManager.pblast_debug = bpy.props.BoolProperty('INVOKE_DEFAULT', name="Debug", default=False)
bpy.utils.register_class(BGBLAST_OT_playblast_anim)
bpy.utils.register_class(BGBLAST_OT_playblast_modal_check)
def unregister():
bpy.utils.unregister_class(BGBLAST_OT_playblast_modal_check)
bpy.utils.unregister_class(BGBLAST_OT_playblast_anim)
del bpy.types.WindowManager.pblast_is_rendering
del bpy.types.WindowManager.pblast_completion
del bpy.types.WindowManager.pblast_previous_render
del bpy.types.WindowManager.pblast_debug

129
OP_pseudo_tint.py Normal file
View File

@ -0,0 +1,129 @@
from .utils import get_gp_objects, get_gp_datas, get_addon_prefs
import bpy
def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin):
return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
def get_hue_by_name(name, offset=0):
'''
Get a string and return a hue value
offsetted by int [offset] value based on a range of 255
'''
val = []
add = 0
for i in name:
add += ord(i)*8
#val.append(str(ord(i)))
#number = ''.join(val)
#print("number", number)#Dbg
# print(add, "% 255 =", add % 1000)#Dbg
moduled = (add + offset) % 1000
##avoid red
hue = translate_range(moduled, 0, 1000, 0.1, 0.9)
##avoid pink
#hue = translate_range(moduled, 0, 255, 0.0, 0.7)
return hue
class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
bl_idname = "gp.auto_tint_gp_layers"
bl_label = "Pseudo tint layers"
bl_description = "Put a tint on layers according to namespace (except background)"
bl_options = {"REGISTER", "UNDO"}
# bpy.types.Scene.gp_autotint_offset = bpy.props.IntProperty(name="Tint hue offset", description="offset the tint by this value for better color", default=0, min=-5000, max=5000, soft_min=-999, soft_max=999, step=1)#, subtype='PERCENTAGE'
# bpy.types.Scene.gp_autotint_namespace = bpy.props.BoolProperty(name="use prefix", description="Put same color on layers unsing the same prefix (separated by '_') of full name withjout separator", default=True)
autotint_offset : bpy.props.IntProperty(name="Tint hue offset",
default=0, min=-5000, max=5000, soft_min=-999, soft_max=999, step=1)#, subtype='PERCENTAGE'
reset : bpy.props.BoolProperty(name="Reset GP tints",
description="Put all tint factor to 0", default=False)
selected_GP : bpy.props.BoolProperty(name="Selected",
description="Work on all selected grease pencil objects, else only active one", default=True)
def execute(self, context):
## TODO create a scene string variable to store serialized list of pre-tinted layers
addon_prefs = get_addon_prefs()
separator = addon_prefs.separator
if not separator:separator = '_'
# Define GP object to work on
gp_datas = get_gp_datas(selection = self.selected_GP)
if self.reset:
for gp in gp_datas:
gpl = gp.layers
for l in gpl:
l.tint_factor = 0
# l.tint_color.hsv = (0,0,0)#Reset tint ?
# reset color stored if it was different than black on change
return {"FINISHED"}
for gp in gp_datas:
gpl = gp.layers
layer_ct = len(gpl)
hue_offset = self.autotint_offset#context.scene.gptoolprops.autotint_offset
#context.scene.gp_autotint_offset# scene property or self property
# namespace_order
namespaces=[]
for l in gpl:
ns= l.info.lower().split(separator, 1)[0]
if ns not in namespaces:
namespaces.append(ns)
ns_len =len(namespaces)
namespaces.reverse()
#print("namespaces", namespaces)#Dbg
#print("ns_len", ns_len)#Dbg
print('--------')
### step from 0.1 to 0.9
for i, l in enumerate(gpl):
if l.info.lower() not in ('background',):
print()
print('>', l.info)
ns= l.info.lower().split(separator, 1)[0]#get namespace from separator
print("namespace", ns)#Dbg
if context.scene.gptoolprops.autotint_namespace:
h = get_hue_by_name(ns, hue_offset)#l.info == individuels
else:
h = translate_range((i + hue_offset/100)%layer_ct, 0, layer_ct, 0.1, 0.9)
# h = hueval + hue_offset/10
# hueval += step
print("hue", h)#Dbg
## Desaturate for each color per namespace index between defined range (reperesent define depth).
# s = translate_range(namespaces.index(ns), 0, ns_len, 0.5, 0.8)
s = 0.8
print("index", namespaces.index(ns), '/', ns_len)#Dbg
print("sat", s)#Dbg
#v = 0.8
v = s
l.tint_factor = 1
l.tint_color.hsv = (h,s,v)
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.prop(self, 'autotint_offset')
# layout.prop(context.scene, 'gp_autotint_offset')#, text = "offset"
def invoke(self, context, event):
self.autotint_offset = context.scene.gptoolprops.autotint_offset
return self.execute(context)

482
OP_render.py Normal file
View File

@ -0,0 +1,482 @@
import bpy
import os
from os import listdir, scandir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from pathlib import Path
from time import strftime
C = bpy.context
D = bpy.data
from .utils import open_file, open_folder, get_addon_prefs
### render the png sequences
def initial_render_checks(context=None):
if not context:
context=bpy.context
if not bpy.data.is_saved:
return "File is not saved, render cancelled"
cam = context.scene.camera
if not cam:
return "No active Camera"
if cam.name == 'draw_cam':
if not cam.parent:
return "Camera is draw_cam but has no parent cam to render from..."
context.scene.camera = cam.parent
if cam.name == 'obj_cam':
if not cam.get('maincam_name'):
return "Cannot found main camera from obj_cam. Set main camera manually"
main_cam = context.scene.objects.get(cam['maincam_name'])
if not main_cam:
return f"Main camera not found with name: {cam['main_cam']}"
context.scene.camera = main_cam
return
exclude = (
### add lines here to exclude specific attribute
'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic
)
"""
rd_keep = [
"resolution_percentage",
"resolution_x",
"resolution_y",
"filepath",
"use_stamp",
"stamp_font_size",
]
im_keep = [
'file_format',
'color_mode',
'quality',
'compression',
]
ff_keep = [
'codec',
'format',
'constant_rate_factor',
'ffmpeg_preset',
'gopsize',
'audio_codec',
'audio_bitrate',
]
"""
def render_with_restore():
class RenderFileRestorer:
rd = bpy.context.scene.render
im = rd.image_settings
ff = rd.ffmpeg
# ffmpeg (ff) need to be before image_settings(im) in list
# otherwise __exit__ may try to restore settings of image mode in video mode !
# ex : "RGBA" not found in ('BW', 'RGB') (will still not stop thx to try block)
zones = [rd, ff, im]
obviz = {}
# layviz = []
# matviz = []
closeline = False
val_dic = {}
cam = bpy.context.scene.camera
enter_context = None
def __enter__(self):
self.enter_context = bpy.context
## store attribute of data_path in self.zones list.
for data_path in self.zones:
self.val_dic[data_path] = {}
for attr in dir(data_path):#iterate in attribute of given datapath
if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr):
self.val_dic[data_path][attr] = getattr(data_path, attr)
# cam
if self.cam and self.cam.name == 'draw_cam':
if self.cam.parent:
bpy.context.scene.camera = self.cam.parent
#case of obj cam
if self.cam.name == 'obj_cam':
bpy.context.scene.camera = bpy.context.scene.objects.get(self.cam['main_cam'])
for ob in bpy.context.scene.objects:
self.obviz[ob.name] = ob.hide_render
close_mat = bpy.data.materials.get('closeline')
if close_mat and not close_mat.grease_pencil.hide:
close_mat.grease_pencil.hide = True
self.closeline = True
# for gpo in bpy.context.scene.objects:
# if gpo.type != 'GPENCIL':
# continue
# if not gpo.materials.get('closeline'):
# continue
# self.closelines[gpo] = gpo.materials['closeline'].hide_render
def __exit__(self, type, value, traceback):
## reset header text
# self.enter_context.area.header_text_set(None)
### maybe keep render settings for custom output with right mode
"""
## restore attribute from self.zones list
for data_path, prop_dic in self.val_dic.items():
for attr, val in prop_dic.ietms():
try:
setattr(data_path, attr, val)
except Exception as e:
print(f"/!\ Impossible to re-assign: {attr} = {val}")
print(e)
"""
if self.cam:
bpy.context.scene.camera = self.cam
for obname, val in self.obviz.items():
bpy.context.scene.objects[obname].hide_render = val
if self.closeline:
close_mat = bpy.data.materials.get('closeline')
if close_mat:
close_mat.grease_pencil.hide = False
return RenderFileRestorer()
def set_render_settings():
prefs = get_addon_prefs()
rd = bpy.context.scene.render
rd.use_sequencer = False
rd.use_compositing = False
rd.use_overwrite = True
rd.image_settings.file_format = 'PNG'
rd.image_settings.color_mode = 'RGBA'
rd.image_settings.color_depth = '16'
rd.image_settings.compression = 80 #maybe up the compression a bit...
rd.resolution_percentage = 100
rd.resolution_x, rd.resolution_y = prefs.render_res_x, prefs.render_res_y
rd.use_stamp = False
rd.film_transparent = True
def render_invididually(context, render_list):
'''Receive a list of object to render individually isolated'''
prefs = get_addon_prefs()
scn = context.scene
rd = scn.render
error_list = []
with render_with_restore():
set_render_settings()
# rd.filepath = join(dirname(bpy.data.filepath), basename(bpy.data.filepath))
# rd.frame_path(frame=0, preview=0, view="_sauce")## give absolute render filepath with some suffix
## set filepath
blend = Path(bpy.data.filepath)
### render by object in list
for obname in render_list:
the_obj = scn.objects.get(obname)
if not the_obj:
error_list.append(f'! Could not found {obname} in scene, skipped !')
continue
## Kill renderability of all
for o in scn.objects:
o.hide_render = True
the_obj.hide_render = False
# f'{blend.stem}_'
# fp = blend.parents[1] / "compo" / "base" / obname / (obname+'_')
fp = (blend.parent / prefs.output_path.lstrip(r'\/')).resolve() / obname / (obname+'_')
rd.filepath = str(fp)
# Freeze so impossible to display advance
# context.area.header_text_set(f'rendering > {obname} ...')
### render
# bpy.ops.render.render_wrap(use_view=viewport)
bpy.ops.render.render(animation=True)
# print("render Done :", fp)#Dbg
return error_list
def render_grouped(context, render_list):
'''Receive a list of object to render grouped'''
scn = context.scene
rd = scn.render
error_list = []
with render_with_restore():
set_render_settings()
## Kill renderability of all
for o in scn.objects:
o.hide_render = True
### show all object of the list
for obname in render_list:
the_obj = scn.objects.get(obname)
if not the_obj:
error_list.append(f'! Could not found {obname} in scene, skipped !')
continue
the_obj.hide_render = False
## Use current file path of setup output path else following :
blend = Path(bpy.data.filepath)
outname = context.scene.gptoolprops.name_for_current_render
# fp = blend.parents[1] / "compo" / "base" / outname / (outname+'_')
fp = (blend.parent / prefs.output_path.lstrip(r'\/')).resolve() / outname / (outname+'_')
rd.filepath = str(fp)
### render
# bpy.ops.render.render_wrap(use_view=viewport)
bpy.ops.render.render(animation=True)
# print("render Done :", fp)#Dbg
return error_list
class GPTRD_OT_render_anim(bpy.types.Operator):
bl_idname = "render.render_anim"
bl_label = "render anim"
bl_description = "Launch animation render"
bl_options = {"REGISTER"}
# use_view : bpy.props.BoolProperty(name='use_view', default=False)
to_render = []
mode : bpy.props.StringProperty(name="render mode",
description="change render mode for list rendering", default="INDIVIDUAL")
render_bool : bpy.props.BoolVectorProperty(name="render bools",
description="", default=tuple([True]*32), size=32, subtype='NONE')
def invoke(self, context, event):
# prefs = get_addons_prefs_and_set()
# if not prefs.local_folder:
# self.report({'ERROR'}, f'Project local folder is not specified in addon preferences')
# return {'CANCELLED'}
if self.mode == 'GROUP' and not context.scene.gptoolprops.name_for_current_render:
self.report({'ERROR'}, 'Need to set ouput name')
return {'CANCELLED'}
prefs = get_addon_prefs()
print('exclusions list ->', prefs.render_obj_exclusion)
exclusion_obj = [name.strip() for name in prefs.render_obj_exclusion.split(',')]
print('object exclusion list: ', exclusion_obj)
print('initial self.to_render: ', self.to_render)
self.to_render = []#reset
## check object to render with basic filter
for ob in context.scene.objects:
if ob.type != 'GPENCIL':
continue
if any(x in ob.name.lower() for x in exclusion_obj): #('old', 'rough', 'trash', 'test')
print('Skip', ob.name)
continue
self.to_render.append(ob.name)
if not self.to_render:
self.report({'ERROR'}, 'No GP to render')
return {'CANCELLED'}
## Reset at each render
# self.render_bool = tuple([True]*32)# reset all True
## disable for some name (ex: BG)
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.label(text='Tick objects to render')
for i, name in enumerate(self.to_render):
row = layout.row()
row.prop(self, 'render_bool', index = i, text = name)
# for i, set in enumerate(SETS):
# column.row().prop(context.scene.spritesheet, 'sets', index=i, text=set)
def execute(self, context):
prefs = get_addon_prefs()
err = initial_render_checks(context)
if err:
self.report({'ERROR'}, err)
return {"CANCELLED"}
render_list = []
for i, name in enumerate(self.to_render):
if self.render_bool[i]:
render_list.append(name)
if not render_list:
self.report({'ERROR'}, 'Nothing to render')
return {"CANCELLED"}
# self.report({'INFO'}, f'rendering {render_list}')#Dgb
# return {"FINISHED"}#Dgb
if self.mode == 'INDIVIDUAL':
errlist = render_invididually(context, render_list)
elif self.mode == 'GROUP':
errlist = render_grouped(context, render_list)
blend = Path(bpy.data.filepath)
# out = blend.parents[1] / "compo" / "base"
out = (blend.parent / prefs.output_path.lstrip(r'\/')).resolve()
if out.exists():
open_folder(str(out))
else:
errlist.append('No compo/base folder created')
if errlist:
self.report({'ERROR'}, '\n'.join(errlist))
return {"FINISHED"}
### ---- Setup render path
class GPTRD_OT_setup_render_path(bpy.types.Operator):
bl_idname = "render.setup_render_path"
bl_label = "Setup render"
bl_description = "Setup render settings for normal render of the current state\nHint: F12 to check one frame, ctrl+F12 to render animation"
bl_options = {"REGISTER"}
def execute(self, context):
#get name and check
prefs = get_addon_prefs()
outname = context.scene.gptoolprops.name_for_current_render
if not outname:
self.report({'ERROR'}, 'No output name has been set')
return {"CANCELLED"}
err = initial_render_checks(context)
if err:
self.report({'ERROR'}, err)
return {"CANCELLED"}
set_render_settings()
blend = Path(bpy.data.filepath)
# out = blend.parents[1] / "compo" / "base"
out = (blend.parent / prefs.output_path.lstrip(r'\/')).resolve()
fp = out / outname / (outname+'_')
context.scene.render.filepath = str(fp)
self.report({'INFO'}, f'output setup for "{outname}"')
return {"FINISHED"}
class GPTRD_OT_use_active_object_infos(bpy.types.Operator):
bl_idname = "render.use_active_object_name"
bl_label = "Use object Name"
bl_description = "Write active object name (active layer name with shift click on the button)"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object
def invoke(self, context, event):
# wm = context.window_manager
# return wm.invoke_props_dialog(self)
self.shift = event.shift
return self.execute(context)
def execute(self, context):
ob = context.object
#get name and check
if self.shift:
if ob.type != "GPENCIL":
self.report({'ERROR'}, 'Not a GP, no access to layers')
return {"CANCELLED"}
lay = ob.data.layers.active
if not lay:
self.report({'ERROR'}, 'No active layer found')
return {"CANCELLED"}
context.scene.gptoolprops.name_for_current_render = lay.info
else:
context.scene.gptoolprops.name_for_current_render = ob.name
# self.report({'INFO'}, 'Output Name changed')
return {"FINISHED"}
""" class GPTRD_OT_render_as_is(bpy.types.Operator):
bl_idname = "render.render_as_is"
bl_label = "render current"
bl_description = "Launch animation render with current setup"
bl_options = {"REGISTER"}
def execute(self, context):
err = initial_render_checks(context)
if err:
self.report({'ERROR'}, err)
return {"CANCELLED"}
return {"FINISHED"} """
### --- REGISTER
classes = (
GPTRD_OT_render_anim,
GPTRD_OT_setup_render_path,
GPTRD_OT_use_active_object_infos,
)
def register():
for cl in classes:
bpy.utils.register_class(cl)
def unregister():
for cl in classes:
bpy.utils.unregister_class(cl)
'''
## Potential cancelling method for image sequence rendering.
for cfra in range(start, end+1):
print("Baking frame " + str(cfra))
# update scene to new frame and bake to template image
scene.frame_set(cfra)
ret = bpy.ops.object.bake_image()
if 'CANCELLED' in ret:
return {'CANCELLED'}
'''
"""
class PBLAST_OT_render_wrap(bpy.types.Operator):
bl_idname = "render.render_wrap"
bl_label = "Render wraped"
bl_description = "render"
bl_options = {"REGISTER"}## need hide
use_view : bpy.props.BoolProperty(name='use_view', default=False)
def execute(self, context):
if self.use_view:## openGL
ret = bpy.ops.render.opengl('INVOKE_DEFAULT', animation=True, view_context=True)
else:## normal render
ret = bpy.ops.render.render('INVOKE_DEFAULT', animation=True)
return {"FINISHED"}
"""
""" if __name__ == "__main__":
register() """

166
OP_temp_cutter.py Normal file
View File

@ -0,0 +1,166 @@
import bpy
# https://blenderartists.org/t/how-to-execute-operator-once-when-keymap-is-held-down/1166009
class GPTB_OT_temp_cutter(bpy.types.Operator):
bl_idname = "wm.temp_cutter"
bl_label = "Temporary cutter"
bl_description = "Temporary cutter during press in GP mode"
bl_options = {'REGISTER'}#, 'UNDO' avoid register undo step
_is_running = False# block subsequent 'PRESS' events
bpy.types.Scene.tmp_cutter_org_mode = bpy.props.StringProperty(
name="temp cutter previous mode", description="Use to store mode used before cutter", default="")
# original_mode = None
def execute(self, context):
print('exe so cute')
bpy.ops.wm.tool_set_by_id(name='builtin.cutter')
return {'FINISHED'}
def invoke(self, context, event):
if event.value == 'RELEASE':
__class__._is_running = False
# if self.original_mode:
# bpy.ops.wm.tool_set_by_id(name = self.original_mode)
if context.scene.tmp_cutter_org_mode:
bpy.ops.wm.tool_set_by_id(name = context.scene.tmp_cutter_org_mode)
# self.original_mode = None
# return 'CANCELLED' unless the code is important,
# this prevents updating the view layer unecessarily
return {'CANCELLED'}
elif event.value == 'PRESS':
if not self._is_running:
# self.original_mode = bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname
context.scene.tmp_cutter_org_mode = bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname
__class__._is_running = True
return self.execute(context)
return {'CANCELLED'}
class GPTB_OT_sticky_cutter(bpy.types.Operator):
bl_idname = "wm.sticky_cutter"
bl_label = "Sticky cutter"
bl_description = "Sticky cutter tool"
bl_options = {'REGISTER'}#, 'UNDO'
def execute(self, context):
# toggle code
# if self.original_mode == 'builtin.cutter':#if on cutter, return to draw
# bpy.ops.wm.tool_set_by_id(name='builtin_brush.Draw')
return {'FINISHED'}
def modal(self, context, event):
if event.type == self.key and event.value == 'PRESS':
return {'RUNNING_MODAL'}
elif event.type == self.key and event.value == 'RELEASE':
if self.timeout:
# use release code in here
bpy.ops.wm.tool_set_by_id(name=self.original_mode)
print("released")
return {'FINISHED'}
wm = context.window_manager
wm.event_timer_remove(self.handler)
# return {'FINISHED'}
return self.execute(context)
elif event.type == 'TIMER':
self.timeout = True
wm = context.window_manager
wm.event_timer_remove(self.handler)
if self.timeout:
pass
# print("repeating holding down")
# use holding down code in here
# bpy.ops.wm.tool_set_by_id(name='builtin.cutter')#builtin.cutter cursor
return {'PASS_THROUGH'}
def invoke(self, context, event):
self.key = ''
#get key from keymap
wm = bpy.context.window_manager
#addons : #wm.keyconfigs.addon.keymaps.items()
for cat, keymap in wm.keyconfigs.user.keymaps.items():#user set
for k in keymap.keymap_items:
if k.idname == 'wm.sticky_cutter':
self.key = k.type
if not self.key:
self.report({'ERROR'}, 'Could not found dedicated key in user keymap for "wm.sticky_cutter"')
return {'CANCELLED'}
self.original_mode = bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname
if event.value == 'PRESS':
self.timeout = False
wm = context.window_manager
wm.modal_handler_add(self)
bpy.ops.wm.tool_set_by_id(name='builtin.cutter')
self.handler = wm.event_timer_add(
time_step=0.2, window=context.window)
return {'RUNNING_MODAL'}
return {'CANCELLED'}
## keymaps
'''
tmp_cutter_addon_keymaps = []
def register_keymaps():
# pref = get_addon_prefs()
# if not pref.temp_cutter_use_shortcut:
# return
addon = bpy.context.window_manager.keyconfigs.addon
try:
km = bpy.context.window_manager.keyconfigs.addon.keymaps["3D View"]# Grease Pencil
except Exception as e:
km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D") #3D View
pass
ops_id = 'wm.temp_cutter'# 'wm.sticky_cutter'
if ops_id not in km.keymap_items:
## keymap to operator cam space (in grease pencil mode only ?)
km = addon.keymaps.new(name='3D View', space_type='VIEW_3D')#EMPTY #Grease Pencil #3D View
# use 'ANY' to map both 'PRESS' and 'RELEASE' to the operator
# then use the operator's invoke to deal with each articulation
kmi = km.keymap_items.new(ops_id, type="T", value="ANY")#, alt=pref.use_alt, ctrl=pref.use_ctrl, shift=pref.use_shift, any=False)
tmp_cutter_addon_keymaps.append(km)
def unregister_keymaps():
# wm = bpy.context.window_manager
for km in tmp_cutter_addon_keymaps:
for kmi in km.keymap_items:
km.keymap_items.remove(kmi)
# wm.keyconfigs.addon.keymaps.remove(km)#dont use new km field...
tmp_cutter_addon_keymaps.clear()
# del tmp_cutter_addon_keymaps[:]
'''
def register():
if not bpy.app.background:
bpy.utils.register_class(GPTB_OT_temp_cutter)
bpy.utils.register_class(GPTB_OT_sticky_cutter)
# register_keymaps()
def unregister():
if not bpy.app.background:
# unregister_keymaps()
bpy.utils.unregister_class(GPTB_OT_temp_cutter)
bpy.utils.unregister_class(GPTB_OT_sticky_cutter)
if __name__ == "__main__":
register()

286
README.md Normal file
View File

@ -0,0 +1,286 @@
# GP toolbox
Blender addon - Various tool to help with grease pencil in animation productions.
**[Download latest](https://gitlab.com/autour-de-minuit/blender/GP_toolbox/-/archive/master/GP_toolbox-master.zi)**
<!-- ### [Demo Youtube]() -->
---
## Description
<!-- ![pseudo color demo](https://github.com/Pullusb/images_repo/blob/master/GPT_pseudo_tint.gif)
*In this demo F9 is pressed to call the "redo panel" and modify the hue offset with constant update* -->
In sidebar (N) > Gpencil > Toolbox
<!-- - Pseudo tint color : Tints each GP layer of selected GP objects with a pseudo random tints to quickly identify which lines belong to which layer.
/!\ Using this will override the tint and tint factor on your layer (so they will be lost if you used it for a specific layer) -->
## Addon preferences
important point of addon preferences:
Set path to the palette folder (there is a json palette IO but you an also put a blend and use a blend importer)
Note about palette : For now thoe importer are not working with linked palette is not easy for animator (there are properies of the material you cannot access and the link grey-out fade the r eal color in UIlist preview)
- Mirror flip : If in cam view flip the camera X scale value (you can see and draw mnirrored to see problems)
<!-- - Overlay toggle : (toggle pref and overlay) -->
- quick access to scene camera passepartout toggle and opacity
- quick access to scene camera background images visibility with individual references toggle.
- Basic playblast and viewport playblast:
- dedicated resolution percentage value
- can auto launch and/or auto open folder at finished (option in addon preferences)
- jump to GP keyframe operator
- you need to set two keymap shortcut in _windows_ or _screen(global)_ with indentifier `screen.gp_keyframe_jump`
- Rotate canvas with a clic + modifier combo (default is `ctrl + alt + MID-mouse`), can be change in addon preferences.
- GP paint cutter tool temporary switch shortcut
- Map manually to a key with `wm.temp_cutter` (This one needs "Any" as press mode) or `wm.sticky_cutter` (Modal sticky-key version)
- Snap cursor to GP canvas operator accessible with `view3d.cusor_snap`
- Map nanually (might be autoreplaced according to version) by replacing entry using `view3d.cursor3d` in 3D View category (defaut shortcut `Shift + Right-clic`)
- Follow cursor toggle : When activated the cursor follow the active the active object
- [breakdowner operator for object mode](https://blenderartists.org/t/pose-mode-animation-tools-for-object-mode/1221322), auto-keymap on : Shift + E
- Line extender help closing gaps between lines with control over layer target (-> need also control over frame targets)
- copy paste
### Where ?
Panel in sidebar : 3D view > sidebar 'N' > Gpencil
<!--
## Todo:
- viewport Face GP object -> test in 3D context tester
- move GP keyframes selection and Object keyframe selection simultaneouly (Tom viguier is test)
- -->
---
## Changelog:
0.9.1:
- Public release
- prefs: added fps as part of project settings
- check file use pref fps value (previously used harcoded 24fps value)
0.8.0:
- feat: Added background_rendering playblast, derivating from Tonton's playblaster
- stripped associated properties from properties.py and passed as wm props.
0.7.2:
- fix: Palette importer bug
0.7.0:
- feat: auto create empty frame on color layer
0.6.3:
- shortcut: added 1,2,3 to change sculpt mask mode (like native edit mode shortcut)
0.6.2:
- feat: colorisation, Option to change stop lines length
- Change behavior of `cursor_snap` ops when a non-GP object is selected to mode: `surface project`
- Minor refactor for submodule register
0.6.1:
- feat: render objects grouped, one anim render with all ticked object using manual output name
0.6.0:
- feat: Include GP clipoard's "In place" custom cut/copy/paste using OS clipboard
0.5.9:
- feat: render exporter
- Render a selection of GP object isolated from the rest
- added exclusions names for GP object listing
- setup settings and output according to a name
- open render folder
- check file: set onion skin keyframe filter to 'All_type' on all GP datablock
- check file: set scene resolution to settings in prefs (default 2048x1080)
0.5.8:
- feat: GP material append on active object from single blend file
0.5.7:
- Added warning message for cursor snapping
0.5.5 - 0.5.6:
- check file: added check for placement an projection mode for Gpencil.
- add a slider to change edit_lines_opacity globally for all GP data at once
- check file: auto-check additive drawing (to avoid empty frame with "only selected channel" in Dopesheet)
0.5.4:
- feat: anim manager in his own GP_toolbox submenu:
- button to list disabled anim (allow to quickly check state of the scene)
- disable/enable all fcurve in for GP object or other object separately to paint
- shift clic to target selection only
- check file: added disabled fcurved counter alert with detail in console
0.5.3:
- fix: broken obj cam (add custom prop on objcam to track wich was main cam)
- check file option: change select active tool (choice added in addon preferences)
0.5.2:
- Revert back obj_cam operator for following object (native lock view follow only translation)
- Changed method for canvas rotation to more robust rotate axis.
- Add operators on link checker to open containing folder/file of link
- Refactor: file checkers in their own file
0.5.1:
- fix: error when empty material slot on GP object.
- fix: cursor snap on GP canvas when GP is parented
- change: Deleted obj cam (and related set view) operator
- change: blacker note background for playblast (stamp_background)
- feat: Always playblast from main camera (if in draw_cam)
- feat: Handler added to Remap relative on save (pre)
- ops: Check for broken links with porposition to find missing files
- ops: Added basic hardcoded file checker
- Lock main cam
- set scene percentage at 100
- set show slider and sync range
- set fps to 24
0.4.6:
- feat: basic Palette manager with base material check and warning
0.4.5:
- open blender config folder from addon preference
- fix: obj cam parent on selected object
- added wip rotate canvas axis file. still not ready to replace current canvas rotate:
- freeview : bug when rotating free viewfrom cardianl views
- camview: potential bug when cam is parented with some specific angle (could not reproduce)
0.4.4:
- feat: added cursor follow handlers and UI toggle
0.4.3:
- change playblast out to 'images' and add playblast as name prefix
0.4.2:
- feat: GP canvas cursor snap wiht new `view3d.cusor_snap` operator
- fix: canvas rotate works with parented camera !
- wip: added an attmpt to replicate camera rotate modal with view matrix but no luck.
0.4.1:
- feat: Alternative cameras: parent to main cam (roll without affecting main cam), parent to active object at current view (follow current Grease pencil object)
0.4.0:
- Added a standalone working version of box_deform (stripped preferences keeping only best configuration with autoswap)
0.3.8:
- UI: expose onion skin in interface
- UI: expose autolock in interface
- UI : putted tint layers in a submenu
- code: refactor, pushed most of class register in their owner file
- tool: tool to rename current or all grease pencil datablock with different name than container object
0.3.7:
- UI: new interface with tabs for addon preferences
- UI: possible to disable color panel from preference (might be deleted if unusable)
- docs: change readme changelog format and correct doc
0.3.6:
- UI: Stoplines : add a button for quickly set stoplines visibility.
0.3.5:
- Fix : No more camera rotation undo when ctrl+Z on next stroke (canvas rotate push and undo)
- Fix: Enter key added to valid object-breakdown modal.
0.3.3:
- version 1 beta (stable) of line gap closing tools for better bucket fill tool performance with UI
0.3.3:
- version 1 beta of gmic colorize
- variant of `screen.gp_keyframe_jump` through keymap seetings
0.3.0:
- new homemade [breakdowner operator for object](https://blenderartists.org/t/pose-mode-animation-tools-for-object-mode/1221322) mode with auto keymap : Shift + E
- GP cutter shortcut ops to map with `wm.temp_cutter` (with "Any" as press mode) or `wm.sticky_cutter` (Modal sticky-key version)
0.2.3:
- add operator to `screen.gp_keyframe_jump`
- add shortcut to rotate canvas
- fix duplicate class
0.2.2:
- separated props resolution_percentage parameter
- playblast options for launching folder and opening folder
0.2.1:
- playblast feature
- Button to go zoom 100% or fit screen
- display scene resolution with res indicator
- Fix reference panel : works with video and display in a box layout.
- close pseudo-color panel by default (plan to move it to Gpencil tab)
0.2.0:
- UI: Toggle camera background images from Toolbox panel
- UI: quick access to passepartout
- Feature: option to use namespace for pseudo color
0.1.5:
- added CGC-auto-updater
0.1.3:
- flip cam x
- inital stage of overlay toggle (need pref/multiple pref)
0.1.2:
- subpanel of GP data (instead of direct append)
- initial commit with GP pseudo color

391
UI_tools.py Normal file
View File

@ -0,0 +1,391 @@
from . import addon_updater_ops
from .utils import get_addon_prefs
import bpy
from pathlib import Path
## UI in properties
### dataprop_panel not used --> transferred to sidebar
"""
class GPTB_PT_dataprop_panel(bpy.types.Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
# bl_space_type = 'VIEW_3D'
# bl_region_type = 'UI'
# bl_category = "Tool"
# bl_idname = "ADDONID_PT_panel_name"# identifier, if ommited, takes the name of the class.
bl_label = "Pseudo color"# title
bl_parent_id = "DATA_PT_gpencil_layers"#subpanel of this ID
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
settings = context.scene.gptoolprops
col = layout.column(align = True)
row = col.split(align=False, factor=0.63)
row.prop(settings, 'autotint_offset')
row.prop(settings, 'autotint_namespace')
col.operator("gp.auto_tint_gp_layers", icon = "COLOR").reset = False
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True
"""
## UI in Gpencil sidebar menu
class GPTB_PT_sidebar_panel(bpy.types.Panel):
bl_label = "Toolbox"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
def draw(self, context):
layout = self.layout
# layout.use_property_split = True
rd = context.scene.render
# check for update
addon_updater_ops.check_for_update_background()
# layout.label(text='View options:')
## flip X cam
if context.scene.camera and context.scene.camera.scale.x < 0:
# layout.label(text='! Flipped !')
row = layout.row(align=True)
row.operator('gp.mirror_flipx', text = 'Mirror flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
row.label(text='',icon='LOOP_BACK')
else:
layout.operator('gp.mirror_flipx', text = 'Mirror flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
## draw/manipulation camera
col = layout.column()
if context.scene.camera and context.scene.camera.name.startswith(('draw', 'obj')):
row = col.row(align=True)
row.operator('gp.draw_cam_switch', text = 'Main cam', icon = 'OUTLINER_OB_CAMERA')
row.label(text='',icon='LOOP_BACK')
if context.scene.camera.name.startswith('draw'):
col.operator('gp.reset_cam_rot', text='reset rotation')#.swapmethod ? = CAM
else:
col.operator('gp.set_view_as_cam', text='set view')#.swapmethod ? = CAM
else:
row = col.row(align=True)
row.operator('gp.draw_cam_switch', text = 'Draw cam', icon = 'CON_CAMERASOLVER').cam_mode = 'draw'
row.operator('gp.draw_cam_switch', text = 'Object cam', icon = 'CON_CAMERASOLVER').cam_mode = 'object'
col.label(text='In main camera', icon = 'OUTLINER_OB_CAMERA')
# layout.operator('gp.overlay_presets', text = 'Toggle overlays', icon = 'OVERLAY')
if context.scene.camera:
row = layout.row(align=True)# .split(factor=0.5)
row.label(text='Passepartout')
row.prop(context.scene.camera.data, 'show_passepartout',text='', icon ='OBJECT_HIDDEN' )
row.prop(context.scene.camera.data, 'passepartout_alpha', text='')
row = layout.row(align=True)
row.operator('view3d.zoom_camera_1_to_1', text = 'Zoom 1:1', icon = 'ZOOM_PREVIOUS')# FULLSCREEN_EXIT
row.operator('view3d.view_center_camera', text = 'Zoom fit', icon = 'FULLSCREEN_ENTER')
## background images/videos
if context.scene.camera.data.background_images:
layout.separator()
icon_bg = 'RESTRICT_VIEW_OFF' if context.scene.camera.data.show_background_images else 'RESTRICT_VIEW_ON'# IMAGE_BACKGROUND#IMAGE_PLANE
# icon_bg = 'TRIA_DOWN' if context.scene.camera.data.show_background_images else 'IMAGE_BACKGROUND'
box = layout.box()
box.prop(context.scene.camera.data, 'show_background_images', text='Ref in cam', icon=icon_bg)
if context.scene.camera.data.show_background_images:
# box = layout.box()
for bg_img in context.scene.camera.data.background_images:
if bg_img.source == 'IMAGE' and bg_img.image:
row = box.row(align=True)
row.label(text=bg_img.image.name, icon='IMAGE_RGB')# FILE_IMAGE
# row.prop(bg_img, 'alpha', text='')# options={'HIDDEN'}
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
if bg_img.source == 'MOVIE_CLIP' and bg_img.clip:
row = box.row(align=True)
row.label(text=bg_img.clip.name, icon='FILE_MOVIE')
# row.prop(bg_img, 'alpha', text='')# options={'HIDDEN'}
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
## playblast params
layout.separator()
layout.label(text = 'Playblast:')
row = layout.row(align=False)#split(factor=0.6)
row.label(text = f'{rd.resolution_x * context.scene.gptoolprops.resolution_percentage // 100} x {rd.resolution_y * context.scene.gptoolprops.resolution_percentage // 100}')
row.prop(context.scene.gptoolprops, 'resolution_percentage', text='')
# row.prop(rd, 'resolution_percentage', text='')#real percent scene percentage
row = layout.row(align=True)
row.operator('render.thread_playblast', text = 'Playblast', icon = 'RENDER_ANIMATION')# non blocking background render playblast
# row.operator('render.playblast_anim', text = 'Playblast', icon = 'RENDER_ANIMATION').use_view = False # old (but robust) blocking playblast
row.operator('render.playblast_anim', text = 'Viewport').use_view = True
else:
layout.label(text='No camera !', icon = 'ERROR')
## Options
layout.separator()
layout.label(text = 'Options:')
# row = layout.row(align=False)
## maybe remove cursor_follow icon that look like
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
layout.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
layout.prop(context.space_data.overlay, 'use_gpencil_onion_skin')
if context.object and context.object.type == 'GPENCIL':
layout.prop(context.object.data, 'use_autolock_layers')
layout.prop(context.object, 'show_in_front', text='X-ray')#default text "In Front"
## rename datablock temporary layout
if context.object.name != context.object.data.name:
box = layout.box()
box.label(text='different name for object and data:', icon='INFO')
row = box.row(align=False)
row.operator('gp.rename_data_from_obj').rename_all = False
row.operator('gp.rename_data_from_obj', text='Rename all').rename_all = True
## Check base palette
if not all(x in [m.name for m in context.object.data.materials if m] for x in ("line", "invisible")):
box = layout.box()
box.label(text='Missing base material setup', icon='INFO')
box.operator('gp.load_default_palette')
else:
layout.label(text='No GP object selected')
layout.prop(context.scene.gptoolprops, 'edit_lines_opacity')
## Create empty frame on layer (ops stored under GP_colorize... might be best to separate in another panel )
layout.operator('gp.create_empty_frames', icon = 'DECORATE_KEYFRAME')
## File checker
row = layout.row(align=True)
row.operator('gp.file_checker', text = 'Check file', icon = 'SCENE_DATA')
row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED')
# Mention update as notice
addon_updater_ops.update_notice_box_ui(self, context)
# row = layout.row(align=False)
# row.label(text='arrow choice')
# row.operator("my_operator.multi_op", text='', icon='TRIA_LEFT').left = 1
# row.operator("my_operator.multi_op", text='', icon='TRIA_RIGHT').left = 0
class GPTB_PT_anim_manager(bpy.types.Panel):
bl_label = "Animation manager"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_parent_id = "GPTB_PT_sidebar_panel"
bl_options = {'DEFAULT_CLOSED'}
# def draw_header(self,context):
# self.layout.prop(context.scene.camera.data, "show_background_images", text="")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
## Animation enable disable anim (shift click to select) OP_helpers.GPTB_OT_toggle_mute_animation
layout.operator('gp.list_disabled_anims')
## Objs ()
row = layout.row(align=True)
row.label(text='Obj anims:')
ops = row.operator('gp.toggle_mute_animation', text = 'ON')#, icon = 'GRAPH'
ops.skip_gp = True
ops.skip_obj = False
ops.mute = False
ops = row.operator('gp.toggle_mute_animation', text = 'OFF')#, icon = 'GRAPH'
ops.skip_gp = True
ops.skip_obj = False
ops.mute = True
## Gps
row = layout.row(align=True)
row.label(text='Gp anims:')
ops = row.operator('gp.toggle_mute_animation', text = 'ON')#, icon = 'GRAPH'
ops.skip_gp = False
ops.skip_obj = True
ops.mute = False
ops = row.operator('gp.toggle_mute_animation', text = 'OFF')#, icon = 'GRAPH'
ops.skip_gp = False
ops.skip_obj = True
ops.mute = True
class GPTB_PT_tint_layers(bpy.types.Panel):
bl_label = "Tint layers"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_parent_id = "GPTB_PT_sidebar_panel"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.scene.camera
# def draw_header(self,context):
# self.layout.prop(context.scene.camera.data, "show_background_images", text="")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
## pseudo color layers
# layout.separator()
col = layout.column(align = True)
row = col.split(align=False, factor=0.63)
row.prop(context.scene.gptoolprops, 'autotint_offset')
row.prop(context.scene.gptoolprops, 'autotint_namespace')
col.operator("gp.auto_tint_gp_layers", icon = "COLOR").reset = False
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True
class GPTB_PT_render(bpy.types.Panel):
bl_label = "Render"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_parent_id = "GPTB_PT_sidebar_panel"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.scene.camera
# def draw_header(self,context):
# self.layout.prop(context.scene.camera.data, "show_background_images", text="")
def draw(self, context):
layout = self.layout
layout.operator('render.render_anim', text = 'Render invividually', icon = 'RENDERLAYERS').mode = 'INDIVIDUAL'#RENDER_STILL #RESTRICT_RENDER_OFF
layout.operator('render.render_anim', text = 'Render grouped', icon = 'IMAGE_RGB').mode = 'GROUP'
layout.separator()
row = layout.row()
row.prop(context.scene.gptoolprops, 'name_for_current_render', text = 'Output name')#icon = 'OUTPUT'
row.operator('render.use_active_object_name', text = '', icon='OUTLINER_DATA_GP_LAYER')#icon = 'OUTPUT'
layout.operator('render.setup_render_path', text = 'Setup output', icon = 'TOOL_SETTINGS')#SETTINGS
blend = Path(bpy.data.filepath)
out = blend.parents[1] / "compo" / "base"
layout.operator("wm.path_open", text='Open render folder', icon='FILE_FOLDER').filepath = str(out)
"""
## unused -- (integrated in sidebar_panel)
class GPTB_PT_cam_ref_panel(bpy.types.Panel):
bl_label = "Background imgs"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_parent_id = "GPTB_PT_sidebar_panel"
@classmethod
def poll(cls, context):
return context.scene.camera
def draw_header(self,context):
self.layout.prop(context.scene.camera.data, "show_background_images", text="")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
if context.scene.camera.data.show_background_images:
for bg_img in context.scene.camera.data.background_images:
if bg_img.image:
row = layout.row(align=False)
row.label(text=bg_img.image.name, icon='IMAGE_RGB')
row.prop(bg_img, 'show_background_image', text='')# options={'HIDDEN'}
"""
def palette_manager_menu(self, context):
"""Palette menu to append in existing menu"""
# GPENCIL_MT_material_context_menu
layout = self.layout
# {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
layout.separator()
prefs = get_addon_prefs()
layout.operator("gp.load_palette", text='Load json Palette', icon='IMPORT').filepath = prefs.palette_path
layout.operator("gp.save_palette", text='Save json Palette', icon='EXPORT').filepath = prefs.palette_path
layout.operator("gp.load_blend_palette", text='Load color Palette', icon='COLOR').filepath = prefs.palette_path
classes = (
GPTB_PT_sidebar_panel,
GPTB_PT_anim_manager,
GPTB_PT_tint_layers,
GPTB_PT_render,
## GPTB_PT_cam_ref_panel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
def unregister():
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
"""
## direct panel def append (no submenu with arrow)
## need to use append and remove in register/unregister
# bpy.types.DATA_PT_gpencil_layers.append(UI_tools.GPdata_toolbox_panel)
# bpy.types.DATA_PT_gpencil_layers.remove(UI_tools.GPdata_toolbox_panel)
def GPdata_toolbox_panel(self, context):
layout = self.layout
layout.use_property_split = True
settings = context.scene.gptoolprops
col = layout.column(align = True)
col.prop(settings, 'autotint_offset')
col.operator("gp.auto_tint_gp_layers", icon = "COLOR").reset = False
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True
"""
### old
"""
col = layout.column(align = True)
col.operator("gpencil.stroke_change_color", text="Move to Color",icon = "COLOR")
col.operator("transform.shear", text="Shear")
col.operator("gpencil.stroke_cyclical_set", text="Toggle Cyclic").type = 'TOGGLE'
col.operator("gpencil.stroke_subdivide", text="Subdivide",icon = "OUTLINER_DATA_MESH")
row = layout.row(align = True)
row.operator("gpencil.stroke_join", text="Join").type = 'JOIN'
row.operator("grease_pencil.stroke_separate", text = "Separate")
col.operator("gpencil.stroke_flip", text="Flip Direction",icon = "ARROW_LEFTRIGHT")
col = layout.column(align = True)
col.operator("gptools.randomise",icon = 'RNDCURVE')
col.operator("gptools.thickness",icon = 'LINE_DATA')
col.operator("gptools.angle_split",icon = 'MOD_BEVEL',text='Angle Splitting')
col.operator("gptools.stroke_uniform_density",icon = 'MESH_DATA',text = 'Density')
row = layout.row(align = True)
row.prop(settings,"extra_tools",text='',icon = "DOWNARROW_HLT" if settings.extra_tools else "RIGHTARROW",emboss = False)
row.label("Extra tools")
if settings.extra_tools :
layout.operator_menu_enum("gpencil.stroke_arrange", text="Arrange Strokes...", property="direction") """

462
__init__.py Normal file
View File

@ -0,0 +1,462 @@
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
bl_info = {
"name": "GP toolbox",
"description": "Set of tools for Grease Pencil in animation production",
"author": "Samuel Bernou",
"version": (0, 9, 1),
"blender": (2, 91, 0),
"location": "sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "",
"doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox",
"category": "3D View",
}
from . import addon_updater_ops
from .utils import *
from .functions import *
## GMIC
from .GP_guided_colorize import GP_colorize
## direct tools
from . import OP_breakdowner
from . import OP_temp_cutter
from . import OP_canvas_rotate
from . import OP_playblast_bg
from . import OP_playblast
from . import OP_helpers
from . import OP_box_deform
from . import OP_cursor_snap_canvas
from . import OP_palettes
from . import OP_file_checker
from . import OP_render
from . import OP_copy_paste
from . import keymaps
from .OP_pseudo_tint import GPT_OT_auto_tint_gp_layers
from . import UI_tools
from .properties import GP_PG_ToolsSettings
from bpy.props import (FloatProperty,
BoolProperty,
EnumProperty,
StringProperty,
IntProperty)
import bpy
from bpy.app.handlers import persistent
from pathlib import Path
# from .eyedrop import EyeDropper
# from .properties import load_icons,remove_icons
### prefs
# def set_palette_path(self, context):
# print('value set')
# self.palette_path = Path(bpy.path.abspath(self["palette_path"])).as_posix()
class GPTB_prefs(bpy.types.AddonPreferences):
bl_idname = __name__
## tabs
pref_tabs : bpy.props.EnumProperty(
items=(('PREF', "Preferences", "Change some preferences of the modal"),
('MAN_OPS', "Operator", "Operator to add Manually"),
# ('TUTO', "Tutorial", "How to use the tool"),
# ('GMIC', "Gmic color", "Options to use gmic to colorize"),
('UPDATE', "Update", "Check and apply updates"),
# ('KEYMAP', "Keymap", "customise the default keymap"),
),
default='PREF')
## addon pref updater props
auto_check_update : BoolProperty(
name="Auto-check for Update",
description="If enabled, auto-check for updates using an interval",
default=False,
)
updater_intrval_months : IntProperty(
name='Months',
description="Number of months between checking for updates",
default=0,
min=0
)
updater_intrval_days : IntProperty(
name='Days',
description="Number of days between checking for updates",
default=7,
min=0,
max=31
)
updater_intrval_hours : IntProperty(
name='Hours',
description="Number of hours between checking for updates",
default=0,
min=0,
max=23
)
updater_intrval_minutes : IntProperty(
name='Minutes',
description="Number of minutes between checking for updates",
default=0,
min=0,
max=59
)
## addon prefs
## Project preferences
# subtype (string) Enumerator in ['FILE_PATH', 'DIR_PATH', 'FILE_NAME', 'BYTE_STRING', 'PASSWORD', 'NONE'].
## fps
fps : IntProperty(
name='Frame Rate',
description="Fps of the project, Used to conform the file when you use Check file operator",
default=25,
min=1,
max=10000
)
## output settings for automated renders
output_parent_level = IntProperty(
name='Parent level',
description="Go up in folder to define a render path relative to the file in upper directotys",
default=0,
min=0,
max=20
)
output_path : StringProperty(
name="Output path",
description="Path relative to blend to place render",
default="//render", maxlen=0, subtype='DIR_PATH')
separator : StringProperty(
name="Namespace separator",
description="Character delimiter to use for detecting namespace (prefix), default is '_', space if nothing specified",
default="_", maxlen=0, subtype='NONE')
palette_path : StringProperty(
name="Palettes directory",
description="Path to palette containing palette.json files to save and load",
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
## Playblast prefs
playblast_auto_play : BoolProperty(
name="Playblast auto play",
description="Open rendered playblast when finished",
default=True,
)
playblast_auto_open_folder : BoolProperty(
name="Playblast auto open location",
description="Open folder of rendered playblast when finished",
default=False,
)
## Canvas rotate
canvas_use_shortcut: BoolProperty(
name = "Use Default Shortcut",
description = "Use default shortcut: mouse double-click + modifier",
default = True)
mouse_click : EnumProperty(
name="Mouse button", description="click on right/left/middle mouse button in combination with a modifier to trigger alignement",
default='RIGHTMOUSE',
items=(
('RIGHTMOUSE', 'Right click', 'Use click on Right mouse button', 'MOUSE_RMB', 0),
('LEFTMOUSE', 'Left click', 'Use click on Left mouse button', 'MOUSE_LMB', 1),
('MIDDLEMOUSE', 'Mid click', 'Use click on Mid mouse button', 'MOUSE_MMB', 2),
))
use_shift: BoolProperty(
name = "combine with shift",
description = "add shift combined with double click to trigger alignement",
default = False)
use_alt: BoolProperty(
name = "combine with alt",
description = "add alt combined with double click to trigger alignement (default)",
default = True)
use_ctrl: BoolProperty(
name = "combine with ctrl",
description = "add ctrl combined with double click to trigger alignement",
default = True)
## default active tool to use
select_active_tool : EnumProperty(
name="Default selection tool", description="Active tool to set when launching check fix scene",
default='builtin.select_lasso',
items=(
('none', 'Dont change', 'Let the current active tool without change', 0),#'MOUSE_RMB'
('builtin.select', 'Select tweak', 'Use active select tweak active tool', 1),#'MOUSE_RMB'
('builtin.select_box', 'Select box', 'Use active select box active tool', 2),#'MOUSE_LMB'
('builtin.select_circle', 'Select circle', 'Use active select circle active tool', 3),#'MOUSE_MMB'
('builtin.select_lasso', 'Select lasso', 'Use active select lasso active tool', 4),#'MOUSE_MMB'
))
## render settings
render_obj_exclusion : StringProperty(
name="GP obj exclude filter",
description="List comma separated words to exclude from render list",
default="old,rough,trash,test")#, subtype='FILE_PATH')
render_res_x : IntProperty(
name='Resolution X',
description="Resolution on X",
default=2048,
min=1,
max=10000
)
render_res_y : IntProperty(
name='Resolution Y',
description="Resolution on Y",
default=1080,
min=1,
max=10000
)
## GMIColor
use_color_tools : BoolProperty(
name = "Use color tools",
description = "Enable guided color tools panel",
default = False)
#-# gmic tools not ready
gmic_path : StringProperty(
name="Path to gmic", description="Need to specify path to gmic binary to allow color pixel propagation features",
default="", subtype='FLIE_PATH')
## Temp cutter
# temp_cutter_use_shortcut: BoolProperty(
# name = "Use temp cutter Shortcut",
# description = "Auto assign shortcut for temp_cutter",
# default = True)
def draw(self, context):
layout = self.layout## random color
# layout.use_property_split = True
row= layout.row(align=True)
row.prop(self, "pref_tabs", expand=True)
if self.pref_tabs == 'PREF':
box = layout.box()
box.label(text='Random color options:')
box.prop(self, 'separator')
box = layout.box()
box.label(text='Project settings')
## Render
# box.label(text='Render option:')
box.prop(self, 'fps')
row = box.row(align = True)
row.label(text='Render resolution')
row.prop(self, 'render_res_x', text='X')
row.prop(self, 'render_res_y', text='Y')
## Palette
box.label(text='Palette library folder:')
box.prop(self, 'palette_path')
## render output
## ?? maybe add an option for absolute path (not really usefull in prod) ??
box.prop(self, 'output_path')
### TODO add render settings
# layout.separator()## Playblast
box = layout.box()
box.label(text='Playblast options:')
box.prop(self, 'playblast_auto_play')
box.prop(self, 'playblast_auto_open_folder')
# box.separator()## Canvas
box = layout.box()
box.label(text='Canvas rotate options:')
box.prop(self, "canvas_use_shortcut", text='Bind shortcuts')
if self.canvas_use_shortcut:
row = box.row()
row.label(text="After changes, use the Bind/Rebind button")#icon=""
row.operator("prefs.rebind_shortcut", text='Bind/Rebind shortcuts', icon='FILE_REFRESH')#EVENT_SPACEKEY
row = box.row(align = True)
row.prop(self, "use_ctrl", text='Ctrl')#, expand=True
row.prop(self, "use_alt", text='Alt')#, expand=True
row.prop(self, "use_shift", text='Shift')#, expand=True
row.prop(self, "mouse_click",text='')#expand=True
if not self.use_ctrl and not self.use_alt and not self.use_shift:
box.label(text="Choose at least one modifier to combine with click (default: Ctrl+Alt)", icon="ERROR")# INFO
else:
box.label(text="No hotkey has been set automatically. Following operators needs to be set manually:", icon="ERROR")
box.label(text="view3d.rotate_canvas")
# TODO get that off once proper register update from properties
if self.canvas_use_shortcut:
OP_canvas_rotate.register_keymaps()
else:
OP_canvas_rotate.unregister_keymaps()
## Active tool
box = layout.box()
box.label(text='Autofix check button options:')
box.prop(self, "select_active_tool", icon='RESTRICT_SELECT_OFF')
box.prop(self, "render_obj_exclusion", icon='FILTER')#
# if self.pref_tabs == 'GMIC':
# gmic (Disabled - tool complete WIP)
box = layout.box()
box.label(text='Colorisation:')
box.prop(self, 'use_color_tools')
if self.use_color_tools:
col = box.column(align=False)
## Delete if gmic is unused
col.prop(self, 'gmic_path')
if not self.gmic_path:
box=col.box()
box.label(text='Gmic is missing. (needed for pixel color tools)', icon='INFO')
row = box.row()
row.label(text='1.Download GMIC CLI (Command-line interface) here:')
row.operator("wm.url_open", text="Get gmic").url = "https://gmic.eu/download.shtml"
box.label(text='2.unzip it somewhere and point to gmic.exe')
box.label(text='(If gmic is already in your PATH, just write "gmic")')
if self.pref_tabs == 'MAN_OPS':
# layout.separator()## notes
# layout.label(text='Notes:')
layout.label(text='Following operators ID have to be set manually :')
## keyframe jump
box = layout.box()
box.label(text='GP keyframe jump (consider only GP keyframe, multiple options available at setup)')
row = box.row()
row.label(text='screen.gp_keyframe_jump')
row.operator('wm.copytext', icon='COPYDOWN').text = 'screen.gp_keyframe_jump'
# layout.separator()
## Snap cursor to GP
box = layout.box()
box.label(text='Snap cursor to GP canvas (if not autoset)')
row = box.row()
row.label(text='Look for default 3d snap operators by searching "view3d.cursor3d"')
row.operator('wm.copytext', text='Copy "view3d.cursor3d"', icon='COPYDOWN').text = 'view3d.cursor3d'
row = box.row()
row.label(text='Replace wanted by "view3d.cusor_snap"')
row.operator('wm.copytext', text='Copy "view3d.cusor_snap"', icon='COPYDOWN').text = 'view3d.cusor_snap'
box.label(text='Or just create a new shortcut using cursor_snap')
## user prefs
box = layout.box()
box.label(text='Note: You can access user pref file and startup file in config folder')
box.operator("wm.path_open", text='Open config location').filepath = bpy.utils.user_resource('CONFIG')
if self.pref_tabs == 'UPDATE':
addon_updater_ops.update_settings_ui(self, context)
### --- REGISTER ---
# class GP_PG_ToolsSettings(bpy.types.PropertyGroup) :
# autotint_offset = bpy.props.IntProperty(name="Tint hue offset", description="offset the tint by this value for better color", default=0, min=-5000, max=5000, soft_min=-999, soft_max=999, step=1)#, subtype='PERCENTAGE'
@persistent
def remap_relative(dummy):
all_path = [lib for lib in bpy.utils.blend_paths(local=True)]
bpy.ops.file.make_paths_relative()
for i, lib in enumerate(bpy.utils.blend_paths(local=True)):
if all_path[i] != lib:
print('Remapped:', all_path[i], '\n>> ', lib)
classes = (
GPTB_prefs,
GP_PG_ToolsSettings,
GPT_OT_auto_tint_gp_layers,
)
# register, unregister = bpy.utils.register_classes_factory(classes)
def register():
addon_updater_ops.register(bl_info)
for cls in classes:
bpy.utils.register_class(cls)
OP_box_deform.register()
OP_helpers.register()
OP_file_checker.register()
OP_breakdowner.register()
OP_temp_cutter.register()
GP_colorize.register()## GP_guided_colorize.
OP_playblast_bg.register()
OP_playblast.register()
OP_palettes.register()
OP_canvas_rotate.register()
OP_cursor_snap_canvas.register()
OP_render.register()
OP_copy_paste.register()
UI_tools.register()
keymaps.register()
bpy.types.Scene.gptoolprops = bpy.props.PointerProperty(type = GP_PG_ToolsSettings)
bpy.app.handlers.save_pre.append(remap_relative)
def unregister():
bpy.app.handlers.save_pre.remove(remap_relative)
keymaps.unregister()
addon_updater_ops.unregister()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
UI_tools.unregister()
OP_copy_paste.unregister()
OP_render.unregister()
OP_cursor_snap_canvas.unregister()
OP_canvas_rotate.unregister()
OP_palettes.unregister()
OP_file_checker.unregister()
OP_helpers.unregister()
OP_breakdowner.unregister()
OP_temp_cutter.unregister()
GP_colorize.unregister()## GP_guided_colorize.
OP_playblast_bg.unregister()
OP_playblast.unregister()
OP_box_deform.unregister()
del bpy.types.Scene.gptoolprops
if __name__ == "__main__":
register()

1673
addon_updater.py Normal file

File diff suppressed because it is too large Load Diff

1503
addon_updater_ops.py Normal file

File diff suppressed because it is too large Load Diff

381
functions.py Normal file
View File

@ -0,0 +1,381 @@
import bpy
from random import randint
from mathutils import Vector
from math import radians
from random import random as rand
import numpy as np
from bpy_extras.object_utils import world_to_camera_view as cam_space
import bmesh
from .utils import link_vert,gp_stroke_to_bmesh,draw_gp_stroke,remapping
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('there might be an errror when finding view coordinate')
return view_loc
def to_bl_image(array,img):
# Write the result to Blender preview
width = len(array[0])
height = len(array)
image = bpy.data.images.get(img)
if not image :
image = bpy.data.images.new(img,width,height)
image.generated_width = width
image.generated_height = height
output_pixels = []
for y in range (0,height):
for x in range(0,width):
col = array[y][x]
if not isinstance(col,list) :
col = [col]*3
#print(col)
output_pixels.append(col[0])
output_pixels.append(col[1])
output_pixels.append(col[2])
output_pixels.append(1)
image.pixels = output_pixels
def bm_angle_split(bm,angle) :
bm.verts.ensure_lookup_table()
loop = link_vert(bm.verts[0],[bm.verts[0]])
splitted = []
verts_to_split = [v for v in loop if len(v.link_edges) == 2 and v.calc_edge_angle() > radians(angle)]
for i,v in enumerate(verts_to_split) :
split_verts = bmesh.utils.vert_separate(v, v.link_edges)
splitted.append(split_verts[0])
if i == 0 :
splitted.append(split_verts[1])
bm.verts.ensure_lookup_table()
if splitted :
loops = []
for v in splitted :
loop = link_vert(v,[v])
loops.append(loop)
else :
loops = [loop]
return loops
def bm_uniform_density(bm,cam,max_spacing):
from bpy_extras.object_utils import world_to_camera_view as cam_space
scene = bpy.context.scene
ratio = scene.render.resolution_y/scene.render.resolution_x
for edge in bm.edges[:] :
first = Vector(cam_space(scene,cam,edge.verts[0].co)[:-1])
last = Vector(cam_space(scene,cam,edge.verts[1].co)[:-1])
first[1]*= ratio
last[1]*= ratio
length = (last-first).length
#print(length)
if length > max_spacing :
bmesh.ops.subdivide_edges(bm, edges = [edge],cuts = round(length/max_spacing)-1)
return bm
def gp_stroke_angle_split (frame,strokes,angle):
strokes_info = gp_stroke_to_bmesh(strokes)
new_strokes = []
for stroke_info in strokes_info :
bm = stroke_info['bmesh']
palette = stroke_info['color']
line_width = stroke_info['line_width']
strength = bm.verts.layers.float['strength']
pressure = bm.verts.layers.float['pressure']
select = bm.verts.layers.int['select']
splitted_loops = bm_angle_split(bm,angle)
frame.strokes.remove(stroke_info['stroke'])
for loop in splitted_loops :
loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop]
new_stroke = draw_gp_stroke(loop_info,frame,palette,width = line_width)
new_strokes.append(new_stroke)
return new_strokes
def gp_stroke_uniform_density(cam,frame,strokes,max_spacing):
strokes_info = gp_stroke_to_bmesh(strokes)
new_strokes = []
for stroke_info in strokes_info :
bm = stroke_info['bmesh'].copy()
palette = stroke_info['color']
line_width = stroke_info['line_width']
strength = bm.verts.layers.float['strength']
pressure = bm.verts.layers.float['pressure']
select = bm.verts.layers.int['select']
bm_uniform_density(bm,cam,max_spacing)
frame.strokes.remove(stroke_info['stroke'])
bm.verts.ensure_lookup_table()
loop = link_vert(bm.verts[0],[bm.verts[0]])
loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop]
new_stroke = draw_gp_stroke(loop_info,frame,palette,width = line_width)
new_strokes.append(new_stroke)
return new_strokes
def along_stroke(stroke,attr,length,min,max) :
strokelen = len(stroke.points)
for index,point in enumerate(stroke.points) :
value = getattr(point,attr)
if index < length :
remap = remapping(index/length,0,1,min,max)
setattr(point,attr,value*remap)
if index > strokelen-length :
remap = remapping((strokelen-index)/length,0,1,min,max)
setattr(point,attr,value*remap)
def randomise_points(mat,points,attr,strength) :
for point in points :
if attr is 'co' :
random_x = (rand()-0.5)
random_y = (rand()-0.5)
x = (random_x*strength, 0.0, 0.0)
y = (0.0, random_y*strength, 0.0)
point.co+= mat * Vector(x) - mat.to_translation()
point.co+= mat * Vector(y) - mat.to_translation()
else :
value = getattr(point,attr)
random = (rand()-0.5)
setattr(point,attr,value+random*strength)
def zoom_to_object(cam,resolution,box,margin=0.01) :
min_x= box[0]
max_x= box[1]
min_y= box[2]
max_y= box[3]
ratio = resolution[0]/resolution[1]
zoom_cam = cam.copy()
zoom_cam.data = zoom_cam.data.copy()
center = ((max_x+min_x)/2,(max_y+min_y)/2)
factor = max((max_x-min_x),(max_y-min_y))+margin
zoom_cam.data.shift_x += (center[0]-0.5)/factor
zoom_cam.data.shift_y += (center[1]-0.5)/factor/ratio
zoom_cam.data.lens /= factor
bpy.context.scene.objects.link(zoom_cam)
resolution = (int(resolution[0]*factor),int(resolution[1]*factor))
scene = bpy.context.scene
res_x = scene.render.resolution_x
res_y =scene.render.resolution_y
scene.render.resolution_x = resolution[0]
scene.render.resolution_y = resolution[1]
frame = zoom_cam.data.view_frame(scene)
frame = [zoom_cam.matrix_world * corner for corner in frame]
modelview_matrix = zoom_cam.matrix_world.inverted().copy()
projection_matrix = zoom_cam.calc_matrix_camera(resolution[0],resolution[1],1,1).copy()
#bpy.data.cameras.remove(zoom_cam.data)
#bpy.data.objects.remove(zoom_cam)
#bpy.context.scene.objects.link(zoom_cam)
scene.render.resolution_x = res_x
scene.render.resolution_y = res_y
#print(matrix,resolution)
return modelview_matrix,projection_matrix,frame,resolution
def set_viewport_matrix(width,height,mat):
from bgl import glViewport,glMatrixMode,GL_PROJECTION,glLoadMatrixf,Buffer,GL_FLOAT,glMatrixMode,GL_MODELVIEW,glLoadIdentity
glViewport(0,0,width,height)
#glLoadIdentity()
glMatrixMode(GL_PROJECTION)
projection = [mat[j][i] for i in range(4) for j in range(4)]
glLoadMatrixf(Buffer(GL_FLOAT, 16, projection))
#glMatrixMode( GL_MODELVIEW )
#glLoadIdentity()
# get object info
def get_object_info(mesh_groups,order_list = []) :
scene = bpy.context.scene
cam = scene.camera
#scale = scene.render.resolution_percentage / 100.0
res_x = int(scene.render.resolution_x)
res_y = int(scene.render.resolution_y)
scene.render.resolution_x = 1024
scene.render.resolution_y = 1024
cam_coord = cam.matrix_world.to_translation()
convert_table = {(255,255,255):-1,(0,0,0):0}
mesh_info = []
color_index = 1
for i,mesh_group in enumerate(mesh_groups) :
for ob in mesh_group["objects"] :
ob_info = {"object": ob, "materials" : [],"group_index" : i,'color_indexes':[]}
namespace = mesh_group['namespace']
ob_info['namespace'] = namespace
l_name = ob.name
if l_name.startswith(namespace+'_') :
l_name = namespace+'_'+'COLO_'+ob.name.split('_',1)[1]
else :
l_name = namespace+'_'+'COLO_'+l_name
ob_info['name'] = l_name
bm = bmesh.new()
bm.from_object(ob,scene)
ob_info["bm"] = bm
if not bm.verts : continue
ob_info["matrix"] = ob.matrix_world
if mesh_group.get("dupli_object") :
ob_info["matrix"] = mesh_group["dupli_object"].matrix_world * ob.matrix_world
global_bbox = [ob_info["matrix"] * Vector(v) for v in ob.bound_box]
global_bbox_center = Vector(np.mean(global_bbox,axis =0))
bbox_cam_space = [cam_space(scene,cam,p)[:-1] for p in global_bbox]
sorted_x = sorted(bbox_cam_space,key = lambda x : x[0])
sorted_y = sorted(bbox_cam_space,key = lambda x : x[1])
ob_info['box_2d']=[sorted_x[0][0],sorted_x[-1][0],sorted_y[0][1],sorted_y[-1][1]]
#print(ob_info['box_2d'])
'''
{
'x' : int(sorted_x[0][0]*res_x)-1,
'y' : int(sorted_y[0][1]*res_y)-1,
'width' : int(sorted_x[-1][0]*res_x - sorted_x[0][0]*res_x)+1,
'height' : int(sorted_y[-1][1]*res_y - sorted_y[0][1]*res_y)+1,
}
'''
#bbox_depth = [Vector(p - cam_coord).length for p in global_bbox]
#ob_info["depth"] = min(bbox_depth)
ob_info["depth"] = Vector(global_bbox_center - cam_coord).length
for slot in ob.material_slots :
mat = slot.material
mat_info = {'index' : color_index}
if mat :
color = [pow(v,1/2.2) for v in mat.diffuse_color]
name = mat.name
else :
color = [1,0,1]
name = "default"
#seed(i)
random_color = (randint(0,255),randint(0,255),randint(0,255))
if name.startswith(namespace+'_') :
name = namespace+'_'+'COLO_'+ name.split('_',1)[1]
else :
name = namespace+'_'+'COLO_'+name
mat_info["name"] = name
mat_info["color"] = color
mat_info["random_color"] = random_color
ob_info["materials"].append(mat_info)
ob_info["color_indexes"].append(color_index)
convert_table[random_color] = color_index
color_index +=1
if not ob.material_slots :
random_color = (randint(0,255),randint(0,255),randint(0,255))
ob_info["random_color"] = random_color
ob_info["color"] = (0.5,0.5,0.5)
ob_info["color_indexes"].append(color_index)
convert_table[random_color] = color_index
color_index +=1
mesh_info.append(ob_info)
mesh_info = sorted(mesh_info,key = lambda x : x['depth'],reverse=True)
#print("###")
#print([i['name'] for i in mesh_info])
if order_list :
for name in [i['name'] for i in mesh_info] :
if name not in order_list :
order_list.append(name)
mesh_info = sorted(mesh_info,key = lambda x : order_list.index(x['name']))
scene.render.resolution_x = res_x
scene.render.resolution_y = res_y
return mesh_info,convert_table

View File

@ -0,0 +1,9 @@
{
"last_check": "",
"backup_date": "",
"update_ready": false,
"ignore": false,
"just_restored": false,
"just_updated": false,
"version_text": {}
}

45
keymaps.py Normal file
View File

@ -0,0 +1,45 @@
## Pure keymaping additions
import bpy
addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
# km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")# in 3D context
# km = addon.keymaps.new(name = "Window", space_type = "EMPTY")# from everywhere
## try initiate
km = addon.keymaps.new(name = "Grease Pencil Stroke Sculpt Mode", space_type = "EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new('wm.context_toggle', type='ONE', value='PRESS')
kmi.properties.data_path='scene.tool_settings.use_gpencil_select_mask_point'
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new('wm.context_toggle', type='TWO', value='PRESS')
kmi.properties.data_path='scene.tool_settings.use_gpencil_select_mask_stroke'
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new('wm.context_toggle', type='THREE', value='PRESS')
kmi.properties.data_path='scene.tool_settings.use_gpencil_select_mask_segment'
addon_keymaps.append((km, kmi))
def unregister_keymaps():
# wm = bpy.context.window_manager
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
# wm.keyconfigs.addon.keymaps.remove(km)
addon_keymaps.clear()
def register():
if not bpy.app.background:
register_keymaps()
def unregister():
if not bpy.app.background:
unregister_keymaps()
if __name__ == "__main__":
register()

67
properties.py Normal file
View File

@ -0,0 +1,67 @@
import bpy
from bpy.props import (
IntProperty,
BoolProperty,
StringProperty,
FloatProperty,
)
from .OP_cursor_snap_canvas import cursor_follow_update
def change_edit_lines_opacity(self, context):
# for o in context.scene.objects:
# if o.type != 'GPENCIL':
# continue
# o.data.edit_line_color[3]=self.edit_lines_opacity
for gp in bpy.data.grease_pencils:
if not gp.is_annotation:
gp.edit_line_color[3]=self.edit_lines_opacity
class GP_PG_ToolsSettings(bpy.types.PropertyGroup) :
autotint_offset : IntProperty(
name="Tint hue offset", description="offset the tint by this value for better color",
default=0, min=-5000, max=5000, soft_min=-999, soft_max=999, step=1,
options={'HIDDEN'})#, subtype='PERCENTAGE'
autotint_namespace : BoolProperty(
name="Use prefix", description="Put same color on layers unsing the same prefix (separated by '_') of full name withjout separator",
default=True,
options={'HIDDEN'})
resolution_percentage: IntProperty(
name="Resolution %", description="Overrides resolution percentage for playblast",
default = 50, min=1, max= 100, subtype='PERCENTAGE')#, precision=0
cursor_follow : BoolProperty(
name='Cursor Follow', description="3D cursor follow active object animation when activated",
default=False, update=cursor_follow_update)
edit_lines_opacity : FloatProperty(
name="edit lines Opacity", description="Change edit lines opacity for all grease pencils", default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)#, get=None, set=None
## render
name_for_current_render : StringProperty(
name="Render_name", description="Name use for render current",
default="")
"""
reconnect_parent = bpy.props.PointerProperty(type =bpy.types.Object,poll=poll_armature)
render_settings = bpy.props.BoolProperty(default = False)
render_color = bpy.props.BoolProperty(default = True)
render_contour = bpy.props.BoolProperty(default = False)
precision = bpy.props.IntProperty(default = 75,subtype = 'PERCENTAGE',min=0,max=100)
border_render = bpy.props.BoolProperty(default = False)
spacialize = bpy.props.BoolProperty(default = False)
depth = bpy.props.FloatProperty(default = 2.0)
extra_tools = bpy.props.BoolProperty(default = False)
enable_ob_filter = bpy.props.BoolProperty(default = False)
auto_cursor = bpy.props.BoolProperty(default = True)
opacity_layers = bpy.props.FloatProperty(min=0,max=1,default = 1,update = update_layers_opacity)
stroke_select = bpy.props.EnumProperty(items = [("POINT","Point",""),("STROKE","Stroke","")],update = update_selection_mode)
"""

688
utils.py Normal file
View File

@ -0,0 +1,688 @@
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):
''' 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'
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
## 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'):
def draw(self, context):
for l in _message:
if isinstance(l, str):
self.layout.label(text=l)
else:
self.layout.label(text=l[0], icon=l[1])
if isinstance(_message, str):
_message = [_message]
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)