Reattach wires to a terminal that moved a little since the project was saved

A project saves, for every placed symbol, where each terminal docked, its
orientation and its number. When the symbol's definition is replaced by one
whose terminal sits a few units elsewhere, wires were matched by exact
position and so were dropped on the next open: legacy wires by Terminal::
fromXml(), uuid wires by the uuid derived from the terminal's new place.

Element::fromXml() now pairs each saved terminal that nothing matched
exactly with the live terminal of the same orientation whose docking point
is within 5 units in x and y, only when each is the other's only candidate.
A pair registers the saved number, and the uuid derived from the old place,
which Diagram::findTerminals() checks after the derived uuid of the current
place. Wire ends reattached this way are counted and logged once per folio.

tst_movedterminals covers a move of 3 and 5 units (number and derived
uuid), 6 units, two candidates, and an unmoved terminal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-15 20:26:30 +13:00
parent 6af9c097d8
commit 786f8574d9
5 changed files with 344 additions and 1 deletions
+109 -1
View File
@@ -19,6 +19,7 @@
#include "../qetapp.h"
#include "../qetproject.h"
#include "../dataBase/projectdatabase.h"
#include "../ElementsCollection/terminaluuids.h"
#include "../PropertiesEditor/propertieseditordialog.h"
#include "../autoNum/assignvariables.h"
#include "../autoNum/elementautonumschemecommand.h"
@@ -972,6 +973,107 @@ bool Element::valideXml(QDomElement &e)
return(true);
}
/**
@brief Element::matchMovedTerminals
Pair the saved terminals that no live terminal matched exactly with the
live terminals that moved a little: the definition of the symbol was
replaced or redrawn since the project was saved. A saved record gives
the old docking point and orientation of its terminal, so it is paired
with a live terminal of the same orientation whose docking point is
within moved_terminal_reach in x and y, when each is the only candidate
of the other. Anything less clear stays unmatched, as before.
A pair registers the record's legacy id, and the uuid TerminalUuids
derived for the terminal at its old place, so that a wire naming either
reattaches (see Diagram::findTerminals()).
@param records : the saved terminals of this symbol
@param matched_records : indexes in records already matched
@param matched_terminals : the live terminals already matched
@param id_adr : receives the legacy id of each pair
*/
void Element::matchMovedTerminals(const QList<QDomElement> &records,
const QSet<int> &matched_records,
const QSet<Terminal *> &matched_terminals,
QHash<int, Terminal *> &id_adr)
{
if (matched_records.size() == records.size()) return;
const auto dock_of = [](const QDomElement &r) {
return QPointF(r.attribute(QStringLiteral("x")).toDouble(),
r.attribute(QStringLiteral("y")).toDouble());
};
const auto orientation_of = [](const QDomElement &r) {
return r.attribute(QStringLiteral("orientation")).toInt();
};
QHash<int, QList<Terminal *>> candidates; // record -> live terminals in reach
QHash<Terminal *, QList<int>> claimants; // live terminal -> records in reach
for (int i = 0; i < records.size(); ++i) {
if (matched_records.contains(i)) continue;
const QPointF dock = dock_of(records.at(i));
for (Terminal *t : std::as_const(m_terminals)) {
if (matched_terminals.contains(t)) continue;
if (static_cast<int>(t->orientation()) != orientation_of(records.at(i))) continue;
if (qAbs(t->dock_elmt_.x() - dock.x()) > moved_terminal_reach
|| qAbs(t->dock_elmt_.y() - dock.y()) > moved_terminal_reach) continue;
candidates[i] << t;
claimants[t] << i;
}
}
for (auto it = candidates.constBegin(); it != candidates.constEnd(); ++it) {
Terminal *t = it.value().value(0);
if (it.value().size() != 1 || claimants.value(t).size() != 1) continue;
const QDomElement &record = records.at(it.key());
const QPointF dock = dock_of(record);
//The old place of the terminal: its docking point less the
//stub that Terminal::init() added
QPointF old_pos = dock;
switch (orientation_of(record)) {
case Qet::North: old_pos -= QPointF(0, Terminal::terminalSize); break;
case Qet::East : old_pos -= QPointF(-Terminal::terminalSize, 0); break;
case Qet::West : old_pos -= QPointF(Terminal::terminalSize, 0); break;
case Qet::South: old_pos -= QPointF(0, -Terminal::terminalSize); break;
}
//The rank among the records at the same place, as the project
//counted it when it gave the terminals their uuids
int rank = 0;
for (int j = 0; j < it.key(); ++j) {
const QPointF other = dock_of(records.at(j));
if (orientation_of(records.at(j)) == orientation_of(record)
&& qAbs(other.x() - dock.x()) < 1e-6
&& qAbs(other.y() - dock.y()) < 1e-6)
++rank;
}
id_adr.insert(record.attribute(QStringLiteral("id")).toInt(), t);
m_old_place_terminals.insert(TerminalUuids::derived(old_pos.x(), old_pos.y(),
orientation_of(record), rank), t);
m_moved_terminals << t;
}
}
/**
@return the terminal that was paired with the saved terminal whose uuid
derived from its old place is @p old_place_uuid, if any
@see matchMovedTerminals()
*/
Terminal *Element::terminalFromOldPlace(const QUuid &old_place_uuid) const
{
return m_old_place_terminals.value(old_place_uuid, nullptr);
}
/**
@return true if @p terminal was paired with a saved terminal at another
place than its own
@see matchMovedTerminals()
*/
bool Element::terminalWasMoved(const Terminal *terminal) const
{
return m_moved_terminals.contains(const_cast<Terminal *>(terminal));
}
/**
@brief Element::fromXml
Import the parameters of this element from a xml document.
@@ -999,22 +1101,28 @@ bool Element::fromXml(QDomElement &e,
}
QHash<int, Terminal *> priv_id_adr;
QSet<Terminal *> matched_terminals;
QSet<int> matched_records;
for (auto *qgi : childItems())
{
if (auto terminal_ = qgraphicsitem_cast<Terminal *>(qgi))
{
for(auto qde : liste_terminals)
for (int i = 0; i < liste_terminals.size(); ++i)
{
QDomElement qde = liste_terminals.at(i);
if (terminal_ -> fromXml(qde))
{
priv_id_adr.insert(qde.attribute(QStringLiteral("id")).toInt(),
terminal_);
matched_terminals << terminal_;
matched_records << i;
}
}
}
}
matchMovedTerminals(liste_terminals, matched_records, matched_terminals, priv_id_adr);
//Check that associated id/address doesn't conflict with table_id_adr
for(auto found_id : priv_id_adr.keys())