move material to layer feature

3.1.0

- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
gpv2
pullusb 2024-05-30 18:33:05 +02:00
parent 92e53f8368
commit 01ce06201e
5 changed files with 251 additions and 57 deletions

View File

@ -1,5 +1,9 @@
# Changelog
3.1.0
- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
3.0.2
- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.

View File

@ -0,0 +1,171 @@
import bpy
from bpy.types import Operator
import mathutils
from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils
from . import utils
# def get_layer_list(self, context):
# '''return (identifier, name, description) of enum content'''
# if not context:
# return [('None', 'None','None')]
# if not context.object:
# return [('None', 'None','None')]
# return [(l.info, l.info, '') for l in context.object.data.layers] # if l != context.object.data.layers.active
## in Class
# bl_property = "layers_enum"
# layers_enum : bpy.props.EnumProperty(
# name="Send Material To Layer",
# description="Send active material to layer",
# items=get_layer_list,
# options={'HIDDEN'},
# )
class GPTB_OT_move_material_to_layer(Operator) :
bl_idname = "gp.move_material_to_layer"
bl_label = 'Move Material To Layer'
bl_description = 'Move active material to an existing or new layer'
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
layer_name : bpy.props.StringProperty(
name='Layer Name', default='', options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def invoke(self, context, event):
if self.layer_name:
return self.execute(context)
if not len(context.object.data.layers):
self.report({'WARNING'}, 'No layers on current GP object')
return {'CANCELLED'}
mat = context.object.data.materials[context.object.active_material_index]
self.mat_name = mat.name
# wm.invoke_search_popup(self)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
# layout.operator_context = "INVOKE_DEFAULT"
layout.label(text=f'Move material "{self.mat_name}" to layer:', icon='MATERIAL')
col = layout.column()
col.prop(self, 'layer_name', text='', icon='ADD')
# if self.layer_name:
# col.label(text='Ok/Enter to create new layer', icon='INFO')
col.separator()
for l in reversed(context.object.data.layers):
icon = 'GREASEPENCIL' if l == context.object.data.layers.active else 'BLANK1'
row = col.row()
row.alignment = 'LEFT'
col.operator('gp.move_material_to_layer', text=l.info, icon=icon, emboss=False).layer_name = l.info
def execute(self, context):
if not self.layer_name:
print('Out')
return {'CANCELLED'}
## Active + selection
pool = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
if not context.object in pool:
pool.append(context.object)
mat = context.object.data.materials[context.object.active_material_index]
print(f'Moving strokes using material "{mat.name}" on {len(pool)} object(s)')
# import time
# t = time.time() # Dbg
total = 0
oct = 0
for ob in pool:
mat_index = next((i for i, ms in enumerate(ob.material_slots) if ms.material and ms.material == mat), None)
if mat_index is None:
print(f'/!\ {ob.name} has no Material {mat.name} in stack')
continue
gpl = ob.data.layers
if not (target_layer := gpl.get(self.layer_name)):
target_layer = gpl.new(self.layer_name)
## List existing frames
key_dict = {f.frame_number : f for f in target_layer.frames}
### Move Strokes to a new key (or existing key if comming for yet another layer)
fct = 0
sct = 0
for l in gpl:
if l == target_layer:
## ! infinite loop if target layer is included
continue
for f in l.frames:
## skip if no stroke has active material
if not next((s for s in f.strokes if s.material_index == mat_index), None):
continue
## Get/Create a destination frame and keep a reference to it
if not (dest_key := key_dict.get(f.frame_number)):
dest_key = target_layer.frames.new(f.frame_number)
key_dict[dest_key.frame_number] = dest_key
print(f'{ob.name} : frame {f.frame_number}')
## Replicate strokes in dest_keys
stroke_to_delete = []
for s in f.strokes:
if s.material_index == mat_index:
utils.copy_stroke_to_frame(s, dest_key)
stroke_to_delete.append(s)
## Debug
# if time.time() - t > 10:
# print('TIMEOUT')
# return {'CANCELLED'}
sct += len(stroke_to_delete)
# print('Removing frames') # Dbg
## Remove from source frame (f)
for s in reversed(stroke_to_delete):
f.strokes.remove(s)
## ? Remove frame if layer is empty ? -> probably not, will show previous frame
fct += 1
l.frames.update()
if fct:
oct += 1
print(f'{ob.name}: Moved {fct} frames -> {sct} Strokes') # Dbg
total += fct
report_type = 'INFO' if total else 'WARNING'
self.report({report_type}, f'Moved {total} frames accross {oct} object(s)')
return {'FINISHED'}
# def menu_duplicate_and_send_to_layer(self, context):
# if context.space_data.ui_mode == 'GPENCIL':
# self.layout.operator_context = 'INVOKE_REGION_WIN'
# self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
# self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
classes = (
GPTB_OT_move_material_to_layer,
)
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

@ -432,6 +432,8 @@ def palette_manager_menu(self, context):
layout.separator()
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.separator()
layout.operator("gp.move_material_to_layer", text='Move Material To Layer', icon='MATERIAL')
def expose_use_channel_color_pref(self, context):

View File

@ -4,7 +4,7 @@ bl_info = {
"name": "GP toolbox",
"description": "Tool set for Grease Pencil in animation production",
"author": "Samuel Bernou, Christophe Seux",
"version": (3, 0, 2),
"version": (3, 1, 0),
"blender": (4, 0, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "",
@ -46,6 +46,7 @@ from . import OP_git_update
from . import OP_layer_namespace
from . import OP_pseudo_tint
from . import OP_follow_curve
from . import OP_material_move_to_layer
# from . import OP_eraser_brush
# from . import TOOL_eraser_brush
from . import handler_draw_cam
@ -803,6 +804,7 @@ addon_modules = (
OP_layer_picker,
OP_layer_nav,
OP_follow_curve,
OP_material_move_to_layer,
# OP_eraser_brush,
# TOOL_eraser_brush, # experimental eraser brush
handler_draw_cam,

127
utils.py
View File

@ -2,12 +2,13 @@ import bpy, os
import numpy as np
import bmesh
import mathutils
from mathutils import Vector
import math
from math import sqrt
from sys import platform
import subprocess
from math import sqrt
from mathutils import Vector
from sys import platform
""" def get_gp_parent(layer) :
@ -263,55 +264,6 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
### GP funcs
# -----------------
""" V1
def get_gp_draw_plane(obj=None):
''' return tuple with plane coordinate and normal
of the curent drawing accordign to geometry'''
context = bpy.context
settings = context.scene.tool_settings
orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR'
loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE'
if obj:
mat = obj.matrix_world
else:
mat = context.object.matrix_world if context.object else None
# -> placement
if loc == "CURSOR":
plane_co = context.scene.cursor.location
else: # ORIGIN (also on origin if set to 'SURFACE', 'STROKE')
if not context.object:
plane_co = None
else:
plane_co = context.object.matrix_world.to_translation()# context.object.location
# -> orientation
if orient == 'VIEW':
#only depth is important, no need to get view vector
plane_no = None
elif orient == 'AXIS_Y':#front (X-Z)
plane_no = Vector((0,1,0))
plane_no.rotate(mat)
elif orient == 'AXIS_X':#side (Y-Z)
plane_no = Vector((1,0,0))
plane_no.rotate(mat)
elif orient == 'AXIS_Z':#top (X-Y)
plane_no = Vector((0,0,1))
plane_no.rotate(mat)
elif orient == 'CURSOR':
plane_no = Vector((0,0,1))
plane_no.rotate(context.scene.cursor.matrix)
return plane_co, plane_no
"""
## V2
def get_gp_draw_plane(obj=None, orient=None):
''' return tuple with plane coordinate and normal
of the curent drawing according to geometry'''
@ -459,14 +411,14 @@ def get_gp_datas(selection=True):
print('EOL. No active GP object')
return []
def get_gp_layer(gp_data_block,name) :
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) :
def get_gp_frame(layer, frame_nb=None) :
scene = bpy.context.scene
if not frame_nb :
frame_nb = scene.frame_current
@ -532,9 +484,72 @@ def selected_strokes(frame):
stlist.append(s)
return stlist
from math import sqrt
from mathutils import Vector
## Copy stroke to a frame
def copy_stroke_to_frame(s, frame, select=True):
'''Copy stroke to given frame
return created stroke
'''
ns = frame.strokes.new()
## Set strokes attr
stroke_attr = [
'line_width',
'material_index',
'draw_cyclic',
'use_cyclic',
'uv_scale',
'uv_rotation',
'hardness',
'uv_translation',
'vertex_color_fill',
]
for attr in stroke_attr:
if not hasattr(s, attr):
continue
# print(f'transfer stroke {attr}') # Dbg
setattr(ns, attr, getattr(s, attr))
## create points
point_count = len(s.points)
ns.points.add(len(s.points))
## Set points attr
# for p, np in zip(s.points, ns.points):
flat_list = [0.0] * point_count
flat_uv_fill_list = [0.0, 0.0] * point_count
flat_vector_list = [0.0, 0.0, 0.0] * point_count
flat_color_list = [0.0, 0.0, 0.0, 0.0] * point_count
single_attr = [
'pressure',
'strength',
'uv_factor',
'uv_rotation',
]
for attr in single_attr:
# print(f'transfer point {attr}') # Dbg
s.points.foreach_get(attr, flat_list)
ns.points.foreach_set(attr, flat_list)
# print(f'transfer point co') # Dbg
s.points.foreach_get('co', flat_vector_list)
ns.points.foreach_set('co', flat_vector_list)
# print(f'transfer point uv_fill') # Dbg
s.points.foreach_get('uv_fill', flat_uv_fill_list)
ns.points.foreach_set('uv_fill', flat_uv_fill_list)
# print(f'transfer point vertex_color') # Dbg
s.points.foreach_get('vertex_color', flat_color_list)
ns.points.foreach_set('vertex_color', flat_color_list)
ns.select = select
ns.points.update()
return ns
# -----------------
### Vector utils 3d