[img-editor] Refined eraser brush algorithm.

This commit is contained in:
23rd
2026-03-11 09:27:15 +03:00
committed by John Preston
parent 537375b92b
commit 91df896f8e
7 changed files with 178 additions and 16 deletions
+1
View File
@@ -110,6 +110,7 @@ photoEditorMarkerSizeMultiplier: 2.5;
photoEditorArrowHeadLengthFactor: 2.5;
photoEditorArrowHeadMinDistanceFactor: 1.5;
photoEditorArrowHeadAngleDegrees: 26;
photoEditorEraserPreviewOpacity: 0.25;
photoEditorBrushSizeControlLeftSkip: 0px;
photoEditorBrushSizeControlHeight: 280px;
@@ -68,7 +68,10 @@ Paint::Paint(
_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_view->setFrameStyle(int(QFrame::NoFrame) | QFrame::Plain);
_view->setBackgroundBrush(Qt::transparent);
_view->setAttribute(Qt::WA_TranslucentBackground, true);
_viewport->setAutoFillBackground(false);
_viewport->setAttribute(Qt::WA_TranslucentBackground, true);
_viewport->installEventFilter(this);
// Undo / Redo.
+134 -2
View File
@@ -19,6 +19,81 @@ namespace {
using ItemPtr = std::shared_ptr<NumberedItem>;
class ItemEraser final : public NumberedItem {
public:
struct Target {
std::shared_ptr<ItemLine> item;
QPixmap before;
};
ItemEraser(
QPixmap mask,
QPointF maskPos,
std::vector<Target> targets)
: _mask(std::move(mask))
, _maskPos(maskPos)
, _targets(std::move(targets)) {
}
void apply() {
for (const auto &target : _targets) {
target.item->applyEraser(_mask, _maskPos);
}
}
void revert() {
for (const auto &target : _targets) {
target.item->setPixmap(target.before);
}
}
QRectF boundingRect() const override {
return QRectF();
}
void paint(
QPainter *,
const QStyleOptionGraphicsItem *,
QWidget *) override {
}
bool hasState(SaveState state) const override {
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
return saved.saved;
}
void save(SaveState state) override {
auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
saved = {
.saved = true,
.status = status(),
};
}
void restore(SaveState state) override {
if (!hasState(state)) {
return;
}
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
setStatus(saved.status);
if (saved.status == Status::Normal) {
apply();
} else if (saved.status == Status::Undid) {
revert();
}
}
private:
QPixmap _mask;
QPointF _maskPos;
std::vector<Target> _targets;
struct {
bool saved = false;
NumberedItem::Status status;
} _saved, _keeped;
};
bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
return event->isAccepted() || (event->button() == Qt::RightButton);
}
@@ -34,9 +109,54 @@ Scene::Scene(const QRectF &rect)
_canvas->grabContentRequests(
) | rpl::on_next([=](ItemCanvas::Content &&content) {
if (content.clear) {
auto mask = std::move(content.pixmap);
if (mask.isNull()) {
return;
}
const auto maskPos = content.position;
const auto maskSize = mask.size()
/ float64(mask.devicePixelRatio());
const auto maskRect = QRectF(maskPos, maskSize);
auto targets = std::vector<ItemEraser::Target>();
const auto hits = QGraphicsScene::items(
maskRect,
Qt::IntersectsItemBoundingRect,
Qt::DescendingOrder);
for (auto *raw : hits) {
const auto it = _itemsByPointer.find(raw);
if (it == end(_itemsByPointer)) {
continue;
}
const auto &item = it->second;
if (!item->isNormalStatus()) {
continue;
}
const auto line = std::dynamic_pointer_cast<ItemLine>(item);
if (!line) {
continue;
}
auto before = line->pixmap();
if (!line->applyEraser(mask, maskPos)) {
continue;
}
targets.push_back({
.item = line,
.before = std::move(before),
});
}
if (!targets.empty()) {
const auto eraser = std::make_shared<ItemEraser>(
std::move(mask),
maskPos,
std::move(targets));
addItem(eraser);
_canvas->setZValue(++_lastLineZ);
}
return;
}
const auto item = std::make_shared<ItemLine>(
std::move(content.pixmap),
content.clear);
std::move(content.pixmap));
item->setPos(content.position);
addItem(item);
_canvas->setZValue(++_lastLineZ);
@@ -53,7 +173,9 @@ void Scene::addItem(ItemPtr item) {
}
item->setNumber(_itemNumber++);
QGraphicsScene::addItem(item.get());
const auto raw = item.get();
_items.push_back(std::move(item));
_itemsByPointer.emplace(raw, _items.back());
_addsItem.fire({});
}
@@ -147,6 +269,9 @@ void Scene::performUndo() {
const auto it = ranges::find_if(filtered, &NumberedItem::isNormalStatus);
if (it != filtered.end()) {
if (const auto eraser = dynamic_cast<ItemEraser*>(it->get())) {
eraser->revert();
}
(*it)->setStatus(NumberedItem::Status::Undid);
}
}
@@ -156,6 +281,9 @@ void Scene::performRedo() {
const auto it = ranges::find_if(filtered, &NumberedItem::isUndidStatus);
if (it != filtered.end()) {
if (const auto eraser = dynamic_cast<ItemEraser*>(it->get())) {
eraser->apply();
}
(*it)->setStatus(NumberedItem::Status::Normal);
}
}
@@ -173,6 +301,10 @@ void Scene::removeIf(Fn<bool(const ItemPtr &)> proj) {
}
}
_items = std::move(copy);
_itemsByPointer.clear();
for (const auto &item : _items) {
_itemsByPointer.emplace(item.get(), item);
}
}
void Scene::clearRedoList() {
@@ -65,6 +65,7 @@ private:
const std::shared_ptr<float64> _lastZ;
std::vector<ItemPtr> _items;
std::unordered_map<QGraphicsItem*, ItemPtr> _itemsByPointer;
float64 _lastLineZ = 0.;
int _itemNumber = 0;
@@ -382,7 +382,7 @@ void ItemCanvas::paint(
p->fillRect(_rectToUpdate, Qt::transparent);
if (_brushData.tool == Brush::Tool::Eraser) {
p->save();
p->setCompositionMode(QPainter::CompositionMode_Clear);
p->setOpacity(st::photoEditorEraserPreviewOpacity);
p->drawPixmap(0, 0, _pixmap);
p->restore();
} else {
@@ -8,13 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/scene/scene_item_line.h"
#include <QGraphicsScene>
#include <QtGui/QPainter>
namespace Editor {
ItemLine::ItemLine(QPixmap &&pixmap, bool clear)
ItemLine::ItemLine(QPixmap &&pixmap)
: _pixmap(std::move(pixmap))
, _rect(QPointF(), _pixmap.size() / float64(style::DevicePixelRatio()))
, _clear(clear) {
, _rect(QPointF(), _pixmap.size() / float64(style::DevicePixelRatio())) {
}
QRectF ItemLine::boundingRect() const {
@@ -25,16 +25,39 @@ void ItemLine::paint(
QPainter *p,
const QStyleOptionGraphicsItem *,
QWidget *) {
if (_clear) {
p->save();
p->setCompositionMode(QPainter::CompositionMode_Clear);
p->drawPixmap(0, 0, _pixmap);
p->restore();
return;
}
p->drawPixmap(0, 0, _pixmap);
}
const QPixmap &ItemLine::pixmap() const {
return _pixmap;
}
void ItemLine::setPixmap(QPixmap pixmap) {
_pixmap = std::move(pixmap);
update();
}
bool ItemLine::applyEraser(const QPixmap &mask, const QPointF &maskPos) {
if (mask.isNull()) {
return false;
}
const auto maskSize = mask.size() / float64(mask.devicePixelRatio());
const auto localTopLeft = maskPos - pos();
const auto localRect = QRectF(localTopLeft, maskSize);
if (!localRect.intersects(_rect)) {
return false;
}
auto image = _pixmap.toImage().convertToFormat(
QImage::Format_ARGB32_Premultiplied);
auto p = QPainter(&image);
p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
p.drawPixmap(localTopLeft, mask);
p.end();
_pixmap = QPixmap::fromImage(std::move(image));
update();
return true;
}
bool ItemLine::collidesWithItem(
const QGraphicsItem *,
Qt::ItemSelectionMode) const {
@@ -13,12 +13,15 @@ namespace Editor {
class ItemLine : public NumberedItem {
public:
ItemLine(QPixmap &&pixmap, bool clear = false);
ItemLine(QPixmap &&pixmap);
QRectF boundingRect() const override;
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
[[nodiscard]] const QPixmap &pixmap() const;
void setPixmap(QPixmap pixmap);
bool applyEraser(const QPixmap &mask, const QPointF &maskPos);
bool hasState(SaveState state) const override;
void save(SaveState state) override;
@@ -31,9 +34,8 @@ protected:
const QPainterPath &,
Qt::ItemSelectionMode) const override;
private:
const QPixmap _pixmap;
QPixmap _pixmap;
const QRectF _rect;
const bool _clear = false;
struct {
bool saved = false;