12.3a: guides, the layout grid, Snap To and the View entries (Compositor 1.1.7)

This commit is contained in:
雷电芽衣 committed 2026-09-26 20:12:11 -04:00
1 parent 4bfc6c237e
commit 338451fc35
25 files changed
+1029 -52

No files matched your search

+6 -34
View File
@@ -133,8 +133,7 @@ QRectF CropSnap::apply(QRectF rect, const CropDrag &drag, QPointF point, std::op
return result;
}
namespace {
QRectF boxAround(const LayerTransform &transform)
QRectF TransformSnap::box(const LayerTransform &transform)
{
const Corners corners = DistortWarp::corners(transform);
QPointF low = corners[0], high = corners[0];
@@ -144,25 +143,11 @@ QRectF boxAround(const LayerTransform &transform)
}
return QRectF(low, high);
}
}
// The canvas's edges and middle, and every other visible layer's.
// View > Snap To's targets, centres included.
SnapGuides EditorSession::transformSnapTargets(const QSet<QUuid> &moving) const
{
SnapGuides targets;
if (!m_document)
return targets;
const QSizeF size = m_document->size();
targets.xs = {0, size.width() / 2, size.width()};
targets.ys = {0, size.height() / 2, size.height()};
for (const ImageLayer &layer : m_document->renderLayers()) {
if (!layer.asset || moving.contains(layer.id))
continue;
const QRectF box = boxAround(displayedTransform(layer));
targets.xs.insert(targets.xs.end(), {std::round(box.left()), std::round(box.center().x()), std::round(box.right())});
targets.ys.insert(targets.ys.end(), {std::round(box.top()), std::round(box.center().y()), std::round(box.bottom())});
}
return targets;
return alignmentSnapTargets(moving, true);
}
// `draft` nudged onto a nearby edge or middle, within `tolerance`.
@@ -173,7 +158,7 @@ LayerTransform EditorSession::snappedMove(const LayerTransform &draft, const QSe
return draft;
}
const SnapGuides targets = transformSnapTargets(moving);
const TransformSnap::Offset snap = TransformSnap::offset(boxAround(draft), targets.xs, targets.ys, tolerance);
const TransformSnap::Offset snap = TransformSnap::offset(TransformSnap::box(draft), targets.xs, targets.ys, tolerance);
snapGuides = {snap.x ? std::vector<double>{*snap.x} : std::vector<double>{}, snap.y ? std::vector<double>{*snap.y} : std::vector<double>{}};
if (snap.offset.isNull())
return draft;
@@ -182,23 +167,10 @@ LayerTransform EditorSession::snappedMove(const LayerTransform &draft, const QSe
return snapped;
}
// The canvas's edges and every visible layer's upright bounds.
// View > Snap To's targets, without the centres.
SnapGuides EditorSession::cropSnapTargets() const
{
SnapGuides targets;
if (!m_document)
return targets;
const QSizeF size = m_document->size();
targets.xs = {0, size.width()};
targets.ys = {0, size.height()};
for (const ImageLayer &layer : m_document->renderLayers()) {
if (!layer.asset)
continue;
const QRectF box = boxAround(displayedTransform(layer));
targets.xs.insert(targets.xs.end(), {std::round(box.left()), std::round(box.right())});
targets.ys.insert(targets.ys.end(), {std::round(box.top()), std::round(box.bottom())});
}
return targets;
return alignmentSnapTargets({}, false);
}
std::optional<QRectF> EditorSession::visibleCropRect() const
+1 -1
View File
@@ -45,7 +45,7 @@ CanvasDocument::CanvasDocument(int width, int height)
CanvasDocument::CanvasDocument(const ProjectSnapshot &snapshot)
: id(snapshot.manifest.documentID), width(int(snapshot.manifest.width)), height(int(snapshot.manifest.height)),
resolution(snapshot.manifest.resolution.value_or(72))
resolution(snapshot.manifest.resolution.value_or(72)), guides(snapshot.manifest.guides.value_or(std::vector<CanvasGuide>()))
{
for (const ProjectLayerRecord &record : snapshot.manifest.layers) {
const auto image = snapshot.images.find(record.id);
+3
View File
@@ -1,6 +1,7 @@
#pragma once
#include "Document/LayerAdjustment.h"
#include "Document/LayerAppearance.h"
#include "Document/Guides.h"
#include "Document/LayerGroups.h"
#include "Document/LayerMask.h"
#include "Document/LayerTransform.h"
@@ -63,6 +64,8 @@ struct CanvasDocument {
int height;
double resolution = 72;
std::vector<ImageLayer> layers; // Bottom to top.
// Saved with the project; undo covers them.
std::vector<CanvasGuide> guides;
// Part of the document so undo covers it; never saved.
std::optional<DocumentSelection> selection;
+5
View File
@@ -15,6 +15,9 @@ std::optional<ProjectSnapshot> EditorSession::projectSnapshot() const
snapshot.masks.insert({layer.id, layer.mask->asset});
snapshot.manifest.layers.push_back(layer.hierarchyRecord());
}
// No guides leave the key out, as Swift writes nil.
if (!m_document->guides.empty())
snapshot.manifest.guides = m_document->guides;
return snapshot;
}
@@ -23,6 +26,7 @@ void EditorSession::installProject(const ProjectSnapshot &snapshot, const QStrin
m_collapsedGroupIDs.clear();
m_isMaskSelected = false;
m_cropRect.reset();
m_guideDrag = std::nullopt;
m_transformEdit = std::nullopt;
m_document = CanvasDocument(snapshot);
// Install loads shapes and text; Swift's resize rebuild does not.
@@ -53,6 +57,7 @@ void EditorSession::clearProject()
m_isMaskSelected = false;
m_cropRect.reset();
m_transformEdit = std::nullopt;
m_guideDrag = std::nullopt;
m_document = std::nullopt;
setActiveLayerID(std::nullopt);
m_renamingLayerID = std::nullopt;
+12
View File
@@ -297,6 +297,7 @@ public:
#include "Document/LayerAdjustment+Session.h"
#include "Document/AdjustmentEditing+Session.h"
#include "Document/LayerEffects+Session.h"
#include "Document/Guides+Session.h"
signals:
void changed();
@@ -340,6 +341,17 @@ private:
std::optional<QString> m_projectPath;
bool m_showsPixelGrid = true;
bool m_snappingEnabled = true;
// The layout grid starts off; guides show; rulers hide.
bool m_showsGrid = false;
bool m_showsGuides = true;
bool m_showsRulers = false;
bool m_snapEnabled = true;
bool m_snapToGuides = true;
bool m_snapToGrid = false;
bool m_snapToLayers = true;
bool m_snapToDocumentBounds = true;
bool m_locksGuides = false;
std::optional<GuideDrag> m_guideDrag;
bool m_showsSampleRing = true;
std::optional<QRectF> m_cropRect;
QString m_cropRatioChoice = QStringLiteral("Free");
+39
View File
@@ -0,0 +1,39 @@
// Swift's Guides extension, and the View menu's guide state.
public:
static constexpr double guideHitDistance = 5;
bool showsGrid() const { return m_showsGrid; }
void setShowsGrid(bool shows);
bool showsGuides() const { return m_showsGuides; }
void setShowsGuides(bool shows);
bool showsRulers() const { return m_showsRulers; }
void setShowsRulers(bool shows);
// View > Snap To's master switch, beside 1.1.6's Snap.
bool snapEnabled() const { return m_snapEnabled; }
void setSnapEnabled(bool enabled);
bool snapToGuides() const { return m_snapToGuides; }
void setSnapToGuides(bool snaps);
bool snapToGrid() const { return m_snapToGrid; }
void setSnapToGrid(bool snaps);
bool snapToLayers() const { return m_snapToLayers; }
void setSnapToLayers(bool snaps);
bool snapToDocumentBounds() const { return m_snapToDocumentBounds; }
void setSnapToDocumentBounds(bool snaps);
bool locksGuides() const { return m_locksGuides; }
void setLocksGuides(bool locks);
const std::optional<GuideDrag> &guideDrag() const { return m_guideDrag; }
bool canClearGuides() const;
bool canEditGuides() const;
// The guides as shown, a drag under way included.
std::vector<CanvasGuide> displayedGuides() const;
std::optional<CanvasGuide> hitGuide(QPointF viewPoint, double tolerance = guideHitDistance) const;
void beginGuideCreation(CanvasGuide::Axis axis, double position);
void beginGuideMove(const CanvasGuide &guide);
void moveGuideDrag(double position);
// Dropped on a ruler, the guide goes.
void finishGuideDrag(bool removing);
void cancelGuideDrag();
void clearGuides();
void addGuide(const CanvasGuide &guide);
// What a move or crop snaps to, per Snap To.
SnapGuides alignmentSnapTargets(const QSet<QUuid> &moving, bool includeCenters) const;
double snappedGuidePosition(double position, CanvasGuide::Axis axis, std::optional<QUuid> excluding) const;
+313
View File
@@ -0,0 +1,313 @@
#include "Document/Guides.h"
#include "Document/EditorSession.h"
#include <algorithm>
#include <cmath>
CanvasGuide CanvasGuide::offset(double x, double y) const
{
CanvasGuide guide = *this;
guide.position += axis == Axis::vertical ? x : y;
return guide;
}
CanvasGuide CanvasGuide::scaled(double x, double y) const
{
CanvasGuide guide = *this;
guide.position *= axis == Axis::vertical ? x : y;
return guide;
}
CanvasGuide CanvasGuide::mirrored(bool horizontally, double center) const
{
CanvasGuide guide = *this;
if ((horizontally && axis == Axis::vertical) || (!horizontally && axis == Axis::horizontal))
guide.position = 2 * center - guide.position;
return guide;
}
QString rawValue(CanvasGuide::Axis axis)
{
return axis == CanvasGuide::Axis::vertical ? QStringLiteral("vertical") : QStringLiteral("horizontal");
}
std::optional<CanvasGuide::Axis> guideAxis(const QString &text)
{
if (text == QLatin1String("vertical"))
return CanvasGuide::Axis::vertical;
if (text == QLatin1String("horizontal"))
return CanvasGuide::Axis::horizontal;
return std::nullopt;
}
std::vector<double> LayoutGrid::lines(double length)
{
if (!(length >= 0))
return {0};
std::vector<double> result;
for (double value = 0; value <= length + 0.001; value += step)
result.push_back(std::round(value));
return result;
}
bool LayoutGrid::isMajor(double value)
{
return std::abs(std::fmod(std::round(value), spacing)) < 0.001;
}
void EditorSession::setShowsGrid(bool shows)
{
m_showsGrid = shows;
notify();
}
void EditorSession::setShowsGuides(bool shows)
{
m_showsGuides = shows;
notify();
}
void EditorSession::setShowsRulers(bool shows)
{
m_showsRulers = shows;
notify();
}
void EditorSession::setSnapEnabled(bool enabled)
{
m_snapEnabled = enabled;
notify();
}
void EditorSession::setSnapToGuides(bool snaps)
{
m_snapToGuides = snaps;
notify();
}
void EditorSession::setSnapToGrid(bool snaps)
{
m_snapToGrid = snaps;
notify();
}
void EditorSession::setSnapToLayers(bool snaps)
{
m_snapToLayers = snaps;
notify();
}
void EditorSession::setSnapToDocumentBounds(bool snaps)
{
m_snapToDocumentBounds = snaps;
notify();
}
void EditorSession::setLocksGuides(bool locks)
{
m_locksGuides = locks;
notify();
}
bool EditorSession::canClearGuides() const
{
return m_document && !m_document->guides.empty();
}
bool EditorSession::canEditGuides() const
{
return m_document && !m_locksGuides && !m_isProjectBusy && !m_isImporting && !m_showsNewDocument && !m_levels && !m_hueSaturation
&& !m_filterEdit && !m_renamingLayerID;
}
std::vector<CanvasGuide> EditorSession::displayedGuides() const
{
std::vector<CanvasGuide> guides = m_document ? m_document->guides : std::vector<CanvasGuide>();
if (!m_guideDrag)
return guides;
const CanvasGuide current{m_guideDrag->id, m_guideDrag->axis, m_guideDrag->position};
const auto found = std::find_if(guides.begin(), guides.end(), [&](const CanvasGuide &guide) { return guide.id == current.id; });
if (found != guides.end())
guides[size_t(found - guides.begin())] = current;
else if (m_guideDrag->isNew)
guides.push_back(current);
return guides;
}
std::optional<CanvasGuide> EditorSession::hitGuide(QPointF viewPoint, double tolerance) const
{
if (!m_showsGuides || m_locksGuides || !m_document)
return std::nullopt;
std::optional<CanvasGuide> best;
double nearest = 0;
for (const CanvasGuide &guide : displayedGuides()) {
const QPointF at = viewport.viewPoint(QPointF(guide.position, guide.position), m_document->size());
const double distance = guide.axis == CanvasGuide::Axis::vertical ? std::abs(viewPoint.x() - at.x()) : std::abs(viewPoint.y() - at.y());
if (distance <= tolerance && (!best || distance < nearest)) {
best = guide;
nearest = distance;
}
}
return best;
}
void EditorSession::beginGuideCreation(CanvasGuide::Axis axis, double position)
{
if (!canEditGuides())
return;
m_showsGuides = true;
m_guideDrag = GuideDrag{QUuid::createUuid(), axis, snappedGuidePosition(position, axis, std::nullopt), true, std::nullopt};
notify();
}
void EditorSession::beginGuideMove(const CanvasGuide &guide)
{
if (!canEditGuides())
return;
m_guideDrag = GuideDrag{guide.id, guide.axis, guide.position, false, guide.position};
notify();
}
void EditorSession::moveGuideDrag(double position)
{
if (!m_guideDrag)
return;
m_guideDrag->position = snappedGuidePosition(position, m_guideDrag->axis, m_guideDrag->id);
notify();
}
void EditorSession::finishGuideDrag(bool removing)
{
if (!m_guideDrag)
return;
const GuideDrag drag = *std::exchange(m_guideDrag, std::nullopt);
if (removing) {
// A new guide dropped on a ruler was never there.
if (!drag.isNew) {
beginEdit(QStringLiteral("Delete Guide"));
std::erase_if(m_document->guides, [&](const CanvasGuide &guide) { return guide.id == drag.id; });
endEdit();
}
notify();
return;
}
if (drag.isNew) {
beginEdit(QStringLiteral("New Guide"));
m_document->guides.push_back({drag.id, drag.axis, drag.position});
endEdit();
} else if (drag.original != drag.position) {
beginEdit(QStringLiteral("Move Guide"));
for (CanvasGuide &guide : m_document->guides) {
if (guide.id == drag.id)
guide.position = drag.position;
}
endEdit();
}
notify();
}
void EditorSession::cancelGuideDrag()
{
m_guideDrag = std::nullopt;
notify();
}
void EditorSession::clearGuides()
{
if (!canClearGuides())
return;
beginEdit(QStringLiteral("Clear Guides"));
m_document->guides.clear();
endEdit();
}
void EditorSession::addGuide(const CanvasGuide &guide)
{
if (!canEditGuides())
return;
m_showsGuides = true;
beginEdit(QStringLiteral("New Guide"));
m_document->guides.push_back(guide);
endEdit();
}
SnapGuides EditorSession::alignmentSnapTargets(const QSet<QUuid> &moving, bool includeCenters) const
{
SnapGuides targets;
if (!m_snapEnabled || !m_document)
return targets;
const QSizeF size = m_document->size();
if (m_snapToDocumentBounds) {
targets.xs = {0, size.width()};
targets.ys = {0, size.height()};
if (includeCenters) {
targets.xs.push_back(size.width() / 2);
targets.ys.push_back(size.height() / 2);
}
}
if (m_snapToLayers) {
for (const ImageLayer &layer : m_document->renderLayers()) {
if (!layer.asset || moving.contains(layer.id))
continue;
const QRectF box = TransformSnap::box(displayedTransform(layer));
const QPointF low = box.topLeft(), high = box.bottomRight();
// Swift's order: low edge, the middle when asked, high edge.
targets.xs.push_back(std::round(low.x()));
targets.ys.push_back(std::round(low.y()));
if (includeCenters) {
targets.xs.push_back(std::round((low.x() + high.x()) / 2));
targets.ys.push_back(std::round((low.y() + high.y()) / 2));
}
targets.xs.push_back(std::round(high.x()));
targets.ys.push_back(std::round(high.y()));
}
}
// Hidden extras do not snap, as in Photoshop.
if (m_snapToGrid && m_showsGrid) {
for (double x : LayoutGrid::lines(size.width()))
targets.xs.push_back(x);
for (double y : LayoutGrid::lines(size.height()))
targets.ys.push_back(y);
}
if (m_snapToGuides && m_showsGuides) {
for (const CanvasGuide &guide : displayedGuides())
(guide.axis == CanvasGuide::Axis::vertical ? targets.xs : targets.ys).push_back(guide.position);
}
return targets;
}
double EditorSession::snappedGuidePosition(double position, CanvasGuide::Axis axis, std::optional<QUuid> excluding) const
{
if (!m_snapEnabled || !m_document)
return position;
const double tolerance = TransformSnap::distance / std::max(viewport.pointsPerPixel(), 0.0001);
const bool vertical = axis == CanvasGuide::Axis::vertical;
const double length = vertical ? m_document->size().width() : m_document->size().height();
std::vector<double> targets;
if (m_snapToGrid && m_showsGrid)
targets = LayoutGrid::lines(length);
if (m_snapToGuides && m_showsGuides) {
for (const CanvasGuide &guide : displayedGuides()) {
if (guide.axis == axis && guide.id != excluding)
targets.push_back(guide.position);
}
}
if (m_snapToDocumentBounds)
targets.insert(targets.end(), {0, length / 2, length});
if (m_snapToLayers) {
for (const ImageLayer &layer : m_document->renderLayers()) {
if (!layer.asset)
continue;
const QRectF box = TransformSnap::box(displayedTransform(layer));
const QPointF low = box.topLeft(), high = box.bottomRight();
const double from = vertical ? low.x() : low.y(), to = vertical ? high.x() : high.y();
targets.insert(targets.end(), {std::round(from), std::round((from + to) / 2), std::round(to)});
}
}
// The nearest within reach; the first of equals wins.
std::optional<double> best;
for (const double target : targets) {
if (std::abs(target - position) <= tolerance && (!best || std::abs(*best - position) > std::abs(target - position)))
best = target;
}
return best.value_or(position);
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <QColor>
#include <QString>
#include <QUuid>
#include <optional>
#include <vector>
// A user-placed alignment line, in document pixels.
struct CanvasGuide {
enum class Axis { horizontal, vertical };
QUuid id;
Axis axis;
// Y for a horizontal guide, X for a vertical one.
double position;
CanvasGuide offset(double x, double y) const;
CanvasGuide scaled(double x, double y) const;
// Mirrored when it crosses the flip, staying on content.
CanvasGuide mirrored(bool horizontally, double center) const;
friend bool operator==(const CanvasGuide &, const CanvasGuide &) = default;
};
// Swift's raw values, "horizontal" and "vertical".
QString rawValue(CanvasGuide::Axis axis);
std::optional<CanvasGuide::Axis> guideAxis(const QString &text);
// The non-printing grid: majors every 64 px, eight subdivisions.
namespace LayoutGrid {
inline constexpr double spacing = 64;
inline constexpr int subdivisions = 8;
inline constexpr double step = spacing / subdivisions;
// Every line along a document edge, whole pixels.
std::vector<double> lines(double length);
bool isMajor(double value);
}
// A create or move under way; the document changes after.
struct GuideDrag {
QUuid id;
CanvasGuide::Axis axis;
double position;
bool isNew;
std::optional<double> original;
friend bool operator==(const GuideDrag &, const GuideDrag &) = default;
};
+2
View File
@@ -67,5 +67,7 @@ void EditorSession::flipCanvas(bool horizontally)
const QTransform mirror = horizontally ? QTransform(-1, 0, 0, 1, size.width(), 0) : QTransform(1, 0, 0, -1, 0, size.height());
m_document->selection->path = mirror.map(m_document->selection->path);
}
for (CanvasGuide &guide : m_document->guides)
guide = guide.mirrored(horizontally, axis);
endEdit();
}
+2
View File
@@ -108,4 +108,6 @@ struct Offset {
Offset offset(const QRectF &box, const std::vector<double> &xs, const std::vector<double> &ys,
double tolerance);
// The upright box round a transform's corners; in Crop.cpp.
QRectF box(const LayerTransform &transform);
}
+6 -1
View File
@@ -20,7 +20,12 @@ ProjectSnapshot resized(const ProjectSnapshot &snapshot, const CanvasSizeOptions
if (options.width == old.width && options.height == old.height && offset == QPointF(0, 0))
return snapshot;
ProjectManifest manifest{.resolution = old.resolution, .documentID = old.documentID, .width = options.width,
.height = options.height, .activeLayerID = old.activeLayerID, .layers = {}};
.height = options.height, .activeLayerID = old.activeLayerID, .layers = {}, .guides = old.guides};
// Guides move with the pixels.
if (manifest.guides) {
for (CanvasGuide &guide : *manifest.guides)
guide = guide.offset(offset.x(), offset.y());
}
for (const ProjectLayerRecord &layer : old.layers) {
LayerTransform transform = layer.transform;
transform.origin += offset;
+6 -1
View File
@@ -33,7 +33,7 @@ ProjectSnapshot resized(const ProjectSnapshot &snapshot, const ImageSizeOptions
throw ProjectError(ProjectError::Kind::tooLarge);
const ProjectManifest &old = snapshot.manifest;
ProjectManifest manifest{.resolution = options.resolution, .documentID = old.documentID, .width = options.width,
.height = options.height, .activeLayerID = old.activeLayerID, .layers = {}};
.height = options.height, .activeLayerID = old.activeLayerID, .layers = {}, .guides = old.guides};
if (old.width == options.width && old.height == options.height) {
manifest.layers = old.layers;
return {.manifest = manifest, .images = snapshot.images, .masks = snapshot.masks};
@@ -41,6 +41,11 @@ ProjectSnapshot resized(const ProjectSnapshot &snapshot, const ImageSizeOptions
if (options.width * options.height > 100'000'000)
throw ProjectError(ProjectError::Kind::tooLarge);
const double sx = double(options.width) / double(old.width), sy = double(options.height) / double(old.height);
// Guides scale with the pixels.
if (manifest.guides) {
for (CanvasGuide &guide : *manifest.guides)
guide = guide.scaled(sx, sy);
}
std::map<QUuid, ImportedImage> images, masks;
qint64 usedPixels = 0, usedMaskPixels = 0;
for (const ProjectLayerRecord &layer : old.layers) {
+28 -1
View File
@@ -115,6 +115,26 @@ LayerTextStyle textStyle(const QJsonValue &value)
.boxSize = optional(object, "boxSize", size)};
}
CanvasGuide guide(const QJsonValue &value)
{
const QJsonObject object = value.toObject();
const std::optional<CanvasGuide::Axis> axis = guideAxis(string(object.value("axis")));
// A value that is no object has no axis: refused.
if (!axis)
refuse();
return {uuid(object.value("id")), *axis, number(object.value("position"))};
}
std::vector<CanvasGuide> guides(const QJsonValue &value)
{
if (!value.isArray())
refuse();
std::vector<CanvasGuide> result;
for (const QJsonValue &each : value.toArray())
result.push_back(guide(each));
return result;
}
ProjectLayerRecord layer(const QJsonValue &value)
{
const QJsonObject object = value.toObject();
@@ -211,6 +231,12 @@ QByteArray ProjectManifest::encoded() const
object.insert("resolution", *resolution);
if (activeLayerID)
object.insert("activeLayerID", uuidString(*activeLayerID));
if (guides) {
QJsonArray lines;
for (const CanvasGuide &guide : *guides)
lines.append(QJsonObject{{"id", uuidString(guide.id)}, {"axis", rawValue(guide.axis)}, {"position", guide.position}});
object.insert("guides", lines);
}
// Swift indents by two; four would outgrow 4 MiB sooner.
QByteArray json;
for (const QByteArray &line : QJsonDocument(object).toJson(QJsonDocument::Indented).split('\n')) {
@@ -232,7 +258,8 @@ ProjectManifest ProjectManifest::decoded(const QByteArray &data)
return {.format = string(object.value("format")), .version = integer(object.value("version")),
.colorSpace = string(object.value("colorSpace")), .resolution = optional(object, "resolution", number),
.documentID = uuid(object.value("documentID")), .width = integer(object.value("width")),
.height = integer(object.value("height")), .activeLayerID = optional(object, "activeLayerID", uuid), .layers = records};
.height = integer(object.value("height")), .activeLayerID = optional(object, "activeLayerID", uuid), .layers = records,
.guides = optional(object, "guides", ::guides)};
}
std::pair<QString, qint64> ProjectManifest::header(const QByteArray &data)
+21
View File
@@ -38,6 +38,26 @@ QString description(ProjectError::Kind kind)
constexpr qint64 manifestBytes = 4 * 1024 * 1024, assetBytes = 512 * 1024 * 1024;
// Guides came with version 8: a thousand, each once, bounded.
void validateGuides(const ProjectManifest &manifest)
{
const std::vector<CanvasGuide> guides = manifest.guides.value_or(std::vector<CanvasGuide>());
if (manifest.version < 8) {
if (!guides.empty())
throw ProjectError(ProjectError::Kind::invalid);
return;
}
if (guides.size() > 1'000)
throw ProjectError(ProjectError::Kind::tooLarge);
QSet<QUuid> ids;
for (const CanvasGuide &guide : guides) {
// The range refuses NaN and infinity as well.
if (ids.contains(guide.id) || !(std::abs(guide.position) <= 1'000'000))
throw ProjectError(ProjectError::Kind::invalid);
ids.insert(guide.id);
}
}
void validate(const ProjectManifest &manifest)
{
if (manifest.format != QLatin1String("com.compositor.project"))
@@ -88,6 +108,7 @@ void validate(const ProjectManifest &manifest)
}
if (manifest.activeLayerID && !ids.contains(*manifest.activeLayerID))
throw ProjectError(ProjectError::Kind::invalid);
validateGuides(manifest);
}
// Images share 100 million pixels; masks share another.
+3
View File
@@ -1,4 +1,5 @@
#pragma once
#include "Document/Guides.h"
#include "Document/LayerAdjustment.h"
#include "Document/LayerAppearance.h"
#include "Document/LayerEffects.h"
@@ -60,6 +61,8 @@ struct ProjectManifest {
qint64 height;
std::optional<QUuid> activeLayerID;
std::vector<ProjectLayerRecord> layers;
// Alignment guides; versions 1 to 7 carry none.
std::optional<std::vector<CanvasGuide>> guides = std::nullopt;
// Pretty JSON with sorted keys; absent optionals are left out.
QByteArray encoded() const;
+35
View File
@@ -129,6 +129,32 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
add(view, QStringLiteral("transformControls"), QStringLiteral("Show Transform Controls"), QKeySequence(Qt::CTRL | Qt::Key_H),
[this] { session().setShowsTransformControls(!session().showsTransformControls()); })
->setCheckable(true);
// Swift 1.1.7's guides, grid, rulers and Snap To.
view->addSeparator();
const auto toggle = [this](QMenu *menu, const QString &name, const QString &text, const QKeySequence &key, bool (EditorSession::*read)() const,
void (EditorSession::*write)(bool)) {
add(menu, name, text, key, [this, read, write] { (session().*write)(!(session().*read)()); })->setCheckable(true);
};
QMenu *show = view->addMenu(QStringLiteral("Show"));
toggle(show, QStringLiteral("showGrid"), QStringLiteral("Grid"), QKeySequence(Qt::CTRL | Qt::Key_Apostrophe), &EditorSession::showsGrid,
&EditorSession::setShowsGrid);
toggle(show, QStringLiteral("showGuides"), QStringLiteral("Guides"), QKeySequence(Qt::CTRL | Qt::Key_Semicolon), &EditorSession::showsGuides,
&EditorSession::setShowsGuides);
toggle(view, QStringLiteral("showRulers"), QStringLiteral("Rulers"), QKeySequence(Qt::CTRL | Qt::Key_R), &EditorSession::showsRulers,
&EditorSession::setShowsRulers);
view->addSeparator();
toggle(view, QStringLiteral("snapEnabled"), QStringLiteral("Snap"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Semicolon),
&EditorSession::snapEnabled, &EditorSession::setSnapEnabled);
QMenu *snapTo = view->addMenu(QStringLiteral("Snap To"));
toggle(snapTo, QStringLiteral("snapToGuides"), QStringLiteral("Guides"), QKeySequence(), &EditorSession::snapToGuides, &EditorSession::setSnapToGuides);
toggle(snapTo, QStringLiteral("snapToGrid"), QStringLiteral("Grid"), QKeySequence(), &EditorSession::snapToGrid, &EditorSession::setSnapToGrid);
toggle(snapTo, QStringLiteral("snapToLayers"), QStringLiteral("Layers"), QKeySequence(), &EditorSession::snapToLayers, &EditorSession::setSnapToLayers);
toggle(snapTo, QStringLiteral("snapToDocumentBounds"), QStringLiteral("Document Bounds"), QKeySequence(), &EditorSession::snapToDocumentBounds,
&EditorSession::setSnapToDocumentBounds);
view->addSeparator();
toggle(view, QStringLiteral("lockGuides"), QStringLiteral("Lock Guides"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Semicolon),
&EditorSession::locksGuides, &EditorSession::setLocksGuides);
add(view, QStringLiteral("clearGuides"), QStringLiteral("Clear Guides"), QKeySequence(), [this] { session().clearGuides(); });
QMenu *select = bar.addMenu(QStringLiteral("&Select"));
// Text fields keep their own Select All.
@@ -294,6 +320,15 @@ void CompositorMenus::synchronize()
action(QString::fromLatin1(name))->setEnabled(drawn);
action(QStringLiteral("pixelGrid"))->setChecked(s.showsPixelGrid());
action(QStringLiteral("snap"))->setChecked(s.snappingEnabled());
const bool opened = s.document().has_value();
for (const auto &[name, value] : std::initializer_list<std::pair<const char *, bool>>{
{"showGrid", s.showsGrid()}, {"showGuides", s.showsGuides()}, {"showRulers", s.showsRulers()}, {"snapEnabled", s.snapEnabled()},
{"snapToGuides", s.snapToGuides()}, {"snapToGrid", s.snapToGrid()}, {"snapToLayers", s.snapToLayers()},
{"snapToDocumentBounds", s.snapToDocumentBounds()}, {"lockGuides", s.locksGuides()}}) {
action(QString::fromLatin1(name))->setChecked(value);
action(QString::fromLatin1(name))->setEnabled(opened);
}
action(QStringLiteral("clearGuides"))->setEnabled(s.canClearGuides());
action(QStringLiteral("transformControls"))->setChecked(s.showsTransformControls());
action(QStringLiteral("transformControls"))->setEnabled(s.tool() == NavigationTool::move && s.document().has_value());
const bool selected = s.selection().has_value();
+2 -1
View File
@@ -135,7 +135,8 @@ const std::vector<ShortcutDefinition> &ShortcutDefinition::all()
entry("Canvas Size", "c", 3, true), entry("Image Size", "i", 3, true), entry("Transform Layer / Selection", "t", 1, true),
entry("Duplicate / Layer via Copy", "j", 1, true), entry("Toggle Clipping Mask", "g", 3, true),
entry("Group Layers", "g", 1, true), entry("New Blank Layer", "n", 9, true), entry("Move Layer Up", "]", 1, true),
entry("Move Layer Down", "[", 1, true), entry("Merge Layers", "e", 1, true)};
entry("Move Layer Down", "[", 1, true), entry("Merge Layers", "e", 1, true), entry("Show Grid", "'", 1, true),
entry("Show Guides", ";", 1, true), entry("Show Rulers", "r", 1, true), entry("Snap", ";", 9, true), entry("Lock Guides", ";", 3, true)};
const std::vector<std::pair<const char *, QString>> tools{
{"Select tool", "a"}, {"Move / Transform tool", "v"}, {"Hand tool", "h"}, {"Zoom tool", "z"}, {"Brush tool", "b"},
{"Eraser", "e"}, {"Spot Healing", "j"}, {"Clone Stamp", "s"}, {"Type tool", "t"}, {"Gradient tool", "g"},