14.4: Image › Rotate Canvas 90° Clockwise and Counterclockwise; quarter turns on whole pixels copy exactly

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 03:55:25 -04:00
1 parent 28cbe856e6
commit d8bb7b38b2
21 files changed
+419 -54

No files matched your search

+1 -1
View File
@@ -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);
+4
View File
@@ -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);
+51
View File
@@ -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();
}
+1 -1
View File
@@ -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 -1
View File
@@ -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;
+1
View File
@@ -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"
+2
View File
@@ -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;
};
+1 -1
View File
@@ -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);
+5
View File
@@ -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};
+4
View File
@@ -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.