Files
OmaPhoto/src/Rendering/EditorCanvas+Brush.cpp
T

245 lines
11 KiB
C++

#include "Rendering/EditorCanvas.h"
#include "IO/ImageExporter.h"
#include "IO/ProjectStore.h"
#include "Logging.h"
#include <QKeyEvent>
#include <cmath>
// Swift's brush on the canvas: strokes, tip drag, circle.
void CanvasView::brushMouseDown(QPointF point, Qt::KeyboardModifiers modifiers)
{
const QPointF pixel = m_session.viewport.documentPoint(point, m_session.document().value().size());
// Alt-click with Clone Stamp sets where it copies from.
if (m_session.tool() == NavigationTool::cloneStamp && modifiers.testFlag(Qt::AltModifier)) {
m_session.setCloneSource(pixel);
updateBrushCursor();
return;
}
m_brushPointer = point;
// Shift paints a line on from the last stroke's end.
const bool shift = modifiers.testFlag(Qt::ShiftModifier);
const std::optional<QPointF> from = shift ? m_session.shiftLineStart() : std::nullopt;
if (from) {
m_session.beginBrush(*from);
m_session.continueBrush(pixel);
} else {
m_session.beginBrush(pixel);
}
m_brushAxisAnchor = shift ? std::optional(pixel) : std::nullopt;
m_brushAxisHorizontal = std::nullopt;
m_brushLastPixel = pixel;
synchronizeDisplay();
}
// The circle follows; a drag goes on, Shift keeping axis.
bool CanvasView::brushMouseMove(QPointF point, Qt::KeyboardModifiers modifiers, bool dragging)
{
m_brushPointer = point;
updateBrushCursor();
// Swift's mouseMoved paints nothing; mouseDragged does.
if (!dragging || (!m_session.brushStroke() && !m_session.warpStroke()) || m_session.isProjectBusy() || !m_session.document())
return false;
QPointF pixel = m_session.viewport.documentPoint(point, m_session.document()->size());
if (modifiers.testFlag(Qt::ShiftModifier)) {
// The first three pixels settle the axis; it never flips.
const QPointF anchor = m_brushAxisAnchor ? *m_brushAxisAnchor : m_brushLastPixel.value_or(pixel);
if (!m_brushAxisAnchor) {
m_brushAxisAnchor = anchor;
m_brushAxisHorizontal = std::nullopt;
}
if (!m_brushAxisHorizontal && std::hypot(pixel.x() - anchor.x(), pixel.y() - anchor.y()) >= 3)
m_brushAxisHorizontal = std::abs(pixel.x() - anchor.x()) >= std::abs(pixel.y() - anchor.y());
if (m_brushAxisHorizontal)
pixel = *m_brushAxisHorizontal ? QPointF(pixel.x(), anchor.y()) : QPointF(anchor.x(), pixel.y());
else
pixel = anchor;
} else {
// Re-anchoring resets the axis, so the anchor alone goes.
m_brushAxisAnchor = std::nullopt;
}
m_brushLastPixel = pixel;
m_session.continueBrush(pixel);
synchronizeDisplay();
return true;
}
// The release reaches its own point, then the stroke lands.
void CanvasView::brushMouseUp(QPointF point)
{
if ((!m_session.brushStroke() && !m_session.warpStroke()) || m_session.isProjectBusy())
return;
if (m_session.document())
m_session.continueBrush(m_session.viewport.documentPoint(point, m_session.document()->size()));
m_session.finishBrushImmediately();
synchronizeDisplay();
}
// Right-drag: size across, hardness with Shift; the circle stays.
void CanvasView::beginBrushTipDrag(QPointF point, Qt::KeyboardModifiers modifiers)
{
const BrushSettings &settings = m_session.brushSettings();
m_brushTipDrag = BrushTipDrag{point, settings.diameter, settings.hardness, modifiers.testFlag(Qt::ShiftModifier)};
m_brushPointer = point;
updateBrushCursor();
}
void CanvasView::dragBrushTip(QPointF point, Qt::KeyboardModifiers modifiers)
{
BrushTipDrag &drag = m_brushTipDrag.value();
drag.hardnessShown = modifiers.testFlag(Qt::ShiftModifier);
const double dx = point.x() - drag.start.x();
BrushSettings settings = m_session.brushSettings();
if (drag.hardnessShown) {
// The full range across 200 points.
settings.hardness = std::clamp(drag.hardness + dx / 200, 0.0, 1.0);
settings.diameter = drag.diameter;
} else {
// The circle's edge follows the pointer on screen.
const double perPixel = std::max(0.0001, m_session.viewport.pointsPerPixel());
settings.diameter = std::clamp(std::round(drag.diameter + 2 * dx / perPixel), 1.0, 2000.0);
settings.hardness = drag.hardness;
}
m_session.setBrushSettings(settings);
m_brushPointer = drag.start;
updateBrushCursor();
}
void CanvasView::endBrushTipDrag(QPointF point)
{
m_brushTipDrag = std::nullopt;
m_brushPointer = point;
updateBrushCursor();
}
// The circle at the pointer, a stroke's own size.
void CanvasView::updateBrushCursor()
{
const bool shows = isBrushTool(m_session.tool()) && !m_spaceHeld && !picking() && !m_middlePanPoint && !m_middleZoom;
const double diameter = m_session.brushStroke() ? m_session.brushStroke()->settings.diameter : m_session.brushSettings().diameter;
// Clone Stamp marks its source; between strokes, previews a click.
std::optional<QPointF> sample;
QImage preview;
const std::optional<CanvasDocument> &document = m_session.document();
if (shows && m_session.tool() == NavigationTool::cloneStamp && m_brushPointer && document) {
const QPointF point = m_session.viewport.documentPoint(*m_brushPointer, document->size());
if (const std::optional<QPointF> source = m_session.cloneSamplePoint(point))
sample = m_session.viewport.viewPoint(*source, document->size());
const std::optional<QSizeF> offset = m_session.cloneStrokeOffset(point);
if (!m_session.brushStroke() && !m_optionHeld && offset)
preview = clonePreview(point + QPointF(offset->width(), offset->height()), diameter, *document);
}
const std::optional<double> hardness = m_brushTipDrag && m_brushTipDrag->hardnessShown ? std::optional(m_session.brushSettings().hardness) : std::nullopt;
const QImage tip = preview.isNull() ? QImage() : cloneTip(diameter, m_session.brushSettings().hardness);
update(m_brushCursor.update(shows ? m_brushPointer : std::nullopt, std::max(1.0, diameter * m_session.viewport.pointsPerPixel()), hardness, sample,
preview, m_session.brushSettings().opacity, tip));
}
// The brush engine paints one click; the preview softens alike.
QImage CanvasView::cloneTip(double diameter, double hardness)
{
if (m_cloneTip && m_cloneTip->diameter == diameter && m_cloneTip->hardness == hardness)
return m_cloneTip->image;
const int side = std::max(1, int(std::ceil(diameter)));
const QSizeF size(side, side);
BrushSettings settings{.diameter = diameter, .hardness = hardness, .red = 1, .green = 1, .blue = 1};
QImage image;
try {
BrushStroke stroke(ImageLayer(QStringLiteral("Tip"), size), false, settings, size);
stroke.append(QPointF(side / 2.0, side / 2.0));
stroke.flush();
const PaintSnapshot painted = stroke.paintSnapshot();
QImage context = BrushRaster::context(side, side, false);
QPainter painter(&context);
BrushRaster::draw(painted.asset.image(), painted.bounds, painter);
painter.end();
image = context;
} catch (const ProjectError &error) {
qCWarning(lcRendering) << "the clone tip cannot be painted:" << error.what();
} catch (const ExportError &error) {
qCWarning(lcRendering) << "the clone tip cannot be painted:" << error.what();
}
m_cloneTip = CloneTip{diameter, hardness, image};
return image;
}
// The source around `center` at screen resolution, kept until changed.
QImage CanvasView::clonePreview(QPointF center, double diameter, const CanvasDocument &document)
{
const double scale = m_session.viewport.pointsPerPixel() * m_session.viewport.backingScale;
const ClonePreviewKey key{center, diameter, scale, m_session.brushRevision(), m_session.history.undoCount(),
m_session.cloneSettings().sampleAllLayers, m_session.activeLayerID()};
if (m_clonePreview && m_clonePreview->key == key)
return m_clonePreview->image;
const int side = std::min(1024, std::max(1, int(std::ceil(diameter * scale))));
QImage image;
try {
QImage context = BrushRaster::context(side, side, false);
QPainter painter(&context);
const double perPixel = side / diameter;
painter.scale(perPixel, perPixel);
painter.translate(diameter / 2 - center.x(), diameter / 2 - center.y());
// A painted asset flattens here; the catch covers it too.
if (m_session.cloneSettings().sampleAllLayers) {
m_session.drawLiveComposite(document, painter);
} else if (const std::optional<ImageLayer> layer = m_session.activeLayer(); layer && layer->asset) {
const LayerTransform transform = m_session.displayedTransform(*layer);
LayerRenderer::draw(layer->asset->image(), transform, transform.center(), painter, {});
}
painter.end();
image = context;
} catch (const ExportError &error) {
qCWarning(lcRendering) << "the clone preview cannot be drawn:" << error.what();
}
m_clonePreview = ClonePreview{key, image};
return image;
}
// Swift's brush keys: Tab, B, E, digits, brackets.
bool CanvasView::brushKey(const QKeyEvent &key)
{
if (key.modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))
return false;
if (key.key() == Qt::Key_Tab && !key.modifiers().testFlag(Qt::ShiftModifier)) {
m_session.cycleToolMode();
updateCursor();
updateBrushCursor();
return true;
}
if (key.key() == Qt::Key_B || key.key() == Qt::Key_E) {
m_session.selectTool(NavigationTool::brush);
m_session.setBrushMode(key.key() == Qt::Key_E ? BrushToolMode::erase : BrushToolMode::paint);
return true;
}
const QString text = key.text();
if (text.size() == 1 && text.front().isDigit() && m_session.usesOpacityKeys()) {
m_session.typeOpacityDigit(text.front().digitValue());
return true;
}
return brushBracket(text);
}
// Brackets: size, and hardness with Shift, for brushes.
bool CanvasView::brushBracket(const QString &text)
{
if (!isBrushTool(m_session.tool()) || (text != u"[" && text != u"]" && text != u"{" && text != u"}"))
return false;
if (text == u"[" || text == u"]")
m_session.changeBrushSize(text == u"]");
else
m_session.changeBrushHardness(text == u"}");
return true;
}
// The stroke's dirty rect in view points, two points out.
std::optional<QRectF> CanvasView::strokeDirtyRect() const
{
const BrushStroke *stroke = m_session.brushStroke();
const std::optional<CanvasDocument> &document = m_session.document();
if (!stroke || !document || !stroke->dirtyDocumentRect())
return std::nullopt;
const QRectF &dirty = *stroke->dirtyDocumentRect();
const QPointF origin = m_session.viewport.viewPoint(dirty.topLeft(), document->size());
const double scale = m_session.viewport.pointsPerPixel();
return QRectF(origin, QSizeF(dirty.width() * scale, dirty.height() * scale)).adjusted(-2, -2, 2, 2);
}