2026-05-11 23:06:39 +03:00
/*
* InfiniPaint
* Copyright ( C ) 2025 - 2026 Yousef Khadadeh
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < https : //www.gnu.org/licenses/>.
*/
2025-07-06 20:43:53 +03:00
# include "EraserTool.hpp"
2025-12-26 10:40:58 +03:00
# include "../DrawingProgram.hpp"
# include "../../MainProgram.hpp"
# include "../../DrawData.hpp"
2026-03-20 16:24:52 +03:00
# include <include/core/SkBlendMode.h>
2025-11-18 18:15:50 +03:00
# include <include/core/SkPathBuilder.h>
2026-05-24 11:58:52 +03:00
# include "../../CanvasComponents/BrushComponentCode.hpp"
2026-05-24 20:32:21 +03:00
# include "../EditCanvasComponentWorldUndoAction.hpp"
2026-05-26 17:08:43 +03:00
# include <include/pathops/SkPathOps.h>
2025-07-06 20:43:53 +03:00
2026-03-30 12:32:41 +03:00
# include "../../GUIStuff/ElementHelpers/TextLabelHelpers.hpp"
# include "../../GUIStuff/ElementHelpers/RadioButtonHelpers.hpp"
2026-05-24 20:32:21 +03:00
# include "../../GUIStuff/ElementHelpers/CheckBoxHelpers.hpp"
2026-03-30 12:32:41 +03:00
2025-07-06 20:43:53 +03:00
EraserTool : : EraserTool ( DrawingProgram & initDrawP ) :
2025-08-02 20:10:33 +03:00
DrawingProgramToolBase ( initDrawP )
{ }
DrawingProgramToolType EraserTool : : get_type ( ) {
return DrawingProgramToolType : : ERASER ;
2025-07-06 20:43:53 +03:00
}
2026-04-12 12:36:00 +03:00
void EraserTool : : gui_toolbox ( Toolbar & t ) {
2026-03-30 12:32:41 +03:00
using namespace GUIStuff ;
using namespace ElementHelpers ;
2026-03-31 13:17:58 +03:00
auto & gui = drawP . world . main . g . gui ;
gui . new_id ( " eraser tool " , [ & ] {
text_label_centered ( gui , " Eraser " ) ;
2026-03-30 18:51:14 +03:00
drawP . world . main . toolConfig . relative_width_gui ( drawP , " Size " ) ;
2026-03-31 13:17:58 +03:00
text_label ( gui , " Erase from: " ) ;
radio_button_selector ( gui , " layer selector " , & drawP . controls . layerSelector , {
2026-03-30 18:51:14 +03:00
{ " Layer being edited " , DrawingProgramLayerManager : : LayerSelector : : LAYER_BEING_EDITED } ,
{ " All visible layers " , DrawingProgramLayerManager : : LayerSelector : : ALL_VISIBLE_LAYERS }
} ) ;
2026-05-24 20:32:21 +03:00
checkbox_boolean_field ( gui , " erase details " , " Erase details " , & drawP . world . main . toolConfig . eraser . eraseDetail ) ;
2026-04-30 12:09:03 +03:00
} ) ;
}
void EraserTool : : gui_phone_toolbox ( PhoneDrawingProgramScreen & t ) {
using namespace GUIStuff ;
using namespace ElementHelpers ;
auto & gui = drawP . world . main . g . gui ;
gui . new_id ( " eraser tool " , [ & ] {
drawP . world . main . toolConfig . relative_width_gui ( drawP , " Size " ) ;
text_label ( gui , " Erase from: " ) ;
radio_button_selector ( gui , " layer selector " , & drawP . controls . layerSelector , {
{ " Layer being edited " , DrawingProgramLayerManager : : LayerSelector : : LAYER_BEING_EDITED } ,
{ " All visible layers " , DrawingProgramLayerManager : : LayerSelector : : ALL_VISIBLE_LAYERS }
} ) ;
2026-05-24 20:32:21 +03:00
checkbox_boolean_field ( gui , " erase details " , " Erase details " , & drawP . world . main . toolConfig . eraser . eraseDetail ) ;
2026-03-30 12:32:41 +03:00
} ) ;
2025-07-06 20:43:53 +03:00
}
2026-04-12 12:36:00 +03:00
void EraserTool : : right_click_popup_gui ( Toolbar & t , Vector2f popupPos ) {
2025-12-01 17:33:14 +03:00
t . paint_popup ( popupPos ) ;
}
2026-03-03 12:06:48 +03:00
void EraserTool : : input_mouse_button_on_canvas_callback ( const InputManager : : MouseButtonCallbackArgs & button ) {
if ( button . button = = InputManager : : MouseButton : : LEFT ) {
2026-03-31 18:41:19 +03:00
if ( button . down & & ! isErasing & & ! drawP . world . main . g . gui . cursor_obstructed ( ) ) {
2026-05-25 14:46:22 +03:00
auto relativeWidthResult = drawP . world . main . toolConfig . get_relative_width_stroke_size ( drawP , drawP . world . drawData . cam . c . inverseScale ) ;
if ( ! relativeWidthResult . first . has_value ( ) ) {
drawP . world . main . toolConfig . print_relative_width_fail_message ( relativeWidthResult . second ) ;
return ;
}
BrushComponentCode : : mouse_button ( drawP , genData , drawP . world . drawData . cam . c , button , relativeWidthResult . first . value ( ) ) ;
2026-03-03 12:06:48 +03:00
isErasing = true ;
}
2026-05-25 14:46:22 +03:00
else if ( ! button . down & & isErasing ) {
2026-05-26 17:08:43 +03:00
// If not real time eraser, we can benefit from simplifying the path
std : : optional < SkPath > simplified = Simplify ( erasePath ) ;
if ( simplified . has_value ( ) )
erasePath = simplified . value ( ) ;
2026-05-25 14:46:22 +03:00
erase_on_path ( ) ;
reset_erasing_stroke ( ) ;
commit_erase ( ) ;
isErasing = false ;
}
2026-03-03 12:06:48 +03:00
}
}
void EraserTool : : input_mouse_motion_callback ( const InputManager : : MouseMotionCallbackArgs & motion ) {
2026-05-25 14:46:22 +03:00
if ( isErasing ) {
auto & toolConfig = drawP . world . main . toolConfig ;
BrushComponentCode : : mouse_motion ( drawP , genData , motion . pos , toolConfig . get_relative_width_stroke_size ( drawP , genData . coords . inverseScale ) . first . value ( ) ) ;
}
2026-03-03 12:06:48 +03:00
}
2026-05-25 14:46:22 +03:00
void EraserTool : : input_pen_axis_callback ( const InputManager : : PenAxisCallbackArgs & axis ) {
if ( axis . axis = = SDL_PEN_AXIS_PRESSURE & & drawP . world . main . conf . tabletOptions . pressureAffectsBrushWidth ) {
genData . penWidth = axis . value ;
if ( genData . penWidth ! = 0.0f & & isErasing ) {
auto & toolConfig = drawP . world . main . toolConfig ;
float width = toolConfig . get_relative_width_stroke_size ( drawP , genData . coords . inverseScale ) . first . value ( ) ;
BrushComponentCode : : pen_pressure ( drawP , genData , width ) ;
}
}
}
void EraserTool : : erase_on_path ( ) {
2026-05-26 11:59:55 +03:00
auto cCWorldBounds = genData . coords . collider_to_world < SCollision : : AABB < WorldScalar > , SCollision : : AABB < float > > ( erasePath . getBounds ( ) ) ;
2026-05-24 11:58:52 +03:00
drawP . drawCache . traverse_bvh_run_function ( cCWorldBounds , [ & ] ( const auto & bvhNode ) {
2026-03-03 12:06:48 +03:00
if ( bvhNode & &
2026-05-26 11:59:55 +03:00
erasePath . contains ( convert_vec2 < SkPoint > ( genData . coords . to_space ( bvhNode - > bounds . min ) ) ) & &
erasePath . contains ( convert_vec2 < SkPoint > ( genData . coords . to_space ( bvhNode - > bounds . max ) ) ) & &
erasePath . contains ( convert_vec2 < SkPoint > ( genData . coords . to_space ( bvhNode - > bounds . top_right ( ) ) ) ) & &
erasePath . contains ( convert_vec2 < SkPoint > ( genData . coords . to_space ( bvhNode - > bounds . bottom_left ( ) ) ) ) ) {
2026-03-03 12:06:48 +03:00
drawP . drawCache . invalidate_cache_at_aabb ( bvhNode - > bounds ) ;
drawP . drawCache . traverse_bvh_run_function_starting_at_node_no_collision_check ( bvhNode , [ & ] ( const auto & bvhNodeChild ) {
drawP . drawCache . node_loop_erase_if_components ( bvhNodeChild , [ & ] ( auto c ) {
if ( drawP . layerMan . component_passes_layer_selector ( c , drawP . controls . layerSelector ) ) {
2026-05-24 20:32:21 +03:00
auto it = updatedComponents . find ( c ) ;
if ( it ! = updatedComponents . end ( ) ) {
c - > obj - > get_comp ( ) . set_data_from ( * ( it - > second ) ) ;
updatedComponents . erase ( it ) ;
}
2026-03-03 12:06:48 +03:00
erasedComponents . emplace ( c ) ;
return true ;
}
return false ;
} ) ;
return true ;
} ) ;
return false ;
}
drawP . drawCache . node_loop_erase_if_components ( bvhNode , [ & ] ( auto c ) {
2026-05-24 20:32:21 +03:00
if ( drawP . world . main . toolConfig . eraser . eraseDetail ) {
if ( drawP . layerMan . component_passes_layer_selector ( c , drawP . controls . layerSelector ) ) {
auto dataCopy = c - > obj - > get_comp ( ) . get_data_copy ( ) ;
2026-05-26 11:59:55 +03:00
CanvasComponentEraseDetailResult result = c - > obj - > collides_with_erase_detail ( genData . coords , erasePath ) ;
2026-05-24 20:32:21 +03:00
switch ( result ) {
case CanvasComponentEraseDetailResult : : NO_CHANGE :
return false ;
case CanvasComponentEraseDetailResult : : CHANGED : {
if ( ! updatedComponents . contains ( c ) )
updatedComponents . emplace ( c , std : : move ( dataCopy ) ) ;
2026-05-25 01:14:41 +03:00
// NOTE: commit_update is what should be run here, but invalidate_cache is being run instead. This is for a few reasons:
// - Mesh initialize_draw_data does nothing, so commit_update isnt necessary
// - worldAABB only shrinks, which means that invalidate_cache_at_optional_aabb will invalidate a "good enough" space even if worldAABB isn't updated (which commit_update does)
// - commit_update can't be run inside a traverse_bvh_run_function call (causes segfault and other issues). If that were necessary, we would have to defer that call for after the traversal is done
// - commit_update will be run at switch_tool time instead
drawP . drawCache . invalidate_cache_at_optional_aabb ( c - > obj - > get_world_bounds ( ) ) ;
2026-05-24 20:32:21 +03:00
return false ;
}
case CanvasComponentEraseDetailResult : : REMOVED :
auto it = updatedComponents . find ( c ) ;
if ( it ! = updatedComponents . end ( ) ) {
c - > obj - > get_comp ( ) . set_data_from ( * ( it - > second ) ) ;
updatedComponents . erase ( it ) ;
}
2026-05-25 14:46:22 +03:00
else
c - > obj - > get_comp ( ) . set_data_from ( * dataCopy ) ;
2026-05-24 20:32:21 +03:00
erasedComponents . emplace ( c ) ;
drawP . drawCache . invalidate_cache_at_optional_aabb ( c - > obj - > get_world_bounds ( ) ) ;
return true ;
}
}
}
else {
2026-05-26 11:59:55 +03:00
if ( drawP . layerMan . component_passes_layer_selector ( c , drawP . controls . layerSelector ) & & c - > obj - > collides_with ( genData . coords , erasePath ) ) {
2026-05-24 20:32:21 +03:00
erasedComponents . emplace ( c ) ;
drawP . drawCache . invalidate_cache_at_optional_aabb ( c - > obj - > get_world_bounds ( ) ) ;
return true ;
}
2026-03-03 12:06:48 +03:00
}
return false ;
} ) ;
return true ;
} ) ;
}
2026-05-25 14:46:22 +03:00
void EraserTool : : reset_erasing_stroke ( ) {
if ( ! genData . brushPoints . empty ( ) ) {
auto lastBrushPoint = genData . brushPoints . back ( ) ;
genData . brushPoints . clear ( ) ;
genData . brushPoints . emplace_back ( lastBrushPoint ) ;
genData . prevPointUnaltered = genData . coords . get_mouse_pos ( drawP . world ) ;
genData . addedTemporaryPoint = false ;
}
}
2025-12-30 19:46:08 +03:00
void EraserTool : : erase_component ( CanvasComponentContainer : : ObjInfo * erasedComp ) {
2025-12-15 15:36:42 +03:00
erasedComponents . erase ( erasedComp ) ;
2026-05-24 20:32:21 +03:00
updatedComponents . erase ( erasedComp ) ;
2025-12-15 15:36:42 +03:00
}
2026-05-25 14:46:22 +03:00
void EraserTool : : commit_erase ( ) {
2026-05-26 17:08:43 +03:00
bool placedNewObject = false ;
std : : unordered_map < DrawingProgramLayerListItem * , std : : vector < std : : pair < CanvasComponentContainer : : ObjInfoIterator , CanvasComponentContainer * > > > newObjectsToPlace ;
std : : erase_if ( updatedComponents , [ & ] ( auto & p ) {
auto & [ comp , oldData ] = p ;
std : : vector < CanvasComponentContainer * > splitComps = comp - > obj - > get_comp ( ) . attempt_split ( drawP ) ;
if ( ! splitComps . empty ( ) ) {
for ( CanvasComponentContainer * c : splitComps )
newObjectsToPlace [ comp - > obj - > parentLayer ] . emplace_back ( std : : next ( comp - > obj - > objInfo ) , c ) ;
placedNewObject = true ;
erasedComponents . emplace ( comp ) ;
comp - > obj - > get_comp ( ) . set_data_from ( * oldData ) ;
return true ;
}
return false ;
} ) ;
bool firstPlacement = true ;
for ( auto & [ layer , compMap ] : newObjectsToPlace ) {
drawP . layerMan . add_many_components_to_layer ( layer , compMap , firstPlacement ) ;
firstPlacement = false ;
}
bool objectsToErase = ! erasedComponents . empty ( ) ;
if ( objectsToErase )
drawP . layerMan . erase_component_container ( erasedComponents , ! placedNewObject ) ;
bool first = ! objectsToErase & & ! placedNewObject ;
2026-05-24 20:32:21 +03:00
for ( auto & [ comp , oldData ] : updatedComponents ) {
2026-05-25 01:33:03 +03:00
comp - > obj - > commit_update_dont_invalidate_cache ( drawP ) ;
2026-05-24 20:32:21 +03:00
comp - > obj - > send_comp_update ( drawP , true ) ;
2026-05-25 01:14:41 +03:00
if ( first ) {
2026-05-24 20:32:21 +03:00
first = false ;
drawP . world . undo . push ( std : : make_unique < EditCanvasComponentWorldUndoAction > ( std : : move ( oldData ) , drawP . world . undo . get_undoid_from_netid ( comp - > obj . get_net_id ( ) ) ) ) ;
}
else
drawP . world . undo . push_on_last ( std : : make_unique < EditCanvasComponentWorldUndoAction > ( std : : move ( oldData ) , drawP . world . undo . get_undoid_from_netid ( comp - > obj . get_net_id ( ) ) ) ) ;
}
erasedComponents . clear ( ) ;
updatedComponents . clear ( ) ;
2026-05-25 14:46:22 +03:00
}
void EraserTool : : switch_tool ( DrawingProgramToolType newTool ) {
commit_erase ( ) ;
}
void EraserTool : : commit_data ( ) {
if ( isErasing ) {
erasePath = BrushComponentCode : : brush_stroke_to_skpath ( genData . brushPoints , drawP . world . main . toolConfig . brush . hasRoundCaps ) ;
if ( drawP . world . main . conf . realTimeEraser ) {
erase_on_path ( ) ;
reset_erasing_stroke ( ) ;
}
}
else {
auto relativeWidthResult = drawP . world . main . toolConfig . get_relative_width_stroke_size ( drawP , drawP . world . drawData . cam . c . inverseScale ) ;
if ( ! relativeWidthResult . first . has_value ( ) ) {
erasePath = SkPath ( ) ;
return ;
}
float width = relativeWidthResult . first . value ( ) * 0.5f ;
erasePath = SkPath : : Circle ( drawP . world . main . input . mouse . pos . x ( ) , drawP . world . main . input . mouse . pos . y ( ) , width ) ;
}
2025-07-23 16:04:16 +03:00
}
void EraserTool : : tool_update ( ) {
2026-03-31 18:41:19 +03:00
if ( ! drawP . world . main . g . gui . cursor_obstructed ( ) )
2025-07-23 16:04:16 +03:00
drawP . world . main . input . hideCursor = true ;
2026-05-24 11:58:52 +03:00
using namespace BrushComponentCode ;
2026-05-25 14:46:22 +03:00
commit_data ( ) ;
2025-07-23 16:04:16 +03:00
}
2025-07-27 16:53:22 +03:00
bool EraserTool : : prevent_undo_or_redo ( ) {
return drawP . controls . leftClickHeld ;
}
2025-07-06 20:43:53 +03:00
void EraserTool : : draw ( SkCanvas * canvas , const DrawData & drawData ) {
2026-05-25 16:53:42 +03:00
if ( ! drawP . world . main . input . isTouchDevice & & ( ! drawData . main - > g . gui . cursor_obstructed ( ) | | isErasing ) & & ! erasePath . isEmpty ( ) ) {
2026-05-26 11:59:55 +03:00
if ( isErasing ) {
CanvasComponentContainer : : TransformData drawTransform = CanvasComponentContainer : : calculate_draw_transform ( drawData . cam . c , genData . coords ) ;
canvas - > save ( ) ;
CanvasComponentContainer : : canvas_do_transform ( canvas , drawTransform ) ;
}
2026-05-24 11:58:52 +03:00
SkPaint linePaint ;
linePaint . setAntiAlias ( drawData . skiaAA ) ;
linePaint . setColor4f ( { 0.0f , 0.0f , 0.0f , 0.4f } ) ;
linePaint . setStyle ( SkPaint : : kFill_Style ) ;
canvas - > drawPath ( erasePath , linePaint ) ;
linePaint . setColor4f ( { 1.0f , 1.0f , 1.0f , 0.4f } ) ;
2026-05-25 14:46:22 +03:00
canvas - > drawPath ( erasePath , linePaint ) ;
2026-05-26 11:59:55 +03:00
if ( isErasing )
canvas - > restore ( ) ;
2025-07-06 20:43:53 +03:00
}
}