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 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)\ \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): 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'} 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 removed_ct = 0 avoid_ct = 0 for layer in layers: 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 = [] ## 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: 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 ## 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 --- ## 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 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 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 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'} def draw_delete_view_bound_ui(self, context): layout = self.layout layout.operator('grease_pencil.delete_view_bound', text='Delete View bound', icon='LOCKVIEW_ON') # RESTRICT_VIEW_ON classes = ( # GP_OT_delete_strokes_view_bound, # GP_OT_delete_points_view_bound, GP_OT_delete_view_bound, ) def register(): if bpy.app.background: return for cl in classes: bpy.utils.register_class(cl) bpy.types.VIEW3D_MT_edit_greasepencil_delete.append(draw_delete_view_bound_ui) ## make scene property for empty key preservation and bake movement for layers... # register_keymaps() def unregister(): if bpy.app.background: return bpy.types.VIEW3D_MT_edit_greasepencil_delete.remove(draw_delete_view_bound_ui) # unregister_keymaps() for cl in reversed(classes): bpy.utils.unregister_class(cl)