Keep the symbols and wires a folio cannot load, and save them back

Opening a project dropped, with only a debug line, a symbol whose
definition could not be found (missing from the project's collection and
from the library) and every wire on it; and a wire whose terminal could
not be matched, after the symbol's definition was redrawn. The next save
wrote the project without them. One save of a forum upload whose
collection was incomplete kept 1 of 219 symbols and none of its 299 wires.

A folio now keeps what it could not load as it was read
(Diagram::m_kept_symbols, m_kept_wires) and writes it back when it is
saved whole (Diagram::writeKept()):
- a symbol, its terminals given ids after those of the folio's symbols;
- a wire, an end found written from its terminal as Conductor::toXml()
  does, an end not found as the symbol and terminal uuids it names; for a
  wire saved by legacy ids, the terminal's uuid derived from where it was
  saved (Element::savedTerminalUuid(), out of matchMovedTerminals()), so
  it joins again once the definition has it there. A wire whose ends
  cannot be read, or an end naming no terminal of the file, is kept so
  that it joins nothing.
A symbol kept counts as waiting (projectDataBase::waitingSymbols()), so a
partner keeps its link to it. The open warning now lists the symbols not
loaded too, and says both are kept.

Once the definition is back, the symbol and its wires are built again:
741.qet with one transistor definition removed (8 symbols, 33 wires on
them) saves all 67 wires, and with the definition put back opens with
all 65 symbols and 67 wires, the same folio as before.

tst_resaveunchanged::unloadableItemsKept: fixtures/coil_contact_wired.qet
(coil, linked contact, two wires; built with the script API) with the
coil's definition removed, or the contact's terminals moved and given new
uuids: both symbols, wires and links saved, a second save the same, and
the coil's definition put back gives the undamaged project's symbols and
wires. Fails on master (1 symbol; 0 wires).

FINDINGS F127, F128.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-19 16:45:55 +13:00
parent 3abaf21302
commit 0bcd0ca4b6
9 changed files with 823 additions and 47 deletions
+37 -26
View File
@@ -975,6 +975,41 @@ bool Element::valideXml(QDomElement &e)
return(true);
}
/**
@brief Element::savedTerminalUuid
@return the uuid TerminalUuids derives for the terminal saved as
@p records [@p index] (a symbol's saved terminals, in file order) at the
place it was saved: its docking point less the stub Terminal::init()
adds, and its rank among the records at that place, as the project
counted it when it gave the terminals their uuids
*/
QUuid Element::savedTerminalUuid(const QList<QDomElement> &records, int index)
{
const auto dock_of = [](const QDomElement &r) {
return QPointF(r.attribute(QStringLiteral("x")).toDouble(),
r.attribute(QStringLiteral("y")).toDouble());
};
const QDomElement &record = records.at(index);
const int orientation = record.attribute(QStringLiteral("orientation")).toInt();
const QPointF dock = dock_of(record);
QPointF old_pos = dock;
switch (orientation) {
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;
}
int rank = 0;
for (int j = 0; j < index; ++j) {
const QPointF other = dock_of(records.at(j));
if (records.at(j).attribute(QStringLiteral("orientation")).toInt() == orientation
&& qAbs(other.x() - dock.x()) < 1e-6
&& qAbs(other.y() - dock.y()) < 1e-6)
++rank;
}
return TerminalUuids::derived(old_pos.x(), old_pos.y(), orientation, rank);
}
/**
@brief Element::matchMovedTerminals
Pair the saved terminals that no live terminal matched exactly with the
@@ -1033,32 +1068,8 @@ void Element::matchMovedTerminals(const QList<QDomElement> &records,
//Pair a record with a live terminal: register its legacy id, and the
//uuid derived for the terminal at its old place
const auto pair_up = [&](int index, Terminal *t) {
const QDomElement &record = records.at(index);
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 < index; ++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);
id_adr.insert(records.at(index).attribute(QStringLiteral("id")).toInt(), t);
m_old_place_terminals.insert(savedTerminalUuid(records, index), t);
m_moved_terminals << t;
};
const auto same_place = [](const QPointF &a, const QPointF &b) {