#include "Document/LayerTransform.h" #include "Document/BrushStroke.h" #include #include #include #include namespace { bool sameSize(QSizeF lhs, QSizeF rhs) { return lhs.width() == rhs.width() && lhs.height() == rhs.height(); } struct Shift { double move = 0; std::optional target; }; Shift shift(const std::array &guides, const std::vector &targets, double tolerance) { Shift best; for (double guide : guides) { for (double target : targets) { const double move = target - guide; if (std::abs(move) > tolerance) continue; if (best.target && std::abs(best.move) <= std::abs(move)) continue; best = {move, target}; } } return best; } } QString rawValue(LayerSampling sampling) { switch (sampling) { case LayerSampling::nearest: return QStringLiteral("Nearest"); case LayerSampling::smooth: return QStringLiteral("Smooth"); case LayerSampling::high: return QStringLiteral("High quality"); } throw std::logic_error("unknown LayerSampling"); } std::optional layerSampling(const QString &rawValue) { for (const LayerSampling sampling : {LayerSampling::nearest, LayerSampling::smooth, LayerSampling::high}) { if (rawValue == ::rawValue(sampling)) return sampling; } return std::nullopt; } InterpolationQuality quality(LayerSampling sampling) { switch (sampling) { case LayerSampling::nearest: return InterpolationQuality::none; case LayerSampling::smooth: return InterpolationQuality::low; case LayerSampling::high: return InterpolationQuality::high; } throw std::logic_error("unknown LayerSampling"); } const std::array LayerTransform::handles = { QPointF(0, 0), QPointF(0.5, 0), QPointF(1, 0), QPointF(1, 0.5), QPointF(1, 1), QPointF(0.5, 1), QPointF(0, 1), QPointF(0, 0.5)}; bool operator==(const LayerTransform &lhs, const LayerTransform &rhs) { return lhs.origin.x() == rhs.origin.x() && lhs.origin.y() == rhs.origin.y() && sameSize(lhs.size, rhs.size) && lhs.rotation == rhs.rotation && lhs.flipX == rhs.flipX && lhs.flipY == rhs.flipY && lhs.sampling == rhs.sampling; } QPointF LayerTransform::center() const { return {origin.x() + size.width() / 2, origin.y() + size.height() / 2}; } double LayerTransform::radians() const { return std::fmod(rotation, 360) * std::numbers::pi / 180; } double LayerTransform::degrees() const { return std::fmod(rotation, 360); } bool LayerTransform::isValid() const { const std::array values = {origin.x(), origin.y(), size.width(), size.height(), rotation}; return std::all_of(values.begin(), values.end(), [](double value) { return std::isfinite(value); }) && size.width() >= 1 && size.width() <= 300'000 && size.height() >= 1 && size.height() <= 300'000 && std::abs(origin.x()) <= 1'000'000 && std::abs(origin.y()) <= 1'000'000; } QPointF LayerTransform::point(QPointF unit) const { const double x = (unit.x() - 0.5) * size.width(), y = (unit.y() - 0.5) * size.height(); const QPointF middle = center(); return {middle.x() + x * std::cos(radians()) - y * std::sin(radians()), middle.y() + x * std::sin(radians()) + y * std::cos(radians())}; } bool LayerTransform::contains(QPointF point) const { const double x = point.x() - center().x(), y = point.y() - center().y(); return std::abs(x * std::cos(radians()) + y * std::sin(radians())) <= size.width() / 2 && std::abs(-x * std::sin(radians()) + y * std::cos(radians())) <= size.height() / 2; } double LayerTransform::scalePercent(QSizeF pixelSize) const { return size.width() / std::max(1.0, pixelSize.width()) * 100; } LayerTransform LayerTransform::scaled(double toPercent, QSizeF pixelSize) const { LayerTransform result = *this; result.size = {pixelSize.width() * toPercent / 100, pixelSize.height() * toPercent / 100}; result.origin = {center().x() - result.size.width() / 2, center().y() - result.size.height() / 2}; return result; } LayerTransform LayerTransform::rounded() const { LayerTransform result = *this; result.origin = {std::round(origin.x()), std::round(origin.y())}; result.size = {std::max(1.0, std::round(size.width())), std::max(1.0, std::round(size.height()))}; result.rotation = std::round(rotation); return result; } QTransform LayerTransform::unitToDocument() const { return BrushRaster::pixelToDocument(*this, 1, 1); } LayerTransform LayerTransform::placing(const QTransform &map) const { const double sign = flipX ? -1 : 1; const double angle = std::atan2(map.m12() * sign, map.m11() * sign); const double along = -map.m21() * std::sin(angle) + map.m22() * std::cos(angle); const QPointF middle = map.map(QPointF(0.5, 0.5)); LayerTransform result = *this; result.size = {std::hypot(map.m11(), map.m12()), std::abs(along)}; const double degrees = angle * 180 / std::numbers::pi; result.rotation = degrees + std::round((rotation - degrees) / 360) * 360; result.flipY = along < 0; result.origin = {middle.x() - result.size.width() / 2, middle.y() - result.size.height() / 2}; return result; } LayerTransform LayerTransform::following(const LayerTransform &old, const LayerTransform &updated) const { if (old == updated) return *this; if (sameSize(old.size, updated.size) && old.rotation == updated.rotation && old.flipX == updated.flipX && old.flipY == updated.flipY) { LayerTransform moved = *this; moved.origin += updated.origin - old.origin; return moved; } return placing(unitToDocument() * old.unitToDocument().inverted() * updated.unitToDocument()); } bool LayerTransform::samePlacement(const LayerTransform &other) const { LayerTransform copy = *this; copy.sampling = other.sampling; return copy == other; } std::optional TransformDrag::corners(QPointF to, bool shift) const { if (!originalCorners) return std::nullopt; double dx = to.x() - start.x(), dy = to.y() - start.y(); if (shift) { if (std::abs(dx) >= std::abs(dy)) dy = 0; else dx = 0; } std::vector moved; if (mode.kind == Kind::distort) { const int corner = mode.index / 2; moved = mode.index % 2 == 0 ? std::vector{corner} : std::vector{corner, (corner + 1) % 4}; } else if (mode.kind == Kind::move) { moved = {0, 1, 2, 3}; } else { return std::nullopt; } Corners result = *originalCorners; for (int corner : moved) result[corner] += QPointF(dx, dy); return result; } LayerTransform TransformDrag::updated(QPointF to, bool lockRatio, bool shift, bool option) const { LayerTransform result = original; const double radians = original.radians(); switch (mode.kind) { case Kind::distort: break; case Kind::move: { double dx = to.x() - start.x(), dy = to.y() - start.y(); if (shift) { if (std::abs(dx) >= std::abs(dy)) dy = 0; else dx = 0; } result.origin += QPointF(dx, dy); break; } case Kind::rotate: { const QPointF center = original.center(); const double delta = std::atan2(to.y() - center.y(), to.x() - center.x()) - std::atan2(start.y() - center.y(), start.x() - center.x()); result.rotation += delta * 180 / std::numbers::pi; if (shift) result.rotation = std::round(result.rotation / 15) * 15; break; } case Kind::resize: { const QPointF handle = LayerTransform::handles[mode.index]; const QPointF anchorUnit = option ? QPointF(0.5, 0.5) : QPointF(1 - handle.x(), 1 - handle.y()); const QPointF anchor = original.point(anchorUnit); const QPointF initialHandle = original.point(handle); const double dx = initialHandle.x() + to.x() - start.x() - anchor.x(); const double dy = initialHandle.y() + to.y() - start.y() - anchor.y(); const double span = option ? 2 : 1; const double localX = (dx * std::cos(radians) + dy * std::sin(radians)) * span; const double localY = (-dx * std::sin(radians) + dy * std::cos(radians)) * span; const double sx = handle.x() * 2 - 1, sy = handle.y() * 2 - 1; const double originalWidth = original.size.width(), originalHeight = original.size.height(); const double rawWidth = sx == 0 ? originalWidth : localX * sx; const double rawHeight = sy == 0 ? originalHeight : localY * sy; const bool mirroredX = rawWidth < 0, mirroredY = rawHeight < 0; double width = std::max(1.0, std::abs(rawWidth)); double height = std::max(1.0, std::abs(rawHeight)); if (lockRatio != shift) { double factor; if (sx == 0) factor = height / originalHeight; else if (sy == 0) factor = width / originalWidth; else factor = std::max(1 / std::min(originalWidth, originalHeight), (localX * sx * originalWidth + localY * sy * originalHeight) / (originalWidth * originalWidth + originalHeight * originalHeight)); width = originalWidth * factor; height = originalHeight * factor; } result.size = {width, height}; if (mirroredX) result.flipX = !result.flipX; if (mirroredY) result.flipY = !result.flipY; const double offsetX = (0.5 - anchorUnit.x()) * width * (mirroredX ? -1 : 1); const double offsetY = (0.5 - anchorUnit.y()) * height * (mirroredY ? -1 : 1); const QPointF center(anchor.x() + offsetX * std::cos(radians) - offsetY * std::sin(radians), anchor.y() + offsetX * std::sin(radians) + offsetY * std::cos(radians)); result.origin = {center.x() - width / 2, center.y() - height / 2}; break; } } return result.isValid() ? result : original; } TransformSnap::Offset TransformSnap::offset(const QRectF &box, const std::vector &xs, const std::vector &ys, double tolerance) { const Shift horizontal = shift({box.left(), box.center().x(), box.right()}, xs, tolerance); const Shift vertical = shift({box.top(), box.center().y(), box.bottom()}, ys, tolerance); return {QSizeF(horizontal.move, vertical.move), horizontal.target, vertical.target}; }