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 -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;