From 82262c598019e7cce8845f2f57686d28fa232553 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 21 Sep 2026 18:48:40 +1200 Subject: [PATCH] Let a script number a conductor and link a cross-reference Two gaps left over from the drawing verbs. A script could create a conductor but not say what it was -- no number, colour, section or formula -- and could not link a master to its slave, although LinkElementCommand has been there all along and nothing bound it. conductors() what is on this folio, and how to address it conductorProperty() setConductorProperty() num, formula, function, bus, cable, tension_protocol, conductor_color, conductor_section, color, text_color elementLinkType() simple / master / slave / next_report / ... linkedElements() linkElements() two folio indices: a master and its slave are normally on different folios unlinkElement() The property names are the ones the .qet file uses for the same fields, so what a script sets is what a reader of the file sees rather than a third spelling invented here. A property is applied to every conductor of the same electrical potential, not to the one conductor named. That is the rule the application already follows -- SearchAndReplaceWorker pushes one QPropertyUndoCommand per conductor of relatedPotentialConductors() inside a macro -- because a wire number describes a potential, not one drawn segment; setting it on one and leaving the rest of the potential disagreeing would produce a file no GUI action could have produced. Linking asks LinkElementCommand::isLinkable() rather than re-deriving its rules, so a script cannot make a link the GUI would refuse: master to master, a PLC master to a non-PLC slave, a next-report to another next-report, or anything to an already-taken target. A conductor is addressed as "the conductor on terminal i of element U". It has no identity of its own to use instead: conductors carry no persisted uuid, and the terminal1/terminal2 ids in the file are folio-scoped integers QElectroTech renumbers on every save. Since the change is potential-wide, any terminal of the potential names it equally well, so in practice a potential is addressed from one of its leaves; a terminal carrying several conductors names none of them and is refused rather than guessed at. Verified headlessly. Conductor: num, section and colour set from one end of a potential and read back from the other, saved and reloaded, present in the XML. Propagation shown to discriminate, which took two tries -- the first attempt wired A.0-B.0 and B.1-C.0 and saw no propagation, correctly, because a coil's two terminals are opposite ends of the coil and not one potential. Wiring a real hub at A.0 instead, a number set via the B leaf appears on the C conductor too, in memory and in the saved file. Cross-reference: a master on one folio linked to a slave on another, linkedElements() agreeing from both ends, surviving save and reload with link_uuid written on both folios; master-to-master, self-link, unlink and relink all behave. Unknown property, invalid colour, bare terminal and ambiguous terminal all decline with a reason. Qt 6.10.2, build clean, ctest matches master, qet-coherence-check clean on the example corpus, qet-lint clean on the generated projects. Co-Authored-By: Claude Opus 5 (1M context) --- sources/scripting/qetscriptapi.cpp | 283 +++++++++++++++++++++++++++++ sources/scripting/qetscriptapi.h | 39 ++++ 2 files changed, 322 insertions(+) diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index f9bfec17f..0f0430e43 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -29,6 +29,7 @@ #include "../qetmessagebox.h" #include "../qetproject.h" #include "../qetresult.h" +#include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/terminal.h" #include "../qetinformation.h" #include "../titleblockproperties.h" @@ -36,6 +37,7 @@ #include "../undocommand/changeelementinformationcommand.h" #include "../undocommand/changetitleblockcommand.h" #include "../undocommand/deleteqgraphicsitemcommand.h" +#include "../undocommand/linkelementcommand.h" #include "../utils/conductorcreator.h" #include @@ -291,6 +293,95 @@ Terminal *QetScriptApi::findTerminal(int folioIndex, const QString &elementUuid, return terminals.at(terminalIndex); } +/** + @brief QetScriptApi::findConductor + The single conductor attached to a terminal, or nullptr. + + Conductors carry no persisted uuid, and the terminal1/terminal2 ids the + file uses for their ends are folio-scoped integers QElectroTech + renumbers on every save, so a conductor has no name that survives a + save/load cycle. Naming one by a terminal it is attached to does, and + it reads the way the question is usually asked ("the wire on A1 of + KM1"). A terminal with several conductors on it does not name one, so + refuse rather than silently take the first. +*/ +Conductor *QetScriptApi::findConductor(int folioIndex, const QString &elementUuid, + int terminalIndex, const QString &caller) +{ + Terminal *terminal = findTerminal(folioIndex, elementUuid, terminalIndex, caller); + if (!terminal) return nullptr; + const QList conductors = terminal->conductors(); + if (conductors.isEmpty()) { + log(QStringLiteral("qet.%1: terminal %2 of %3 has no conductor on it") + .arg(caller).arg(terminalIndex).arg(elementUuid)); + return nullptr; + } + if (conductors.count() > 1) { + log(QStringLiteral("qet.%1: terminal %2 of %3 carries %4 conductors, so it does " + "not name one -- use a terminal with a single conductor") + .arg(caller).arg(terminalIndex).arg(elementUuid).arg(conductors.count())); + return nullptr; + } + return conductors.first(); +} + +namespace { + +/** + Read or write one named conductor property. The names are the ones the + project file uses for the same fields (ConductorProperties::toXml), so + that what a script sets is what a reader of the .qet sees, rather than + a third spelling invented here. +*/ +QString conductorPropertyValue(const ConductorProperties &p, const QString &name) +{ + if (name == QLatin1String("num")) return p.text; + if (name == QLatin1String("formula")) return p.m_formula; + if (name == QLatin1String("function")) return p.m_function; + if (name == QLatin1String("bus")) return p.m_bus; + if (name == QLatin1String("cable")) return p.m_cable; + if (name == QLatin1String("tension_protocol")) return p.m_tension_protocol; + if (name == QLatin1String("conductor_color")) return p.m_wire_color; + if (name == QLatin1String("conductor_section")) return p.m_wire_section; + if (name == QLatin1String("color")) return p.color.name(); + if (name == QLatin1String("text_color")) return p.text_color.name(); + return QString(); +} + +bool setConductorPropertyValue(ConductorProperties &p, const QString &name, const QString &value) +{ + if (name == QLatin1String("num")) { p.text = value; return true; } + if (name == QLatin1String("formula")) { p.m_formula = value; return true; } + if (name == QLatin1String("function")) { p.m_function = value; return true; } + if (name == QLatin1String("bus")) { p.m_bus = value; return true; } + if (name == QLatin1String("cable")) { p.m_cable = value; return true; } + if (name == QLatin1String("tension_protocol")) { p.m_tension_protocol = value; return true; } + if (name == QLatin1String("conductor_color")) { p.m_wire_color = value; return true; } + if (name == QLatin1String("conductor_section")) { p.m_wire_section = value; return true; } + // The two real colours are QColor, not free text: an unparseable name + // would otherwise be stored as an invalid colour and drawn as black. + if (name == QLatin1String("color") || name == QLatin1String("text_color")) + { + const QColor c(value); + if (!c.isValid()) return false; + if (name == QLatin1String("color")) p.color = c; else p.text_color = c; + return true; + } + return false; +} + +const QStringList &conductorPropertyNames() +{ + static const QStringList names { + QStringLiteral("num"), QStringLiteral("formula"), QStringLiteral("function"), + QStringLiteral("bus"), QStringLiteral("cable"), QStringLiteral("tension_protocol"), + QStringLiteral("conductor_color"), QStringLiteral("conductor_section"), + QStringLiteral("color"), QStringLiteral("text_color")}; + return names; +} + +} // namespace + bool QetScriptApi::setInfoKey(int folioIndex, const QString &elementUuid, const QString &key, const QString &value, const QString &caller) { @@ -630,6 +721,198 @@ bool QetScriptApi::addConductor(int folioIndex, return t1->isLinkedTo(t2); } +/** + @brief QetScriptApi::conductors + One line per conductor on the folio: which terminals it joins and its + number, in the form setConductorProperty() addresses them. Descriptive + rather than structured for the same reason elementTerminals() is -- it + exists so a script, or a person reading its output, can see what is + there before changing it. +*/ +QStringList QetScriptApi::conductors(int folioIndex) const +{ + QStringList list; + if (!m_project) return list; + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return list; + + auto describe = [](Terminal *t) -> QString { + if (!t || !t->parentElement()) return QStringLiteral("?"); + return QStringLiteral("%1 terminal %2") + .arg(t->parentElement()->uuid().toString()) + .arg(t->parentElement()->terminals().indexOf(t)); + }; + + DiagramContent content(diagrams.at(folioIndex), false); + const QList all = content.conductors(DiagramContent::AnyConductor); + for (Conductor *c : all) + { + list << QStringLiteral("%1 -- %2 : num='%3'") + .arg(describe(c->terminal1), describe(c->terminal2), c->properties().text); + } + return list; +} + +QString QetScriptApi::conductorProperty(int folioIndex, const QString &elementUuid, + int terminalIndex, const QString &property) const +{ + // const_cast: findConductor logs, and log() writes to stderr, which is + // not a const operation on this object. The lookup itself changes + // nothing. + auto *self = const_cast(this); + Conductor *conductor = self->findConductor(folioIndex, elementUuid, terminalIndex, + QStringLiteral("conductorProperty")); + if (!conductor) return QString(); + return conductorPropertyValue(conductor->properties(), property); +} + +/** + @brief QetScriptApi::setConductorProperty + Set one property on the conductor attached to a terminal -- and on + every other conductor of the same electrical potential. + + That is not a convenience, it is the rule the application already + follows: SearchAndReplaceWorker does exactly this, pushing one + QPropertyUndoCommand per conductor of relatedPotentialConductors() + inside a single macro, because a wire number, colour or section + describes a potential and not one drawn segment. Setting it on one + conductor and leaving the rest of the potential disagreeing would + produce a file no GUI action could have produced. + @return true if anything was changed, or if it already held that value +*/ +bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUuid, + int terminalIndex, const QString &property, + const QString &value) +{ + if (!m_project) return false; + const QString caller = QStringLiteral("setConductorProperty"); + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.%1: project is read-only").arg(caller)); + return false; + } + if (!conductorPropertyNames().contains(property)) { + log(QStringLiteral("qet.%1: unknown property '%2'; expected one of %3") + .arg(caller, property, conductorPropertyNames().join(QStringLiteral(", ")))); + return false; + } + Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller); + if (!conductor) return false; + + ConductorProperties properties = conductor->properties(); + if (!setConductorPropertyValue(properties, property, value)) { + log(QStringLiteral("qet.%1: '%2' is not a valid value for %3") + .arg(caller, value, property)); + return false; + } + if (properties == conductor->properties()) return true; // already so + + QSet potential = conductor->relatedPotentialConductors(true); + potential << conductor; + + m_project->undoStack()->beginMacro(QObject::tr("Modifier les propriétés du conducteur")); + for (Conductor *c : std::as_const(potential)) + { + QVariant old_value, new_value; + old_value.setValue(c->properties()); + new_value.setValue(properties); + m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value)); + } + m_project->undoStack()->endMacro(); + return true; +} + +QString QetScriptApi::elementLinkType(int folioIndex, const QString &elementUuid) const +{ + Element *element = findElement(folioIndex, elementUuid); + if (!element) return QString(); + switch (element->linkType()) + { + case Element::Simple: return QStringLiteral("simple"); + case Element::NextReport: return QStringLiteral("next_report"); + case Element::PreviousReport: return QStringLiteral("previous_report"); + case Element::Master: return QStringLiteral("master"); + case Element::Slave: return QStringLiteral("slave"); + case Element::Terminale: return QStringLiteral("terminal"); + default: return QStringLiteral("unknown"); + } +} + +QStringList QetScriptApi::linkedElements(int folioIndex, const QString &elementUuid) const +{ + QStringList list; + Element *element = findElement(folioIndex, elementUuid); + if (!element) return list; + const QList linked = element->linkedElements(); + for (Element *e : linked) { + list << e->uuid().toString(); + } + return list; +} + +/** + @brief QetScriptApi::linkElements + Link two elements -- a master to a slave, or one report to its + counterpart. Two folio indices because a master and its slave normally + sit on different folios; that is the usual case, not the exception. + + Whether a given pair may be linked is not decided here. + LinkElementCommand::isLinkable() already holds those rules -- that a + master takes a slave and not another master, that a PLC master pairs + only with a PLC slave, that a next-report pairs only with a + previous-report, and that the target is free -- and asking it rather + than re-deriving them is what keeps a script from producing a link the + GUI would refuse to make. +*/ +bool QetScriptApi::linkElements(int folioIndexA, const QString &elementUuidA, + int folioIndexB, const QString &elementUuidB) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.linkElements: project is read-only")); + return false; + } + Element *a = findElement(folioIndexA, elementUuidA); + Element *b = findElement(folioIndexB, elementUuidB); + if (!a || !b) { + log(QStringLiteral("qet.linkElements: %1 does not resolve to an element") + .arg(a ? elementUuidB : elementUuidA)); + return false; + } + if (a == b) { + log(QStringLiteral("qet.linkElements: an element cannot be linked to itself")); + return false; + } + if (!LinkElementCommand::isLinkable(a, b)) { + log(QStringLiteral("qet.linkElements: %1 (%2) cannot be linked to %3 (%4) -- " + "check the two link types, and that the target is still free") + .arg(elementUuidA, elementLinkType(folioIndexA, elementUuidA), + elementUuidB, elementLinkType(folioIndexB, elementUuidB))); + return false; + } + + auto *cmd = new LinkElementCommand(a); + cmd->setLink(b); + m_project->undoStack()->push(cmd); + return a->linkedElements().contains(b); +} + +bool QetScriptApi::unlinkElement(int folioIndex, const QString &elementUuid) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.unlinkElement: project is read-only")); + return false; + } + Element *element = findElement(folioIndex, elementUuid); + if (!element) return false; + if (element->linkedElements().isEmpty()) return true; // nothing to undo + + auto *cmd = new LinkElementCommand(element); + cmd->unlinkAll(); + m_project->undoStack()->push(cmd); + return element->linkedElements().isEmpty(); +} + int QetScriptApi::addFolio() { if (!m_project) return -1; diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 2544f483f..ef6673772 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -26,6 +26,7 @@ class QETProject; class DiagramView; class Element; class Terminal; +class Conductor; /** @brief The QetScriptApi class @@ -94,6 +95,27 @@ class Terminal; catalog .elmt definition, empty for most of the installed base and, where present, identical across every instance of that element -- so it does not distinguish one placed coil's A1 from another's. + - @b Conductor properties and @b cross-references: set a conductor's + number, formula, colour or section, and link a master to a slave or + one report to another. Both follow the application's own rules rather + than writing the field: a conductor property is applied to every + conductor of the same electrical potential, which is what the GUI and + search-and-replace both do -- a wire number belongs to a potential, + not to one drawn segment -- and a link is refused unless + LinkElementCommand::isLinkable() allows it, which is where the + master/slave, PLC-pairing and report-direction rules already live. + linkElements() takes a folio index for each end because a master and + its slave are usually on different ones. + + A conductor is addressed as "the conductor on terminal i of element + U", not by an identity of its own: conductors have no persisted uuid, + and the folio-scoped integer ids the file uses for their ends are + renumbered on every save, so there is nothing stable to name one by. + Since the change is potential-wide anyway, any terminal of the + potential names it equally well. A terminal carrying more than one + conductor is ambiguous and is refused rather than guessed at -- which + in practice means a potential is addressed from one of its leaf + terminals, not from the hub several conductors meet at. - @b Navigating and @b messaging: select an element, zoom the active view, and show the user a message. Deliberately narrow: selection and messaging work with no view at all (headless `--run`); zoom is a no-op @@ -174,6 +196,21 @@ class QetScriptApi : public QObject const QString &elementUuidA, int terminalIndexA, const QString &elementUuidB, int terminalIndexB); + // -- conductor properties, applied to the whole potential -- + Q_INVOKABLE QStringList conductors(int folioIndex) const; + Q_INVOKABLE QString conductorProperty(int folioIndex, const QString &elementUuid, + int terminalIndex, const QString &property) const; + Q_INVOKABLE bool setConductorProperty(int folioIndex, const QString &elementUuid, + int terminalIndex, const QString &property, + const QString &value); + + // -- cross-references: master/slave and report links -- + Q_INVOKABLE QString elementLinkType(int folioIndex, const QString &elementUuid) const; + Q_INVOKABLE QStringList linkedElements(int folioIndex, const QString &elementUuid) const; + Q_INVOKABLE bool linkElements(int folioIndexA, const QString &elementUuidA, + int folioIndexB, const QString &elementUuidB); + Q_INVOKABLE bool unlinkElement(int folioIndex, const QString &elementUuid); + // -- folios -- Q_INVOKABLE int addFolio(); Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title); @@ -199,6 +236,8 @@ class QetScriptApi : public QObject Element *findElement(int folioIndex, const QString &elementUuid) const; Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex, const QString &caller); + Conductor *findConductor(int folioIndex, const QString &elementUuid, int terminalIndex, + const QString &caller); bool setInfoKey(int folioIndex, const QString &elementUuid, const QString &key, const QString &value, const QString &caller);