#include "Document/Distort.h" #include "Document/EditorSession.h" #include bool EditorSession::canTransform() const { if (!canEditLayers()) return false; if (transformsAsGroup()) return !groupTransformMembers().empty(); const std::optional active = activeLayer(); return active && active->asset && m_document->effectiveVisibleIDs().contains(active->id); } bool EditorSession::transformsAsGroup() const { const std::optional active = activeLayer(); return m_selectedLayerIDs.size() > 1 || (m_selectedLayerIDs.size() == 1 && active && active->isGroup); } std::vector EditorSession::groupTransformMembers() const { if (!transformsAsGroup() || !m_document) return {}; QHash> parents; for (const ImageLayer &layer : m_document->layers) parents.insert(layer.id, layer.parentID); const QSet visible = m_document->effectiveVisibleIDs(); std::vector 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 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 EditorSession::groupTransformBox() const { const std::vector 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 layer = activeLayer(); if (!canTransform() || !layer) return; m_tool = NavigationTool::move; if (transformsAsGroup()) { const std::optional box = groupTransformBox(); if (!box) return; QHash 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 selection = m_selectedLayerIDs; // A selected folder carries its selected contents along. QSet carried; for (const QUuid &id : selection) carried.unite(descendantIDs(id)); std::vector 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 copies = m_selectedLayerIDs - selection; // Past the layer limit nothing was copied. if (copies.isEmpty()) { endEdit(); selectLayers(selection, primary); return; } m_transformDuplicate = TransformDuplicate{std::vector(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 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 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 removed(duplicate.copies.begin(), duplicate.copies.end()); for (const QUuid © : 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 EditorSession::transformPixelSize() const { if (m_transformEdit && m_transformEdit->group) return m_transformEdit->group->box.size; if (!m_transformEdit && transformsAsGroup()) { const std::optional 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 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 active = activeLayer(); return m_isMaskSelected && active && active->mask && !active->mask->isLinked; } LayerTransform EditorSession::displayedTransform(const ImageLayer &layer) const { if (const std::optional 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 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 = m_transformEdit->corners) { Corners moved = *corners; for (QPointF &corner : moved) corner += QPointF(dx, dy); previewCorners(moved); } if (!alreadyEditing) commitTransform(); }