14.4: Image › Rotate Canvas 90° Clockwise and Counterclockwise; quarter turns on whole pixels copy exactly
This commit is contained in:
21 files changed
+419
-54
No files matched your search
@@ -92,7 +92,7 @@ QTransform BrushRaster::pixelToDocument(const LayerTransform &transform, int wid
|
||||
const QPointF center = transform.center();
|
||||
QTransform map;
|
||||
map.translate(center.x(), center.y());
|
||||
map.rotateRadians(transform.radians());
|
||||
map.rotate(transform.degrees());
|
||||
map.scale(transform.size.width() / width * (transform.flipX ? -1 : 1),
|
||||
transform.size.height() / height * (transform.flipY ? -1 : 1));
|
||||
map.translate(-width / 2.0, -height / 2.0);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Swift's CanvasRotation extension: Image › Rotate Canvas 90°.
|
||||
public:
|
||||
// A quarter turn of everything; layers placed, not resampled.
|
||||
void rotateCanvas(bool clockwise);
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include <cmath>
|
||||
|
||||
LayerTransform LayerTransform::quarterTurned(bool clockwise, QSizeF canvas) const
|
||||
{
|
||||
LayerTransform result = *this;
|
||||
// The middle moves where the turn takes it.
|
||||
const QPointF middle = clockwise ? QPointF(canvas.height() - center().y(), center().x()) : QPointF(center().y(), canvas.width() - center().x());
|
||||
result.origin = QPointF(middle.x() - size.width() / 2, middle.y() - size.height() / 2);
|
||||
result.rotation = std::fmod(rotation + (clockwise ? 90 : -90), 360.0);
|
||||
return result;
|
||||
}
|
||||
|
||||
CanvasGuide CanvasGuide::quarterTurned(bool clockwise, QSizeF canvas) const
|
||||
{
|
||||
CanvasGuide guide = *this;
|
||||
if (axis == Axis::vertical) {
|
||||
guide.axis = Axis::horizontal;
|
||||
if (!clockwise)
|
||||
guide.position = canvas.width() - position;
|
||||
} else {
|
||||
guide.axis = Axis::vertical;
|
||||
if (clockwise)
|
||||
guide.position = canvas.height() - position;
|
||||
}
|
||||
return guide;
|
||||
}
|
||||
|
||||
void EditorSession::rotateCanvas(bool clockwise)
|
||||
{
|
||||
commitTransform();
|
||||
cancelCrop();
|
||||
if (!canEditLayers())
|
||||
return;
|
||||
const QSizeF canvas = m_document->size();
|
||||
beginEdit(clockwise ? QStringLiteral("Rotate Canvas 90° Clockwise") : QStringLiteral("Rotate Canvas 90° Counterclockwise"));
|
||||
for (ImageLayer &layer : m_document->layers) {
|
||||
layer.transform = layer.transform.quarterTurned(clockwise, canvas);
|
||||
if (layer.mask && layer.mask->placement)
|
||||
layer.mask->placement = layer.mask->placement->quarterTurned(clockwise, canvas);
|
||||
}
|
||||
std::swap(m_document->width, m_document->height);
|
||||
for (CanvasGuide &guide : m_document->guides)
|
||||
guide = guide.quarterTurned(clockwise, canvas);
|
||||
if (m_document->selection) {
|
||||
const QTransform turn = clockwise ? QTransform(0, 1, -1, 0, canvas.height(), 0) : QTransform(0, -1, 1, 0, 0, canvas.width());
|
||||
m_document->selection->path = turn.map(m_document->selection->path);
|
||||
}
|
||||
viewport.fit(m_document->size());
|
||||
endEdit();
|
||||
}
|
||||
@@ -222,7 +222,7 @@ Corners DistortWarp::carried(const LayerTransform &placement, const LayerTransfo
|
||||
{
|
||||
QTransform toDocument;
|
||||
toDocument.translate(transform.center().x(), transform.center().y());
|
||||
toDocument.rotateRadians(transform.radians());
|
||||
toDocument.rotate(transform.degrees());
|
||||
toDocument.scale(transform.size.width(), transform.size.height());
|
||||
toDocument.translate(-0.5, -0.5);
|
||||
const QTransform toUnit = toDocument.inverted();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Logging.h"
|
||||
#include <cmath>
|
||||
#include <tuple>
|
||||
|
||||
EditorSession::EditorSession(QObject *parent) : QObject(parent), m_busyTimer(this)
|
||||
{
|
||||
@@ -229,7 +230,10 @@ void EditorSession::restore(const DocumentHistory::Snapshot &snapshot)
|
||||
{
|
||||
cancelCrop();
|
||||
cancelGradient();
|
||||
const auto canvas = [](const std::optional<CanvasDocument> &document) { return document ? std::optional(document->id) : std::nullopt; };
|
||||
const auto canvas = [](const std::optional<CanvasDocument> &document) {
|
||||
return document ? std::optional(std::tuple(document->id, document->width, document->height)) : std::nullopt;
|
||||
};
|
||||
// Another canvas, or another size (a rotation): fit anew.
|
||||
const bool changedCanvas = canvas(m_document) != canvas(snapshot.document);
|
||||
// The mask stays the target while layer and mask remain.
|
||||
const bool keepsMaskTarget = m_isMaskSelected && m_activeLayerID == snapshot.activeLayerID;
|
||||
|
||||
@@ -276,6 +276,7 @@ public:
|
||||
#include "Document/Distort+Session.h"
|
||||
#include "Document/LayerMerge+Session.h"
|
||||
#include "Document/LayerFlip+Session.h"
|
||||
#include "Document/CanvasRotation+Session.h"
|
||||
#include "Document/Selection+Session.h"
|
||||
#include "Document/MagicWand+Session.h"
|
||||
#include "Document/SubjectRemoval+Session.h"
|
||||
|
||||
@@ -19,6 +19,8 @@ struct CanvasGuide {
|
||||
CanvasGuide scaled(double x, double y) const;
|
||||
// Mirrored when it crosses the flip, staying on content.
|
||||
CanvasGuide mirrored(bool horizontally, double center) const;
|
||||
// Turned with the canvas: the other axis, same content.
|
||||
CanvasGuide quarterTurned(bool clockwise, QSizeF canvas) const;
|
||||
friend bool operator==(const CanvasGuide &, const CanvasGuide &) = default;
|
||||
};
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ void FolderMaskClip::apply(QPointF center, const QPainter &context, QImage &cove
|
||||
{
|
||||
QTransform placement;
|
||||
placement.translate(center.x(), center.y());
|
||||
placement.rotateRadians(transform.radians());
|
||||
placement.rotate(transform.degrees());
|
||||
placement.scale(transform.flipX ? -1 : 1, transform.flipY ? -1 : 1);
|
||||
QPainter masking(&veil);
|
||||
masking.setRenderHint(QPainter::SmoothPixmapTransform, quality(transform.sampling) != InterpolationQuality::none);
|
||||
|
||||
@@ -89,6 +89,11 @@ double LayerTransform::radians() const
|
||||
return std::fmod(rotation, 360) * std::numbers::pi / 180;
|
||||
}
|
||||
|
||||
double LayerTransform::degrees() const
|
||||
{
|
||||
return std::fmod(rotation, 360);
|
||||
}
|
||||
|
||||
bool LayerTransform::isValid() const
|
||||
{
|
||||
const std::array<double, 5> values = {origin.x(), origin.y(), size.width(), size.height(), rotation};
|
||||
|
||||
@@ -36,6 +36,8 @@ struct LayerTransform {
|
||||
friend bool operator==(const LayerTransform &lhs, const LayerTransform &rhs);
|
||||
QPointF center() const;
|
||||
double radians() const;
|
||||
// The turn in degrees: QTransform::rotate makes quarter turns exact.
|
||||
double degrees() const;
|
||||
bool isValid() const;
|
||||
QPointF point(QPointF unit) const;
|
||||
bool contains(QPointF point) const;
|
||||
@@ -48,6 +50,8 @@ struct LayerTransform {
|
||||
bool samePlacement(const LayerTransform &other) const;
|
||||
// Mirrored across a line at `axis`; defined in LayerFlip.cpp.
|
||||
LayerTransform mirrored(bool horizontally, double axis) const;
|
||||
// Turned a quarter with its canvas; nothing resampled.
|
||||
LayerTransform quarterTurned(bool clockwise, QSizeF canvas) const;
|
||||
};
|
||||
|
||||
// Several layers under one box; each follows it.
|
||||
|
||||
@@ -53,13 +53,13 @@ void LayerRenderer::draw(const QImage &image, const LayerTransform &transform, Q
|
||||
// Large reductions draw from sharp halvings; QPainter does the rest.
|
||||
const double device = deviceScale(context);
|
||||
const Reduced source = reduced(image, width, device, transform.sampling);
|
||||
const InterpolationQuality quality = interpolation(
|
||||
transform.sampling, width * device / std::max(1, image.width()) * (1 << source.level), transform.radians() == 0);
|
||||
const QRectF bounds(-width / 2, -height / 2, width, height);
|
||||
QTransform placement;
|
||||
placement.translate(center.x(), center.y());
|
||||
placement.rotateRadians(transform.radians());
|
||||
placement.rotate(transform.degrees());
|
||||
placement.scale(transform.flipX ? -1 : 1, transform.flipY ? -1 : 1);
|
||||
const InterpolationQuality quality = interpolation(transform.sampling, width * device / std::max(1, image.width()) * (1 << source.level),
|
||||
upright(transform, placement, bounds, context));
|
||||
|
||||
if (options.mask.isNull() && options.clip.isNull() && !HslBlend::handles(options.blendMode)) {
|
||||
const QPainter::CompositionMode mode = compositionMode(options.blendMode);
|
||||
@@ -115,7 +115,7 @@ void LayerRenderer::drawBrushPreview(const QImage &image, const LayerTransform &
|
||||
};
|
||||
QTransform placement;
|
||||
placement.translate(center.x(), center.y());
|
||||
placement.rotateRadians(transform.radians());
|
||||
placement.rotate(transform.degrees());
|
||||
placement.scale(transform.flipX ? -1 : 1, transform.flipY ? -1 : 1);
|
||||
QRectF extent = bounds;
|
||||
for (const BrushPatch &patch : preview.patches)
|
||||
@@ -282,7 +282,7 @@ void LayerRenderer::drawCoverage(const QImage &image, const LayerTransform &tran
|
||||
}
|
||||
QTransform placement;
|
||||
placement.translate(transform.center().x(), transform.center().y());
|
||||
placement.rotateRadians(transform.radians());
|
||||
placement.rotate(transform.degrees());
|
||||
placement.scale(transform.flipX ? -1 : 1, transform.flipY ? -1 : 1);
|
||||
context.save();
|
||||
setSampling(context, quality(transform.sampling), transform.sampling);
|
||||
@@ -292,6 +292,22 @@ void LayerRenderer::drawCoverage(const QImage &image, const LayerTransform &tran
|
||||
context.restore();
|
||||
}
|
||||
|
||||
// QPainter's fixed-point bilinear strays a level where CoreGraphics copies.
|
||||
bool LayerRenderer::upright(const LayerTransform &transform, const QTransform &placement, const QRectF &bounds, const QPainter &context)
|
||||
{
|
||||
if (transform.radians() == 0)
|
||||
return true;
|
||||
if (std::remainder(transform.rotation, 90) != 0)
|
||||
return false;
|
||||
const QTransform device = placement * context.deviceTransform();
|
||||
for (const QPointF corner : {bounds.topLeft(), bounds.bottomRight()}) {
|
||||
const QPointF mapped = device.map(corner);
|
||||
if (std::abs(mapped.x() - std::round(mapped.x())) > 0.001 || std::abs(mapped.y() - std::round(mapped.y())) > 0.001)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
InterpolationQuality LayerRenderer::interpolation(LayerSampling sampling, double finalFactor, bool upright)
|
||||
{
|
||||
if (sampling == LayerSampling::nearest || (upright && std::abs(finalFactor - 1) < 0.001))
|
||||
|
||||
@@ -59,6 +59,8 @@ void composite(QPainter &context, const QTransform &placement, const QRectF &ext
|
||||
const std::function<void(QPainter &)> &body, const std::function<void(QPainter &)> &veil = {});
|
||||
// An upright layer drawn pixel for pixel copies straight across.
|
||||
InterpolationQuality interpolation(LayerSampling sampling, double finalFactor, bool upright = false);
|
||||
// Swift's upright, or a quarter turn on whole pixels.
|
||||
bool upright(const LayerTransform &transform, const QTransform &placement, const QRectF &bounds, const QPainter &context);
|
||||
Reduced reduced(const QImage &image, double width, double device, LayerSampling sampling);
|
||||
QRectF coverage(const Reduced &reduced, const QRectF &bounds);
|
||||
double deviceScale(const QPainter &context);
|
||||
|
||||
@@ -150,13 +150,13 @@ void withFrame(int pixelWidth, int pixelHeight, const QRectF &painted, const QRe
|
||||
return;
|
||||
const double device = LayerRenderer::deviceScale(context);
|
||||
const int level = transform.sampling == LayerSampling::nearest ? 0 : DownsampleCache::level(width * device / pixelWidth);
|
||||
const InterpolationQuality quality = LayerRenderer::interpolation(transform.sampling, width * device / pixelWidth * (1 << level),
|
||||
transform.radians() == 0);
|
||||
QTransform placement;
|
||||
placement.translate(center.x(), center.y());
|
||||
placement.rotateRadians(transform.radians());
|
||||
placement.rotate(transform.degrees());
|
||||
placement.scale(transform.flipX ? -1 : 1, transform.flipY ? -1 : 1);
|
||||
const QRectF bounds(-width / 2, -height / 2, width, height);
|
||||
const InterpolationQuality quality = LayerRenderer::interpolation(transform.sampling, width * device / pixelWidth * (1 << level),
|
||||
LayerRenderer::upright(transform, placement, bounds, context));
|
||||
const QRectF shown = placement.inverted().mapRect(BrushRaster::visibleRect(context));
|
||||
const double sx = pixelWidth / width, sy = pixelHeight / height;
|
||||
const Frame frame{bounds, double(pixelWidth), double(pixelHeight), level, device, transform.sampling,
|
||||
|
||||
@@ -213,6 +213,9 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
|
||||
add(image, QStringLiteral("imageSize"), QStringLiteral("Image Size…"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_I), [this] { projects().imageSize(); });
|
||||
add(image, QStringLiteral("trim"), QStringLiteral("Trim…"), QKeySequence(), [this] { projects().trim(); });
|
||||
image->addSeparator();
|
||||
add(image, QStringLiteral("rotateCanvasClockwise"), QStringLiteral("Rotate Canvas 90° Clockwise"), QKeySequence(), [this] { session().rotateCanvas(true); });
|
||||
add(image, QStringLiteral("rotateCanvasCounterclockwise"), QStringLiteral("Rotate Canvas 90° Counterclockwise"), QKeySequence(),
|
||||
[this] { session().rotateCanvas(false); });
|
||||
add(image, QStringLiteral("flipCanvasHorizontal"), QStringLiteral("Flip Canvas Horizontal"), QKeySequence(), [this] { session().flipCanvas(true); });
|
||||
add(image, QStringLiteral("flipCanvasVertical"), QStringLiteral("Flip Canvas Vertical"), QKeySequence(), [this] { session().flipCanvas(false); });
|
||||
|
||||
@@ -386,6 +389,8 @@ void CompositorMenus::synchronize()
|
||||
action(QStringLiteral("canvasSize"))->setEnabled(drawn && p.canStart());
|
||||
action(QStringLiteral("imageSize"))->setEnabled(drawn && p.canStart());
|
||||
action(QStringLiteral("trim"))->setEnabled(drawn && p.canStart());
|
||||
action(QStringLiteral("rotateCanvasClockwise"))->setEnabled(s.canEditLayers());
|
||||
action(QStringLiteral("rotateCanvasCounterclockwise"))->setEnabled(s.canEditLayers());
|
||||
action(QStringLiteral("flipCanvasHorizontal"))->setEnabled(s.canEditLayers());
|
||||
action(QStringLiteral("flipCanvasVertical"))->setEnabled(s.canEditLayers());
|
||||
action(QStringLiteral("newAdjustmentLayer"))->setEnabled(s.canEditLayers());
|
||||
|
||||
Reference in new issue
Block a user