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 "BrushTool.hpp"
# include <Helpers/ConvertVec.hpp>
2025-12-26 10:40:58 +03:00
# include "../../GUIStuff/GUIManager.hpp"
# include "../DrawingProgram.hpp"
# include "../../MainProgram.hpp"
2025-08-02 20:10:33 +03:00
# include "DrawingProgramToolBase.hpp"
2025-12-26 10:40:58 +03:00
# include "../../CanvasComponents/BrushStrokeCanvasComponent.hpp"
2025-12-14 18:57:40 +03:00
# include "Helpers/NetworkingObjects/NetObjTemporaryPtr.decl.hpp"
2025-12-26 10:40:58 +03:00
# include "../../CanvasComponents/CanvasComponentContainer.hpp"
2025-07-06 20:43:53 +03:00
2026-03-29 20:23:38 +03:00
# include "../../GUIStuff/ElementHelpers/TextLabelHelpers.hpp"
# include "../../GUIStuff/ElementHelpers/CheckBoxHelpers.hpp"
2025-07-06 20:43:53 +03:00
# define MINIMUM_DISTANCE_TO_NEXT_POINT 0.002f
2026-05-15 17:38:42 +03:00
# define DRAW_MINIMUM_LIMIT 1.0f
2025-07-06 20:43:53 +03:00
BrushTool : : BrushTool ( DrawingProgram & initDrawP ) :
2025-08-02 20:10:33 +03:00
DrawingProgramToolBase ( initDrawP )
{ }
DrawingProgramToolType BrushTool : : get_type ( ) {
return DrawingProgramToolType : : BRUSH ;
2025-07-06 20:43:53 +03:00
}
2025-08-02 20:39:21 +03:00
void BrushTool : : switch_tool ( DrawingProgramToolType newTool ) {
2025-07-06 20:43:53 +03:00
commit_stroke ( ) ;
}
2025-12-30 19:46:08 +03:00
void BrushTool : : erase_component ( CanvasComponentContainer : : ObjInfo * erasedComp ) {
2026-05-18 17:57:02 +03:00
if ( objInfoBeingEdited = = erasedComp ) {
2025-12-15 15:36:42 +03:00
objInfoBeingEdited = nullptr ;
2026-05-18 17:57:02 +03:00
commitUpdate = false ;
}
2025-12-15 15:36:42 +03:00
}
2026-05-15 17:38:42 +03:00
void BrushTool : : smooth_out_points ( float smoothFactor ) {
if ( objInfoBeingEdited ) {
NetworkingObjects : : NetObjOwnerPtr < CanvasComponentContainer > & containerPtr = objInfoBeingEdited - > obj ;
BrushStrokeCanvasComponent & brushStroke = static_cast < BrushStrokeCanvasComponent & > ( containerPtr - > get_comp ( ) ) ;
auto & brushPoints = * brushStroke . d . points ;
if ( brushPoints . size ( ) > = 2 ) {
brushPoints . back ( ) . width = std : : max ( brushPoints [ brushPoints . size ( ) - 1 ] . width , brushPoints [ brushPoints . size ( ) - 2 ] . width * smoothFactor ) ;
for ( size_t i = brushPoints . size ( ) - 1 ; i > 0 ; i - - ) {
if ( brushPoints [ i ] . width * smoothFactor > brushPoints [ i - 1 ] . width )
brushPoints [ i - 1 ] . width = brushPoints [ i ] . width * smoothFactor ;
else
break ;
}
}
}
}
void BrushTool : : fix_tip ( ) {
if ( objInfoBeingEdited ) {
NetworkingObjects : : NetObjOwnerPtr < CanvasComponentContainer > & containerPtr = objInfoBeingEdited - > obj ;
BrushStrokeCanvasComponent & brushStroke = static_cast < BrushStrokeCanvasComponent & > ( containerPtr - > get_comp ( ) ) ;
auto & brushPoints = * brushStroke . d . points ;
if ( brushPoints . size ( ) > = 2 ) {
brushPoints [ brushPoints . size ( ) - 2 ] . width = brushPoints [ brushPoints . size ( ) - 1 ] . width = std : : max ( brushPoints [ brushPoints . size ( ) - 1 ] . width , brushPoints [ brushPoints . size ( ) - 2 ] . width ) ;
}
}
}
2026-03-03 22:31:48 +03:00
void BrushTool : : input_mouse_button_on_canvas_callback ( const InputManager : : MouseButtonCallbackArgs & button ) {
if ( button . button = = InputManager : : MouseButton : : LEFT ) {
auto & toolConfig = drawP . world . main . toolConfig ;
2026-03-31 18:41:19 +03:00
if ( button . down & & drawP . layerMan . is_a_layer_being_edited ( ) & & ! objInfoBeingEdited & & ! drawP . world . main . g . gui . cursor_obstructed ( ) ) {
2026-05-16 22:14:02 +03:00
if ( button . deviceType = = InputManager : : MouseDeviceType : : PEN & & drawP . world . main . conf . tabletOptions . pressureAffectsBrushWidth ) {
2026-03-03 22:31:48 +03:00
penWidth = drawP . world . main . input . pen . pressure ;
if ( penWidth ! = 0.0f ) {
2026-03-31 13:17:58 +03:00
float brushMinSize = drawP . world . main . conf . tabletOptions . brushMinimumSize ;
2026-03-03 22:31:48 +03:00
penWidth = brushMinSize + penWidth * ( 1.0f - brushMinSize ) ;
}
}
else
penWidth = 1.0f ;
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 ;
}
float width = relativeWidthResult . first . value ( ) * penWidth ;
CanvasComponentContainer * newBrushStrokeContainer = new CanvasComponentContainer ( drawP . world . netObjMan , CanvasComponentType : : BRUSHSTROKE ) ;
BrushStrokeCanvasComponent & newBrushStroke = static_cast < BrushStrokeCanvasComponent & > ( newBrushStrokeContainer - > get_comp ( ) ) ;
BrushStrokeCanvasComponentPoint p ;
p . pos = button . pos ;
p . width = width ;
prevPointUnaltered = p . pos ;
newBrushStroke . d . points - > emplace_back ( p ) ;
newBrushStroke . d . color = toolConfig . globalConf . foregroundColor ;
newBrushStroke . d . hasRoundCaps = toolConfig . brush . hasRoundCaps ;
newBrushStrokeContainer - > coords = drawP . world . drawData . cam . c ;
objInfoBeingEdited = drawP . layerMan . add_component_to_layer_being_edited ( newBrushStrokeContainer ) ;
addedTemporaryPoint = false ;
}
else if ( ! button . down & & objInfoBeingEdited )
commit_stroke ( ) ;
}
}
void BrushTool : : input_mouse_motion_callback ( const InputManager : : MouseMotionCallbackArgs & motion ) {
if ( objInfoBeingEdited ) {
auto & toolConfig = drawP . world . main . toolConfig ;
NetworkingObjects : : NetObjOwnerPtr < CanvasComponentContainer > & containerPtr = objInfoBeingEdited - > obj ;
BrushStrokeCanvasComponent & brushStroke = static_cast < BrushStrokeCanvasComponent & > ( containerPtr - > get_comp ( ) ) ;
auto & brushPoints = * brushStroke . d . points ;
float width = toolConfig . get_relative_width_stroke_size ( drawP , containerPtr - > coords . inverseScale ) . first . value ( ) * penWidth ;
BrushStrokeCanvasComponentPoint p ;
p . pos = containerPtr - > coords . from_cam_space_to_this ( drawP . world , motion . pos ) ;
p . width = width ;
2026-05-15 17:38:42 +03:00
// Temporary point is a point that follows the cursor until it is placed
2026-03-03 22:31:48 +03:00
if ( ! addedTemporaryPoint ) {
2026-05-15 17:38:42 +03:00
// Temporary point is close enough to be placed
2026-03-03 22:31:48 +03:00
if ( extensive_point_checking ( brushStroke , p . pos ) ) {
brushPoints . emplace_back ( p ) ;
addedTemporaryPoint = true ;
}
else
2026-05-15 17:38:42 +03:00
// Temporary point not close enough yet, so change the width of the previous point based on the current pen width until it can be placed
2026-03-03 22:31:48 +03:00
brushPoints . back ( ) . width = std : : max ( brushPoints . back ( ) . width , p . width ) ;
}
if ( addedTemporaryPoint ) {
const BrushStrokeCanvasComponentPoint & prevP = brushPoints [ brushPoints . size ( ) - 2 ] ;
float distToPrev = ( p . pos - prevP . pos ) . norm ( ) ;
2026-05-15 17:38:42 +03:00
// If the cursor isnt too close to the previous points, move the temporary point to the cursor and change the width of the temporary point to the current pen width
2026-03-03 22:31:48 +03:00
if ( extensive_point_checking_back ( brushStroke , p . pos ) ) {
brushPoints . back ( ) . pos = p . pos ;
brushPoints . back ( ) . width = std : : max ( brushPoints . back ( ) . width , p . width ) ;
}
2026-05-15 17:38:42 +03:00
float maxWidthToCompareTo = std : : max ( brushPoints . back ( ) . width , p . width ) ;
maxWidthToCompareTo = std : : max ( maxWidthToCompareTo , 5.0f ) ;
if ( brushPoints . size ( ) > = 2 )
maxWidthToCompareTo = std : : max ( brushPoints [ brushPoints . size ( ) - 2 ] . width , maxWidthToCompareTo ) ;
if ( brushPoints . size ( ) > = 3 )
maxWidthToCompareTo = std : : max ( brushPoints [ brushPoints . size ( ) - 3 ] . width , maxWidthToCompareTo ) ;
// If the temporary point (cursor) is far enough from the point before it, make the temporary point permanent
if ( distToPrev > = maxWidthToCompareTo * DRAW_MINIMUM_LIMIT ) {
brushPoints . back ( ) . pos = p . pos ;
brushPoints . back ( ) . width = std : : max ( brushPoints . back ( ) . width , p . width ) ;
2026-03-03 22:31:48 +03:00
addedTemporaryPoint = false ;
2026-05-15 17:38:42 +03:00
// Midway interpolation moves the point before the last one to a position that leads to a smoother line
if ( brushPoints . size ( ) ! = 2 ) // Don't interpolate the first point
brushPoints [ brushPoints . size ( ) - 2 ] . pos = ( prevPointUnaltered + p . pos ) * 0.5 ;
prevPointUnaltered = p . pos ;
2026-03-03 22:31:48 +03:00
}
}
2026-05-16 20:40:23 +03:00
smooth_out_points ( drawP . world . main . conf . tabletOptions . brushPressureSmoothingFactor ) ;
2026-05-18 17:57:02 +03:00
commitUpdate = true ;
2026-03-03 22:31:48 +03:00
}
}
void BrushTool : : input_pen_axis_callback ( const InputManager : : PenAxisCallbackArgs & axis ) {
2026-05-16 22:14:02 +03:00
if ( axis . axis = = SDL_PEN_AXIS_PRESSURE & & drawP . world . main . conf . tabletOptions . pressureAffectsBrushWidth ) {
2026-03-03 22:31:48 +03:00
penWidth = axis . value ;
if ( penWidth ! = 0.0f ) {
2026-03-31 13:17:58 +03:00
float brushMinSize = drawP . world . main . conf . tabletOptions . brushMinimumSize ;
2026-03-03 22:31:48 +03:00
penWidth = brushMinSize + penWidth * ( 1.0f - brushMinSize ) ;
2026-05-15 17:38:42 +03:00
if ( objInfoBeingEdited ) {
auto & toolConfig = drawP . world . main . toolConfig ;
NetworkingObjects : : NetObjOwnerPtr < CanvasComponentContainer > & containerPtr = objInfoBeingEdited - > obj ;
BrushStrokeCanvasComponent & brushStroke = static_cast < BrushStrokeCanvasComponent & > ( containerPtr - > get_comp ( ) ) ;
auto & brushPoints = * brushStroke . d . points ;
float width = toolConfig . get_relative_width_stroke_size ( drawP , containerPtr - > coords . inverseScale ) . first . value ( ) * penWidth ;
brushPoints . back ( ) . width = std : : max ( brushPoints . back ( ) . width , width ) ;
2026-05-16 20:40:23 +03:00
smooth_out_points ( drawP . world . main . conf . tabletOptions . brushPressureSmoothingFactor ) ;
2026-05-18 17:57:02 +03:00
commitUpdate = true ;
2026-05-15 17:38:42 +03:00
}
2026-03-03 22:31:48 +03:00
}
}
}
2025-12-14 18:57:40 +03:00
bool BrushTool : : extensive_point_checking ( const BrushStrokeCanvasComponent & brushStroke , const Vector2f & newPoint ) {
2026-01-27 16:55:09 +03:00
auto & points = * brushStroke . d . points ;
2025-07-06 20:43:53 +03:00
if ( points . size ( ) > = 1 & & ( newPoint - points [ points . size ( ) - 1 ] . pos ) . norm ( ) < MINIMUM_DISTANCE_TO_NEXT_POINT )
return false ;
if ( points . size ( ) > = 2 & & ( newPoint - points [ points . size ( ) - 2 ] . pos ) . norm ( ) < MINIMUM_DISTANCE_TO_NEXT_POINT )
return false ;
if ( points . size ( ) > = 3 & & ( newPoint - points [ points . size ( ) - 3 ] . pos ) . norm ( ) < MINIMUM_DISTANCE_TO_NEXT_POINT )
return false ;
return true ;
}
2025-12-14 18:57:40 +03:00
bool BrushTool : : extensive_point_checking_back ( const BrushStrokeCanvasComponent & brushStroke , const Vector2f & newPoint ) {
2026-01-27 16:55:09 +03:00
auto & points = * brushStroke . d . points ;
2025-07-06 20:43:53 +03:00
if ( points . size ( ) > = 2 & & ( newPoint - points [ points . size ( ) - 2 ] . pos ) . norm ( ) < MINIMUM_DISTANCE_TO_NEXT_POINT )
return false ;
if ( points . size ( ) > = 3 & & ( newPoint - points [ points . size ( ) - 3 ] . pos ) . norm ( ) < MINIMUM_DISTANCE_TO_NEXT_POINT )
return false ;
if ( points . size ( ) > = 4 & & ( newPoint - points [ points . size ( ) - 4 ] . pos ) . norm ( ) < MINIMUM_DISTANCE_TO_NEXT_POINT )
return false ;
return true ;
}
void BrushTool : : tool_update ( ) {
2026-03-31 18:41:19 +03:00
if ( ! drawP . world . main . g . gui . cursor_obstructed ( ) )
2025-07-06 20:43:53 +03:00
drawP . world . main . input . hideCursor = true ;
2026-05-18 17:57:02 +03:00
if ( commitUpdate & & objInfoBeingEdited ) {
NetworkingObjects : : NetObjOwnerPtr < CanvasComponentContainer > & containerPtr = objInfoBeingEdited - > obj ;
containerPtr - > commit_update ( drawP ) ;
2026-05-18 18:35:40 +03:00
containerPtr - > send_comp_update ( drawP , false ) ;
2026-05-18 17:57:02 +03:00
commitUpdate = false ;
}
2025-07-06 20:43:53 +03:00
}
2025-12-14 18:57:40 +03:00
void BrushTool : : commit_stroke ( ) {
2025-12-15 16:55:09 +03:00
if ( objInfoBeingEdited ) {
NetworkingObjects : : NetObjOwnerPtr < CanvasComponentContainer > & containerPtr = objInfoBeingEdited - > obj ;
2026-05-15 17:38:42 +03:00
fix_tip ( ) ;
2025-12-15 16:55:09 +03:00
containerPtr - > commit_update ( drawP ) ;
containerPtr - > send_comp_update ( drawP , true ) ;
2026-05-18 18:35:40 +03:00
commitUpdate = false ;
2026-04-03 14:18:04 +03:00
if ( containerPtr - > get_world_bounds ( ) . has_value ( ) )
drawP . layerMan . add_undo_place_component ( objInfoBeingEdited ) ;
else {
auto & components = containerPtr - > parentLayer - > get_layer ( ) . components ;
components - > erase ( components , containerPtr - > objInfo ) ;
}
2025-12-15 16:55:09 +03:00
objInfoBeingEdited = nullptr ;
}
2025-12-14 18:57:40 +03:00
}
2026-04-12 12:36:00 +03:00
void BrushTool : : gui_toolbox ( Toolbar & t ) {
2026-03-29 20:23:38 +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 ( " brush tool " , [ & ] {
text_label_centered ( gui , " Brush " ) ;
checkbox_boolean_field ( gui , " hasroundcaps " , " Round Caps " , & drawP . world . main . toolConfig . brush . hasRoundCaps ) ;
2026-03-30 18:51:14 +03:00
drawP . world . main . toolConfig . relative_width_gui ( drawP , " Size " ) ;
} ) ;
2025-07-06 20:43:53 +03:00
}
2026-04-30 12:09:03 +03:00
void BrushTool : : gui_phone_toolbox ( PhoneDrawingProgramScreen & t ) {
using namespace GUIStuff ;
using namespace ElementHelpers ;
auto & gui = drawP . world . main . g . gui ;
gui . new_id ( " brush tool " , [ & ] {
checkbox_boolean_field ( gui , " hasroundcaps " , " Round Caps " , & drawP . world . main . toolConfig . brush . hasRoundCaps ) ;
drawP . world . main . toolConfig . relative_width_gui ( drawP , " Size " ) ;
} ) ;
}
2026-04-12 12:36:00 +03:00
void BrushTool : : right_click_popup_gui ( Toolbar & t , Vector2f popupPos ) {
2025-12-01 17:33:14 +03:00
t . paint_popup ( popupPos ) ;
}
2025-07-27 16:53:22 +03:00
bool BrushTool : : prevent_undo_or_redo ( ) {
2025-12-30 19:46:08 +03:00
return objInfoBeingEdited ;
2025-07-06 20:43:53 +03:00
}
void BrushTool : : draw ( SkCanvas * canvas , const DrawData & drawData ) {
2026-03-31 18:41:19 +03:00
if ( ! drawP . world . main . input . isTouchDevice & & ! drawData . main - > g . gui . cursor_obstructed ( ) ) {
2026-01-29 20:33: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 ( ) ) {
float width = relativeWidthResult . first . value ( ) ;
if ( objInfoBeingEdited )
width * = penWidth * 0.5f ;
else
width * = 0.5f ;
2026-03-20 16:24:52 +03:00
width + = 1.0f ;
2026-01-29 20:33:22 +03:00
Vector2f pos = drawData . main - > input . mouse . pos ;
2026-03-20 16:24:52 +03:00
SkPaint linePaint ;
2026-03-20 20:53:59 +03:00
linePaint . setAntiAlias ( drawData . skiaAA ) ;
2026-03-20 16:24:52 +03:00
linePaint . setColor4f ( { 1.0f , 1.0f , 1.0f , 1.0f } ) ;
linePaint . setStyle ( SkPaint : : kStroke_Style ) ;
linePaint . setStrokeCap ( SkPaint : : kRound_Cap ) ;
linePaint . setStrokeWidth ( 0.0f ) ;
SkPath circ = SkPath : : Circle ( pos . x ( ) , pos . y ( ) , width ) ;
canvas - > drawPath ( circ , linePaint ) ;
2026-04-15 20:58:25 +03:00
linePaint . setColor4f ( { 0.0f , 0.0f , 0.0f , 1.0f } ) ;
circ = SkPath : : Circle ( pos . x ( ) , pos . y ( ) , width - 1.0f ) ;
canvas - > drawPath ( circ , linePaint ) ;
2026-01-29 20:33:22 +03:00
}
2025-07-06 20:43:53 +03:00
}
}