From 9fd3b4af83e5b705d2f4f48932c240a6a8fb6ccc Mon Sep 17 00:00:00 2001 From: pullusb Date: Wed, 30 Jul 2025 12:13:04 +0200 Subject: [PATCH] Fix delete view bound and add multiframe support Also better report --- CHANGELOG.md | 7 +- OP_delete_viewbound.py | 176 ++++++++--------------------------------- 2 files changed, 38 insertions(+), 145 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af3bf91..54e5195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,16 @@ # Changelog + 4.2.0 - added: Delete Grease pencil strokes or points view bound (selection outside of viewport region is untouched) - No preset shortcut - - `grease_pencil.delete_view_bound` - - Added to Delete menu (au) + - operator id_name: `grease_pencil.delete_view_bound` + - Added to Delete menu + - support multiframe + - Only do a dissolve (delete for full stroke) 4.1.3 diff --git a/OP_delete_viewbound.py b/OP_delete_viewbound.py index da434bb..49d080a 100644 --- a/OP_delete_viewbound.py +++ b/OP_delete_viewbound.py @@ -24,147 +24,16 @@ def is_stroke_in_view(stroke, matrix, region, region_3d): 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): bl_idname = "grease_pencil.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'} # def invoke(self, context, event): + ## TODO: add scope (default to automatic) + ## TODO: add option for normal delete (currenly only dissolve), probably another operator. @classmethod 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 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] region = context.region region_3d = context.space_data.region_3d - ## TODO for frames, handle multiframe editing - + removed_ct = 0 avoid_ct = 0 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 + if not drawing or not drawing.strokes: + continue 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)] for sid, stroke in enumerate(drawing.strokes): 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) if select_mode == 'STROKE': stroke_indices_to_delete.append(sid) + removed_ct += 1 continue ## --- 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]) 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) continue @@ -241,13 +123,21 @@ class GP_OT_delete_view_bound(bpy.types.Operator): p.radius = point_attrs[i][2] p.opacity = point_attrs[i][3] 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: # 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} out of view") + if removed_ct and avoid_ct: + 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'}