diagrams-fable-qt/tests/tst_router.cpp
Ilya Ashikhmin 18d614ec67 feat(core): add orthogonal grid router with intersection jump-over arcs
Manhattan routing snapped to the rectangular grid with port-side-aware exit
stubs, right-angle crossing detection between routed polylines, and painter
paths bridging crossings with semicircular arcs (configurable radius).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:14:11 +02:00

129 lines
4.3 KiB
C++

#include "core/Router.h"
#include <QTest>
using namespace diag;
namespace {
bool isOrthogonal(const QList<QPointF>& poly)
{
for (int i = 0; i + 1 < poly.size(); ++i) {
const QPointF a = poly.at(i);
const QPointF b = poly.at(i + 1);
if (qAbs(a.x() - b.x()) > 1e-6 && qAbs(a.y() - b.y()) > 1e-6)
return false;
}
return true;
}
} // namespace
class TestRouter : public QObject {
Q_OBJECT
private slots:
void route_isOrthogonalAndKeepsEndpoints()
{
const QList<QPointF> poly =
Router::route({63, 37}, Side::Right, {305, 143}, Side::Left, 20);
QVERIFY(poly.size() >= 2);
QCOMPARE(poly.first(), QPointF(63, 37));
QCOMPARE(poly.last(), QPointF(305, 143));
QVERIFY(isOrthogonal(poly));
}
void route_intermediatePointsOnGrid()
{
const QList<QPointF> poly =
Router::route({63, 37}, Side::Right, {305, 143}, Side::Left, 20);
// The vertical middle segment must run on a grid line.
bool foundGridX = false;
for (int i = 1; i + 1 < poly.size(); ++i) {
const double x = poly.at(i).x();
if (qAbs(x - Router::snap(x, 20)) < 1e-6)
foundGridX = true;
}
QVERIFY(foundGridX);
}
void route_leavesPortInSideDirection()
{
const QList<QPointF> down =
Router::route({50, 50}, Side::Bottom, {250, 250}, Side::Top, 20);
QVERIFY(down.at(1).y() > down.at(0).y());
QCOMPARE(down.at(1).x(), down.at(0).x());
const QList<QPointF> left =
Router::route({300, 50}, Side::Left, {50, 50}, Side::Right, 20);
QVERIFY(left.at(1).x() < left.at(0).x());
}
void route_mixedAxesSingleCorner()
{
const QList<QPointF> poly =
Router::route({100, 100}, Side::Right, {200, 300}, Side::Top, 20);
QVERIFY(isOrthogonal(poly));
QCOMPARE(poly.first(), QPointF(100, 100));
QCOMPARE(poly.last(), QPointF(200, 300));
}
void crossings_detectsPerpendicularCross()
{
const QList<QPointF> horizontal{{0, 100}, {200, 100}};
const QList<QPointF> vertical{{100, 0}, {100, 200}};
const auto hits = Router::crossings(horizontal, vertical);
QCOMPARE(hits.size(), 1);
QCOMPARE(hits.first(), QPointF(100, 100));
// Symmetric case reports the same point.
QCOMPARE(Router::crossings(vertical, horizontal).size(), 1);
}
void crossings_ignoresParallelAndTouching()
{
const QList<QPointF> a{{0, 100}, {200, 100}};
const QList<QPointF> parallel{{0, 120}, {200, 120}};
QCOMPARE(Router::crossings(a, parallel).size(), 0);
// Vertical segment ending exactly on the line: a T-joint, not a cross.
const QList<QPointF> touching{{100, 0}, {100, 100}};
QCOMPARE(Router::crossings(a, touching).size(), 0);
}
void toPath_arcAddsDetourLength()
{
const QList<QPointF> poly{{0, 100}, {200, 100}};
const QPainterPath plain = Router::toPath(poly, {}, 6);
const QPainterPath hopped = Router::toPath(poly, {QPointF(100, 100)}, 6);
QVERIFY(hopped.length() > plain.length() + 1.0);
// The arc bulges upward: path bounding box extends above the line.
QVERIFY(hopped.boundingRect().top() < 100.0 - 4.0);
QCOMPARE(hopped.currentPosition(), QPointF(200, 100));
}
void toPath_hopTooCloseToCornerSkipped()
{
const QList<QPointF> poly{{0, 100}, {200, 100}};
const QPainterPath plain = Router::toPath(poly, {}, 6);
const QPainterPath nearEnd = Router::toPath(poly, {QPointF(198, 100)}, 6);
QCOMPARE(nearEnd.length(), plain.length());
}
void polyline_lengthAndPointAt()
{
const QList<QPointF> poly{{0, 0}, {100, 0}, {100, 100}};
QCOMPARE(Router::polylineLength(poly), 200.0);
QCOMPARE(Router::pointAt(poly, 0.25), QPointF(50, 0));
QCOMPARE(Router::pointAt(poly, 0.75), QPointF(100, 50));
QCOMPARE(Router::pointAt(poly, 1.0), QPointF(100, 100));
}
void snap_roundsToGrid()
{
QCOMPARE(Router::snap(47, 20), 40.0);
QCOMPARE(Router::snap(51, 20), 60.0);
QCOMPARE(Router::snapPoint({47, 51}, 20), QPointF(40, 60));
}
};
QTEST_GUILESS_MAIN(TestRouter)
#include "tst_router.moc"