Let a script wire, label and rotate, not only place

The scripting API (bugtracker #162) could place an element and move it,
and could count conductors but not make one. So a script could put a
coil and a motor on a folio and had no way to connect them, which is
most of what drawing is. This adds the missing verbs:

  addConductor()      wire terminal i of one element to terminal j of another
  rotateElement()
  setElementInfo()    any information key
  setElementLabel()   the label key, by name, since it is the one people want
  addFolio()
  setFolioTitle()
  elementUuids()      what is on this folio
  elementName()
  elementTerminals()  which terminal index is which, before wiring it

Each goes through the command the GUI already uses, so a script's edits
undo like manual ones and reach the project database the same way:
ConductorCreator (the drag-a-rectangle-over-terminals path, which is
what makes a new conductor inherit an existing potential's properties
and join auto-numbering), ChangeElementInformationCommand,
QETProject::addNewDiagram(), ChangeTitleBlockCommand. rotateElement()
pushes the same QPropertyUndoCommand on "rotation" that
RotateSelectionCommand pushes for an Element, rather than
RotateSelectionCommand itself, which works on the diagram's selection
and would mean rewriting the user's selection to rotate one element.

Terminals are addressed by index, not uuid. Terminal::uuid() is a
property of the catalog .elmt definition: empty for most of the
installed base, and where present, identical across every instance of
that element -- two coils of the same type placed side by side have
byte-identical terminal uuids, so a uuid cannot say which coil's A1 is
meant. elementTerminals() exists so a script can see the indexing
instead of guessing it.

The one real hazard is that ConductorCreator asks the user which
potential to inherit from when the two terminals sit on two different
existing ones, and it asks with a plain modal QDialog that
QET::QetMessageBox's non-interactive mode does not cover -- so under
headless --run there is nobody to answer and the call never returns.
Measured: with the check removed, that one call hangs until killed;
with it, it declines in 0.4 s. addConductor() therefore refuses that
case, the same way and for the same reason addElement() already refuses
the import-conflict dialog.

To make that check without duplicating the condition, existingPotential()
becomes static over an explicit terminal list and ConductorCreator gains
a public needsPotentialChoice() predicate. Behaviour of the GUI path is
unchanged; setUpPropertieToUse() passes m_terminals_list to the same code
it called before.

Verified headlessly against a copy of examples/ArduinoLCD.qet: new folio
titled, two coils placed, wired, labelled, an info key set and the
element rotated; saved, reloaded, and the conductor, label, title and
rotation (persisted as orientation="1") all read back. Re-saving the
result is byte-identical. qet-lint clean on the generated project;
qet-coherence-check clean on it and on the 24-project example corpus,
and shown to report 9 findings on a deliberately broken copy of the same
file, so the clean result discriminates. Qt 6.10.2, ctest identical to
master (the 61 failures are the vendored KDE ECM suite, present on both).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-21 17:57:29 +12:00
parent c256e2dd1a
commit c740cdf1ac
4 changed files with 339 additions and 12 deletions
+29 -5
View File
@@ -95,6 +95,29 @@ void ConductorCreator::create(Diagram *d, const QPolygonF &polygon)
}
}
/**
@brief ConductorCreator::needsPotentialChoice
Whether creating a potential between these terminals would ask the user
to choose which of several existing potentials to inherit from -- that
is, whether the constructor would reach PotentialSelectorDialog.
This exists for callers with nobody there to answer: the dialog is a
plain QDialog::exec(), not routed through QET::QetMessageBox, so its
non-interactive mode does not cover it and a headless caller would hang
on it indefinitely. Such a caller can check this first and decline.
Exposed here, rather than reimplemented by the caller, so the condition
cannot drift away from the one setUpPropertieToUse() actually applies.
@param terminals_list the terminals a potential would be created between
@return true if the constructor would open the dialog
*/
bool ConductorCreator::needsPotentialChoice(const QList<Terminal *> &terminals_list)
{
if (terminals_list.size() <= 1) {
return false;
}
return existingPotential(terminals_list).size() >= 2;
}
/**
@brief ConductorCreator::propertieToUse
@return true if the caller should proceed with conductor creation,
@@ -104,7 +127,7 @@ void ConductorCreator::create(Diagram *d, const QPolygonF &polygon)
*/
bool ConductorCreator::setUpPropertieToUse()
{
QList<Conductor *> potentials = existingPotential();
QList<Conductor *> potentials = existingPotential(m_terminals_list);
//There is an existing potential
//we get one of them
@@ -145,14 +168,15 @@ bool ConductorCreator::setUpPropertieToUse()
@brief ConductorCreator::existingPotential
Return the list of existing potential of
the terminal list
@param terminals_list the terminals to inspect
@return c_list QList<Conductor *>
*/
QList<Conductor *> ConductorCreator::existingPotential()
QList<Conductor *> ConductorCreator::existingPotential(const QList<Terminal *> &terminals_list)
{
QList<Conductor *> c_list;
QList<Terminal *> t_exclude;
for (Terminal *t : m_terminals_list)
for (Terminal *t : terminals_list)
{
if (t_exclude.contains(t)) {
continue;
@@ -166,9 +190,9 @@ QList<Conductor *> ConductorCreator::existingPotential()
//in the same potential of c, and if true, exclude this terminal from the search.
for (Conductor *c : t->conductors().first()->relatedPotentialConductors(false))
{
if (m_terminals_list.contains(c->terminal1)) {
if (terminals_list.contains(c->terminal1)) {
t_exclude.append(c->terminal1);
} else if (m_terminals_list.contains(c->terminal2)) {
} else if (terminals_list.contains(c->terminal2)) {
t_exclude.append(c->terminal2);
}
}