diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index eacbeb712..37bb3d2e6 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -21,7 +21,9 @@ #include "../diagramposition.h" #include "../elementprovider.h" #include "../qetapp.h" +#include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/element.h" +#include "../qetgraphicsitem/terminal.h" #include "../qetinformation.h" #include "../qetproject.h" @@ -87,6 +89,7 @@ void projectDataBase::updateDB() populateDiagramInfoTable(); populateElementTable(); populateElementInfoTable(); + populateConductorTable(); emit dataBaseUpdated(); } @@ -245,6 +248,132 @@ void projectDataBase::diagramOrderChanged() { } +/** + @brief projectDataBase::addConductor + @param conductor +*/ +void projectDataBase::addConductor(Conductor *conductor) +{ + if (!conductor || !conductor->diagram()) { + qDebug() << "projectDataBase::addConductor: null conductor or diagram"; + return; + } + + //Both endpoints must belong to an element: the terminal table is keyed + //on (terminal, element) and a terminal with no parent has no identity + //to key on. Terminals whose *definition* predates terminal uuids are + //fine -- Terminal::stableUuid() derives one from the terminal's local + //position, which is what the project format itself matches on. + if (!conductor->terminal1->parentElement() + || !conductor->terminal2->parentElement()) { + return; + } + + insertTerminal(conductor->terminal1); + insertTerminal(conductor->terminal2); + + watchConductor(conductor); + bindConductorValues(m_insert_conductor_query, conductor, conductor->diagram()); + if (!m_insert_conductor_query.exec()) { + qDebug() << "projectDataBase::addConductor insert error : " << m_insert_conductor_query.lastError(); + } else { + emit dataBaseUpdated(); + } +} + +/** + @brief projectDataBase::removeConductor + @param conductor +*/ +void projectDataBase::removeConductor(Conductor *conductor) +{ + m_remove_conductor_query.bindValue(":uuid", conductor->uuid().toString()); + if (!m_remove_conductor_query.exec()) { + qDebug() << "projectDataBase::removeConductor delete error : " << m_remove_conductor_query.lastError(); + } else { + emit dataBaseUpdated(); + } +} + +/** + @brief projectDataBase::updateConductor + Refresh the mutable columns of an already-inserted conductor. + + Only the text (the wire number) can change without the conductor being + removed and re-added: its endpoints are fixed for its lifetime. Without + this, renaming a wire left the database holding the old number and the + wiring list showed a stale value until the next full repopulate. + @param conductor +*/ +void projectDataBase::updateConductor(Conductor *conductor) +{ + if (!conductor) { + return; + } + + m_update_conductor_query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString()); + m_update_conductor_query.bindValue(QStringLiteral(":text"), conductor->properties().text); + if (!m_update_conductor_query.exec()) { + qDebug() << "projectDataBase::updateConductor update error : " << m_update_conductor_query.lastError(); + } + + //Deliberately no dataBaseUpdated() here, unlike add/remove. The only + //column this touches is the wire text, which no view watched by + //ProjectDBModel displays -- the nomenclature shows elements, and its + //wire_count changes when a conductor appears or disappears, not when + //it is renamed. Emitting would make every ProjectDBModel re-run its + //query, and auto-numbering renames every conductor in the project in + //one pass. +} + +/** + @brief projectDataBase::watchConductor + Keep this conductor's row in step with its properties. + + Conductor::setProperties() has a dozen call sites (auto-numbering, the + properties dialog, element moves, deletion re-links...), so listening to + the signal it already emits is the only way to catch them all -- and the + only way to catch the ones added later. Qt::UniqueConnection makes a + repeated insert or a full repopulate harmless. + @param conductor +*/ +void projectDataBase::watchConductor(Conductor *conductor) +{ + connect(conductor, &Conductor::propertiesChange, + this, &projectDataBase::conductorPropertiesChanged, + Qt::UniqueConnection); +} + +/** + @brief projectDataBase::conductorPropertiesChanged +*/ +void projectDataBase::conductorPropertiesChanged() +{ + if (auto *conductor = qobject_cast(sender())) { + updateConductor(conductor); + } +} + +/** + @brief projectDataBase::bindConductorValues + One binder for both insert paths, so a conductor added to a live diagram + and one read from a file can never drift apart -- the same reason + bindElementValues() exists for elements. + @param query + @param conductor + @param diagram : the diagram the conductor belongs to +*/ +void projectDataBase::bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram) +{ + query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString()); + query.bindValue(QStringLiteral(":diagram_uuid"), diagram->uuid().toString()); + query.bindValue(QStringLiteral(":terminal1_uuid"), conductor->terminal1->stableUuid().toString()); + query.bindValue(QStringLiteral(":terminal1_element_uuid"), conductor->terminal1->parentElement()->uuid().toString()); + query.bindValue(QStringLiteral(":terminal2_uuid"), conductor->terminal2->stableUuid().toString()); + query.bindValue(QStringLiteral(":terminal2_element_uuid"), conductor->terminal2->parentElement()->uuid().toString()); + query.bindValue(QStringLiteral(":text"), conductor->properties().text); +} + /** @brief projectDataBase::createDataBase Create the data base @@ -323,6 +452,57 @@ bool projectDataBase::createDataBase() qDebug() << " element_info_table query : " << query_.lastError(); } + //Create the terminal table. + //Terminal::uuid() is the terminal-position id baked into the catalog + //.elmt definition (e.g. "the top terminal") -- identical across every + //placed instance of that catalog element, not a per-instance id. A + //terminal instance is only uniquely identified by (uuid, element_uuid) + //together, so that pair is the primary key here, not uuid alone. + QString terminal_table("CREATE TABLE terminal" + "( " + "uuid VARCHAR(50) NOT NULL, " + "element_uuid VARCHAR(50) NOT NULL," + "name VARCHAR(50)," + "PRIMARY KEY (uuid, element_uuid)," + "FOREIGN KEY (element_uuid) REFERENCES element (uuid)" + ")"); + if (!query_.exec(terminal_table)) { + qDebug() << "terminal_table query : "<< query_.lastError(); + } + + //Create the conductor table + QString conductor_table("CREATE TABLE conductor" + "( " + "uuid VARCHAR(50) PRIMARY KEY NOT NULL, " + "diagram_uuid VARCHAR(50) NOT NULL," + "terminal1_uuid VARCHAR(50) NOT NULL," + "terminal1_element_uuid VARCHAR(50) NOT NULL," + "terminal2_uuid VARCHAR(50) NOT NULL," + "terminal2_element_uuid VARCHAR(50) NOT NULL," + "text VARCHAR(100)," + "FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid)," + "FOREIGN KEY (terminal1_uuid, terminal1_element_uuid) REFERENCES terminal (uuid, element_uuid)," + "FOREIGN KEY (terminal2_uuid, terminal2_element_uuid) REFERENCES terminal (uuid, element_uuid)" + ")"); + if (!query_.exec(conductor_table)) { + qDebug() << "conductor_table query : "<< query_.lastError(); + } + + //The element-facing columns are looked up per element row, not per + //conductor row: element_nomenclature_view carries a correlated + //subquery counting the wires touching each element. Without these + //indexes each element row full-scans the conductor table, which grows + //as elements x conductors. + for (const QString &index_ : { + QStringLiteral("CREATE INDEX idx_conductor_terminal1_element ON conductor (terminal1_element_uuid)"), + QStringLiteral("CREATE INDEX idx_conductor_terminal2_element ON conductor (terminal2_element_uuid)"), + QStringLiteral("CREATE INDEX idx_conductor_diagram ON conductor (diagram_uuid)") }) + { + if (!query_.exec(index_)) { + qDebug() << "conductor index query : " << query_.lastError(); + } + } + createElementNomenclatureView(); createSummaryView(); prepareQuery(); @@ -534,6 +714,58 @@ void projectDataBase::populateDiagramInfoTable() } } +/** + @brief projectDataBase::populateConductorTable + Populate the terminal and conductor tables. Terminals only matter here + in the context of a conductor referencing them, so their population is + folded into this method rather than tracked independently. +*/ +void projectDataBase::populateConductorTable() +{ + QSqlQuery query(m_data_base); + query.exec(QStringLiteral("DELETE FROM conductor")); + query.exec(QStringLiteral("DELETE FROM terminal")); + + for (auto *diagram : m_project->diagrams()) + { + const auto conductor_list = diagram->conductors(); + for (auto *conductor : conductor_list) + { + //See addConductor(): only a terminal with no parent element is + //skipped. A missing terminal uuid is handled by stableUuid(). + if (!conductor->terminal1->parentElement() + || !conductor->terminal2->parentElement()) { + continue; + } + + insertTerminal(conductor->terminal1); + insertTerminal(conductor->terminal2); + + watchConductor(conductor); + bindConductorValues(m_insert_conductor_query, conductor, diagram); + if (!m_insert_conductor_query.exec()) { + qDebug() << "projectDataBase::populateConductorTable insert error : " << m_insert_conductor_query.lastError(); + } + } + } +} + +/** + @brief projectDataBase::insertTerminal + Insert (or, if already present -- e.g. a junction shared by several + conductors -- silently keep) @terminal in the terminal table. + @param terminal +*/ +void projectDataBase::insertTerminal(Terminal *terminal) +{ + m_insert_terminal_query.bindValue(":uuid", terminal->stableUuid().toString()); + m_insert_terminal_query.bindValue(":element_uuid", terminal->parentElement()->uuid().toString()); + m_insert_terminal_query.bindValue(":name", terminal->name()); + if (!m_insert_terminal_query.exec()) { + qDebug() << "projectDataBase::insertTerminal insert error : " << m_insert_terminal_query.lastError(); + } +} + void projectDataBase::prepareQuery() { //INSERT DIAGRAM @@ -606,6 +838,23 @@ void projectDataBase::prepareQuery() update_str.append(" WHERE element_uuid = :uuid"); m_update_element_query = QSqlQuery(m_data_base); m_update_element_query.prepare(update_str); + + //INSERT TERMINAL + m_insert_terminal_query = QSqlQuery(m_data_base); + m_insert_terminal_query.prepare("INSERT OR IGNORE INTO terminal (uuid, element_uuid, name) VALUES (:uuid, :element_uuid, :name)"); + + //INSERT CONDUCTOR + m_insert_conductor_query = QSqlQuery(m_data_base); + m_insert_conductor_query.prepare("INSERT INTO conductor (uuid, diagram_uuid, terminal1_uuid, terminal1_element_uuid, terminal2_uuid, terminal2_element_uuid, text) " + "VALUES (:uuid, :diagram_uuid, :terminal1_uuid, :terminal1_element_uuid, :terminal2_uuid, :terminal2_element_uuid, :text)"); + + //UPDATE CONDUCTOR + m_update_conductor_query = QSqlQuery(m_data_base); + m_update_conductor_query.prepare(QStringLiteral("UPDATE conductor SET text = :text WHERE uuid = :uuid")); + + //REMOVE CONDUCTOR + m_remove_conductor_query = QSqlQuery(m_data_base); + m_remove_conductor_query.prepare("DELETE FROM conductor WHERE uuid=:uuid"); } /** diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index c2e4c838c..cddf53468 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -27,6 +27,8 @@ class Element; class QETProject; class Diagram; +class Conductor; +class Terminal; class sqlite3; /** @@ -58,6 +60,16 @@ class projectDataBase : public QObject void diagramInfoChanged (Diagram *diagram); void diagramOrderChanged(); + void addConductor (Conductor *conductor); + void removeConductor (Conductor *conductor); + void updateConductor (Conductor *conductor); + + private slots: + //Refresh the sender()'s row after Conductor::setProperties(). + void conductorPropertiesChanged(); + + public: + signals: void dataBaseUpdated(); @@ -69,6 +81,10 @@ class projectDataBase : public QObject void populateElementTable(); void populateElementInfoTable(); void populateDiagramInfoTable(); + void populateConductorTable(); + void bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram); + void watchConductor(Conductor *conductor); + void insertTerminal(Terminal *terminal); void prepareQuery(); static QHash elementInfoToString( Element *elmt); @@ -86,7 +102,11 @@ class projectDataBase : public QObject m_insert_diagram_info_query, m_update_diagram_info_query, m_diagram_order_changed, - m_diagram_info_order_changed; + m_diagram_info_order_changed, + m_insert_terminal_query, + m_insert_conductor_query, + m_update_conductor_query, + m_remove_conductor_query; #ifdef QET_EXPORT_PROJECT_DB public: diff --git a/sources/diagram.cpp b/sources/diagram.cpp index fa707e4a5..cf552d14a 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -1666,6 +1666,7 @@ void Diagram::addItem(QGraphicsItem *item) conductor->terminal1->addConductor(conductor); conductor->terminal2->addConductor(conductor); conductor->calculateTextItemPosition(); + m_project->dataBase()->addConductor(conductor); break; } default: {break;} @@ -1696,6 +1697,7 @@ void Diagram::removeItem(QGraphicsItem *item) Conductor *conductor = static_cast(item); conductor->terminal1->removeConductor(conductor); conductor->terminal2->removeConductor(conductor); + m_project->dataBase()->removeConductor(conductor); break; } default: {break;} diff --git a/sources/diagramcommands.cpp b/sources/diagramcommands.cpp index ab3a6fdeb..65273de7a 100644 --- a/sources/diagramcommands.cpp +++ b/sources/diagramcommands.cpp @@ -82,6 +82,13 @@ void PasteDiagramCommand::redo() { first_redo = false; + //make new uuid for every pasted conductor, because old uuid are + //the uuid of the copied conductor + const QList all_pasted_conductors = content.conductors(); + for (Conductor *c : all_pasted_conductors) { + c -> newUuid(); + } + //this is the first paste, we do some actions for the new element const QList elmts_list = content.m_elements; for (Element *e : elmts_list) diff --git a/sources/elementspanelwidget.cpp b/sources/elementspanelwidget.cpp index 129e49512..4b1167119 100644 --- a/sources/elementspanelwidget.cpp +++ b/sources/elementspanelwidget.cpp @@ -17,6 +17,7 @@ */ #include "elementspanelwidget.h" #include "diagram.h" +#include "qetgraphicsitem/conductor.h" #include "editor/ui/qetelementeditor.h" #include "elementscategoryeditor.h" #include "qetapp.h" @@ -684,6 +685,13 @@ void ElementsPanelWidget::duplicateDiagram() elmt->newUuid(); new_diagram->restoreText(elmt); } + else if (Conductor *cond = dynamic_cast(item)) { + // Same reasoning for conductors: conductor.uuid is the PRIMARY + // KEY of the conductor table, and its insert is a plain INSERT, + // so a duplicated uuid fails and the wire silently disappears + // from the wiring list and the per-element wire count. + cond->newUuid(); + } } } diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index 0b105a9c0..4f538e91f 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -91,6 +91,7 @@ Conductor::Conductor(Terminal *p1, Terminal* p2) : //set Zvalue at 11 to be upper than the DiagramImageItem and element setZValue(11); m_previous_z_value = zValue(); + m_uuid = QUuid::createUuid(); //Add this conductor to the list of conductors of each of the two terminals bool ajout_p1 = terminal1 -> addConductor(this); @@ -1005,6 +1006,18 @@ void Conductor::pointsToSegments(const QList& points_list) { */ bool Conductor::fromXml(QDomElement &dom_element) { + //Older project files have no conductor uuid attribute at all -- + //generate one on load, same treatment terminal uuids got when + //that field was introduced (see terminal1/terminal2 handling in + //toXml() below). + m_uuid = QUuid(dom_element.attribute(QStringLiteral("uuid"))); + if (m_uuid.isNull()) { + //Absent, empty or malformed: mint one. A null uuid is not a usable + //identity -- every conductor carrying one would collide with every + //other on the conductor table's primary key. + m_uuid = QUuid::createUuid(); + } + setPos(dom_element.attribute("x", nullptr).toDouble(), dom_element.attribute("y", nullptr).toDouble()); @@ -1043,6 +1056,7 @@ QDomElement Conductor::toXml(QDomDocument &dom_document, { QDomElement dom_element = dom_document.createElement("conductor"); + dom_element.setAttribute("uuid", m_uuid.toString()); dom_element.setAttribute("x", QString::number(pos().x())); dom_element.setAttribute("y", QString::number(pos().y())); diff --git a/sources/qetgraphicsitem/conductor.h b/sources/qetgraphicsitem/conductor.h index dc4d5586b..91ca92afb 100644 --- a/sources/qetgraphicsitem/conductor.h +++ b/sources/qetgraphicsitem/conductor.h @@ -21,6 +21,7 @@ #include "../conductorproperties.h" #include +#include class ConductorProfile; class ConductorSegmentProfile; @@ -77,6 +78,8 @@ class Conductor : public QGraphicsObject int type() const override { return Type; } Diagram *diagram() const; ConductorTextItem *textItem() const; + QUuid uuid() const {return m_uuid;} + void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this conductor void updatePath(const QRectF & = QRectF()); //This method do nothing, it's only made to be used with Q_PROPERTY @@ -205,6 +208,7 @@ class Conductor : public QGraphicsObject Highlight must_highlight_; bool m_valid; bool m_freeze_label = false; + QUuid m_uuid; /// QPen et QBrush objects used to draw conductors static QPen conductor_pen; diff --git a/sources/qetgraphicsitem/terminal.cpp b/sources/qetgraphicsitem/terminal.cpp index b838eb1ce..119639b4e 100644 --- a/sources/qetgraphicsitem/terminal.cpp +++ b/sources/qetgraphicsitem/terminal.cpp @@ -820,6 +820,60 @@ QUuid Terminal::uuid() const return d->m_uuid; } +/** + @brief Terminal::stableUuid + An identity for this terminal that exists on every element, not only on + those saved by a uuid-aware element editor. + + uuid() comes from the catalog .elmt definition and is empty for every + element authored before that field existed -- which is most of the + installed base. Anything keyed on uuid() alone therefore cannot see those + elements at all. + + When there is no uuid, derive one from the terminal's local position and + orientation inside its element. That is not an arbitrary choice: it is the + same thing the project format itself uses to match a conductor back to a + terminal ("each connection is made by using the local position of the + terminal and a dynamic id" -- TerminalData::m_uuid). m_pos is the position + read from the definition and is not touched by moving the element on the + folio, so the result is stable across loads, saves and folio moves, and it + is unique within an element except where a definition genuinely declares + two terminals at the same point -- three cases in the whole example corpus, + and harmless, because two terminals sharing a position and orientation are + indistinguishable in every observable respect: they merge to one terminal + row and every conductor on either of them still resolves to the right + element and name. + + Derived values are UUID v5 in a fixed namespace, so they are reproducible + without being written to the file, and cannot collide with the v4 uuids + the element editor generates. + + @return the terminal's own uuid when it has one, otherwise a derived one +*/ +QUuid Terminal::stableUuid() const +{ + if (!d->m_uuid.isNull()) { + return d->m_uuid; + } + + //Fixed namespace for terminal identities derived from geometry. + static const QUuid derived_ns(QStringLiteral("{6b1f6d1e-6a1a-5f7e-9a3d-9c0a5b2d7e11}")); + + //Position and orientation only. The name is deliberately excluded: it + //is not stable across a save cycle -- QET rewrites a terminal named + //"_" as unnamed, which would silently change the identity of 1421 of + //industrial.qet's 1790 terminals on the first resave. It is also not + //needed: keying on geometry alone produces exactly the same number of + //collisions across the example corpus, and it means renaming a + //terminal does not change what it is. + const QString key = QStringLiteral("%1|%2|%3") + .arg(d->m_pos.x(), 0, 'f', 4) + .arg(d->m_pos.y(), 0, 'f', 4) + .arg(static_cast(d->m_orientation)); + + return QUuid::createUuidV5(derived_ns, key); +} + QString Terminal::name() const { if (d->m_use_master_label && parent_element_) { diff --git a/sources/qetgraphicsitem/terminal.h b/sources/qetgraphicsitem/terminal.h index 53606b698..a6eae784c 100644 --- a/sources/qetgraphicsitem/terminal.h +++ b/sources/qetgraphicsitem/terminal.h @@ -75,6 +75,7 @@ class Terminal : public QGraphicsObject Diagram *diagram () const; Element *parentElement () const; QUuid uuid () const; + QUuid stableUuid () const; QString name () const; QString baseName () const; TerminalData::Type terminalType() const;