Files
OmaPhoto/src/Document/EditorSession+Transform.cpp
T

307 lines
11 KiB
C++

#include "Document/Distort.h"
#include "Document/EditorSession.h"
#include <algorithm>
bool EditorSession::canTransform() const
{
if (!canEditLayers())
return false;
if (transformsAsGroup())
return !groupTransformMembers().empty();
const std::optional<ImageLayer> active = activeLayer();
return active && active->asset && m_document->effectiveVisibleIDs().contains(active->id);
}
bool EditorSession::transformsAsGroup() const
{
const std::optional<ImageLayer> active = activeLayer();
return m_selectedLayerIDs.size() > 1 || (m_selectedLayerIDs.size() == 1 && active && active->isGroup);
}
std::vector<ImageLayer> EditorSession::groupTransformMembers() const
{
if (!transformsAsGroup() || !m_document)
return {};
QHash<QUuid, std::optional<QUuid>> parents;
for (const ImageLayer &layer : m_document->layers)
parents.insert(layer.id, layer.parentID);
const QSet<QUuid> visible = m_document->effectiveVisibleIDs();
std::vector<ImageLayer> members;
for (const ImageLayer &layer : m_document->layers) {
if (!layer.asset || !visible.contains(layer.id))
continue;
// Selected itself, or inside a selected folder.
std::optional<QUuid> current = layer.id;
for (int step = 0; step < 64 && current; ++step) {
if (m_selectedLayerIDs.contains(*current)) {
members.push_back(layer);
break;
}
current = parents.value(*current);
}
}
return members;
}
std::optional<LayerTransform> EditorSession::groupTransformBox() const
{
const std::vector<ImageLayer> members = groupTransformMembers();
if (members.empty())
return std::nullopt;
QPointF low = DistortWarp::corners(members.front().transform)[0], high = low;
for (const ImageLayer &member : members) {
for (const QPointF corner : DistortWarp::corners(member.transform)) {
low = {std::min(low.x(), corner.x()), std::min(low.y(), corner.y())};
high = {std::max(high.x(), corner.x()), std::max(high.y(), corner.y())};
}
}
// Rounding can leave a unit layer's box under one pixel.
return LayerTransform{.origin = low, .size = {std::max(1.0, high.x() - low.x()), std::max(1.0, high.y() - low.y())}};
}
void EditorSession::beginTransform(bool persistent, bool fromFields)
{
cancelCrop();
const std::optional<ImageLayer> layer = activeLayer();
if (!canTransform() || !layer)
return;
m_tool = NavigationTool::move;
if (transformsAsGroup()) {
const std::optional<LayerTransform> box = groupTransformBox();
if (!box)
return;
QHash<QUuid, LayerTransform> originals;
for (const ImageLayer &member : groupTransformMembers())
originals.insert(member.id, member.transform);
m_transformEdit = TransformEdit{layer->id, *box, TransformGroup{*box, originals}, false, persistent, std::nullopt, nullptr, fromFields};
} else {
// Linked, layer and mask move together as the layer.
const bool maskAlone = transformTargetsMask();
m_transformEdit = TransformEdit{layer->id, maskAlone ? layer->maskTransform() : layer->transform, std::nullopt, maskAlone, persistent, std::nullopt, nullptr, fromFields};
}
notify();
}
void EditorSession::setTransformAutoSelect(bool picks)
{
m_transformAutoSelect = picks;
ToolDefaults::set(picks, QStringLiteral("autoSelect"));
notify();
}
void EditorSession::setShowsTransformControls(bool shows)
{
m_showsTransformControls = shows;
ToolDefaults::set(shows, QStringLiteral("transformControls"));
notify();
}
void EditorSession::setLocksTransformRatio(bool locks)
{
m_locksTransformRatio = locks;
notify();
}
// Alt-drag copies selected roots with their contents.
void EditorSession::beginDuplicateTransform()
{
if (m_transformDuplicate || !m_activeLayerID)
return;
const QUuid primary = *m_activeLayerID;
commitTransform();
if (!canTransform())
return;
const QSet<QUuid> selection = m_selectedLayerIDs;
// A selected folder carries its selected contents along.
QSet<QUuid> carried;
for (const QUuid &id : selection)
carried.unite(descendantIDs(id));
std::vector<QUuid> targets;
for (const ImageLayer &layer : m_document->layers) {
if (selection.contains(layer.id) && !carried.contains(layer.id))
targets.push_back(layer.id);
}
if (targets.empty())
return;
beginEdit(targets.size() > 1 ? QStringLiteral("Duplicate Layers") : QStringLiteral("Duplicate Layer"));
// Stacked as Duplicate Layer stacks them.
duplicateLayers(targets);
const QSet<QUuid> copies = m_selectedLayerIDs - selection;
// Past the layer limit nothing was copied.
if (copies.isEmpty()) {
endEdit();
selectLayers(selection, primary);
return;
}
m_transformDuplicate = TransformDuplicate{std::vector<QUuid>(copies.begin(), copies.end()), selection, primary};
selectLayers(copies, m_activeLayerID);
beginTransform(false);
}
// The duplicate's step closes with its transform.
void EditorSession::endDuplicateTransform()
{
if (!m_transformDuplicate)
return;
m_transformDuplicate = std::nullopt;
endEdit();
}
void EditorSession::previewTransform(const LayerTransform &value)
{
if (!value.isValid() || !m_transformEdit)
return;
m_transformEdit->draft = value;
notify();
}
void EditorSession::commitTransform()
{
snapGuides = {};
// Whatever ends a transform ends these two as well.
if (m_blendPreview) {
m_blendPreview = std::nullopt;
notify();
}
finishOpacityEdit();
if (!m_transformEdit)
return;
if (m_transformEdit->floating) {
// Unchanged: restored exactly, so soft edges keep no seam.
const TransformEdit edit = *std::exchange(m_transformEdit, std::nullopt);
if (edit.draft == edit.floating->original && !edit.corners)
cancelFloatingTransform(*edit.floating);
else
mergeFloatingTransform(edit, *edit.floating);
return;
}
if (m_transformEdit->mask) {
commitMaskTransform();
return;
}
if (m_transformEdit->corners) {
const TransformEdit edit = *m_transformEdit;
m_transformEdit = std::nullopt;
commitDistort(edit, *edit.corners);
endDuplicateTransform();
return;
}
// One transaction: an edit that moves nothing records nothing.
beginEdit(m_transformEdit->group ? QStringLiteral("Transform Layers") : QStringLiteral("Transform Layer"));
for (int index = 0; index < int(m_document->layers.size()); ++index) {
ImageLayer &layer = m_document->layers[size_t(index)];
// A layer the box squeezes under a pixel stays put.
const std::optional<LayerTransform> moved = pendingTransform(layer);
if (!moved || !moved->isValid())
continue;
// A linked mask goes along; an unlinked one stays.
if (layer.mask)
layer.mask->placement = layer.mask->placementMovingLayer(layer.transform, *moved);
layer.transform = *moved;
redrawShape(index);
}
m_transformEdit = std::nullopt;
endEdit();
endDuplicateTransform();
}
void EditorSession::cancelTransform()
{
snapGuides = {};
if (!m_transformEdit)
return;
const std::shared_ptr<const FloatingTransform> floating = std::exchange(m_transformEdit, std::nullopt)->floating;
if (m_transformDuplicate) {
// The copies go and the old selection returns.
const TransformDuplicate duplicate = *m_transformDuplicate;
QSet<QUuid> removed(duplicate.copies.begin(), duplicate.copies.end());
for (const QUuid &copy : duplicate.copies)
removed.unite(descendantIDs(copy));
std::erase_if(m_document->layers, [&](const ImageLayer &layer) { return removed.contains(layer.id); });
m_collapsedGroupIDs.subtract(removed);
selectLayers(duplicate.source, duplicate.primary);
endDuplicateTransform();
}
if (floating)
cancelFloatingTransform(*floating);
notify();
}
std::optional<QSizeF> EditorSession::transformPixelSize() const
{
if (m_transformEdit && m_transformEdit->group)
return m_transformEdit->group->box.size;
if (!m_transformEdit && transformsAsGroup()) {
const std::optional<LayerTransform> box = groupTransformBox();
return box ? std::optional(box->size) : std::nullopt;
}
// A mask has no pixel size to scale against.
if (transformTargetsMask())
return std::nullopt;
if (m_transformEdit && m_transformEdit->floating)
return m_transformEdit->floating->pixelSize;
const std::optional<ImageLayer> active = activeLayer();
return active && active->asset ? std::optional(QSizeF(active->asset->size())) : std::nullopt;
}
bool EditorSession::transformTargetsMask() const
{
if (m_transformEdit)
return m_transformEdit->mask;
const std::optional<ImageLayer> active = activeLayer();
return m_isMaskSelected && active && active->mask && !active->mask->isLinked;
}
LayerTransform EditorSession::displayedTransform(const ImageLayer &layer) const
{
if (const std::optional<LayerTransform> pending = pendingTransform(layer))
return *pending;
// Content-Aware Fill past the edge previews on the grown layer.
if (m_filterEdit && m_filterEdit->preparedTransform && m_filterEdit->previewImage(layer.id))
return *m_filterEdit->preparedTransform;
return layer.transform;
}
LayerTransform EditorSession::editedTransform(const ImageLayer &layer) const
{
if (m_transformEdit && m_transformEdit->layerID == layer.id)
return m_transformEdit->draft;
// Before an edit the handles sit on the group's box.
if (!m_transformEdit && layer.id == m_activeLayerID && transformsAsGroup())
return groupTransformBox().value_or(layer.transform);
return layer.id == m_activeLayerID && transformTargetsMask() ? layer.maskTransform() : layer.transform;
}
std::optional<LayerTransform> EditorSession::pendingTransform(const ImageLayer &layer) const
{
// An edit of the mask alone moves no layer.
if (!m_transformEdit || m_transformEdit->mask)
return std::nullopt;
if (m_transformEdit->group) {
const TransformGroup &group = *m_transformEdit->group;
const auto original = group.originals.constFind(layer.id);
return original == group.originals.constEnd() ? std::nullopt : std::optional(original->following(group.box, m_transformEdit->draft));
}
return m_transformEdit->layerID == layer.id ? std::optional(m_transformEdit->draft) : std::nullopt;
}
void EditorSession::nudgeLayer(double dx, double dy)
{
const bool alreadyEditing = m_transformEdit.has_value();
if (!alreadyEditing)
beginTransform(false);
if (!m_transformEdit)
return;
LayerTransform value = m_transformEdit->draft;
value.origin += QPointF(dx, dy);
previewTransform(value);
if (const std::optional<Corners> corners = m_transformEdit->corners) {
Corners moved = *corners;
for (QPointF &corner : moved)
corner += QPointF(dx, dy);
previewCorners(moved);
}
if (!alreadyEditing)
commitTransform();
}