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"
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-03-03 12:06:48 +03:00
isErasing = true ;
2026-05-24 11:58:52 +03:00
start = end = button . pos ;
2026-03-03 12:06:48 +03:00
}
else if ( ! button . down & & isErasing )
switch_tool ( get_type ( ) ) ;
}
}
void EraserTool : : input_mouse_motion_callback ( const InputManager : : MouseMotionCallbackArgs & motion ) {
2026-05-24 11:58:52 +03:00
end = motion . pos ;
2026-03-03 12:06:48 +03:00
}
2026-05-24 11:58:52 +03:00
void EraserTool : : erase_between_points ( ) {
auto cCWorldBounds = drawP . world . drawData . cam . c . collider_to_world < SCollision : : AABB < WorldScalar > , SCollision : : AABB < float > > ( erasePath . getBounds ( ) ) ;
drawP . drawCache . traverse_bvh_run_function ( cCWorldBounds , [ & ] ( const auto & bvhNode ) {
2026-03-03 12:06:48 +03:00
if ( bvhNode & &
2026-05-24 11:58:52 +03:00
erasePath . contains ( convert_vec2 < SkPoint > ( drawP . world . drawData . cam . c . to_space ( bvhNode - > bounds . min ) ) ) & &
erasePath . contains ( convert_vec2 < SkPoint > ( drawP . world . drawData . cam . c . to_space ( bvhNode - > bounds . max ) ) ) & &
erasePath . contains ( convert_vec2 < SkPoint > ( drawP . world . drawData . cam . c . to_space ( bvhNode - > bounds . top_right ( ) ) ) ) & &
erasePath . contains ( convert_vec2 < SkPoint > ( drawP . world . drawData . cam . c . 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 ( ) ;
CanvasComponentEraseDetailResult result = c - > obj - > collides_with_erase_detail ( drawP . world . drawData . cam . c , erasePath ) ;
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 ) ;
}
erasedComponents . emplace ( c ) ;
drawP . drawCache . invalidate_cache_at_optional_aabb ( c - > obj - > get_world_bounds ( ) ) ;
return true ;
}
}
}
else {
if ( drawP . layerMan . component_passes_layer_selector ( c , drawP . controls . layerSelector ) & & c - > obj - > collides_with ( drawP . world . drawData . cam . c , erasePath ) ) {
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 ;
} ) ;
}
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
}
2025-08-02 20:39:21 +03:00
void EraserTool : : switch_tool ( DrawingProgramToolType newTool ) {
2026-05-25 01:14:41 +03:00
bool first = erasedComponents . empty ( ) ; // erase_component_container will call EraserTool::erase_component, which will clear erasedComponents eventually
2025-12-31 10:06:26 +03:00
drawP . layerMan . erase_component_container ( erasedComponents ) ;
2026-05-24 20:32:21 +03:00
for ( auto & [ comp , oldData ] : updatedComponents ) {
2026-05-25 01:14:41 +03:00
comp - > obj - > commit_update ( 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-03-03 12:06:48 +03:00
isErasing = false ;
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
auto relativeWidthResult = drawP . world . main . toolConfig . get_relative_width_stroke_size ( drawP , drawP . world . drawData . cam . c . inverseScale ) ;
if ( ! relativeWidthResult . first . has_value ( ) ) {
isErasing = false ;
erasePath = eraseBorderPath = SkPath ( ) ;
return ;
}
float width = relativeWidthResult . first . value ( ) * 0.5f ;
using namespace BrushComponentCode ;
std : : vector < BrushPoint > brushPoints ;
brushPoints . emplace_back ( start , width ) ;
if ( start ! = end )
brushPoints . emplace_back ( end , width ) ;
erasePath = brush_stroke_to_skpath ( brushPoints , true ) ;
brushPoints . clear ( ) ;
brushPoints . emplace_back ( start , width + 1.0f ) ;
if ( start ! = end )
brushPoints . emplace_back ( end , width + 1.0f ) ;
eraseBorderPath = brush_stroke_to_skpath ( brushPoints , true ) ;
if ( isErasing )
erase_between_points ( ) ;
start = end ;
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-24 11:58:52 +03:00
if ( ! drawP . world . main . input . isTouchDevice & & ! drawData . main - > g . gui . cursor_obstructed ( ) & & ! erasePath . isEmpty ( ) & & ! eraseBorderPath . isEmpty ( ) ) {
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 } ) ;
canvas - > drawPath ( eraseBorderPath , linePaint ) ;
2025-07-06 20:43:53 +03:00
}
}