14.22: effects, text and shapes outlast size changes and adjustments
This commit is contained in:
17 files changed
+226
-38
No files matched your search
@@ -0,0 +1,162 @@
|
||||
#include "Document/CanvasSize.h"
|
||||
#include "Document/ImageTrim.h"
|
||||
#include "IO/CanvasResizer.h"
|
||||
#include "IO/ImageResizer.h"
|
||||
#include "SessionFixtures.h"
|
||||
#include <QtTest>
|
||||
|
||||
// Swift 1.4.9's: effects, live text and shapes outlast size changes.
|
||||
class LayerEffectsSurviveTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void canvasSizeKeepsEffectsTextAndShapes();
|
||||
void cropKeepsEffectsTextAndShapes();
|
||||
void trimKeepsEffectsTextAndShapes();
|
||||
void resolutionOnlyImageSizeKeepsEffectsTextAndShapes();
|
||||
void imageSizeKeepsEffectsScaledWithTheImage();
|
||||
void aStretchScalesByTheFactorsMeanWithinRange();
|
||||
};
|
||||
|
||||
namespace {
|
||||
LayerEffects effects()
|
||||
{
|
||||
LayerEffects made;
|
||||
made.stroke = StrokeEffect{.size = 4};
|
||||
made.shadow = ShadowEffect{.distance = 20, .blur = 10};
|
||||
made.outerGlow = OuterGlowEffect{.size = 8};
|
||||
return made;
|
||||
}
|
||||
|
||||
// What a layer carries beyond its pixels.
|
||||
struct Extras {
|
||||
std::optional<LayerEffects> effects;
|
||||
std::optional<LayerTextStyle> text;
|
||||
std::optional<LayerShapeStyle> shape;
|
||||
friend bool operator==(const Extras &, const Extras &) = default;
|
||||
};
|
||||
|
||||
std::vector<Extras> extras(const EditorSession &session, const std::vector<QUuid> &ids)
|
||||
{
|
||||
std::vector<Extras> found;
|
||||
for (const QUuid id : ids) {
|
||||
const ImageLayer layer = layerWith(session, id);
|
||||
found.push_back({layer.effects, layer.liveText() ? std::optional(layer.liveText()->style) : std::nullopt,
|
||||
layer.liveShape() ? std::optional(layer.liveShape()->style) : std::nullopt});
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// A clear canvas holding text and a shape, with effects.
|
||||
std::unique_ptr<EditorSession> textAndShape(std::vector<QUuid> &ids)
|
||||
{
|
||||
auto session = std::make_unique<EditorSession>();
|
||||
session->createDocument(200, 120);
|
||||
session->selectTool(NavigationTool::type);
|
||||
session->beginText(QPointF(30, 60));
|
||||
TextDraft draft = session->textDraft().value();
|
||||
draft.style.content = QStringLiteral("Text");
|
||||
if (!session->applyText(draft))
|
||||
throw std::runtime_error("no text");
|
||||
const QUuid text = session->activeLayerID().value();
|
||||
session->selectTool(NavigationTool::shape);
|
||||
session->beginShape(QPointF(120, 30));
|
||||
session->dragShape(QPointF(170, 80), false, false);
|
||||
session->finishShape();
|
||||
const QUuid shape = session->activeLayerID().value();
|
||||
rewrite(*session, [&](ProjectSnapshot &snapshot) { record(snapshot, text).effects = record(snapshot, shape).effects = effects(); });
|
||||
ids = {text, shape};
|
||||
const std::vector<Extras> before = extras(*session, ids);
|
||||
if (shape == text || !before[0].text || !before[1].shape)
|
||||
throw std::runtime_error("no live text or shape");
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
void LayerEffectsSurviveTests::canvasSizeKeepsEffectsTextAndShapes()
|
||||
{
|
||||
std::vector<QUuid> ids;
|
||||
const auto session = textAndShape(ids);
|
||||
const std::vector<Extras> before = extras(*session, ids);
|
||||
session->applyDocumentSize(CanvasResizer::resize(session->projectSnapshot().value(), CanvasSizeOptions{.width = 260, .height = 160, .anchor = 4}),
|
||||
QStringLiteral("Canvas Size"));
|
||||
QCOMPARE(session->document().value().width, 260);
|
||||
QVERIFY(extras(*session, ids) == before);
|
||||
}
|
||||
|
||||
void LayerEffectsSurviveTests::cropKeepsEffectsTextAndShapes()
|
||||
{
|
||||
std::vector<QUuid> ids;
|
||||
const auto session = textAndShape(ids);
|
||||
const std::vector<Extras> before = extras(*session, ids);
|
||||
session->selectTool(NavigationTool::crop);
|
||||
session->setCropRect(QRectF(10, 10, 180, 100));
|
||||
bool done = false;
|
||||
session->commitCrop([&done] { done = true; });
|
||||
QTRY_VERIFY(done);
|
||||
QCOMPARE(session->document().value().width, 180);
|
||||
QVERIFY(extras(*session, ids) == before);
|
||||
}
|
||||
|
||||
void LayerEffectsSurviveTests::trimKeepsEffectsTextAndShapes()
|
||||
{
|
||||
std::vector<QUuid> ids;
|
||||
const auto session = textAndShape(ids);
|
||||
const std::vector<Extras> before = extras(*session, ids);
|
||||
session->applyDocumentSize(ImageTrim::trim(session->projectSnapshot().value(), TrimOptions()).value(), QStringLiteral("Trim"));
|
||||
QVERIFY(session->document().value().width < 200);
|
||||
QVERIFY(extras(*session, ids) == before);
|
||||
}
|
||||
|
||||
void LayerEffectsSurviveTests::resolutionOnlyImageSizeKeepsEffectsTextAndShapes()
|
||||
{
|
||||
std::vector<QUuid> ids;
|
||||
const auto session = textAndShape(ids);
|
||||
const std::vector<Extras> before = extras(*session, ids);
|
||||
session->applyImageSize(ImageResizer::resize(session->projectSnapshot().value(), ImageSizeOptions{.width = 200, .height = 120, .resolution = 300}));
|
||||
QCOMPARE(session->document().value().resolution, 300.0);
|
||||
QVERIFY(extras(*session, ids) == before);
|
||||
}
|
||||
|
||||
// Resampled pixels take their effects with them, as Scale Styles.
|
||||
void LayerEffectsSurviveTests::imageSizeKeepsEffectsScaledWithTheImage()
|
||||
{
|
||||
std::vector<QUuid> ids;
|
||||
const auto session = textAndShape(ids);
|
||||
session->applyImageSize(ImageResizer::resize(session->projectSnapshot().value(), ImageSizeOptions{.width = 100, .height = 60, .resolution = 72}));
|
||||
QCOMPARE(session->document().value().width, 100);
|
||||
for (const Extras &kept : extras(*session, ids)) {
|
||||
QVERIFY(!kept.text && !kept.shape);
|
||||
const LayerEffects scaled = kept.effects.value();
|
||||
QVERIFY(scaled.stroke.value().size == 2 && scaled.outerGlow.value().size == 4);
|
||||
QVERIFY(scaled.shadow.value().distance == 10 && scaled.shadow.value().blur == 5 && scaled.shadow.value().angle == effects().shadow.value().angle);
|
||||
}
|
||||
}
|
||||
|
||||
void LayerEffectsSurviveTests::aStretchScalesByTheFactorsMeanWithinRange()
|
||||
{
|
||||
LayerEffects made;
|
||||
made.stroke = StrokeEffect{.size = 300};
|
||||
made.shadow = ShadowEffect{.distance = 3000, .blur = 300, .opacity = 0.4};
|
||||
made.innerShadow = InnerShadowEffect{.distance = 10, .blur = 4};
|
||||
made.innerGlow = InnerGlowEffect{.size = 6};
|
||||
made.colorOverlay = ColorOverlayEffect{.opacity = 0.3};
|
||||
const LayerEffects doubled = made.scaled(2);
|
||||
// Each held to the range the effect accepts.
|
||||
QVERIFY(doubled.stroke.value().size == 500 && doubled.shadow.value().distance == 5000 && doubled.shadow.value().blur == 500);
|
||||
QVERIFY(doubled.innerShadow.value().distance == 20 && doubled.innerShadow.value().blur == 8 && doubled.innerGlow.value().size == 12);
|
||||
QVERIFY(doubled.shadow.value().opacity == 0.4 && doubled.colorOverlay == made.colorOverlay && !doubled.outerGlow && doubled.isValid());
|
||||
// Stretched 4 by 1: the mean factor is 2.
|
||||
EditorSession session;
|
||||
session.createDocument(20, 20);
|
||||
QImage pixels(20, 20, QImage::Format_RGBA8888_Premultiplied);
|
||||
pixels.fill(Qt::red);
|
||||
session.insert(ImportedImage(pixels, pixels, QStringLiteral("Red")));
|
||||
const QUuid id = session.activeLayerID().value();
|
||||
rewrite(session, [&](ProjectSnapshot &snapshot) { record(snapshot, id).effects = effects(); });
|
||||
session.applyImageSize(ImageResizer::resize(session.projectSnapshot().value(), ImageSizeOptions{.width = 80, .height = 20, .resolution = 72}));
|
||||
QVERIFY(layerWith(session, id).effects == effects().scaled(2));
|
||||
QCOMPARE(layerWith(session, id).effects.value().stroke.value().size, 8.0);
|
||||
}
|
||||
|
||||
QTEST_MAIN(LayerEffectsSurviveTests)
|
||||
#include "LayerEffectsSurviveTests.moc"
|
||||
+15
-12
@@ -75,7 +75,7 @@ private slots:
|
||||
void decodingRefusesWhatSwiftRefuses();
|
||||
void effectsSaveLoadAndExportWithTheirLayer();
|
||||
void duplicatesAndBrushCommitsKeepThem();
|
||||
void rebuildsCopiesAndResizesDropThem();
|
||||
void rebuildsAndResizesKeepThem();
|
||||
};
|
||||
|
||||
void LayerEffectsTests::kindsKeepSwiftsNamesAndOrder()
|
||||
@@ -308,28 +308,28 @@ void LayerEffectsTests::duplicatesAndBrushCommitsKeepThem()
|
||||
QVERIFY(session->activeLayer().value().effects == every());
|
||||
}
|
||||
|
||||
void LayerEffectsTests::rebuildsCopiesAndResizesDropThem()
|
||||
// Swift 1.4.9 (`94e90bd`): every rebuild keeps them.
|
||||
void LayerEffectsTests::rebuildsAndResizesKeepThem()
|
||||
{
|
||||
// Swift rebuilds these layers from lists that leave effects out.
|
||||
const auto dropped = [](const std::function<void(EditorSession &)> &edit, const char *name) {
|
||||
const auto kept = [](const std::function<void(EditorSession &)> &edit, const char *name) {
|
||||
const std::unique_ptr<EditorSession> session = withEffects();
|
||||
edit(*session);
|
||||
return session->history.undoName() == QString::fromLatin1(name) && !session->activeLayer().value().effects;
|
||||
return session->history.undoName() == QString::fromLatin1(name) && session->activeLayer().value().effects == every();
|
||||
};
|
||||
QVERIFY(dropped([](EditorSession &session) { QVERIFY(awaited([&](std::function<void()> done) { session.invertPixels(std::move(done)); })); }, "Invert"));
|
||||
QVERIFY(dropped([](EditorSession &session) {
|
||||
QVERIFY(kept([](EditorSession &session) { QVERIFY(awaited([&](std::function<void()> done) { session.invertPixels(std::move(done)); })); }, "Invert"));
|
||||
QVERIFY(kept([](EditorSession &session) {
|
||||
session.beginLevels();
|
||||
LevelsSettings settings;
|
||||
settings.ranges[0].outputWhite = 100;
|
||||
session.updateLevels(settings, false);
|
||||
QVERIFY(awaited([&](std::function<void()> done) { session.commitLevels(std::move(done)); }));
|
||||
}, "Levels"));
|
||||
QVERIFY(dropped([](EditorSession &session) {
|
||||
QVERIFY(kept([](EditorSession &session) {
|
||||
session.beginHueSaturation();
|
||||
session.updateHueSaturation(HueSaturationSettings(90), false);
|
||||
QVERIFY(awaited([&](std::function<void()> done) { session.commitHueSaturation(std::move(done)); }));
|
||||
}, "Hue/Saturation"));
|
||||
QVERIFY(dropped([](EditorSession &session) {
|
||||
QVERIFY(kept([](EditorSession &session) {
|
||||
session.applySelection(rectPath(QRectF(2, 2, 6, 4)), SelectionMode::replace, QStringLiteral("Select"));
|
||||
QVERIFY(awaited([&](std::function<void()> done) { session.beginSelectionTransform(std::move(done)); }));
|
||||
LayerTransform draft = session.transformEdit().value().draft;
|
||||
@@ -337,10 +337,13 @@ void LayerEffectsTests::rebuildsCopiesAndResizesDropThem()
|
||||
session.previewTransform(draft);
|
||||
session.commitTransform();
|
||||
}, "Transform Selection"));
|
||||
// Both resizers rebuild their records without them, as Swift's.
|
||||
// The canvas resizer carries them; the image resizer scales them.
|
||||
const ProjectSnapshot snapshot = withEffects()->projectSnapshot().value();
|
||||
QVERIFY(!ImageResizer::resize(snapshot, ImageSizeOptions{.width = 40, .height = 20, .resolution = 72}).manifest.layers[0].effects);
|
||||
QVERIFY(!CanvasResizer::resize(snapshot, CanvasSizeOptions{.width = 30, .height = 12}).manifest.layers[0].effects);
|
||||
QVERIFY(CanvasResizer::resize(snapshot, CanvasSizeOptions{.width = 30, .height = 12}).manifest.layers[0].effects == every());
|
||||
LayerEffects doubled = every();
|
||||
doubled.stroke->size = 14, doubled.shadow->distance = 24, doubled.shadow->blur = 12;
|
||||
doubled.innerShadow->distance = 6, doubled.innerShadow->blur = 4, doubled.outerGlow->size = 66, doubled.innerGlow->size = 28;
|
||||
QVERIFY(ImageResizer::resize(snapshot, ImageSizeOptions{.width = 40, .height = 20, .resolution = 72}).manifest.layers[0].effects == doubled);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(LayerEffectsTests)
|
||||
|
||||
@@ -379,9 +379,13 @@ void ShapeLayerTests::resizersKeepOrDropTheShape()
|
||||
QCOMPARE(canvas.manifest.layers[1].shape, snapshot.manifest.layers[1].shape);
|
||||
const ProjectSnapshot image = ImageResizer::resize(snapshot, {.width = 200, .height = 160, .resolution = 72});
|
||||
QVERIFY(!image.manifest.layers[1].shape);
|
||||
// Swift's rebuild after a resize leaves every shape out.
|
||||
// Since 1.4.9 the rebuild keeps the shape live.
|
||||
session->applyDocumentSize(canvas, QStringLiteral("Canvas Size"));
|
||||
QCOMPARE(session->history.undoName(), QString("Canvas Size"));
|
||||
QVERIFY(session->document().value().layers[1].liveShape());
|
||||
QCOMPARE(session->document().value().layers[1].shape.value().style, snapshot.manifest.layers[1].shape.value());
|
||||
// Image Size resamples it into pixels.
|
||||
session->applyImageSize(ImageResizer::resize(session->projectSnapshot().value(), {.width = 240, .height = 180, .resolution = 72}));
|
||||
QVERIFY(!session->document().value().layers[1].shape);
|
||||
}
|
||||
|
||||
|
||||
@@ -267,6 +267,7 @@ compositor_test(AdjustmentLayerTests)
|
||||
compositor_test(AdjustmentEditorTests)
|
||||
compositor_test(AdjustmentPanelTests)
|
||||
compositor_test(LayerEffectsTests)
|
||||
compositor_test(LayerEffectsSurviveTests)
|
||||
compositor_test(OuterGlowTests)
|
||||
compositor_test(InnerGlowTests)
|
||||
compositor_test(ColorOverlayTests)
|
||||
|
||||
@@ -230,8 +230,11 @@ void TypeStoreTests::resizersKeepOrDropTheText()
|
||||
const ProjectSnapshot canvas = CanvasResizer::resize(snapshot, {.width = 500, .height = 400});
|
||||
QCOMPARE(canvas.manifest.layers[1].text, snapshot.manifest.layers[1].text);
|
||||
QVERIFY(!ImageResizer::resize(snapshot, {.width = 800, .height = 600, .resolution = 72}).manifest.layers[1].text);
|
||||
// Swift's rebuild after a resize leaves the text out.
|
||||
// Since 1.4.9 the rebuild keeps the text live.
|
||||
session->applyDocumentSize(canvas, QStringLiteral("Canvas Size"));
|
||||
QVERIFY(session->document().value().layers[1].liveText());
|
||||
QCOMPARE(session->document().value().layers[1].text.value().style, snapshot.manifest.layers[1].text.value());
|
||||
session->applyImageSize(ImageResizer::resize(session->projectSnapshot().value(), {.width = 1000, .height = 800, .resolution = 72}));
|
||||
QVERIFY(!session->document().value().layers[1].text);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user