Add Eraser Tool
1.5.0 - feat: Eraser Brush Tool (Need to be enable in the preferences)gpv2
parent
3d7a208a50
commit
6eabc02ce8
|
@ -0,0 +1,467 @@
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
import bgl
|
||||||
|
from gpu_extras.presets import draw_circle_2d
|
||||||
|
from time import time
|
||||||
|
from mathutils import Vector, Matrix, Euler
|
||||||
|
from mathutils.kdtree import KDTree
|
||||||
|
from mathutils.geometry import intersect_line_plane, intersect_line_sphere_2d, intersect_line_line
|
||||||
|
from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vector_3d, \
|
||||||
|
location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
|
||||||
|
def get_gp_mat(gp, name, set_active=False):
|
||||||
|
mat = bpy.data.materials.get(name)
|
||||||
|
if not mat:
|
||||||
|
mat = bpy.data.materials.new(name)
|
||||||
|
bpy.data.materials.create_gpencil_data(mat)
|
||||||
|
|
||||||
|
if mat not in gp.data.materials[:]:
|
||||||
|
gp.data.materials.append(mat)
|
||||||
|
mat_index = gp.data.materials[:].index(mat)
|
||||||
|
|
||||||
|
if set_active:
|
||||||
|
gp.active_material_index = mat_index
|
||||||
|
|
||||||
|
return mat
|
||||||
|
|
||||||
|
def get_gp_frame(gp_layer, frame=None):
|
||||||
|
if frame is None:
|
||||||
|
frame = bpy.context.scene.frame_current
|
||||||
|
gp_frame = next((f for f in gp_layer.frames if f.frame_number==frame), None)
|
||||||
|
if not gp_frame:
|
||||||
|
gp_frame = gp_layer.frames.new(frame)
|
||||||
|
|
||||||
|
return gp_frame
|
||||||
|
|
||||||
|
def get_gp_layer(gp, name=None):
|
||||||
|
if not name:
|
||||||
|
return gp.data.layers.active
|
||||||
|
|
||||||
|
layer = gp.data.layers.get(name)
|
||||||
|
if not layer:
|
||||||
|
layer = gp.data.layers.new(name)
|
||||||
|
|
||||||
|
gp.data.layers.active = layer
|
||||||
|
|
||||||
|
return layer
|
||||||
|
|
||||||
|
def co_2d_to_3d(co, depth=0.1):
|
||||||
|
area = bpy.context.area
|
||||||
|
region = bpy.context.region
|
||||||
|
rv3d = area.spaces.active.region_3d
|
||||||
|
view_mat = rv3d.view_matrix.inverted()
|
||||||
|
org = view_mat.to_translation()
|
||||||
|
|
||||||
|
depth_3d = view_mat @ Vector((0, 0, -depth))
|
||||||
|
#org = region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
|
||||||
|
|
||||||
|
return region_2d_to_location_3d(region, rv3d, co, depth_3d)
|
||||||
|
|
||||||
|
|
||||||
|
#vec = (region_2d_to_origin_3d(region, rv3d, co) - org).normalized()
|
||||||
|
|
||||||
|
return org + vec
|
||||||
|
|
||||||
|
return org + region_2d_to_vector_3d(region, rv3d , co)
|
||||||
|
|
||||||
|
def get_cuts_data(strokes, mouse, radius):
|
||||||
|
gp = bpy.context.object
|
||||||
|
|
||||||
|
area = bpy.context.area
|
||||||
|
region = bpy.context.region
|
||||||
|
rv3d = area.spaces.active.region_3d
|
||||||
|
view_mat = rv3d.view_matrix.inverted()
|
||||||
|
org = view_mat.to_translation()
|
||||||
|
mat = gp.matrix_world
|
||||||
|
|
||||||
|
cuts_data = []
|
||||||
|
for s in strokes:
|
||||||
|
is_polyline = 2<=len(s.points)<=5
|
||||||
|
|
||||||
|
if not is_polyline and not s.select:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for i, p in enumerate(s.points):
|
||||||
|
|
||||||
|
if not p.select and not is_polyline:
|
||||||
|
continue
|
||||||
|
# Test if the next or previous is unselected
|
||||||
|
|
||||||
|
edges = []
|
||||||
|
|
||||||
|
if i > 0:
|
||||||
|
prev_p = s.points[i-1]
|
||||||
|
if not prev_p.select and not is_polyline:
|
||||||
|
edges.append((i-1, i))
|
||||||
|
|
||||||
|
if i < len(s.points)-1:
|
||||||
|
next_p = s.points[i+1]
|
||||||
|
if not next_p.select or is_polyline:
|
||||||
|
edges.append((i, i+1))
|
||||||
|
|
||||||
|
for p1_index, p2_index in edges:
|
||||||
|
p1 = s.points[p1_index]
|
||||||
|
p2 = s.points[p2_index]
|
||||||
|
|
||||||
|
length_3d = (p2.co-p1.co).length
|
||||||
|
|
||||||
|
p1_3d = mat @ p1.co
|
||||||
|
p2_3d = mat @ p2.co
|
||||||
|
|
||||||
|
p1_2d = location_3d_to_region_2d(region, rv3d, p1_3d)
|
||||||
|
p2_2d = location_3d_to_region_2d(region, rv3d, p2_3d)
|
||||||
|
|
||||||
|
if p1_2d is None or p2_2d is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
length_2d = (p2_2d-p1_2d).length
|
||||||
|
|
||||||
|
if length_2d <= 4:
|
||||||
|
continue
|
||||||
|
|
||||||
|
intersects = intersect_line_sphere_2d(p1_2d, p2_2d, mouse, radius+2)
|
||||||
|
intersects = [i for i in intersects if i is not None]
|
||||||
|
if not intersects:
|
||||||
|
continue
|
||||||
|
|
||||||
|
close_points = [(p1_2d-i).length < 1 or (p2_2d-i).length < 1 for i in intersects]
|
||||||
|
if any(close_points):
|
||||||
|
#print('close_points', close_points)
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
line_intersects = []
|
||||||
|
for i_2d in intersects:
|
||||||
|
#factor = ((i_2d-p1_2d).length) / length_2d
|
||||||
|
#factor_3d = factor_2d * length_3d
|
||||||
|
#vec = region_2d_to_vector_3d(region, rv3d, i_2d)
|
||||||
|
p3_3d = region_2d_to_location_3d(region, rv3d, i_2d, org)
|
||||||
|
p4_3d = region_2d_to_origin_3d(region, rv3d, i_2d)
|
||||||
|
|
||||||
|
#bpy.context.scene.cursor.location = p3_3d
|
||||||
|
|
||||||
|
line_intersect = intersect_line_line(p1_3d, p2_3d, p3_3d, p4_3d)
|
||||||
|
if not line_intersect:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
i1_3d, _ = line_intersect
|
||||||
|
|
||||||
|
line_intersects += [i1_3d]
|
||||||
|
|
||||||
|
#context.scene.cursor.location = i1_3d
|
||||||
|
|
||||||
|
if line_intersects:
|
||||||
|
line_intersects.sort(key=lambda x : (x-p1_3d).length)
|
||||||
|
#cut_data[-1].sort(key=lambda x : (x-p1_3d).length)
|
||||||
|
cut_data = [p1_index, p2_index, s, line_intersects]
|
||||||
|
cuts_data.append(cut_data)
|
||||||
|
|
||||||
|
return cuts_data
|
||||||
|
|
||||||
|
class GPTB_OT_eraser(Operator):
|
||||||
|
"""Draw a line with the mouse"""
|
||||||
|
bl_idname = "gp.eraser"
|
||||||
|
bl_label = "Eraser Brush"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def draw_callback_px(self):
|
||||||
|
bgl.glEnable(bgl.GL_BLEND)
|
||||||
|
draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 0.85), self.radius, 32)
|
||||||
|
bgl.glDisable(bgl.GL_BLEND)
|
||||||
|
|
||||||
|
def draw_holdout(self, context, event):
|
||||||
|
gp = context.object
|
||||||
|
mat_inv = gp.matrix_world.inverted()
|
||||||
|
mouse_3d = co_2d_to_3d(self.mouse)
|
||||||
|
radius_3d = co_2d_to_3d(self.mouse + Vector((self.radius, 0)))
|
||||||
|
search_radius = (radius_3d-mouse_3d).length
|
||||||
|
|
||||||
|
#print('search_radius', search_radius)
|
||||||
|
#print('radius', self.radius)
|
||||||
|
|
||||||
|
#bpy.context.scene.cursor.location = mouse_3d
|
||||||
|
|
||||||
|
for gp_frame, hld_stroke in self.hld_strokes:
|
||||||
|
#print('Add Point')
|
||||||
|
|
||||||
|
hld_stroke.points.add(count=1)
|
||||||
|
p = hld_stroke.points[-1]
|
||||||
|
p.co = mat_inv @ mouse_3d
|
||||||
|
p.pressure = search_radius * 2000
|
||||||
|
|
||||||
|
#context.scene.cursor.location = mouse_3d
|
||||||
|
|
||||||
|
def get_radius(self, context, event):
|
||||||
|
pressure = event.pressure or 1
|
||||||
|
return context.scene.gptoolprops.eraser_radius * pressure
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def erase(self, context, event):
|
||||||
|
gp = context.object
|
||||||
|
mat_inv = gp.matrix_world.inverted()
|
||||||
|
|
||||||
|
new_points = []
|
||||||
|
|
||||||
|
#print(self.cuts_data)
|
||||||
|
|
||||||
|
for f in self.gp_frames:
|
||||||
|
for s in [s for s in f.strokes if s.material_index==self.hld_index]:
|
||||||
|
f.strokes.remove(s)
|
||||||
|
|
||||||
|
gp.data.materials.pop(index=self.hld_index)
|
||||||
|
bpy.data.materials.remove(self.hld_mat)
|
||||||
|
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
|
||||||
|
context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
|
||||||
|
#context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
|
||||||
|
|
||||||
|
#bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||||
|
|
||||||
|
#for cut_data in self.cuts_data:
|
||||||
|
|
||||||
|
# print(cut_data, len(cut_data))
|
||||||
|
t0 = time()
|
||||||
|
print()
|
||||||
|
|
||||||
|
print('Number of cuts', len(self.mouse_path))
|
||||||
|
|
||||||
|
for mouse in self.mouse_path:
|
||||||
|
t1 = time()
|
||||||
|
|
||||||
|
print()
|
||||||
|
x, y = mouse
|
||||||
|
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||||
|
bpy.ops.gpencil.select_circle(x=x, y=y, radius=self.radius, wait_for_input=False)
|
||||||
|
|
||||||
|
strokes = [s for f in self.gp_frames for s in f.strokes]
|
||||||
|
print('select_circle', time()-t1)
|
||||||
|
|
||||||
|
t2 = time()
|
||||||
|
cut_data = get_cuts_data(strokes, mouse, self.radius)
|
||||||
|
print('get_cuts_data', time()-t2)
|
||||||
|
|
||||||
|
t3 = time()
|
||||||
|
for p1_index, p2_index, stroke, intersects in cut_data[::-1]:
|
||||||
|
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||||
|
|
||||||
|
#print('p1_index', p1_index)
|
||||||
|
#print('p2_index', p2_index)
|
||||||
|
|
||||||
|
p1 = stroke.points[p1_index]
|
||||||
|
p2 = stroke.points[p2_index]
|
||||||
|
|
||||||
|
p1.select = True
|
||||||
|
p2.select = True
|
||||||
|
|
||||||
|
number_cuts = len(intersects)
|
||||||
|
|
||||||
|
bpy.ops.gpencil.stroke_subdivide(number_cuts=number_cuts, only_selected=True)
|
||||||
|
|
||||||
|
|
||||||
|
new_p1 = stroke.points[p1_index+1]
|
||||||
|
new_p1.co = mat_inv@intersects[0]
|
||||||
|
new_points += [(stroke, p1_index+1)]
|
||||||
|
|
||||||
|
#print('number_cuts', number_cuts)
|
||||||
|
|
||||||
|
if number_cuts == 2:
|
||||||
|
new_p2 = stroke.points[p1_index+2]
|
||||||
|
new_p2.co = mat_inv@( (intersects[0] + intersects[1])/2 )
|
||||||
|
#new_points += [new_p2]
|
||||||
|
|
||||||
|
new_p3 = stroke.points[p1_index+3]
|
||||||
|
new_p3.co = mat_inv@intersects[1]
|
||||||
|
new_points += [(stroke, p1_index+3)]
|
||||||
|
|
||||||
|
print('subdivide', time() - t3)
|
||||||
|
|
||||||
|
bpy.ops.gpencil.select_circle(x=x, y=y, radius=self.radius-2, wait_for_input=False)
|
||||||
|
|
||||||
|
'''
|
||||||
|
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
|
||||||
|
tip_points = [p for s in selected_strokes for i, p in enumerate(s.points) if p.select and (i==0 or i == len(s.points)-1)]
|
||||||
|
|
||||||
|
bpy.ops.gpencil.select_less()
|
||||||
|
|
||||||
|
for p in tip_points:
|
||||||
|
p.select = True
|
||||||
|
for stroke, index in new_points:
|
||||||
|
stroke.points[index].select = False
|
||||||
|
'''
|
||||||
|
|
||||||
|
t4 = time()
|
||||||
|
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
|
||||||
|
|
||||||
|
if selected_strokes:
|
||||||
|
bpy.ops.gpencil.delete(type='POINTS')
|
||||||
|
|
||||||
|
print('remove points', time()- t4)
|
||||||
|
|
||||||
|
|
||||||
|
print('Total one cut', time()-t1)
|
||||||
|
|
||||||
|
print('Total all cuts', time()-t0)
|
||||||
|
#bpy.ops.gpencil.select_less()
|
||||||
|
#for stroke, index in new_points:
|
||||||
|
# stroke.points[index].select = False
|
||||||
|
|
||||||
|
#bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
|
||||||
|
#selected_strokes = [s for s in self.gp_frame.strokes if s.select]
|
||||||
|
#bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||||
|
|
||||||
|
self.radius = self.get_radius(context, event)
|
||||||
|
context.area.tag_redraw()
|
||||||
|
|
||||||
|
if event.type == 'LEFTMOUSE':
|
||||||
|
#self.mouse = mouse
|
||||||
|
self.mouse_path.append(self.mouse)
|
||||||
|
self.erase(context, event)
|
||||||
|
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
if (mouse-self.mouse).length < min(self.radius, 2):
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
self.mouse = mouse
|
||||||
|
|
||||||
|
if event.type in {'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE'}:
|
||||||
|
self.draw_holdout(context, event)
|
||||||
|
self.mouse_path.append(self.mouse)
|
||||||
|
#self.update_cuts_data(context, event)
|
||||||
|
#self.erase(context, event)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||||
|
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
gp = context.object
|
||||||
|
matrix = gp.matrix_world
|
||||||
|
|
||||||
|
self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||||
|
self.mouse_path = [self.mouse]
|
||||||
|
|
||||||
|
area = context.area
|
||||||
|
region = context.region
|
||||||
|
w, h = region.width, region.height
|
||||||
|
|
||||||
|
rv3d = area.spaces.active.region_3d
|
||||||
|
view_mat = rv3d.view_matrix.inverted()
|
||||||
|
org = self.org = view_mat.to_translation()
|
||||||
|
|
||||||
|
#org = region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0))
|
||||||
|
|
||||||
|
|
||||||
|
#print('ORG', org)
|
||||||
|
#print('view_mat', view_mat)
|
||||||
|
|
||||||
|
self.radius = self.get_radius(context, event)
|
||||||
|
self.cuts_data = []
|
||||||
|
|
||||||
|
#org = self.view_mat @ Vector((0, 0, -10))
|
||||||
|
#self.plane_no = self.plane_co-self.org
|
||||||
|
|
||||||
|
#bottom_left = region_2d_to_location_3d(region, rv3d , (0, 0), self.plane_co)
|
||||||
|
#bottom_right = region_2d_to_location_3d(region, rv3d , (0, w), self.plane_co)
|
||||||
|
|
||||||
|
#bottom_left = intersect_line_plane(self.org, bottom_left, self.plane_co, self.plane_no)
|
||||||
|
#bottom_right = intersect_line_plane(self.org, bottom_right, self.plane_co, self.plane_no)
|
||||||
|
|
||||||
|
#self.scale_fac = (bottom_right-bottom_left).length / w
|
||||||
|
|
||||||
|
#print('scale_fac', self.scale_fac)
|
||||||
|
#depth_location = view_mat @ Vector((0, 0, -1))
|
||||||
|
#context.scene.cursor.location = depth_location
|
||||||
|
|
||||||
|
#plane_2d = [(0, 0), (0, h), (w, h), (w, h)]
|
||||||
|
#plane_3d = [region_2d_to_location_3d(p)]
|
||||||
|
|
||||||
|
|
||||||
|
t0 = time()
|
||||||
|
gp_mats = gp.data.materials
|
||||||
|
gp_layers = [l for l in gp.data.layers if not l.lock or l.hide]
|
||||||
|
self.gp_frames = [l.active_frame for l in gp_layers]
|
||||||
|
'''
|
||||||
|
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.strokes]
|
||||||
|
points_data = [(s, f, m) for s, f, m in points_data if not m.grease_pencil.hide or m.grease_pencil.lock]
|
||||||
|
print('get_gp_points', time()-t0)
|
||||||
|
|
||||||
|
t0 = time()
|
||||||
|
#points_data = [(s, f, m, p, get_screen_co(p.co, matrix)) for s, f, m in points_data for p in reversed(s.points)]
|
||||||
|
points_data = [(s, f, m, p, org + ((matrix @ p.co)-org).normalized()*1) for s, f, m in points_data for p in reversed(s.points)]
|
||||||
|
print('points_to_2d', time()-t0)
|
||||||
|
|
||||||
|
#print(points_data)
|
||||||
|
self.points_data = [(s, f, m, p, co) for s, f, m, p, co in points_data if co is not None]
|
||||||
|
|
||||||
|
#for s, f, m, p, co in self.points_data:
|
||||||
|
# p.co = co
|
||||||
|
|
||||||
|
|
||||||
|
t0 = time()
|
||||||
|
self.kd_tree = KDTree(len(self.points_data))
|
||||||
|
for i, point_data in enumerate(self.points_data):
|
||||||
|
s, f, m, p, co = point_data
|
||||||
|
self.kd_tree.insert(co, i)
|
||||||
|
|
||||||
|
self.kd_tree.balance()
|
||||||
|
print('create kdtree', time()-t0)
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Create holdout mat
|
||||||
|
self.hld_mat = get_gp_mat(gp, name='Eraser Holdout Stroke')
|
||||||
|
self.hld_mat.grease_pencil.use_stroke_holdout = True
|
||||||
|
self.hld_mat.grease_pencil.show_stroke = True
|
||||||
|
self.hld_mat.grease_pencil.show_fill = False
|
||||||
|
self.hld_mat.grease_pencil.use_overlap_strokes = True
|
||||||
|
|
||||||
|
self.hld_index = gp_mats[:].index(self.hld_mat)
|
||||||
|
|
||||||
|
self.hld_strokes = []
|
||||||
|
for f in self.gp_frames:
|
||||||
|
hld_stroke = f.strokes.new()
|
||||||
|
hld_stroke.start_cap_mode = 'ROUND'
|
||||||
|
hld_stroke.end_cap_mode = 'ROUND'
|
||||||
|
hld_stroke.material_index = self.hld_index
|
||||||
|
|
||||||
|
#hld_stroke.line_width = self.radius
|
||||||
|
|
||||||
|
self.hld_strokes.append((f, hld_stroke))
|
||||||
|
|
||||||
|
self.draw_holdout(context, event)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
context.area.tag_redraw()
|
||||||
|
self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (), 'WINDOW', 'POST_PIXEL')
|
||||||
|
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
### --- REGISTER ---
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
GPTB_OT_eraser,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -0,0 +1,68 @@
|
||||||
|
import bpy
|
||||||
|
from bpy.types import WorkSpaceTool
|
||||||
|
from gpu_extras.presets import draw_circle_2d
|
||||||
|
from time import time
|
||||||
|
from .utils import get_addon_prefs
|
||||||
|
|
||||||
|
|
||||||
|
class GPTB_WT_eraser(WorkSpaceTool):
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_context_mode = 'PAINT_GPENCIL'
|
||||||
|
|
||||||
|
# The prefix of the idname should be your add-on name.
|
||||||
|
bl_idname = "gp.eraser_tool"
|
||||||
|
bl_label = "Eraser"
|
||||||
|
bl_description = (
|
||||||
|
"This is a tooltip\n"
|
||||||
|
"with multiple lines"
|
||||||
|
)
|
||||||
|
bl_icon = "brush.paint_vertex.draw"
|
||||||
|
bl_widget = None
|
||||||
|
bl_keymap = (
|
||||||
|
("gp.eraser", {"type": 'LEFTMOUSE', "value": 'PRESS'},
|
||||||
|
{"properties": []}),
|
||||||
|
("wm.radial_control", {"type": 'F', "value": 'PRESS'},
|
||||||
|
{"properties": [("data_path_primary", 'scene.gptoolprops.eraser_radius')]}),
|
||||||
|
)
|
||||||
|
|
||||||
|
bl_cursor = 'DOT'
|
||||||
|
|
||||||
|
'''
|
||||||
|
def draw_cursor(context, tool, xy):
|
||||||
|
from gpu_extras.presets import draw_circle_2d
|
||||||
|
|
||||||
|
radius = context.scene.gptoolprops.eraser_radius
|
||||||
|
draw_circle_2d(xy, (0.75, 0.25, 0.35, 0.85), radius, 32)
|
||||||
|
'''
|
||||||
|
|
||||||
|
def draw_settings(context, layout, tool):
|
||||||
|
layout.prop(context.scene.gptoolprops, "eraser_radius")
|
||||||
|
|
||||||
|
### --- REGISTER ---
|
||||||
|
|
||||||
|
## --- KEYMAP
|
||||||
|
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')
|
||||||
|
kmi = km.keymap_items.new("gp.eraser", type='LEFTMOUSE', value="PRESS", ctrl=True)
|
||||||
|
|
||||||
|
prefs = get_addon_prefs()
|
||||||
|
kmi.active = prefs.use_precise_eraser
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
def unregister_keymaps():
|
||||||
|
for km, kmi in addon_keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
addon_keymaps.clear()
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_tool(GPTB_WT_eraser, after={"builtin.cursor"})
|
||||||
|
#bpy.context.window_manager.keyconfigs.default.keymaps['Grease Pencil Stroke Paint (Draw brush)'].keymap_items[3].idname = 'gp.eraser'
|
||||||
|
|
||||||
|
register_keymaps()
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.utils.unregister_tool(GPTB_WT_eraser)
|
||||||
|
unregister_keymaps()
|
29
__init__.py
29
__init__.py
|
@ -14,8 +14,8 @@
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "GP toolbox",
|
"name": "GP toolbox",
|
||||||
"description": "Set of tools for Grease Pencil in animation production",
|
"description": "Set of tools for Grease Pencil in animation production",
|
||||||
"author": "Samuel Bernou",
|
"author": "Samuel Bernou, Christophe Seux",
|
||||||
"version": (1, 4, 3),
|
"version": (1, 5, 0),
|
||||||
"blender": (2, 91, 0),
|
"blender": (2, 91, 0),
|
||||||
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||||
"warning": "",
|
"warning": "",
|
||||||
|
@ -48,6 +48,8 @@ from . import OP_copy_paste
|
||||||
from . import OP_realign
|
from . import OP_realign
|
||||||
from . import OP_depth_move
|
from . import OP_depth_move
|
||||||
from . import OP_key_duplicate_send
|
from . import OP_key_duplicate_send
|
||||||
|
from . import OP_eraser_brush
|
||||||
|
from . import TOOL_eraser_brush
|
||||||
from . import handler_draw_cam
|
from . import handler_draw_cam
|
||||||
from . import keymaps
|
from . import keymaps
|
||||||
|
|
||||||
|
@ -97,9 +99,23 @@ def remap_on_save_update(self, context):
|
||||||
if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
|
if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
|
||||||
bpy.app.handlers.save_pre.remove(remap_relative)
|
bpy.app.handlers.save_pre.remove(remap_relative)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def update_use_precise_eraser(self, context):
|
||||||
|
|
||||||
|
km, kmi = TOOL_eraser_brush.addon_keymaps[0]
|
||||||
|
|
||||||
|
kmi.active = self.use_precise_eraser
|
||||||
|
|
||||||
|
|
||||||
class GPTB_prefs(bpy.types.AddonPreferences):
|
class GPTB_prefs(bpy.types.AddonPreferences):
|
||||||
bl_idname = __name__
|
bl_idname = __name__
|
||||||
|
|
||||||
|
use_precise_eraser : BoolProperty(
|
||||||
|
name='Precise Eraser',
|
||||||
|
default=False,
|
||||||
|
update=update_use_precise_eraser
|
||||||
|
)
|
||||||
## tabs
|
## tabs
|
||||||
|
|
||||||
pref_tabs : EnumProperty(
|
pref_tabs : EnumProperty(
|
||||||
|
@ -335,7 +351,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||||
|
|
||||||
# box.separator()## Keyframe jumper
|
# box.separator()## Keyframe jumper
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.label(text='Keyframe Jump option:')
|
box.label(text='Keyframe Jump options:')
|
||||||
|
|
||||||
box.prop(self, "kfj_use_shortcut", text='Bind shortcuts')
|
box.prop(self, "kfj_use_shortcut", text='Bind shortcuts')
|
||||||
if self.kfj_use_shortcut:
|
if self.kfj_use_shortcut:
|
||||||
|
@ -381,6 +397,9 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||||
box.label(text='Random color options:')
|
box.label(text='Random color options:')
|
||||||
box.prop(self, 'separator')
|
box.prop(self, 'separator')
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text='Tools options:')
|
||||||
|
box.prop(self, 'use_precise_eraser')
|
||||||
|
|
||||||
if self.pref_tabs == 'MAN_OPS':
|
if self.pref_tabs == 'MAN_OPS':
|
||||||
# layout.separator()## notes
|
# layout.separator()## notes
|
||||||
|
@ -457,6 +476,8 @@ def register():
|
||||||
OP_realign.register()
|
OP_realign.register()
|
||||||
OP_depth_move.register()
|
OP_depth_move.register()
|
||||||
OP_key_duplicate_send.register()
|
OP_key_duplicate_send.register()
|
||||||
|
OP_eraser_brush.register()
|
||||||
|
TOOL_eraser_brush.register()
|
||||||
handler_draw_cam.register()
|
handler_draw_cam.register()
|
||||||
UI_tools.register()
|
UI_tools.register()
|
||||||
keymaps.register()
|
keymaps.register()
|
||||||
|
@ -480,6 +501,8 @@ def unregister():
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
UI_tools.unregister()
|
UI_tools.unregister()
|
||||||
handler_draw_cam.unregister()
|
handler_draw_cam.unregister()
|
||||||
|
OP_eraser_brush.unregister()
|
||||||
|
TOOL_eraser_brush.unregister()
|
||||||
OP_key_duplicate_send.unregister()
|
OP_key_duplicate_send.unregister()
|
||||||
OP_depth_move.unregister()
|
OP_depth_move.unregister()
|
||||||
OP_realign.unregister()
|
OP_realign.unregister()
|
||||||
|
|
|
@ -9,12 +9,16 @@ from bpy.props import (
|
||||||
|
|
||||||
from .OP_cursor_snap_canvas import cursor_follow_update
|
from .OP_cursor_snap_canvas import cursor_follow_update
|
||||||
|
|
||||||
|
|
||||||
def change_edit_lines_opacity(self, context):
|
def change_edit_lines_opacity(self, context):
|
||||||
for gp in bpy.data.grease_pencils:
|
for gp in bpy.data.grease_pencils:
|
||||||
if not gp.is_annotation:
|
if not gp.is_annotation:
|
||||||
gp.edit_line_color[3]=self.edit_lines_opacity
|
gp.edit_line_color[3]=self.edit_lines_opacity
|
||||||
|
|
||||||
class GP_PG_ToolsSettings(bpy.types.PropertyGroup) :
|
class GP_PG_ToolsSettings(bpy.types.PropertyGroup) :
|
||||||
|
eraser_radius : IntProperty(
|
||||||
|
name="Tint hue offset", description="Radius of eraser brush",
|
||||||
|
default=20, min=0, max=500, subtype='PIXEL')
|
||||||
|
|
||||||
drawcam_passepartout : BoolProperty(
|
drawcam_passepartout : BoolProperty(
|
||||||
name="Show cam passepartout",
|
name="Show cam passepartout",
|
||||||
|
|
Loading…
Reference in New Issue