diff --git a/sources/undocommand/deleteqgraphicsitemcommand.cpp b/sources/undocommand/deleteqgraphicsitemcommand.cpp index 661e17608..8991198f0 100644 --- a/sources/undocommand/deleteqgraphicsitemcommand.cpp +++ b/sources/undocommand/deleteqgraphicsitemcommand.cpp @@ -16,8 +16,10 @@ along with QElectroTech. If not, see . */ #include "deleteqgraphicsitemcommand.h" +#include "../wiringrules.h" #include "../diagram.h" +#include "../qetproject.h" #include "addgraphicsobjectcommand.h" #include "../qetdiagrameditor.h" #include "../qetgraphicsitem/ViewItem/qetgraphicstableitem.h" @@ -207,17 +209,44 @@ void DeleteQGraphicsItemCommand::setPotentialsOfRemovedElements() } ConductorProperties properties = hub_terminal->conductors().first()->properties(); - for (Terminal *t : terminals_to_connect_list) + + //Every terminal to the hub (a star), or, when the project + //limits the wires per terminal (discussion #1158), one after + //another (a chain): a star gives the hub a wire per other + //terminal, which the limit forbids. + QList> pairs; + const QETProject *project = hub_terminal->diagram() ? hub_terminal->diagram()->project() : nullptr; + if (project && WiringRules::chainsWires(project->wiringRules(), WiringRules::masterEnabled())) + { + QList chain {hub_terminal}; + chain << terminals_to_connect_list; + QList points; + for (Terminal *ct : std::as_const(chain)) { + points << ct->scenePos(); + } + const QList order = WiringRules::chainOrder(points); + for (int i = 1 ; i < order.size() ; ++i) { + pairs.append(std::make_pair(chain.at(order.at(i - 1)), chain.at(order.at(i)))); + } + } + else + { + for (Terminal *t : std::as_const(terminals_to_connect_list)) { + pairs.append(std::make_pair(hub_terminal, t)); + } + } + + for (const auto &new_pair : std::as_const(pairs)) { //If a conductor was already created between these two terminals //in this undo command, from another removed element, we do nothing bool exist_ = false; for (std::pair pair : m_connected_terminals) { - if (pair.first == hub_terminal && pair.second == t) { + if (pair.first == new_pair.first && pair.second == new_pair.second) { exist_ = true; continue; - } else if (pair.first == t && pair.second == hub_terminal) { + } else if (pair.first == new_pair.second && pair.second == new_pair.first) { exist_ = true; continue; } @@ -225,11 +254,11 @@ void DeleteQGraphicsItemCommand::setPotentialsOfRemovedElements() if (exist_ == false) { - m_connected_terminals.append(std::make_pair((Terminal *)hub_terminal, (Terminal *)t)); + m_connected_terminals.append(new_pair); qInfo() << "m_connected_terminals" << m_connected_terminals; - Conductor *new_cond = new Conductor(hub_terminal, t); + Conductor *new_cond = new Conductor(new_pair.first, new_pair.second); new_cond->setProperties(properties); - new AddGraphicsObjectCommand(new_cond, t->diagram(), QPointF(), this); + new AddGraphicsObjectCommand(new_cond, new_pair.second->diagram(), QPointF(), this); } } } diff --git a/sources/utils/conductorcreator.cpp b/sources/utils/conductorcreator.cpp index 77f2009ec..2d2ed3344 100644 --- a/sources/utils/conductorcreator.cpp +++ b/sources/utils/conductorcreator.cpp @@ -26,6 +26,7 @@ #include "../qetgraphicsitem/element.h" #include "../qetgraphicsitem/terminal.h" #include "../ui/potentialselectordialog.h" +#include "../wiringrules.h" #include "qgraphicsitem.h" #include @@ -48,18 +49,19 @@ ConductorCreator::ConductorCreator(Diagram *d, QList terminals_list) if (!setUpPropertieToUse()) { return; } - Terminal *hub_terminal = hubTerminal(); - d->undoStack().beginMacro(QObject::tr("Création de conducteurs")); + const bool chain = d->project() + && WiringRules::chainsWires(d->project()->wiringRules(), WiringRules::masterEnabled()); QList c_list; - for (Terminal *t : m_terminals_list) + for (const auto &pair : terminalPairs(chain)) { - if (t == hub_terminal) { + //Checked as the chain is built: the wire before this one may + //have just filled a terminal. + if (chain && !pair.first->canBeLinkedTo(pair.second)) { continue; } - - Conductor *cond = new Conductor(hub_terminal, t); + Conductor *cond = new Conductor(pair.first, pair.second); cond->setProperties(m_properties); cond->setSequenceNum(m_sequential_number); d->undoStack().push(new AddGraphicsObjectCommand(cond, d)); @@ -224,6 +226,41 @@ QList ConductorCreator::existingPotential(const QList & @brief ConductorCreator::hubTerminal @return hub_terminal */ +/** + @brief ConductorCreator::terminalPairs + @param chain : the project limits the wires per terminal + (discussion #1158, WiringRules::chainsWires()) + @return the pairs of terminals to wire: all to one hub terminal (a + star), or with \p chain one after another (WiringRules::chainOrder()), + since a star gives the hub a wire per other terminal. +*/ +QList> ConductorCreator::terminalPairs(bool chain) +{ + QList> pairs; + if (chain) + { + QList points; + for (Terminal *t : std::as_const(m_terminals_list)) { + points << t->scenePos(); + } + const QList order = WiringRules::chainOrder(points); + for (int i = 1 ; i < order.size() ; ++i) + { + pairs << qMakePair(m_terminals_list.at(order.at(i - 1)), + m_terminals_list.at(order.at(i))); + } + return pairs; + } + + Terminal *hub_terminal = hubTerminal(); + for (Terminal *t : std::as_const(m_terminals_list)) { + if (t != hub_terminal) { + pairs << qMakePair(hub_terminal, t); + } + } + return pairs; +} + Terminal *ConductorCreator::hubTerminal() { Terminal *hub_terminal = m_terminals_list.first(); diff --git a/sources/utils/conductorcreator.h b/sources/utils/conductorcreator.h index ec287bddd..a8c2c9a6a 100644 --- a/sources/utils/conductorcreator.h +++ b/sources/utils/conductorcreator.h @@ -44,6 +44,7 @@ class ConductorCreator static QList existingPotential(const QList &terminals_list); bool setUpPropertieToUse(); Terminal *hubTerminal(); + QList> terminalPairs(bool chain); QList m_terminals_list; diff --git a/sources/wiringrules.cpp b/sources/wiringrules.cpp index 19d7380e4..37cbddc85 100644 --- a/sources/wiringrules.cpp +++ b/sources/wiringrules.cpp @@ -174,6 +174,68 @@ bool WiringRules::hasRoom(int limit, int wires) return limit <= 0 || wires < limit; } +/** + @brief WiringRules::chainsWires + @return true if QElectroTech's own tools that wire several terminals at + once must wire them one after another (a chain) rather than all to one + of them (a star): a star gives that one terminal a wire per other + terminal, which the project's limit forbids. +*/ +bool WiringRules::chainsWires(const Settings &settings, bool master_enabled) +{ + return master_enabled && settings.max_wires > 0; +} + +/** + @brief WiringRules::chainOrder + The order in which to wire terminals at \a points one after another: + from the top left one (smallest x, then smallest y, the same terminal + the star used as its hub), each time to the nearest terminal not yet + wired. Distance is along the grid (|dx| + |dy|), since wires run + horizontally and vertically; a tie goes to the earlier point. + @return indexes into \a points, each once +*/ +QList WiringRules::chainOrder(const QList &points) +{ + QList order; + if (points.isEmpty()) { + return order; + } + + int current = 0; + for (int i = 1 ; i < points.size() ; ++i) { + const QPointF &p = points.at(i); + const QPointF &c = points.at(current); + if (p.x() < c.x() || (p.x() == c.x() && p.y() < c.y())) { + current = i; + } + } + + QList done(points.size(), false); + order << current; + done[current] = true; + while (order.size() < points.size()) + { + int nearest = -1; + qreal nearest_distance = 0; + for (int i = 0 ; i < points.size() ; ++i) { + if (done.at(i)) { + continue; + } + const QPointF d = points.at(i) - points.at(current); + const qreal distance = qAbs(d.x()) + qAbs(d.y()); + if (nearest < 0 || distance < nearest_distance) { + nearest = i; + nearest_distance = distance; + } + } + order << nearest; + done[nearest] = true; + current = nearest; + } + return order; +} + /** @brief WiringRules::turnsRuleOn @param before, after : the rules in force before and after a change diff --git a/sources/wiringrules.h b/sources/wiringrules.h index c4eef1db8..02da242b4 100644 --- a/sources/wiringrules.h +++ b/sources/wiringrules.h @@ -19,6 +19,9 @@ #ifndef WIRINGRULES_H #define WIRINGRULES_H +#include +#include + class QDomElement; /** @@ -79,6 +82,9 @@ namespace WiringRules int limit(const Settings &settings, bool master_enabled, bool is_report); bool hasRoom(int limit, int wires); + + bool chainsWires(const Settings &settings, bool master_enabled); + QList chainOrder(const QList &points); } #endif // WIRINGRULES_H diff --git a/tests/qttest/tst_wiringrules.cpp b/tests/qttest/tst_wiringrules.cpp index be4d283e2..0cf43ec7d 100644 --- a/tests/qttest/tst_wiringrules.cpp +++ b/tests/qttest/tst_wiringrules.cpp @@ -137,6 +137,49 @@ class tst_wiringrules : public QObject return {}; } + // On a fixture with @p rules: a symbol wired to four others is + // deleted, so QElectroTech rewires the four to keep the potential. + // Returns the most wires any one of them ends with. + QString mostWiresAfterDeletingTheHub(const QString &rules) + { + const QString project = fixtureWith(rules); + const QString script_path = m_dir.filePath(QStringLiteral("hub%1.js").arg(m_run)); + QFile script(script_path); + if (project.isEmpty() || !script.open(QIODevice::WriteOnly)) + return {}; + // Placed on a diagonal, so no two terminals line up and nothing + // is auto-connected. + script.write( + "var p = 'embed://import/probe/v2_fuse.elmt';\n" + "var hub = qet.addElement(0, p, 400, 400);\n" + "var others = [];\n" + "for (var i = 1; i <= 4; ++i) others.push(qet.addElement(0, p, 400 + 70 * i, 400 + 90 * i));\n" + "others.forEach(function (o) { qet.addConductor(0, hub, 0, o, 0); });\n" + "qet.deleteElement(0, hub);\n" + "var count = {};\n" + "qet.conductorUuids(0).forEach(function (u) {\n" + " qet.conductorEnds(0, u).forEach(function (e) { count[e] = (count[e] || 0) + 1; }); });\n" + "var most = 0;\n" + "others.forEach(function (o) { most = Math.max(most, count[o + ' terminal 0'] || 0); });\n" + "qet.log('PROBE ' + most);\n"); + script.close(); + + QProcess proc; + proc.setProcessEnvironment(sandbox()); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script_path, project}); + if (!proc.waitForFinished(60000)) + return {}; + const QString out = QString::fromUtf8(proc.readAllStandardOutput() + + proc.readAllStandardError()); + const QString mark = QStringLiteral("PROBE "); + for (const QString &line : out.split(QLatin1Char('\n'))) { + const int i = line.indexOf(mark); + if (i >= 0) + return line.mid(i + mark.size()).trimmed(); + } + return {}; + } + private slots: void initTestCase() { @@ -173,6 +216,39 @@ private slots: QVERIFY(!WiringRules::hasRoom(1, 1)); } + void chainsOnlyUnderALimit() + { + WiringRules::Settings rules; + QVERIFY(!WiringRules::chainsWires(rules, true)); + rules.one_wire_per_report = true; + QVERIFY(!WiringRules::chainsWires(rules, true)); + rules.max_wires = 2; + QVERIFY(WiringRules::chainsWires(rules, true)); + QVERIFY(!WiringRules::chainsWires(rules, false)); + } + + void chainOrder() + { + QCOMPARE(WiringRules::chainOrder({}), QList()); + QCOMPARE(WiringRules::chainOrder({QPointF(5, 5)}), QList({0})); + + // Starts top left, then always the nearest along the grid + const QList row {QPointF(300, 0), QPointF(0, 0), QPointF(200, 0), QPointF(100, 0)}; + QCOMPARE(WiringRules::chainOrder(row), QList({1, 3, 2, 0})); + + // Same x: the higher one starts + const QList column {QPointF(0, 100), QPointF(0, 0), QPointF(0, 50)}; + QCOMPARE(WiringRules::chainOrder(column), QList({1, 2, 0})); + + // Each index exactly once + const QList scattered {QPointF(40, 90), QPointF(10, 300), QPointF(220, 10), + QPointF(10, 10), QPointF(220, 300)}; + QList order = WiringRules::chainOrder(scattered); + QCOMPARE(order.first(), 3); + std::sort(order.begin(), order.end()); + QCOMPARE(order, QList({0, 1, 2, 3, 4})); + } + void projectOverridesApplication() { WiringRules::Settings application; @@ -293,6 +369,18 @@ private slots: // ...and the master switch still turns it off QCOMPARE(addWireToWiredTerminal(plain, true, 1), QStringLiteral("true")); } + + void deletingASymbolChainsTheWires() + { + SKIP_WITHOUT_SCRIPTING; + // No rule: the four are wired to one of them, as on master + QCOMPARE(mostWiresAfterDeletingTheHub(QString()), QStringLiteral("3")); + + // A limit: one after another, none gets more than two + QCOMPARE(mostWiresAfterDeletingTheHub( + QStringLiteral("")), + QStringLiteral("2")); + } }; QTEST_GUILESS_MAIN(tst_wiringrules)