Fix delete view bound and add multiframe support

Also better report
This commit is contained in:
pullusb 2025-07-30 12:13:04 +02:00
parent a86a1fd409
commit 9fd3b4af83
2 changed files with 38 additions and 145 deletions

View File

@ -1,13 +1,16 @@
# Changelog # Changelog
4.2.0 4.2.0
- added: Delete Grease pencil strokes or points view bound (selection outside of viewport region is untouched) - added: Delete Grease pencil strokes or points view bound (selection outside of viewport region is untouched)
- No preset shortcut - No preset shortcut
<!-- - `grease_pencil.delete_points_view_bound` for points --> <!-- - `grease_pencil.delete_points_view_bound` for points -->
<!-- - `grease_pencil.delete_strokes_view_bound` for strokes --> <!-- - `grease_pencil.delete_strokes_view_bound` for strokes -->
- `grease_pencil.delete_view_bound` - operator id_name: `grease_pencil.delete_view_bound`
- Added to Delete menu (au) - Added to Delete menu
- support multiframe
- Only do a dissolve (delete for full stroke)
4.1.3 4.1.3

View File

@ -24,147 +24,16 @@ def is_stroke_in_view(stroke, matrix, region, region_3d):
return False return False
""" # separated operators for strokes and points
class GP_OT_delete_strokes_view_bound(bpy.types.Operator):
bl_idname = "grease_pencil.delete_strokes_view_bound"
bl_label = "Delete Strokes View Bound "
bl_description = "Delete all selected strokes in view only"
bl_options = {'REGISTER', 'UNDO'}
# def invoke(self, context, event):
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_GREASE_PENCIL'
def execute(self, context):
gp = context.grease_pencil
if not gp:
self.report({'ERROR'}, "No Grease Pencil object found")
return {'CANCELLED'}
ob = context.object
if not ob or ob.type != 'GREASEPENCIL':
self.report({'ERROR'}, "Active object is not a Grease Pencil object")
return {'CANCELLED'}
matrix_world = ob.matrix_world.copy()
layers = [l for l in gp.layers if not l.hide and not l.lock]
region = context.region
region_3d = context.space_data.region_3d
## TODO for frames, handle multiframe editing
for layer in layers:
for frame in layer.frames:
drawing = frame.drawing
stroke_indices_to_delete = [sidx for sidx, stroke in enumerate(drawing.strokes) if stroke.select and is_stroke_in_view(stroke, matrix_world, region, region_3d)]
## detailed method
# stroke_indices_to_delete = []
# for sid, stroke in enumerate(drawing.strokes):
# if not stroke.select:
# continue
# # Check if the stroke is within the view bounds
# if is_stroke_in_view(stroke, matrix_world, region, region_3d):
# stroke_indices_to_delete.append(sid)
if stroke_indices_to_delete:
print(f'layer {layer.name} : delete {len(stroke_indices_to_delete)} strokes')
drawing.remove_strokes(indices=stroke_indices_to_delete)
return {'FINISHED'}
class GP_OT_delete_points_view_bound(bpy.types.Operator):
bl_idname = "grease_pencil.delete_points_view_bound"
bl_label = "Delete Points View Bound"
bl_description = "Delete all selected points in view only"
bl_options = {'REGISTER', 'UNDO'}
# def invoke(self, context, event):
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_GREASE_PENCIL'
def execute(self, context):
gp = context.grease_pencil
if not gp:
self.report({'ERROR'}, "No Grease Pencil object found")
return {'CANCELLED'}
ob = context.object
if not ob or ob.type != 'GREASEPENCIL':
self.report({'ERROR'}, "Active object is not a Grease Pencil object")
return {'CANCELLED'}
matrix_world = ob.matrix_world.copy()
layers = [l for l in gp.layers if not l.hide and not l.lock]
region = context.region
region_3d = context.space_data.region_3d
## TODO for frames, handle multiframe editing
avoid_ct = 0
for layer in layers:
for frame in layer.frames:
drawing = frame.drawing
stroke_indices_to_delete = []
for sid, stroke in enumerate(drawing.strokes):
if not stroke.select:
continue
# Check if the stroke is within the view bounds
if not is_stroke_in_view(stroke, matrix_world, region, region_3d):
avoid_ct += 1
continue
## remove points (only remove by an amount from the end . need to rebuild the stroke !)
select_count = len([p for p in stroke.points if p.select])
if select_count == len(stroke.points):
## mark as full delete and go next
stroke_indices_to_delete.append(sid)
continue
## Make a copy of deselected points attributes
point_attrs = []
for p in stroke.points:
if p.select:
continue
point_attrs.append([p.position.copy(), p.rotation, p.radius, p.opacity])
## remove as many points as select_count
stroke.remove_points(select_count)
## re-assign attributes in order
for i, p in enumerate(stroke.points):
if i < len(point_attrs):
p.position = point_attrs[i][0]
p.rotation = point_attrs[i][1]
p.radius = point_attrs[i][2]
p.opacity = point_attrs[i][3]
p.select = False # Deselect all points after processing
if stroke_indices_to_delete:
# print(f'layer {layer.name} : delete {len(stroke_indices_to_delete)} strokes')
drawing.remove_strokes(indices=stroke_indices_to_delete)
if avoid_ct:
self.report({'WARNING'}, f"Skipped {avoid_ct} strokes not in view")
return {'FINISHED'}
"""
class GP_OT_delete_view_bound(bpy.types.Operator): class GP_OT_delete_view_bound(bpy.types.Operator):
bl_idname = "grease_pencil.delete_view_bound" bl_idname = "grease_pencil.delete_view_bound"
bl_label = "Delete View Bound" bl_label = "Delete View Bound"
bl_description = "Delete all selected strokes / points only if there are in view (current viewport region)" bl_description = "Delete all selected strokes / points only if there are in view (current viewport region)\
\nnote: does not work in multiframe yet)"
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
# def invoke(self, context, event): # def invoke(self, context, event):
## TODO: add scope (default to automatic)
## TODO: add option for normal delete (currenly only dissolve), probably another operator.
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -184,20 +53,32 @@ class GP_OT_delete_view_bound(bpy.types.Operator):
select_mode = context.scene.tool_settings.gpencil_selectmode_edit select_mode = context.scene.tool_settings.gpencil_selectmode_edit
matrix_world = ob.matrix_world.copy() matrix_world = ob.matrix_world.copy()
## only visibile and unlocked layer in active GP object
layers = [l for l in gp.layers if not l.hide and not l.lock] layers = [l for l in gp.layers if not l.hide and not l.lock]
region = context.region region = context.region
region_3d = context.space_data.region_3d region_3d = context.space_data.region_3d
## TODO for frames, handle multiframe editing removed_ct = 0
avoid_ct = 0 avoid_ct = 0
for layer in layers: for layer in layers:
for frame in layer.frames: if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
## multiframe, affect all selected frames and active frame
target_frames = [f for f in layer.frames if f.select or f == layer.current_frame()]
else:
## Only active frame
current = layer.current_frame()
if not current:
continue
target_frames = [current]
for frame in target_frames:
drawing = frame.drawing drawing = frame.drawing
if not drawing or not drawing.strokes:
continue
stroke_indices_to_delete = [] stroke_indices_to_delete = []
## For stroke mode ## List comprehention version (For stroke mode only)
# stroke_indices_to_delete = [sidx for sidx, stroke in enumerate(drawing.strokes) if stroke.select and is_stroke_in_view(stroke, matrix_world, region, region_3d)] # stroke_indices_to_delete = [sidx for sidx, stroke in enumerate(drawing.strokes) if stroke.select and is_stroke_in_view(stroke, matrix_world, region, region_3d)]
for sid, stroke in enumerate(drawing.strokes): for sid, stroke in enumerate(drawing.strokes):
if not stroke.select: if not stroke.select:
@ -211,14 +92,15 @@ class GP_OT_delete_view_bound(bpy.types.Operator):
## No need for further check if we are in stroke mod (direct delete) ## No need for further check if we are in stroke mod (direct delete)
if select_mode == 'STROKE': if select_mode == 'STROKE':
stroke_indices_to_delete.append(sid) stroke_indices_to_delete.append(sid)
removed_ct += 1
continue continue
## --- Handle points --- ## --- Handle points ---
## Dissolve mode remove points (only remove by an amount from the end . need to rebuild the stroke !) ## remove points, only Dissolve for now.
select_count = len([p for p in stroke.points if p.select]) select_count = len([p for p in stroke.points if p.select])
if select_count == len(stroke.points): if select_count == len(stroke.points):
## mark as full delete and go next ## Mark as full delete and go next
stroke_indices_to_delete.append(sid) stroke_indices_to_delete.append(sid)
continue continue
@ -242,12 +124,20 @@ class GP_OT_delete_view_bound(bpy.types.Operator):
p.opacity = point_attrs[i][3] p.opacity = point_attrs[i][3]
p.select = False # Deselect all points after processing p.select = False # Deselect all points after processing
removed_ct += 1
## End of stroke loop (here either part if the stroke is deleted or marked for deletion )
if stroke_indices_to_delete: if stroke_indices_to_delete:
# print(f'layer {layer.name} : delete {len(stroke_indices_to_delete)} strokes') # print(f'layer {layer.name} : delete {len(stroke_indices_to_delete)} strokes')
drawing.remove_strokes(indices=stroke_indices_to_delete) drawing.remove_strokes(indices=stroke_indices_to_delete)
if avoid_ct: if removed_ct and avoid_ct:
self.report({'WARNING'}, f"Skipped {avoid_ct} out of view") self.report({'INFO'}, f"Skipped {avoid_ct} out of view")
elif not removed_ct and not avoid_ct:
self.report({'WARNING'}, "Nothing to Delete")
elif not removed_ct and avoid_ct:
self.report({'WARNING'}, "All selected strokes are out of view")
return {'FINISHED'} return {'FINISHED'}