layer group - named regex - fill tool mat picker
1.6.0 - feat: Namespace upgrade - support pseudo group naming - add group and indent button - feat: Fill tool shortcut for material color picker (from closest stroke) - `S` : get material closest *fill* stroke - `Àlt+S` : get closest stroke (fill or stroke)gpv2
parent
7ff96ad205
commit
3974a15ff0
|
@ -1,5 +1,14 @@
|
|||
# Changelog
|
||||
|
||||
1.6.0
|
||||
|
||||
- feat: Namespace upgrade
|
||||
- support pseudo group naming
|
||||
- add group and indent button
|
||||
- feat: Fill tool shortcut for material color picker (from closest stroke)
|
||||
- `S` : get material closest *fill* stroke
|
||||
- `Àlt+S` : get closest stroke (fill or stroke)
|
||||
|
||||
1.5.8
|
||||
|
||||
- feat: Namespace improvement:
|
||||
|
|
|
@ -9,24 +9,76 @@ from .utils import get_addon_prefs
|
|||
|
||||
# --- OPS ---
|
||||
|
||||
# pattern = r'([A-Z]{2})?_?([A-Z]{2})?_?(.*)' # bad ! match whithout separator
|
||||
# PATTERN = r'([A-Z]{2})?_?([A-Z]{2})?_?(.*)' # bad ! match whithout separator
|
||||
# pattern = r'(?:(^[A-Z]{2})_)?(?:([A-Z]{2})_)?(.*)' # matching only two letter
|
||||
# pattern = r'^([A-Z]{2}_)?([A-Z]{2}_)?(.*)' # matching letters with separator
|
||||
|
||||
# pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*)' # matching capital letters from one to six
|
||||
pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*?)(_[A-Z]{2})?$' # 2 letter suffix
|
||||
# pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*?)(_[A-Z]{2})?$' # 2 letter suffix
|
||||
# pattern = r'^(?P<tag>[A-Z]{1,6}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?$' # named
|
||||
# pattern = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?$' # group start ' - '
|
||||
# PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
|
||||
PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
|
||||
|
||||
|
||||
def layer_name_build(layer, prefix='', desc='', suffix=''):
|
||||
'''GET a layer and argumen to build and assign name'''
|
||||
|
||||
|
||||
prefs = get_addon_prefs()
|
||||
sep = prefs.separator
|
||||
name = layer.info
|
||||
|
||||
pattern = PATTERN.replace('_', sep) # set separator
|
||||
|
||||
res = re.search(pattern, name.strip())
|
||||
|
||||
# prefix -> tag
|
||||
# prefix2 -> tag2
|
||||
# desc -> name
|
||||
# suffix -> sfix
|
||||
grp = '' if res.group('grp') is None else res.group('grp')
|
||||
tag = '' if res.group('tag') is None else res.group('tag')
|
||||
# tag2 = '' if res.group('tag2') is None else res.group('tag2')
|
||||
name = '' if res.group('name') is None else res.group('name')
|
||||
sfix = '' if res.group('sfix') is None else res.group('sfix')
|
||||
inc = '' if res.group('inc') is None else res.group('inc')
|
||||
|
||||
if grp:
|
||||
grp = ' ' + grp # name is strip(), so grp first spaces are gones.
|
||||
|
||||
if prefix:
|
||||
if prefix == 'prefixkillcode':
|
||||
tag = ''
|
||||
else:
|
||||
tag = prefix.upper().strip() + sep
|
||||
# if prefix2:
|
||||
# tag2 = prefix2.upper().strip() + sep
|
||||
if desc:
|
||||
name = desc
|
||||
|
||||
if suffix:
|
||||
if suffix == 'suffixkillcode':
|
||||
sfix = ''
|
||||
else:
|
||||
sfix = sep + suffix.upper().strip()
|
||||
|
||||
# check if name is available without the increment ending
|
||||
new = f'{grp}{tag}{name}{sfix}'
|
||||
|
||||
layer.info = new
|
||||
|
||||
"""
|
||||
def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
|
||||
'''GET a layer and infos to build name
|
||||
Can take one or two prefix and description/name of the layer)
|
||||
'''
|
||||
|
||||
global pattern
|
||||
|
||||
prefs = get_addon_prefs()
|
||||
sep = prefs.separator
|
||||
name = layer.info
|
||||
|
||||
|
||||
pattern = pattern.replace('_', sep) # set separator
|
||||
|
||||
res = re.search(pattern, name.strip())
|
||||
|
@ -55,6 +107,7 @@ def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
|
|||
|
||||
new = f'{p1}{p2}{p3}{p4}'
|
||||
layer.info = new
|
||||
"""
|
||||
|
||||
## multi-prefix solution (Caps letters)
|
||||
class GPTB_OT_layer_name_build(Operator):
|
||||
|
@ -68,7 +121,7 @@ class GPTB_OT_layer_name_build(Operator):
|
|||
return True
|
||||
|
||||
prefix : StringProperty(default='', options={'SKIP_SAVE'})
|
||||
prefix2 : StringProperty(default='', options={'SKIP_SAVE'})
|
||||
# prefix2 : StringProperty(default='', options={'SKIP_SAVE'})
|
||||
desc : StringProperty(default='', options={'SKIP_SAVE'})
|
||||
suffix : StringProperty(default='', options={'SKIP_SAVE'})
|
||||
|
||||
|
@ -82,7 +135,69 @@ class GPTB_OT_layer_name_build(Operator):
|
|||
|
||||
for l in gpl:
|
||||
if l.select or l == act:
|
||||
layer_name_build(l, prefix=self.prefix, prefix2=self.prefix2, desc=self.desc, suffix=self.suffix)
|
||||
layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def grp_toggle(l):
|
||||
grp_item_id = ' - '
|
||||
res = re.search(r'^(\s{1,3}-\s{0,3})(.*)', l.info)
|
||||
if not res:
|
||||
# add group prefix after stripping all space and dash
|
||||
l.info = grp_item_id + l.info.lstrip(' -')
|
||||
else:
|
||||
# delete group prefix
|
||||
l.info = res.group(2)
|
||||
|
||||
class GPTB_OT_layer_group_toggle(Operator):
|
||||
bl_idname = "gp.layer_group_toggle"
|
||||
bl_label = "Group Toggle"
|
||||
bl_description = "Group or ungroup a layer"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
# group : StringProperty(default='', options={'SKIP_SAVE'})
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
gpl = ob.data.layers
|
||||
act = gpl.active
|
||||
if not act:
|
||||
self.report({'ERROR'}, 'no layer active')
|
||||
return {"CANCELLED"}
|
||||
for l in gpl:
|
||||
if l.select or l == act:
|
||||
grp_toggle(l)
|
||||
return {"FINISHED"}
|
||||
|
||||
class GPTB_OT_layer_new_group(Operator):
|
||||
bl_idname = "gp.layer_new_group"
|
||||
bl_label = "New Group"
|
||||
bl_description = "Create a group from active layer"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
gpl = ob.data.layers
|
||||
act = gpl.active
|
||||
if not act:
|
||||
self.report({'ERROR'}, 'no layer active')
|
||||
return {"CANCELLED"}
|
||||
res = re.search(PATTERN, act.info)
|
||||
if not res:
|
||||
self.report({'ERROR'}, 'Could not create a group name, create a layer manually')
|
||||
return {"CANCELLED"}
|
||||
name = res.group('name')
|
||||
if not name:
|
||||
self.report({'ERROR'}, f'No name found in {act.info}')
|
||||
return {"CANCELLED"}
|
||||
if name in [l.info for l in gpl]:
|
||||
self.report({'WARNING'}, f'Name already exists: {act.info}')
|
||||
return {"FINISHED"}
|
||||
gpl.new(name, set_active=False)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
@ -120,13 +235,12 @@ class GPTB_OT_select_set_same_prefix(Operator):
|
|||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
global pattern
|
||||
prefs = get_addon_prefs()
|
||||
sep = prefs.separator # '_'
|
||||
gpl = context.object.data.layers
|
||||
act = gpl.active
|
||||
|
||||
res = re.search(pattern, act.info)
|
||||
res = re.search(PATTERN, act.info)
|
||||
if not res:
|
||||
# self.report({'ERROR'}, f'Error scanning {act.info}')
|
||||
return {"CANCELLED"}
|
||||
|
@ -288,7 +402,7 @@ class GPTB_OT_rename_gp_layer(Operator):
|
|||
|
||||
## --- UI layer panel---
|
||||
|
||||
def layer_name_builder(self, context):
|
||||
def layer_name_builder_ui(self, context):
|
||||
'''appended to DATA_PT_gpencil_layers'''
|
||||
|
||||
prefs = get_addon_prefs()
|
||||
|
@ -319,6 +433,8 @@ def layer_name_builder(self, context):
|
|||
## name (description)
|
||||
row = col.row(align=True)
|
||||
row.prop(context.scene.gptoolprops, 'layer_name', text='')
|
||||
row.operator("gp.layer_new_group", text='', icon='COLLECTION_NEW')
|
||||
row.operator("gp.layer_group_toggle", text='', icon='OUTLINER_OB_GROUP_INSTANCE')
|
||||
## no need for desc ops, already trigerred from update
|
||||
# row.operator("gp.layer_name_build", text='', icon='EVENT_RETURN').desc = context.scene.gptoolprops.layer_name
|
||||
|
||||
|
@ -328,6 +444,8 @@ def layer_name_builder(self, context):
|
|||
row = col.row(align=True)
|
||||
row.operator("gp.layer_name_build", text=suffix.upper() ).suffix = suffix
|
||||
row.operator("gp.layer_name_build", text='', icon='X').suffix = 'suffixkillcode'
|
||||
|
||||
|
||||
|
||||
|
||||
## --- UI dopesheet ---
|
||||
|
@ -360,13 +478,16 @@ def obj_layer_name_callback():
|
|||
return
|
||||
if not ob.data.layers.active:
|
||||
return
|
||||
pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*?)(_[A-Z]{2})?$'
|
||||
res = re.search(pattern, ob.data.layers.active.info)
|
||||
|
||||
for l in ob.data.layers:
|
||||
l.select = l == ob.data.layers.active
|
||||
|
||||
res = re.search(PATTERN, ob.data.layers.active.info)
|
||||
if not res:
|
||||
return
|
||||
if not res.group(3):
|
||||
if not res.group('name'):
|
||||
return
|
||||
bpy.context.scene.gptoolprops['layer_name'] = res.group(3)
|
||||
bpy.context.scene.gptoolprops['layer_name'] = res.group('name')
|
||||
|
||||
@persistent
|
||||
def subscribe_handler(dummy):
|
||||
|
@ -387,6 +508,8 @@ def subscribe_handler(dummy):
|
|||
classes=(
|
||||
GPTB_OT_rename_gp_layer,
|
||||
GPTB_OT_layer_name_build,
|
||||
GPTB_OT_layer_group_toggle,
|
||||
GPTB_OT_layer_new_group,
|
||||
GPTB_OT_select_set_same_prefix,
|
||||
GPTB_OT_select_set_same_color,
|
||||
)
|
||||
|
@ -395,7 +518,7 @@ def register():
|
|||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.DATA_PT_gpencil_layers.prepend(layer_name_builder)
|
||||
bpy.types.DATA_PT_gpencil_layers.prepend(layer_name_builder_ui)
|
||||
bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header)
|
||||
bpy.types.GPENCIL_MT_layer_context_menu.append(gpencil_layer_dropdown_menu)
|
||||
bpy.app.handlers.load_post.append(subscribe_handler) # need to restart after first activation
|
||||
|
@ -404,7 +527,7 @@ def unregister():
|
|||
bpy.app.handlers.load_post.remove(subscribe_handler)
|
||||
bpy.types.GPENCIL_MT_layer_context_menu.remove(gpencil_layer_dropdown_menu)
|
||||
bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header)
|
||||
bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder)
|
||||
bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder_ui)
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
import bpy
|
||||
from bpy.types import Operator
|
||||
import mathutils
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
from bpy_extras import view3d_utils
|
||||
from .utils import get_gp_draw_plane, location_to_region, region_to_location
|
||||
|
||||
### passing by 2D projection
|
||||
def get_3d_coord_on_drawing_plane_from_2d(context, co):
|
||||
plane_co, plane_no = get_gp_draw_plane(context)
|
||||
rv3d = context.region_data
|
||||
view_mat = rv3d.view_matrix.inverted()
|
||||
if not plane_no:
|
||||
plane_no = Vector((0,0,1))
|
||||
plane_no.rotate(view_mat)
|
||||
depth_3d = view_mat @ Vector((0, 0, -1000))
|
||||
org = region_to_location(co, view_mat.to_translation())
|
||||
view_point = region_to_location(co, depth_3d)
|
||||
hit = geometry.intersect_line_plane(org, view_point, plane_co, plane_no)
|
||||
|
||||
if hit and plane_no:
|
||||
return context.object, hit, plane_no
|
||||
|
||||
return None, None, None
|
||||
|
||||
class GP_OT_pick_closest_material(Operator):
|
||||
bl_idname = "gp.pick_closest_material"
|
||||
bl_label = "Get Closest Stroke Material"
|
||||
bl_description = "Pick closest stroke material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
|
||||
|
||||
fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||
|
||||
def filter_stroke(self, context):
|
||||
# get stroke under mouse using kdtree
|
||||
point_pair = [(p.co, s) for s in self.stroke_list for p in s.points] # local space
|
||||
|
||||
kd = mathutils.kdtree.KDTree(len(point_pair))
|
||||
for i, pair in enumerate(point_pair):
|
||||
kd.insert(pair[0], i)
|
||||
kd.balance()
|
||||
|
||||
## Get 3D coordinate on drawing plane according to mouse 2d.co on flat 2d drawing
|
||||
_ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse)
|
||||
|
||||
if not hit:
|
||||
return 'No hit on drawing plane', None
|
||||
|
||||
mouse_3d = hit
|
||||
mouse_local = self.inv_mat @ mouse_3d # local space
|
||||
co, index, _dist = kd.find(mouse_local) # local space
|
||||
# co, index, _dist = kd.find(mouse_3d) # world space
|
||||
# context.scene.cursor.location = co # world space
|
||||
s = point_pair[index][1]
|
||||
|
||||
## find point index in stroke
|
||||
self.idx = None
|
||||
for i, p in enumerate(s.points):
|
||||
if p.co == co:
|
||||
self.idx = i
|
||||
break
|
||||
|
||||
del point_pair
|
||||
return s, self.ob.matrix_world @ co
|
||||
|
||||
def invoke(self, context, event):
|
||||
# self.prefs = get_addon_prefs()
|
||||
self.ob = context.object
|
||||
self.gp = self.ob.data
|
||||
|
||||
self.stroke_list = []
|
||||
self.inv_mat = self.ob.matrix_world.inverted()
|
||||
|
||||
if self.gp.use_multiedit:
|
||||
for l in self.gp.layers:
|
||||
if l.lock or l.hide:
|
||||
continue
|
||||
for f in l.frames:
|
||||
if not f.select:
|
||||
continue
|
||||
for s in f.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
else:
|
||||
# [s for l in self.gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
|
||||
for l in self.gp.layers:
|
||||
if l.lock or l.hide or not l.active_frame:
|
||||
continue
|
||||
for s in l.active_frame.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
if self.fill_only:
|
||||
self.stroke_list = [s for s in self.stroke_list if self.ob.data.materials[s.material_index].grease_pencil.show_fill]
|
||||
|
||||
if not self.stroke_list:
|
||||
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
self.stroke, self.coord = self.filter_stroke(context)
|
||||
if isinstance(self.stroke, str):
|
||||
self.report({'ERROR'}, self.stroke)
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
del self.stroke_list
|
||||
|
||||
if self.idx is None:
|
||||
self.report({'WARNING'}, 'No coord found')
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].co
|
||||
self.init_pos = [p.co.copy() for p in self.stroke.points] # need a copy otherwise vector is updated
|
||||
## directly use world position ?
|
||||
# self.pos_world = [self.ob.matrix_world @ co for co in self.init_pos]
|
||||
self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
|
||||
self.plen = len(self.stroke.points)
|
||||
|
||||
# context.scene.cursor.location = self.coord #Dbg
|
||||
return self.execute(context)
|
||||
# context.window_manager.modal_handler_add(self)
|
||||
# return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
self.ob.active_material_index = self.stroke.material_index
|
||||
self.report({'INFO'}, f'Mat: {self.ob.data.materials[self.stroke.material_index].name}')
|
||||
return {'FINISHED'}
|
||||
|
||||
# def modal(self, context, event):
|
||||
# if event.type == 'MOUSEMOVE':
|
||||
# mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
# delta = mouse - self.init_mouse
|
||||
|
||||
# if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
|
||||
# print(f'{self.stroke}, points num {len(self.stroke.points)}, material index:{self.stroke.material_index}')
|
||||
# return {'FINISHED'}
|
||||
|
||||
# if event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
# # for i, p in enumerate(self.stroke.points): # reset position
|
||||
# # self.stroke.points[i].co = self.init_pos[i]
|
||||
# context.area.tag_redraw()
|
||||
# return {'CANCELLED'}
|
||||
|
||||
# return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
addon_keymaps = []
|
||||
def register_keymaps():
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Draw brush)", space_type = "EMPTY", region_type='WINDOW')
|
||||
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY", region_type='WINDOW')
|
||||
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Fill)", space_type = "EMPTY", region_type='WINDOW')
|
||||
kmi = km.keymap_items.new(
|
||||
# name="",
|
||||
idname="gp.pick_closest_material",
|
||||
type="S", # type="LEFTMOUSE",
|
||||
value="PRESS",
|
||||
shift=False,
|
||||
ctrl=False,
|
||||
alt = False,
|
||||
oskey=False,
|
||||
# key_modifier='S', # S like Sample
|
||||
)
|
||||
kmi.properties.fill_only = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
kmi = km.keymap_items.new(
|
||||
# name="",
|
||||
idname="gp.pick_closest_material",
|
||||
type="S", # type="LEFTMOUSE",
|
||||
value="PRESS",
|
||||
alt = True,
|
||||
# key_modifier='S', # S like Sample
|
||||
)
|
||||
|
||||
# kmi = km.keymap_items.new('catname.opsname', type='F5', value='PRESS')
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
def unregister_keymaps():
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
|
||||
classes=(
|
||||
GP_OT_pick_closest_material,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
register_keymaps()
|
||||
|
||||
def unregister():
|
||||
unregister_keymaps()
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
|
@ -15,7 +15,7 @@ bl_info = {
|
|||
"name": "GP toolbox",
|
||||
"description": "Set of tools for Grease Pencil in animation production",
|
||||
"author": "Samuel Bernou, Christophe Seux",
|
||||
"version": (1, 5, 7),
|
||||
"version": (1, 6, 0),
|
||||
"blender": (2, 91, 0),
|
||||
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||
"warning": "",
|
||||
|
@ -49,6 +49,7 @@ from . import OP_realign
|
|||
from . import OP_depth_move
|
||||
from . import OP_key_duplicate_send
|
||||
from . import OP_layer_manager
|
||||
from . import OP_material_picker
|
||||
from . import OP_eraser_brush
|
||||
from . import TOOL_eraser_brush
|
||||
from . import handler_draw_cam
|
||||
|
@ -564,6 +565,7 @@ def register():
|
|||
OP_key_duplicate_send.register()
|
||||
OP_layer_manager.register()
|
||||
OP_eraser_brush.register()
|
||||
OP_material_picker.register()
|
||||
TOOL_eraser_brush.register()
|
||||
handler_draw_cam.register()
|
||||
UI_tools.register()
|
||||
|
@ -589,6 +591,7 @@ def unregister():
|
|||
bpy.utils.unregister_class(cls)
|
||||
UI_tools.unregister()
|
||||
handler_draw_cam.unregister()
|
||||
OP_material_picker.unregister()
|
||||
OP_eraser_brush.unregister()
|
||||
TOOL_eraser_brush.unregister()
|
||||
OP_layer_manager.unregister()
|
||||
|
|
Loading…
Reference in New Issue