/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see .
*/
// SPDX-License-Identifier: GPL-2.0-or-later
#include "conductorrouter.h"
#include
/**
The geometry of ConductorRouter on its own: that a route is made of
horizontal and vertical segments, leaves and enters each terminal in
the terminal's direction, and goes around obstacles rather than
through them.
*/
class TestConductorRouter : public QObject
{
Q_OBJECT
using Direction = ConductorRouter::Direction;
static bool crosses(const QList &points, const QRectF &r)
{
for (int i = 0; i + 1 < points.size(); ++i) {
const QPointF a = points.at(i), b = points.at(i + 1);
const QRectF seg = QRectF(a, b).normalized();
const qreal left = qMax(seg.left(), r.left()), right = qMin(seg.right(), r.right());
const qreal top = qMax(seg.top(), r.top()), bottom = qMin(seg.bottom(), r.bottom());
// a segment is a degenerate rectangle: it crosses the
// interior when it overlaps it on one axis and lies
// strictly inside it on the other
if (seg.width() == 0 && seg.left() > r.left() && seg.left() < r.right() && bottom > top)
return true;
if (seg.height() == 0 && seg.top() > r.top() && seg.top() < r.bottom() && right > left)
return true;
}
return false;
}
static void checkShape(const QList &p, const ConductorRouter::Request &r)
{
QVERIFY(p.size() >= 3);
QCOMPARE(p.first(), r.start);
QCOMPARE(p.last(), r.end);
for (int i = 0; i + 1 < p.size(); ++i)
QVERIFY2(p.at(i).x() == p.at(i + 1).x() || p.at(i).y() == p.at(i + 1).y(),
"a segment is neither horizontal nor vertical");
}
static QPointF unit(Direction d)
{
switch (d) {
case Direction::North: return {0, -1};
case Direction::East: return {1, 0};
case Direction::South: return {0, 1};
case Direction::West: return {-1, 0};
}
return {};
}
static QPointF direction(QPointF from, QPointF to)
{
const QPointF d = to - from;
return {d.x() > 0 ? 1. : d.x() < 0 ? -1. : 0., d.y() > 0 ? 1. : d.y() < 0 ? -1. : 0.};
}
private slots:
void straightWhenNothingIsInTheWay()
{
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction::East;
r.end = {300, 100};
r.end_direction = Direction::West;
const auto result = ConductorRouter::route(r);
checkShape(result.points, r);
// no bend at all: start, the two exit points, end
for (const QPointF &p : result.points) QCOMPARE(p.y(), 100.);
}
void goesAroundASymbol()
{
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction::East;
r.end = {300, 100};
r.end_direction = Direction::West;
const QRectF symbol(170, 60, 60, 80);
r.obstacles << symbol;
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
checkShape(result.points, r);
QVERIFY(!crosses(result.points, symbol.adjusted(-r.margin, -r.margin, r.margin, r.margin)));
}
// The test that the check above can fail: a straight line through
// the symbol is reported as crossing it.
void crossingCheckCanFail()
{
QVERIFY(crosses({{100, 100}, {300, 100}}, QRectF(170, 60, 60, 80)));
}
void leavesAndEntersInTheTerminalsDirections_data()
{
QTest::addColumn("from");
QTest::addColumn("to");
for (int a = 0; a < 4; ++a)
for (int b = 0; b < 4; ++b)
QTest::addRow("%d-%d", a, b) << a << b;
}
void leavesAndEntersInTheTerminalsDirections()
{
QFETCH(int, from);
QFETCH(int, to);
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction(from);
r.end = {250, 180};
r.end_direction = Direction(to);
// each terminal's own symbol, on the side opposite the way
// the terminal points
const QPointF s = unit(r.start_direction), e = unit(r.end_direction);
const QRectF own_start(r.start - s * 40 - QPointF(20, 20), QSizeF(40, 40));
const QRectF own_end(r.end - e * 40 - QPointF(20, 20), QSizeF(40, 40));
r.obstacles << own_start << own_end;
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
checkShape(result.points, r);
const auto &p = result.points;
QCOMPARE(direction(p.at(0), p.at(1)), s);
QCOMPARE(direction(p.at(p.size() - 1), p.at(p.size() - 2)), e);
QVERIFY(!crosses(p, own_start));
QVERIFY(!crosses(p, own_end));
}
void prefersNotToRunAlongAnotherWire()
{
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction::East;
r.end = {300, 100};
r.end_direction = Direction::West;
// a wire lying exactly on the straight route
r.wires << QVector{{110, 100}, {290, 100}};
const auto result = ConductorRouter::route(r);
checkShape(result.points, r);
bool on_it = false;
for (int i = 1; i + 2 < result.points.size(); ++i)
if (result.points.at(i).y() == 100 && result.points.at(i + 1).y() == 100)
on_it = true;
QVERIFY(!on_it);
}
void staysOnTheFolio()
{
ConductorRouter::Request r;
r.start = {100, 30};
r.start_direction = Direction::East;
r.end = {300, 30};
r.end_direction = Direction::West;
r.obstacles << QRectF(170, 0, 60, 200);
r.bounds = QRectF(0, 0, 500, 400);
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
for (const QPointF &p : result.points) QVERIFY(r.bounds.contains(p));
}
void reportsWhenThereIsNoWay()
{
ConductorRouter::Request r;
r.start = {100, 100};
r.start_direction = Direction::East;
r.end = {300, 100};
r.end_direction = Direction::West;
// a wall from the top of the folio to the bottom
r.obstacles << QRectF(170, -10, 60, 520);
r.bounds = QRectF(0, 0, 500, 500);
const auto result = ConductorRouter::route(r);
QVERIFY(result.points.isEmpty());
QVERIFY(!result.error.isEmpty());
}
void terminalsOffTheGrid()
{
ConductorRouter::Request r;
r.start = {103, 97};
r.start_direction = Direction::South;
r.end = {287, 213};
r.end_direction = Direction::North;
r.obstacles << QRectF(120, 140, 200, 30);
const auto result = ConductorRouter::route(r);
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
checkShape(result.points, r);
}
};
QTEST_MAIN(TestConductorRouter)
#include "tst_conductorrouter.moc"