diff --git a/src/app/puzzle/layout/vppiece.cpp b/src/app/puzzle/layout/vppiece.cpp index 219966ce9..ba0d21475 100644 --- a/src/app/puzzle/layout/vppiece.cpp +++ b/src/app/puzzle/layout/vppiece.cpp @@ -31,6 +31,7 @@ #include "../vmisc/def.h" #include "vpsheet.h" +#include "vplayout.h" #include "../vlayout/vtextmanager.h" #include @@ -39,6 +40,86 @@ Q_LOGGING_CATEGORY(pPiece, "p.piece") +namespace +{ +constexpr qreal minStickyDistance = ToPixel(3, Unit::Mm); +constexpr qreal maxStickyDistance = ToPixel(10, Unit::Mm); +constexpr qreal stickyShift = ToPixel(1, Unit::Mm); + +//--------------------------------------------------------------------------------------------------------------------- +auto CutEdge(const QLineF &edge) -> QVector +{ + QVector points; + if (qFuzzyIsNull(stickyShift)) + { + points.append(edge.p1()); + points.append(edge.p2()); + } + else + { + const int n = qFloor(edge.length()/stickyShift); + + if (n <= 0) + { + points.append(edge.p1()); + points.append(edge.p2()); + } + else + { + points.reserve(n); + const qreal nShift = edge.length()/n; + for (int i = 1; i <= n+1; ++i) + { + QLineF l1 = edge; + l1.setLength(nShift*(i-1)); + points.append(l1.p2()); + } + } + } + return points; +} + +//--------------------------------------------------------------------------------------------------------------------- +auto PrepareStickyPath(const QVector &path) -> QVector +{ + if (path.size() < 2) + { + return path; + } + + QVector stickyPath; + + for (int i=0; i &path1, const QVector &path2) -> QLineF +{ + qreal distance = INT_MAX; + QLineF closestDistance; + + for (auto p1 : path1) + { + for (auto p2 : path2) + { + QLineF d(p1, p2); + if (d.length() <= distance) + { + distance = d.length(); + closestDistance = d; + } + } + } + + return closestDistance; +} +} // namespace + //--------------------------------------------------------------------------------------------------------------------- VPPiece::VPPiece(const VLayoutPiece &layoutPiece) : VLayoutPiece(layoutPiece) @@ -239,3 +320,111 @@ void VPPiece::Flip() SetMatrix(pieceMatrix); SetMirror(!IsMirror()); } + +//--------------------------------------------------------------------------------------------------------------------- +auto VPPiece::StickyPosition(qreal &dx, qreal &dy) const -> bool +{ + VPLayoutPtr layout = Layout(); + if (layout.isNull() || not layout->LayoutSettings().GetStickyEdges()) + { + return false; + } + + const qreal pieceGap = layout->LayoutSettings().GetPiecesGap(); + if (pieceGap <= 0) + { + return false; + } + + VPSheetPtr sheet = Sheet(); + if (sheet.isNull()) + { + return false; + } + + QList allPieces = sheet->GetPieces(); + + if (allPieces.count() < 2) + { + return false; + } + + QVector path = GetMappedExternalContourPoints(); + QRectF boundingRect = VLayoutPiece::BoundingRect(path); + const qreal stickyDistance = pieceGap+minStickyDistance; + QRectF stickyZone = QRectF(boundingRect.topLeft().x()-stickyDistance, boundingRect.topLeft().y()-stickyDistance, + boundingRect.width()+stickyDistance*2, boundingRect.height()+stickyDistance*2); + + QVector stickyPath = PrepareStickyPath(path); + QLineF closestDistance; + + for (const auto& piece : allPieces) + { + if (piece.isNull() || piece->GetUniqueID() == GetUniqueID()) + { + continue; + } + + QVector piecePath = piece->GetMappedExternalContourPoints(); + QRectF pieceBoundingRect = VLayoutPiece::BoundingRect(piecePath); + + if (stickyZone.intersects(pieceBoundingRect) || pieceBoundingRect.contains(stickyZone) || + stickyZone.contains(pieceBoundingRect)) + { + if (not VPPiece::PathsSuperposition(path, piecePath)) + { + QVector pieceStickyPath = PrepareStickyPath(piecePath); + closestDistance = ClosestDistance(stickyPath, pieceStickyPath); + } + } + } + + if (closestDistance.isNull()) + { + return false; + } + + const qreal extraZone = qBound(minStickyDistance, pieceGap * 50 / 100, maxStickyDistance); + const qreal length = closestDistance.length(); + + if (length > pieceGap && length <= pieceGap + extraZone) + { + closestDistance.setLength(length - pieceGap); + QPointF diff = closestDistance.p2() - closestDistance.p1(); + dx = diff.x(); + dy = diff.y(); + return true; + } + + if (length < pieceGap && length >= pieceGap - extraZone) + { + closestDistance.setAngle(closestDistance.angle() + 180); + closestDistance.setLength(pieceGap - length); + QPointF diff = closestDistance.p2() - closestDistance.p1(); + dx = diff.x(); + dy = diff.y(); + return true; + } + + return false; +} + +//--------------------------------------------------------------------------------------------------------------------- +auto VPPiece::PathsSuperposition(const QVector &path1, const QVector &path2) -> bool +{ + const QRectF path1Rect = VLayoutPiece::BoundingRect(path1); + const QPainterPath path1Path = VAbstractPiece::PainterPath(path1); + + const QRectF path2Rect = VLayoutPiece::BoundingRect(path2); + const QPainterPath path2Path = VAbstractPiece::PainterPath(path2); + + if (path1Rect.intersects(path2Rect) || path2Rect.contains(path1Rect) || path1Rect.contains(path2Rect)) + { + if (path1Path.contains(path2Path) || path2Path.contains(path1Path) || path1Path.intersects(path2Path)) + { + return true; + } + } + + return false; +} diff --git a/src/app/puzzle/layout/vppiece.h b/src/app/puzzle/layout/vppiece.h index 91254e26d..4541ed210 100644 --- a/src/app/puzzle/layout/vppiece.h +++ b/src/app/puzzle/layout/vppiece.h @@ -112,6 +112,10 @@ public: auto HasSuperpositionWithPieces() const -> bool; void SetHasSuperpositionWithPieces(bool newHasSuperpositionWithPieces); + auto StickyPosition(qreal &dx, qreal &dy) const -> bool; + + static auto PathsSuperposition(const QVector &path1, const QVector &path2) -> bool; + private: Q_DISABLE_COPY(VPPiece) diff --git a/src/app/puzzle/layout/vpsheet.cpp b/src/app/puzzle/layout/vpsheet.cpp index 4d01ee4d3..a07911516 100644 --- a/src/app/puzzle/layout/vpsheet.cpp +++ b/src/app/puzzle/layout/vpsheet.cpp @@ -184,7 +184,7 @@ void VPSheet::ValidateSuperpositionOfPieces() const QVector path2 = p->GetMappedExternalContourPoints(); - bool superposition = PathsSuperposition(path1, path2); + bool superposition = VPPiece::PathsSuperposition(path1, path2); if (superposition) { hasSuperposition = superposition; @@ -313,23 +313,3 @@ void VPSheet::CheckPiecePositionValidity(const VPPiecePtr &piece) const ValidateSuperpositionOfPieces(); } } - -//--------------------------------------------------------------------------------------------------------------------- -auto VPSheet::PathsSuperposition(const QVector &path1, const QVector &path2) const -> bool -{ - const QRectF path1Rect = VLayoutPiece::BoundingRect(path1); - const QPainterPath path1Path = VAbstractPiece::PainterPath(path1); - - const QRectF path2Rect = VLayoutPiece::BoundingRect(path2); - const QPainterPath path2Path = VAbstractPiece::PainterPath(path2); - - if (path1Rect.intersects(path2Rect) || path2Rect.contains(path1Rect) || path1Rect.contains(path2Rect)) - { - if (path1Path.contains(path2Path) || path2Path.contains(path1Path) || path1Path.intersects(path2Path)) - { - return true; - } - } - - return false; -} diff --git a/src/app/puzzle/layout/vpsheet.h b/src/app/puzzle/layout/vpsheet.h index f92fead22..febd396c7 100644 --- a/src/app/puzzle/layout/vpsheet.h +++ b/src/app/puzzle/layout/vpsheet.h @@ -112,7 +112,7 @@ private: VPTransformationOrigon m_transformationOrigin{}; - auto PathsSuperposition(const QVector &path1, const QVector &path2) const -> bool; + }; Q_DECLARE_METATYPE(VPSheetPtr) diff --git a/src/app/puzzle/scene/vpgraphicspiece.cpp b/src/app/puzzle/scene/vpgraphicspiece.cpp index e94b8af1b..8767cf4e1 100644 --- a/src/app/puzzle/scene/vpgraphicspiece.cpp +++ b/src/app/puzzle/scene/vpgraphicspiece.cpp @@ -94,6 +94,7 @@ auto VPGraphicsPiece::boundingRect() const -> QRectF shape.addPath(m_internalPaths); shape.addPath(m_passmarks); shape.addPath(m_placeLabels); + shape.addPath(m_stickyPath); constexpr qreal halfPenWidth = penWidth/2.; @@ -136,6 +137,7 @@ void VPGraphicsPiece::mousePressEvent(QGraphicsSceneMouseEvent *event) m_moveStartPoint = event->pos(); emit HideTransformationHandles(true); + m_hasStickyPosition = false; } } @@ -161,7 +163,26 @@ void VPGraphicsPiece::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { setCursor(Qt::OpenHandCursor); emit HideTransformationHandles(false); + + VPPiecePtr piece = m_piece.toStrongRef(); + if (not piece.isNull()) + { + VPLayoutPtr layout = piece->Layout(); + if (not layout.isNull()) + { + if (layout->LayoutSettings().GetStickyEdges() && m_hasStickyPosition) + { + auto *command = new VPUndoPieceMove(piece, m_stickyTranslateX, m_stickyTranslateY, + allowChangeMerge); + layout->UndoStack()->push(command); + + SetStickyPoints(QVector()); + } + } + } + allowChangeMerge = false; + m_hasStickyPosition = false; } } @@ -218,6 +239,15 @@ void VPGraphicsPiece::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) } } +//--------------------------------------------------------------------------------------------------------------------- +void VPGraphicsPiece::SetStickyPoints(const QVector &newStickyPoint) +{ + m_stickyPoints = newStickyPoint; + + prepareGeometryChange(); + PaintPiece(); // refresh shapes +} + //--------------------------------------------------------------------------------------------------------------------- void VPGraphicsPiece::PaintPieceLabel(const QVector &labelShape, const VTextManager &tm, QPainter *painter) { @@ -329,6 +359,7 @@ void VPGraphicsPiece::PaintPiece(QPainter *painter) m_internalPaths = QPainterPath(); m_passmarks = QPainterPath(); m_placeLabels = QPainterPath(); + m_stickyPath = QPainterPath(); VPPiecePtr piece = m_piece.toStrongRef(); if (piece.isNull()) @@ -457,6 +488,29 @@ void VPGraphicsPiece::PaintPiece(QPainter *painter) PaintPieceLabel(piece->GetPieceLabelRect(), piece->GetPieceLabelData(), painter); PaintPieceLabel(piece->GetPatternLabelRect(), piece->GetPatternLabelData(), painter); + + if (not m_stickyPoints.isEmpty()) + { + m_stickyPath.moveTo(m_stickyPoints.first()); + for (int i = 1; i < m_stickyPoints.size(); i++) + { + m_stickyPath.lineTo(m_stickyPoints.at(i)); + } + + if (painter != nullptr) + { + painter->save(); + painter->setBrush(QBrush(Qt::BDiagPattern)); + + QPen pen = painter->pen(); + pen.setStyle(Qt::DashLine); + pen.setColor(mainColor); + painter->setPen(pen); + + painter->drawPath(m_stickyPath); + painter->restore(); + } + } } //--------------------------------------------------------------------------------------------------------------------- @@ -497,8 +551,28 @@ void VPGraphicsPiece::GroupMove(const QPointF &pos) if (pieces.size() == 1) { - auto *command = new VPUndoPieceMove(pieces.first(), newPos.x(), newPos.y(), allowChangeMerge); + VPPiecePtr p = pieces.first(); + auto *command = new VPUndoPieceMove(piece, newPos.x(), newPos.y(), allowChangeMerge); layout->UndoStack()->push(command); + + if (layout->LayoutSettings().GetStickyEdges()) + { + QVector path; + if (not p.isNull() && p->StickyPosition(m_stickyTranslateX, m_stickyTranslateY)) + { + path = p->GetMappedExternalContourPoints(); + QTransform m; + m.translate(m_stickyTranslateX, m_stickyTranslateY); + path = m.map(path); + m_hasStickyPosition = true; + } + else + { + m_hasStickyPosition = false; + } + + SetStickyPoints(path); + } } else if (pieces.size() > 1) { diff --git a/src/app/puzzle/scene/vpgraphicspiece.h b/src/app/puzzle/scene/vpgraphicspiece.h index f5335d4af..fe9e23737 100644 --- a/src/app/puzzle/scene/vpgraphicspiece.h +++ b/src/app/puzzle/scene/vpgraphicspiece.h @@ -53,6 +53,8 @@ public: virtual int type() const override {return Type;} enum { Type = UserType + static_cast(PGraphicsItem::Piece)}; + void SetStickyPoints(const QVector &newStickyPoint); + signals: void HideTransformationHandles(bool hide); void PieceTransformationChanged(); @@ -91,6 +93,13 @@ private: bool allowChangeMerge{false}; + QVector m_stickyPoints{}; + QPainterPath m_stickyPath{}; + + bool m_hasStickyPosition{false}; + qreal m_stickyTranslateX{0}; + qreal m_stickyTranslateY{0}; + void PaintPieceLabel(const QVector &labelShape, const VTextManager &tm, QPainter *painter=nullptr); void PaintPiece(QPainter *painter=nullptr); diff --git a/src/app/puzzle/scene/vpmaingraphicsview.cpp b/src/app/puzzle/scene/vpmaingraphicsview.cpp index 6fd93d4bd..3bf81f7ce 100644 --- a/src/app/puzzle/scene/vpmaingraphicsview.cpp +++ b/src/app/puzzle/scene/vpmaingraphicsview.cpp @@ -302,7 +302,7 @@ void VPMainGraphicsView::keyPressEvent(QKeyEvent *event) } else { - TranslatePiecesOn(0, 1); + TranslatePiecesOn(0, -1); } } else if (event->key() == Qt::Key_Down) @@ -360,7 +360,48 @@ void VPMainGraphicsView::keyReleaseEvent(QKeyEvent *event) { if (not event->isAutoRepeat()) { + if (m_hasStickyPosition && not m_graphicsPieces.isEmpty()) + { + VPPiecePtr piece = m_graphicsPieces.first()->GetPiece(); + if (not piece.isNull()) + { + VPLayoutPtr layout = piece->Layout(); + if (not layout.isNull() && layout->LayoutSettings().GetStickyEdges()) + { + auto PreparePieces = [layout]() + { + QList pieces; + VPSheetPtr sheet = layout->GetFocusedSheet(); + if (not sheet.isNull()) + { + pieces = sheet->GetSelectedPieces(); + } + + return pieces; + }; + + QList pieces = PreparePieces(); + if (pieces.size() == 1) + { + VPPiecePtr p = pieces.first(); + + auto *command = new VPUndoPieceMove(p, m_stickyTranslateX, m_stickyTranslateY, + m_allowChangeMerge); + layout->UndoStack()->push(command); + + VPGraphicsPiece * gPiece = ScenePiece(p); + if (gPiece != nullptr) + { + gPiece->SetStickyPoints(QVector()); + } + } + } + + } + } + m_allowChangeMerge = false; + m_hasStickyPosition = false; } } @@ -632,8 +673,32 @@ void VPMainGraphicsView::TranslatePiecesOn(qreal dx, qreal dy) QList pieces = PreparePieces(); if (pieces.size() == 1) { - auto *command = new VPUndoPieceMove(pieces.first(), dx, dy, m_allowChangeMerge); + VPPiecePtr p = pieces.first(); + auto *command = new VPUndoPieceMove(p, dx, dy, m_allowChangeMerge); layout->UndoStack()->push(command); + + if (layout->LayoutSettings().GetStickyEdges()) + { + QVector path; + if (not p.isNull() && p->StickyPosition(m_stickyTranslateX, m_stickyTranslateY)) + { + path = p->GetMappedExternalContourPoints(); + QTransform m; + m.translate(m_stickyTranslateX, m_stickyTranslateY); + path = m.map(path); + m_hasStickyPosition = true; + } + else + { + m_hasStickyPosition = false; + } + + VPGraphicsPiece *gPiece = ScenePiece(p); + if (gPiece != nullptr) + { + gPiece->SetStickyPoints(path); + } + } } else if (pieces.size() > 1) { @@ -645,7 +710,7 @@ void VPMainGraphicsView::TranslatePiecesOn(qreal dx, qreal dy) } //--------------------------------------------------------------------------------------------------------------------- -void VPMainGraphicsView::on_PieceSheetChanged(const VPPiecePtr &piece) +auto VPMainGraphicsView::ScenePiece(const VPPiecePtr &piece) const -> VPGraphicsPiece * { VPGraphicsPiece *_graphicsPiece = nullptr; for(auto *graphicPiece : m_graphicsPieces) @@ -656,6 +721,14 @@ void VPMainGraphicsView::on_PieceSheetChanged(const VPPiecePtr &piece) } } + return _graphicsPiece; +} + +//--------------------------------------------------------------------------------------------------------------------- +void VPMainGraphicsView::on_PieceSheetChanged(const VPPiecePtr &piece) +{ + VPGraphicsPiece *graphicsPiece = ScenePiece(piece); + VPLayoutPtr layout = piece->Layout(); if (layout.isNull()) { @@ -665,22 +738,22 @@ void VPMainGraphicsView::on_PieceSheetChanged(const VPPiecePtr &piece) if (piece->Sheet().isNull() || piece->Sheet() == layout->GetTrashSheet() || piece->Sheet() != layout->GetFocusedSheet()) // remove { - if (_graphicsPiece != nullptr) + if (graphicsPiece != nullptr) { - scene()->removeItem(_graphicsPiece); - m_graphicsPieces.removeAll(_graphicsPiece); - delete _graphicsPiece; + scene()->removeItem(graphicsPiece); + m_graphicsPieces.removeAll(graphicsPiece); + delete graphicsPiece; } } else // add { - if(_graphicsPiece == nullptr) + if(graphicsPiece == nullptr) { - _graphicsPiece = new VPGraphicsPiece(piece); - m_graphicsPieces.append(_graphicsPiece); - ConnectPiece(_graphicsPiece); + graphicsPiece = new VPGraphicsPiece(piece); + m_graphicsPieces.append(graphicsPiece); + ConnectPiece(graphicsPiece); } - scene()->addItem(_graphicsPiece); + scene()->addItem(graphicsPiece); } VMainGraphicsView::NewSceneRect(scene(), this); diff --git a/src/app/puzzle/scene/vpmaingraphicsview.h b/src/app/puzzle/scene/vpmaingraphicsview.h index 96ef3813e..2515670a8 100644 --- a/src/app/puzzle/scene/vpmaingraphicsview.h +++ b/src/app/puzzle/scene/vpmaingraphicsview.h @@ -126,11 +126,16 @@ private: qreal m_rotationSum{0}; + bool m_hasStickyPosition{false}; + qreal m_stickyTranslateX{0}; + qreal m_stickyTranslateY{0}; + void ConnectPiece(VPGraphicsPiece *piece); void RotatePiecesByAngle(qreal angle); void TranslatePiecesOn(qreal dx, qreal dy); + auto ScenePiece(const VPPiecePtr &piece) const -> VPGraphicsPiece *; }; #endif // VPMAINGRAPHICSVIEW_H diff --git a/src/app/puzzle/vpmainwindow.cpp b/src/app/puzzle/vpmainwindow.cpp index aceebe2ea..4f521e894 100644 --- a/src/app/puzzle/vpmainwindow.cpp +++ b/src/app/puzzle/vpmainwindow.cpp @@ -909,7 +909,6 @@ void VPMainWindow::InitPropertyTabLayout() { m_layout->LayoutSettings().SetPiecesGapConverted(d); LayoutWasSaved(false); - // TODO update the QGraphicView } }); @@ -2539,6 +2538,19 @@ void VPMainWindow::on_ApplyPieceTransformation() auto *command = new VPUndoPieceMove(piece, pieceDx, pieceDy); m_layout->UndoStack()->push(command); + + if (m_layout->LayoutSettings().GetStickyEdges()) + { + qreal stickyTranslateX = 0; + qreal stickyTranslateY = 0; + if (piece->StickyPosition(stickyTranslateX, stickyTranslateY)) + { + bool allowMerge = selectedPieces.size() == 1; + auto *stickyCommand = new VPUndoPieceMove(piece, stickyTranslateX, stickyTranslateY, + allowMerge); + m_layout->UndoStack()->push(stickyCommand); + } + } } } diff --git a/src/app/puzzle/xml/vplayoutfilereader.cpp b/src/app/puzzle/xml/vplayoutfilereader.cpp index add586b6e..6406e840e 100644 --- a/src/app/puzzle/xml/vplayoutfilereader.cpp +++ b/src/app/puzzle/xml/vplayoutfilereader.cpp @@ -297,7 +297,7 @@ void VPLayoutFileReader::ReadControl(const VPLayoutPtr &layout) ReadAttributeBool(attribs, ML::AttrWarningSuperposition, trueStr)); layout->LayoutSettings().SetWarningPiecesOutOfBound(ReadAttributeBool(attribs, ML::AttrWarningOutOfBound, trueStr)); layout->LayoutSettings().SetStickyEdges(ReadAttributeBool(attribs, ML::AttrStickyEdges, trueStr)); - layout->LayoutSettings().SetPiecesGap(ReadAttributeDouble(attribs, ML::AttrPiecesGap, QChar('0'))); + layout->LayoutSettings().SetPiecesGap(qMax(ReadAttributeDouble(attribs, ML::AttrPiecesGap, QChar('0')), 0.0)); layout->LayoutSettings().SetFollowGrainline(ReadAttributeBool(attribs, ML::AttrFollowGrainline, falseStr)); readElementText();