mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 21:34:12 +02:00
91804d5f5e
A group whose only unlocked member is a shape (its other members locked) kept a centre of (0,0), so Centrer horizontalement and Centrer verticalement pulled every item toward the folio origin and the shape itself landed off the line. The rule that turns a group's members into one item now lives in Alignment::combined(), where every member, shapes included, brings its own centre, and tst_alignment covers it. Also comments why unitFor()'s returned reference is safe, as asked in the review of #1087. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
193 lines
7.6 KiB
C++
193 lines
7.6 KiB
C++
#include <QtTest>
|
|
|
|
#include "alignment.h"
|
|
|
|
class tst_alignment : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
void gridOffset_data()
|
|
{
|
|
QTest::addColumn<QPointF>("pos");
|
|
QTest::addColumn<int>("x_grid");
|
|
QTest::addColumn<int>("y_grid");
|
|
QTest::addColumn<qreal>("divisor");
|
|
QTest::addColumn<QPointF>("expected");
|
|
|
|
QTest::newRow("on the grid") << QPointF(100, 50) << 10 << 10 << 1.0 << QPointF(0, 0);
|
|
QTest::newRow("5 px right, fine nudge") << QPointF(105, 50) << 10 << 10 << 1.0 << QPointF(5, 0);
|
|
QTest::newRow("either side") << QPointF(103, 97) << 10 << 10 << 1.0 << QPointF(-3, 3);
|
|
QTest::newRow("negative, fraction") << QPointF(-13.3, 13.7) << 10 << 10 << 1.0 << QPointF(3.3, -3.7);
|
|
QTest::newRow("uneven grid") << QPointF(12, 13) << 10 << 5 << 1.0 << QPointF(-2, 2);
|
|
QTest::newRow("text grid 1:2") << QPointF(103, 97) << 10 << 10 << 2.0 << QPointF(2, -2);
|
|
QTest::newRow("text grid off") << QPointF(103.4, 96.6) << 10 << 10 << 0.0 << QPointF(-0.4, 0.4);
|
|
}
|
|
|
|
void gridOffset()
|
|
{
|
|
QFETCH(QPointF, pos);
|
|
QFETCH(int, x_grid);
|
|
QFETCH(int, y_grid);
|
|
QFETCH(qreal, divisor);
|
|
QFETCH(QPointF, expected);
|
|
|
|
const QPointF offset = Alignment::gridOffset(pos, x_grid, y_grid, divisor);
|
|
QVERIFY2(qAbs(offset.x() - expected.x()) < 1e-9, qPrintable(QString::number(offset.x())));
|
|
QVERIFY2(qAbs(offset.y() - expected.y()) < 1e-9, qPrintable(QString::number(offset.y())));
|
|
QVERIFY(qAbs(pos.x() + offset.x() - TextGrid::snap(pos, x_grid, y_grid, divisor).x()) < 1e-9);
|
|
}
|
|
|
|
// An item already on the grid must give an exactly null offset, not a
|
|
// rounding residue: the command pushes no undo step only when every
|
|
// offset is null.
|
|
void onGridIsExactlyNull()
|
|
{
|
|
for (int k = -50; k <= 50; ++k) {
|
|
const QPointF on_grid(k * 10, -k * 10);
|
|
QVERIFY(Alignment::gridOffset(on_grid, 10, 10).isNull());
|
|
QVERIFY(Alignment::gridOffset(on_grid + QPointF(1e-10, -1e-10), 10, 10).isNull());
|
|
}
|
|
}
|
|
|
|
// Three items: a 40x20 symbol with its origin 10 px in from its left
|
|
// edge, a 20x40 one with its origin at its centre, and a 60x10 picture
|
|
// whose reference point is its centre.
|
|
static QList<Alignment::Item> threeItems()
|
|
{
|
|
return {
|
|
{QRectF(100, 200, 40, 20), QPointF(110, 210)},
|
|
{QRectF(150, 100, 20, 40), QPointF(160, 120)},
|
|
{QRectF( 70, 300, 60, 10), QPointF(100, 305)}
|
|
};
|
|
}
|
|
|
|
void alignOffsets_data()
|
|
{
|
|
QTest::addColumn<int>("edge");
|
|
QTest::addColumn<QList<QPointF>>("expected");
|
|
|
|
QTest::newRow("left, on the left-most edge")
|
|
<< int(Alignment::Left) << QList<QPointF>{{-30, 0}, {-80, 0}, {0, 0}};
|
|
QTest::newRow("centre, on the mean origin x (123.33)")
|
|
<< int(Alignment::HCenter) << QList<QPointF>{{370.0/3 - 110, 0}, {370.0/3 - 160, 0}, {370.0/3 - 100, 0}};
|
|
QTest::newRow("right, on the right-most edge")
|
|
<< int(Alignment::Right) << QList<QPointF>{{30, 0}, {0, 0}, {40, 0}};
|
|
QTest::newRow("top, on the top-most edge")
|
|
<< int(Alignment::Top) << QList<QPointF>{{0, -100}, {0, 0}, {0, -200}};
|
|
QTest::newRow("middle, on the mean origin y (211.67)")
|
|
<< int(Alignment::VCenter) << QList<QPointF>{{0, 635.0/3 - 210}, {0, 635.0/3 - 120}, {0, 635.0/3 - 305}};
|
|
QTest::newRow("bottom, on the bottom-most edge")
|
|
<< int(Alignment::Bottom) << QList<QPointF>{{0, 90}, {0, 170}, {0, 0}};
|
|
}
|
|
|
|
void alignOffsets()
|
|
{
|
|
QFETCH(int, edge);
|
|
QFETCH(QList<QPointF>, expected);
|
|
|
|
const QList<QPointF> offsets = Alignment::alignOffsets(threeItems(), Alignment::Edge(edge));
|
|
QCOMPARE(offsets.size(), expected.size());
|
|
for (int i = 0 ; i < offsets.size() ; ++i) {
|
|
QVERIFY2(qAbs(offsets.at(i).x() - expected.at(i).x()) < 1e-9, qPrintable(QString("item %1 x %2").arg(i).arg(offsets.at(i).x())));
|
|
QVERIFY2(qAbs(offsets.at(i).y() - expected.at(i).y()) < 1e-9, qPrintable(QString("item %1 y %2").arg(i).arg(offsets.at(i).y())));
|
|
}
|
|
}
|
|
|
|
// Aligning items that are already lined up, and on the grid, moves
|
|
// nothing: the command then pushes no undo step.
|
|
void alreadyAligned()
|
|
{
|
|
const QList<Alignment::Item> items {
|
|
{QRectF(100, 200, 40, 20), QPointF(110, 210)},
|
|
{QRectF(100, 300, 40, 20), QPointF(110, 310)}
|
|
};
|
|
for (int e = Alignment::Left ; e <= Alignment::Bottom ; ++e)
|
|
{
|
|
const auto edge = Alignment::Edge(e);
|
|
if (edge == Alignment::Top || edge == Alignment::VCenter || edge == Alignment::Bottom)
|
|
continue; // they sit one above the other, so these do move
|
|
const QList<QPointF> offsets = Alignment::alignOffsets(items, edge);
|
|
for (int i = 0 ; i < items.size() ; ++i)
|
|
QVERIFY(Alignment::snappedOffset(items.at(i).ref, offsets.at(i), edge, 10, 10).isNull());
|
|
}
|
|
}
|
|
|
|
// The result lands on the grid along the aligned line, and the other
|
|
// coordinate is left alone even when it is off the grid.
|
|
void snapsOnlyAlongTheLine()
|
|
{
|
|
// raw offset puts x at 123.33: rounded to 120; y 207 stays 207
|
|
const QPointF pos(110, 207);
|
|
const QPointF offset = Alignment::snappedOffset(pos, QPointF(370.0/3 - 110, 0), Alignment::HCenter, 10, 10);
|
|
QVERIFY(qAbs(offset.x() - 10) < 1e-9);
|
|
QCOMPARE(offset.y(), 0.0);
|
|
|
|
const QPointF v = Alignment::snappedOffset(QPointF(113, 207), QPointF(0, 7), Alignment::Bottom, 10, 10);
|
|
QCOMPARE(v.x(), 0.0);
|
|
QVERIFY(qAbs(v.y() - 3) < 1e-9); // 207 + 7 = 214, rounded to 210
|
|
}
|
|
|
|
// Two symbols on the grid whose left edges are 5 px apart relative to
|
|
// their origins cannot both have their origin on the grid and their
|
|
// left edges on one line. The origins win: nothing leaves the grid.
|
|
void mixedWidthsStayOnTheGrid()
|
|
{
|
|
const QList<Alignment::Item> items {
|
|
{QRectF(95, 200, 30, 20), QPointF(110, 210)}, // origin 15 px in
|
|
{QRectF(200, 300, 20, 20), QPointF(210, 310)} // origin 10 px in
|
|
};
|
|
const QList<QPointF> offsets = Alignment::alignOffsets(items, Alignment::Left);
|
|
for (int i = 0 ; i < items.size() ; ++i)
|
|
{
|
|
const QPointF moved = items.at(i).ref + Alignment::snappedOffset(items.at(i).ref, offsets.at(i), Alignment::Left, 10, 10);
|
|
QVERIFY(Alignment::gridOffset(moved, 10, 10).isNull());
|
|
}
|
|
// the second one moves 105 px left to 105, a tie, rounded up to 110
|
|
QVERIFY(qAbs(Alignment::snappedOffset(items.at(1).ref, offsets.at(1), Alignment::Left, 10, 10).x() + 100) < 1e-9);
|
|
}
|
|
|
|
// Free texts go to the text grid, which can be finer than the folio's.
|
|
void textGridDivisor()
|
|
{
|
|
const QPointF offset = Alignment::snappedOffset(QPointF(103, 50), QPointF(0, 0), Alignment::Left, 10, 10, 2.0);
|
|
QVERIFY(qAbs(offset.x() - 2) < 1e-9); // 103 -> 105 on a 5 px text grid
|
|
QCOMPARE(offset.y(), 0.0);
|
|
}
|
|
|
|
// A group lines up on the middle of its members' box; a unit with a
|
|
// single member, such as a shape whose group-mate is locked, keeps
|
|
// that member's own centre instead of the origin of the folio.
|
|
void combinedUnits()
|
|
{
|
|
const Alignment::Item a{QRectF(100, 200, 40, 20), QPointF(110, 210)};
|
|
const Alignment::Item b{QRectF(300, 260, 20, 60), QPointF(310, 270)};
|
|
|
|
const Alignment::Item one = Alignment::combined({a});
|
|
QCOMPARE(one.edges, a.edges);
|
|
QCOMPARE(one.ref, a.ref);
|
|
|
|
const Alignment::Item both = Alignment::combined({a, b});
|
|
QCOMPARE(both.edges, QRectF(100, 200, 220, 120));
|
|
QCOMPARE(both.ref, QPointF(210, 260));
|
|
|
|
// a lone shape centred on x = 200 and a symbol at x = 400 meet
|
|
// half way, at 300; with the folio origin as the shape's centre
|
|
// they would meet at 200 and the shape would move 200 px
|
|
const QRectF shape(180, 50, 40, 40);
|
|
const QList<QPointF> offsets = Alignment::alignOffsets(
|
|
{Alignment::combined({{shape, shape.center()}}), {QRectF(390, 0, 20, 20), QPointF(400, 10)}},
|
|
Alignment::HCenter);
|
|
QCOMPARE(offsets.at(0), QPointF(100, 0));
|
|
QCOMPARE(offsets.at(1), QPointF(-100, 0));
|
|
}
|
|
|
|
void emptySelection()
|
|
{
|
|
QVERIFY(Alignment::alignOffsets({}, Alignment::Left).isEmpty());
|
|
}
|
|
};
|
|
|
|
QTEST_APPLESS_MAIN(tst_alignment)
|
|
#include "tst_alignment.moc"
|