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.name, l.name, '') 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'}) copy : bpy.props.BoolProperty( name='Copy to layer', default=False, description='Copy strokes to layer instead of moving', options={'SKIP_SAVE'}) @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' 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.prop(self, 'copy', text='Copy Strokes') action_label = 'Copy' if self.copy else 'Move' layout.label(text=f'{action_label} 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' op = col.operator('gp.move_material_to_layer', text=l.name, icon=icon, emboss=False) op.layer_name = l.name op.copy = self.copy 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 == 'GREASEPENCIL'] 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 layer in gpl: if layer == target_layer: ## ! infinite loop if target layer is included continue for fr in layer.frames: ## skip if no stroke has active material if not next((s for s in fr.drawing.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(fr.frame_number)): dest_key = target_layer.frames.new(fr.frame_number) key_dict[dest_key.frame_number] = dest_key print(f'{ob.name} : frame {fr.frame_number}') ## Replicate strokes in dest_keys stroke_to_delete = [] for s_idx, s in enumerate(fr.drawing.strokes): if s.material_index == mat_index: utils.copy_stroke_to_frame(s, dest_key) stroke_to_delete.append(s_idx) ## Debug # if time.time() - t > 10: # print('TIMEOUT') # return {'CANCELLED'} sct += len(stroke_to_delete) ## Remove from source frame (fr) if not self.copy: # print('Removing frames') # Dbg if stroke_to_delete: fr.drawing.remove_strokes(indices=stroke_to_delete) ## ? Remove frame if layer is empty ? -> probably not, otherwise will show previous frame fct += 1 if fct: oct += 1 print(f'{ob.name}: Moved {fct} frames -> {sct} Strokes') # Dbg total += fct report_type = 'INFO' if total else 'WARNING' if self.copy: self.report({report_type}, f'Copied {total} frames accross {oct} object(s)') else: 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)