From c12cd14f6ec7678bb260cf2c981beeb587a7e01a Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Mon, 25 May 2026 14:46:22 +0300 Subject: [PATCH] Make eraser pressure sensitive --- src/CanvasComponents/BrushComponentCode.cpp | 129 ++++++++++++++++- src/CanvasComponents/BrushComponentCode.hpp | 19 +++ src/DrawingProgram/Tools/BrushTool.cpp | 145 +++----------------- src/DrawingProgram/Tools/BrushTool.hpp | 12 +- src/DrawingProgram/Tools/EraserTool.cpp | 99 ++++++++----- src/DrawingProgram/Tools/EraserTool.hpp | 11 +- src/GlobalConfig.cpp | 2 + src/GlobalConfig.hpp | 2 + src/Toolbar.cpp | 1 + 9 files changed, 244 insertions(+), 176 deletions(-) diff --git a/src/CanvasComponents/BrushComponentCode.cpp b/src/CanvasComponents/BrushComponentCode.cpp index 8227766..92ce695 100644 --- a/src/CanvasComponents/BrushComponentCode.cpp +++ b/src/CanvasComponents/BrushComponentCode.cpp @@ -20,10 +20,14 @@ #include #include #include "../TimePoint.hpp" -#include -#include +#include "../DrawingProgram/DrawingProgram.hpp" +#include "../World.hpp" +#include "../MainProgram.hpp" #define DEFAULT_SMOOTHNESS 3 +#define MINIMUM_DISTANCE_FROM_FIRST_POINT 3.0f +#define MINIMUM_DISTANCE_TO_NEXT_POINT 0.002f +#define DRAW_MINIMUM_LIMIT 1.0f namespace BrushComponentCode { @@ -256,4 +260,125 @@ SkPath create_triangles(const std::vector& regularPoints, const std: return pathBuilder.detach(); } +void smooth_out_points(std::vector& brushPoints, float smoothFactor) { + 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 fix_tip(std::vector& brushPoints) { + 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); +} + +void mouse_button(DrawingProgram& drawP, BrushStrokeGenerationData& genData, const CoordSpaceHelper& strokeCoordSpace, const InputManager::MouseButtonCallbackArgs& button, float brushSize) { + if(button.deviceType == InputManager::MouseDeviceType::PEN && drawP.world.main.conf.tabletOptions.pressureAffectsBrushWidth) { + genData.penWidth = drawP.world.main.input.pen.pressure; + if(genData.penWidth != 0.0f) { + float brushMinSize = drawP.world.main.conf.tabletOptions.brushMinimumSize; + genData.penWidth = brushMinSize + genData.penWidth * (1.0f - brushMinSize); + } + } + else + genData.penWidth = 1.0f; + + float width = brushSize * genData.penWidth; + genData.coords = strokeCoordSpace; + + genData.brushPoints.clear(); + BrushComponentCode::BrushPoint p; + p.pos = button.pos; + p.width = width; + genData.prevPointUnaltered = p.pos; + genData.brushPoints.emplace_back(p); + genData.addedTemporaryPoint = false; +} + +void mouse_motion(DrawingProgram& drawP, BrushStrokeGenerationData& genData, const Vector2f& motionPos, float brushSize) { + BrushComponentCode::BrushPoint p; + p.pos = genData.coords.to_space(drawP.world.drawData.cam.c.from_space(motionPos)); + p.width = brushSize * genData.penWidth; + + // Temporary point is a point that follows the cursor until it is placed + if(!genData.addedTemporaryPoint) { + // Temporary point is close enough to be placed + if(extensive_point_checking(genData.brushPoints, p.pos, genData.brushPoints.size() == 1 ? MINIMUM_DISTANCE_FROM_FIRST_POINT : MINIMUM_DISTANCE_TO_NEXT_POINT)) { + genData.brushPoints.emplace_back(p); + genData.addedTemporaryPoint = true; + } + else + // 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 + genData.brushPoints.back().width = std::max(genData.brushPoints.back().width, p.width); + } + + if(genData.addedTemporaryPoint) { + const BrushComponentCode::BrushPoint& prevP = genData.brushPoints[genData.brushPoints.size() - 2]; + float distToPrev = (p.pos - prevP.pos).norm(); + // 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 + if(extensive_point_checking_back(genData.brushPoints, p.pos)) { + genData.brushPoints.back().pos = p.pos; + genData.brushPoints.back().width = std::max(genData.brushPoints.back().width, p.width); + } + + float maxWidthToCompareTo = std::max(genData.brushPoints.back().width, p.width); + maxWidthToCompareTo = std::max(maxWidthToCompareTo, 5.0f); + if(genData.brushPoints.size() >= 2) + maxWidthToCompareTo = std::max(genData.brushPoints[genData.brushPoints.size() - 2].width, maxWidthToCompareTo); + if(genData.brushPoints.size() >= 3) + maxWidthToCompareTo = std::max(genData.brushPoints[genData.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) { + genData.brushPoints.back().pos = p.pos; + genData.brushPoints.back().width = std::max(genData.brushPoints.back().width, p.width); + genData.addedTemporaryPoint = false; + + // Midway interpolation moves the point before the last one to a position that leads to a smoother line + if(genData.brushPoints.size() != 2) // Don't interpolate the first point + genData.brushPoints[genData.brushPoints.size() - 2].pos = (genData.prevPointUnaltered + p.pos) * 0.5; + genData.prevPointUnaltered = p.pos; + } + } + + smooth_out_points(genData.brushPoints, drawP.world.main.conf.tabletOptions.brushPressureSmoothingFactor); +} + +void pen_pressure(DrawingProgram& drawP, BrushStrokeGenerationData& genData, float brushSize) { + if(drawP.world.main.conf.tabletOptions.pressureAffectsBrushWidth) { + if(genData.penWidth != 0.0f) { + float brushMinSize = drawP.world.main.conf.tabletOptions.brushMinimumSize; + genData.penWidth = brushMinSize + genData.penWidth * (1.0f - brushMinSize); + float width = brushSize * genData.penWidth; + genData.brushPoints.back().width = std::max(genData.brushPoints.back().width, width); + smooth_out_points(genData.brushPoints, drawP.world.main.conf.tabletOptions.brushPressureSmoothingFactor); + } + } +} + +bool extensive_point_checking(const std::vector& points, const Vector2f& newPoint, float minimumDistance) { + if(points.size() >= 1 && (newPoint - points[points.size() - 1].pos).norm() < minimumDistance) + return false; + if(points.size() >= 2 && (newPoint - points[points.size() - 2].pos).norm() < minimumDistance) + return false; + if(points.size() >= 3 && (newPoint - points[points.size() - 3].pos).norm() < minimumDistance) + return false; + return true; +} + +bool extensive_point_checking_back(const std::vector& points, const Vector2f& newPoint) { + 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; +} + } diff --git a/src/CanvasComponents/BrushComponentCode.hpp b/src/CanvasComponents/BrushComponentCode.hpp index 65ce9df..c738c53 100644 --- a/src/CanvasComponents/BrushComponentCode.hpp +++ b/src/CanvasComponents/BrushComponentCode.hpp @@ -19,6 +19,9 @@ #pragma once #include "../CoordSpaceHelper.hpp" #include +#include "../InputManager.hpp" + +class DrawingProgram; struct MeshShapeData { std::vector> contours; @@ -36,8 +39,24 @@ namespace BrushComponentCode { } }; + struct BrushStrokeGenerationData { + bool addedTemporaryPoint = false; + std::vector brushPoints; + Vector2f prevPointUnaltered = {0, 0}; + float penWidth = 1.0f; + CoordSpaceHelper coords; + }; + SkPath brush_stroke_to_skpath(const std::vector& brushPoints, bool hasRoundCaps); SkPath create_triangles(const std::vector& regularPoints, const std::vector& smoothedPoints, bool hasRoundCaps); std::vector get_wedge_indices(const std::vector& points); std::vector smooth_points(const std::vector& points, size_t beginIndex, size_t endIndex, unsigned numOfDivisions); + void smooth_out_points(std::vector& brushPoints, float smoothFactor); + void fix_tip(std::vector& brushPoints); + void mouse_button(DrawingProgram& drawP, BrushStrokeGenerationData& genData, const CoordSpaceHelper& strokeCoordSpace, const InputManager::MouseButtonCallbackArgs& button, float brushSize); + void mouse_motion(DrawingProgram& drawP, BrushStrokeGenerationData& genData, const Vector2f& motionPos, float brushSize); + void pen_pressure(DrawingProgram& drawP, BrushStrokeGenerationData& genData, float brushSize); + bool extensive_point_checking(const std::vector& points, const Vector2f& newPoint, float minimumDistance); + bool extensive_point_checking_back(const std::vector& points, const Vector2f& newPoint); + } diff --git a/src/DrawingProgram/Tools/BrushTool.cpp b/src/DrawingProgram/Tools/BrushTool.cpp index 3621890..fafc134 100644 --- a/src/DrawingProgram/Tools/BrushTool.cpp +++ b/src/DrawingProgram/Tools/BrushTool.cpp @@ -29,6 +29,7 @@ #include "../../GUIStuff/ElementHelpers/TextLabelHelpers.hpp" #include "../../GUIStuff/ElementHelpers/CheckBoxHelpers.hpp" +#define MINIMUM_DISTANCE_FROM_FIRST_POINT 3.0f #define MINIMUM_DISTANCE_TO_NEXT_POINT 0.002f #define DRAW_MINIMUM_LIMIT 1.0f @@ -51,62 +52,26 @@ void BrushTool::erase_component(CanvasComponentContainer::ObjInfo* erasedComp) { } } -void BrushTool::smooth_out_points(float smoothFactor) { - if(objInfoBeingEdited) { - 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) { - 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); - } -} - void BrushTool::input_mouse_button_on_canvas_callback(const InputManager::MouseButtonCallbackArgs& button) { if(button.button == InputManager::MouseButton::LEFT) { auto& toolConfig = drawP.world.main.toolConfig; if(button.down && drawP.layerMan.is_a_layer_being_edited() && !objInfoBeingEdited && !drawP.world.main.g.gui.cursor_obstructed()) { - if(button.deviceType == InputManager::MouseDeviceType::PEN && drawP.world.main.conf.tabletOptions.pressureAffectsBrushWidth) { - penWidth = drawP.world.main.input.pen.pressure; - if(penWidth != 0.0f) { - float brushMinSize = drawP.world.main.conf.tabletOptions.brushMinimumSize; - 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* newMeshContainer = new CanvasComponentContainer(drawP.world.netObjMan, CanvasComponentType::MESH); MeshCanvasComponent& newMesh = static_cast(newMeshContainer->get_comp()); - brushPoints.clear(); - BrushComponentCode::BrushPoint p; - p.pos = button.pos; - p.width = width; - prevPointUnaltered = p.pos; - brushPoints.emplace_back(p); newMesh.d.color = toolConfig.globalConf.foregroundColor; newMeshContainer->coords = drawP.world.drawData.cam.c; + + BrushComponentCode::mouse_button(drawP, genData, newMeshContainer->coords, button, relativeWidthResult.first.value()); + objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newMeshContainer); commit_data(false); - addedTemporaryPoint = false; } else if(!button.down && objInfoBeingEdited) commit_stroke(); @@ -117,124 +82,48 @@ void BrushTool::commit_data(bool final) { if(objInfoBeingEdited) { NetworkingObjects::NetObjOwnerPtr& containerPtr = objInfoBeingEdited->obj; MeshCanvasComponent& newMesh = static_cast(containerPtr->get_comp()); - newMesh.d.meshPath = BrushComponentCode::brush_stroke_to_skpath(brushPoints, drawP.world.main.toolConfig.brush.hasRoundCaps); + newMesh.d.meshPath = BrushComponentCode::brush_stroke_to_skpath(genData.brushPoints, drawP.world.main.toolConfig.brush.hasRoundCaps); containerPtr->commit_update(drawP); containerPtr->send_comp_update(drawP, final); } + commitUpdate = false; } void BrushTool::input_mouse_motion_callback(const InputManager::MouseMotionCallbackArgs& motion) { if(objInfoBeingEdited) { auto& toolConfig = drawP.world.main.toolConfig; NetworkingObjects::NetObjOwnerPtr& containerPtr = objInfoBeingEdited->obj; - float width = toolConfig.get_relative_width_stroke_size(drawP, containerPtr->coords.inverseScale).first.value() * penWidth; - - BrushComponentCode::BrushPoint p; - p.pos = containerPtr->coords.from_cam_space_to_this(drawP.world, motion.pos); - p.width = width; - - // Temporary point is a point that follows the cursor until it is placed - if(!addedTemporaryPoint) { - // Temporary point is close enough to be placed - if(extensive_point_checking(p.pos)) { - brushPoints.emplace_back(p); - addedTemporaryPoint = true; - } - else - // 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 - brushPoints.back().width = std::max(brushPoints.back().width, p.width); - } - - if(addedTemporaryPoint) { - const BrushComponentCode::BrushPoint& prevP = brushPoints[brushPoints.size() - 2]; - float distToPrev = (p.pos - prevP.pos).norm(); - // 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 - if(extensive_point_checking_back(p.pos)) { - brushPoints.back().pos = p.pos; - brushPoints.back().width = std::max(brushPoints.back().width, p.width); - } - - 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); - addedTemporaryPoint = false; - - // 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; - } - } - - smooth_out_points(drawP.world.main.conf.tabletOptions.brushPressureSmoothingFactor); + BrushComponentCode::mouse_motion(drawP, genData, motion.pos, toolConfig.get_relative_width_stroke_size(drawP, containerPtr->coords.inverseScale).first.value()); commitUpdate = true; } } void BrushTool::input_pen_axis_callback(const InputManager::PenAxisCallbackArgs& axis) { if(axis.axis == SDL_PEN_AXIS_PRESSURE && drawP.world.main.conf.tabletOptions.pressureAffectsBrushWidth) { - penWidth = axis.value; - if(penWidth != 0.0f) { - float brushMinSize = drawP.world.main.conf.tabletOptions.brushMinimumSize; - penWidth = brushMinSize + penWidth * (1.0f - brushMinSize); - if(objInfoBeingEdited) { - auto& toolConfig = drawP.world.main.toolConfig; - NetworkingObjects::NetObjOwnerPtr& containerPtr = objInfoBeingEdited->obj; - float width = toolConfig.get_relative_width_stroke_size(drawP, containerPtr->coords.inverseScale).first.value() * penWidth; - brushPoints.back().width = std::max(brushPoints.back().width, width); - smooth_out_points(drawP.world.main.conf.tabletOptions.brushPressureSmoothingFactor); - commitUpdate = true; - } + genData.penWidth = axis.value; + if(genData.penWidth != 0.0f && objInfoBeingEdited) { + auto& toolConfig = drawP.world.main.toolConfig; + NetworkingObjects::NetObjOwnerPtr& containerPtr = objInfoBeingEdited->obj; + float width = toolConfig.get_relative_width_stroke_size(drawP, containerPtr->coords.inverseScale).first.value(); + BrushComponentCode::pen_pressure(drawP, genData, width); + commitUpdate = true; } } } -bool BrushTool::extensive_point_checking(const Vector2f& newPoint) { - auto& points = brushPoints; - 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; -} - -bool BrushTool::extensive_point_checking_back(const Vector2f& newPoint) { - auto& points = brushPoints; - 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() { if(!drawP.world.main.g.gui.cursor_obstructed()) drawP.world.main.input.hideCursor = true; - if(commitUpdate && objInfoBeingEdited) { + if(commitUpdate && objInfoBeingEdited) commit_data(false); - commitUpdate = false; - } } void BrushTool::commit_stroke() { if(objInfoBeingEdited) { NetworkingObjects::NetObjOwnerPtr& containerPtr = objInfoBeingEdited->obj; - fix_tip(); + BrushComponentCode::fix_tip(genData.brushPoints); commit_data(true); - commitUpdate = false; if(containerPtr->get_world_bounds().has_value()) drawP.layerMan.add_undo_place_component(objInfoBeingEdited); else { @@ -284,7 +173,7 @@ void BrushTool::draw(SkCanvas* canvas, const DrawData& drawData) { if(relativeWidthResult.first.has_value()) { float width = relativeWidthResult.first.value(); if(objInfoBeingEdited) - width *= penWidth * 0.5f; + width *= genData.penWidth * 0.5f; else width *= 0.5f; width += 1.0f; diff --git a/src/DrawingProgram/Tools/BrushTool.hpp b/src/DrawingProgram/Tools/BrushTool.hpp index bb5ab04..6876693 100644 --- a/src/DrawingProgram/Tools/BrushTool.hpp +++ b/src/DrawingProgram/Tools/BrushTool.hpp @@ -47,20 +47,10 @@ class BrushTool : public DrawingProgramToolBase { virtual void input_mouse_motion_callback(const InputManager::MouseMotionCallbackArgs& motion) override; virtual void input_pen_axis_callback(const InputManager::PenAxisCallbackArgs& axis) override; private: - bool extensive_point_checking_back(const Vector2f& newPoint); - bool extensive_point_checking(const Vector2f& newPoint); - void smooth_out_points(float smoothFactor); - void fix_tip(); void commit_stroke(); void commit_data(bool final); - float penWidth = 1.0f; - bool addedTemporaryPoint = false; + BrushComponentCode::BrushStrokeGenerationData genData; bool commitUpdate = false; - - std::vector brushPoints; - CanvasComponentContainer::ObjInfo* objInfoBeingEdited = nullptr; - - Vector2f prevPointUnaltered = {0, 0}; }; diff --git a/src/DrawingProgram/Tools/EraserTool.cpp b/src/DrawingProgram/Tools/EraserTool.cpp index 55c6dc3..edeb1b6 100644 --- a/src/DrawingProgram/Tools/EraserTool.cpp +++ b/src/DrawingProgram/Tools/EraserTool.cpp @@ -78,19 +78,43 @@ void EraserTool::right_click_popup_gui(Toolbar& t, Vector2f popupPos) { void EraserTool::input_mouse_button_on_canvas_callback(const InputManager::MouseButtonCallbackArgs& button) { if(button.button == InputManager::MouseButton::LEFT) { if(button.down && !isErasing && !drawP.world.main.g.gui.cursor_obstructed()) { + 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()); isErasing = true; - start = end = button.pos; } - else if(!button.down && isErasing) - switch_tool(get_type()); + else if(!button.down && isErasing) { + erase_on_path(); + reset_erasing_stroke(); + commit_erase(); + isErasing = false; + } } } void EraserTool::input_mouse_motion_callback(const InputManager::MouseMotionCallbackArgs& motion) { - end = motion.pos; + 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()); + } } -void EraserTool::erase_between_points() { +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() { auto cCWorldBounds = drawP.world.drawData.cam.c.collider_to_world, SCollision::AABB>(erasePath.getBounds()); drawP.drawCache.traverse_bvh_run_function(cCWorldBounds, [&](const auto& bvhNode) { if(bvhNode && @@ -141,6 +165,8 @@ void EraserTool::erase_between_points() { c->obj->get_comp().set_data_from(*(it->second)); updatedComponents.erase(it); } + else + c->obj->get_comp().set_data_from(*dataCopy); erasedComponents.emplace(c); drawP.drawCache.invalidate_cache_at_optional_aabb(c->obj->get_world_bounds()); return true; @@ -160,12 +186,22 @@ void EraserTool::erase_between_points() { }); } +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; + } +} + void EraserTool::erase_component(CanvasComponentContainer::ObjInfo* erasedComp) { erasedComponents.erase(erasedComp); updatedComponents.erase(erasedComp); } -void EraserTool::switch_tool(DrawingProgramToolType newTool) { +void EraserTool::commit_erase() { bool first = erasedComponents.empty(); // erase_component_container will call EraserTool::erase_component, which will clear erasedComponents eventually drawP.layerMan.erase_component_container(erasedComponents); for(auto& [comp, oldData] : updatedComponents) { @@ -180,37 +216,38 @@ void EraserTool::switch_tool(DrawingProgramToolType newTool) { } erasedComponents.clear(); updatedComponents.clear(); - isErasing = false; +} + +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); + } } void EraserTool::tool_update() { if(!drawP.world.main.g.gui.cursor_obstructed()) drawP.world.main.input.hideCursor = true; - 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 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; + commit_data(); } bool EraserTool::prevent_undo_or_redo() { @@ -218,7 +255,7 @@ bool EraserTool::prevent_undo_or_redo() { } void EraserTool::draw(SkCanvas* canvas, const DrawData& drawData) { - if(!drawP.world.main.input.isTouchDevice && !drawData.main->g.gui.cursor_obstructed() && !erasePath.isEmpty() && !eraseBorderPath.isEmpty()) { + if(!drawP.world.main.input.isTouchDevice && !drawData.main->g.gui.cursor_obstructed() && !erasePath.isEmpty()) { SkPaint linePaint; linePaint.setAntiAlias(drawData.skiaAA); linePaint.setColor4f({0.0f, 0.0f, 0.0f, 0.4f}); @@ -226,6 +263,6 @@ void EraserTool::draw(SkCanvas* canvas, const DrawData& drawData) { canvas->drawPath(erasePath, linePaint); linePaint.setColor4f({1.0f, 1.0f, 1.0f, 0.4f}); - canvas->drawPath(eraseBorderPath, linePaint); + canvas->drawPath(erasePath, linePaint); } } diff --git a/src/DrawingProgram/Tools/EraserTool.hpp b/src/DrawingProgram/Tools/EraserTool.hpp index ef3e613..dbb7499 100644 --- a/src/DrawingProgram/Tools/EraserTool.hpp +++ b/src/DrawingProgram/Tools/EraserTool.hpp @@ -20,6 +20,7 @@ #include #include "../../DrawData.hpp" #include "DrawingProgramToolBase.hpp" +#include "../../CanvasComponents/BrushComponentCode.hpp" class DrawingProgram; @@ -37,16 +38,18 @@ class EraserTool : public DrawingProgramToolBase { virtual bool prevent_undo_or_redo() override; virtual void input_mouse_button_on_canvas_callback(const InputManager::MouseButtonCallbackArgs& button) override; virtual void input_mouse_motion_callback(const InputManager::MouseMotionCallbackArgs& motion) override; + virtual void input_pen_axis_callback(const InputManager::PenAxisCallbackArgs& axis) override; std::unordered_map> updatedComponents; std::unordered_set erasedComponents; // Pointers will be erased from this set if theyre erased in the main list (done by callback) private: SkPath erasePath; - SkPath eraseBorderPath; - Vector2f start; - Vector2f end; + BrushComponentCode::BrushStrokeGenerationData genData; - void erase_between_points(); + void reset_erasing_stroke(); + void erase_on_path(); + void commit_erase(); + void commit_data(); bool isErasing = false; }; diff --git a/src/GlobalConfig.cpp b/src/GlobalConfig.cpp index 9509e70..9f20512 100644 --- a/src/GlobalConfig.cpp +++ b/src/GlobalConfig.cpp @@ -62,6 +62,7 @@ nlohmann::json GlobalConfig::get_config_json(const InputManager& input) const { toRet["themeInUse"] = themeCurrentlyLoaded; toRet["defaultCanvasBackgroundColor"] = defaultCanvasBackgroundColor; toRet["flipZoomToolDirection"] = flipZoomToolDirection; + toRet["realTimeEraser"] = realTimeEraser; #ifndef __EMSCRIPTEN__ toRet["checkForUpdates"] = checkForUpdates; #endif @@ -132,6 +133,7 @@ void GlobalConfig::set_config_json(InputManager& input, const nlohmann::json& j, if(version >= VersionNumber(0, 3, 0)) try{j.at("defaultCanvasBackgroundColor").get_to(defaultCanvasBackgroundColor);} catch(...) {} try{j.at("flipZoomToolDirection").get_to(flipZoomToolDirection);} catch(...) {} + try{j.at("realTimeEraser").get_to(realTimeEraser);} catch(...) {} #ifndef __EMSCRIPTEN__ try{j.at("checkForUpdates").get_to(checkForUpdates);} catch(...) {} #endif diff --git a/src/GlobalConfig.hpp b/src/GlobalConfig.hpp index 60cabca..1f63b92 100644 --- a/src/GlobalConfig.hpp +++ b/src/GlobalConfig.hpp @@ -71,6 +71,8 @@ class GlobalConfig { bool mobileUI = false; #endif + bool realTimeEraser = true; + struct TabletOptions { bool pressureAffectsBrushWidth = true; uint8_t middleClickButton = 1; diff --git a/src/Toolbar.cpp b/src/Toolbar.cpp index 2fb202f..e0dc0eb 100644 --- a/src/Toolbar.cpp +++ b/src/Toolbar.cpp @@ -1547,6 +1547,7 @@ void Toolbar::general_settings_inner_gui() { main.set_vsync_value(main.conf.vsyncValue); }); + checkbox_boolean_field(gui, "real time eraser", "Eraser works in real time", &main.conf.realTimeEraser); checkbox_boolean_field(gui, "force extension on path", "Force extension on path when saving files", &main.conf.forceExtensionOnPath); #ifndef __EMSCRIPTEN__ checkbox_boolean_field(gui, "apply display scale", "Apply display scale", &main.conf.applyDisplayScale);