From 93b7ad91d8dcae185e5a98290435a6f4e2627b2e Mon Sep 17 00:00:00 2001 From: pullusb Date: Tue, 29 Jul 2025 17:41:07 +0200 Subject: [PATCH] Add special GP stroke deleter bound to vierw 4.2.0 - added: Delete Grease pencil strokes or points view bound (selection outside of viewport region is untouched) - No preset shortcut: - `gp.delete_points_view_bound` for points - `gp.delete_strokes_view_bound` for strokes --- CHANGELOG.md | 7 ++ OP_delete_viewbound.py | 185 +++++++++++++++++++++++++++++++++++++++++ __init__.py | 4 +- 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 OP_delete_viewbound.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0712a..c428572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +4.2.0 + +- added: Delete Grease pencil strokes or points view bound (selection outside of viewport region is untouched) + - No preset shortcut: + - `gp.delete_points_view_bound` for points + - `gp.delete_strokes_view_bound` for strokes + 4.1.3 - fixed: error in "remove stroke duplication" using file checker diff --git a/OP_delete_viewbound.py b/OP_delete_viewbound.py new file mode 100644 index 0000000..8f14016 --- /dev/null +++ b/OP_delete_viewbound.py @@ -0,0 +1,185 @@ +import bpy + +from . import utils +from bpy_extras.view3d_utils import location_3d_to_region_2d + +def is_stroke_in_view(stroke, matrix, region, region_3d): + ## for optimization, use first and last + region_width = region.width + region_height = region.height + point_list = [stroke.points[0], stroke.points[-1]] + # point_list = stroke.points # whole points + + coord_list = [matrix @ point.position for point in point_list] + + for coord in coord_list: + # Convert 3D coordinates to 2D screen coordinates + screen_coord = location_3d_to_region_2d(region, region_3d, coord) + if screen_coord is None: + continue + # Check if the point is within the viewport bounds + if 0 <= screen_coord.x <= region_width and 0 <= screen_coord.y <= region_height: + ## one point in view, in view + return True + + return False + + +class GPT_OT_delete_strokes_view_bound(bpy.types.Operator): + bl_idname = "gp.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 GPT_OT_delete_points_view_bound(bpy.types.Operator): + bl_idname = "gp.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({'INFO'}, f"Skipped {avoid_ct} strokes not in view") + return {'FINISHED'} + + + + +classes = ( + GPT_OT_delete_strokes_view_bound, + GPT_OT_delete_points_view_bound, +) + +def register(): + if bpy.app.background: + return + + for cl in classes: + bpy.utils.register_class(cl) + + ## make scene property for empty key preservation and bake movement for layers... + # register_keymaps() + +def unregister(): + if bpy.app.background: + return + + # unregister_keymaps() + for cl in reversed(classes): + bpy.utils.unregister_class(cl) diff --git a/__init__.py b/__init__.py index 810dfa5..82558af 100755 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "GP toolbox", "description": "Tool set for Grease Pencil in animation production", "author": "Samuel Bernou, Christophe Seux", -"version": (4, 1, 3), +"version": (4, 2, 0), "blender": (4, 3, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "", @@ -47,6 +47,7 @@ from . import OP_layer_namespace from . import OP_pseudo_tint from . import OP_follow_curve from . import OP_material_move_to_layer +from . import OP_delete_viewbound # from . import OP_eraser_brush # from . import TOOL_eraser_brush from . import handler_draw_cam @@ -805,6 +806,7 @@ addon_modules = ( OP_layer_nav, OP_follow_curve, OP_material_move_to_layer, + OP_delete_viewbound, # OP_eraser_brush, # TOOL_eraser_brush, # experimental eraser brush handler_draw_cam,