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
+58 -5
View File
@@ -25,6 +25,7 @@
class QETProject;
class DiagramView;
class Element;
class Terminal;
/**
@brief The QetScriptApi class
@@ -65,12 +66,34 @@ class Element;
QPropertyUndoCommand merges consecutive commands on the same
object+property when their text() also matches
(QPropertyUndoCommand::mergeWith(), pre-existing), and
setElementPosition()/moveElement() always use the same text for a
given element -- so several position changes to the same element in a
row collapse into one undo step, the same way dragging an element
does, not one step per call. Verified against exactly that: two
setElementPosition()/moveElement()/rotateElement() always use the
same text for a given element -- so several position changes, or
several rotations, of the same element in a row collapse into one
undo step, the same way dragging or repeatedly rotating an element
does, not one step per call. setElementInfo()/setElementLabel()
behave the same way for the same reason, through
ChangeElementInformationCommand::mergeWith(). Verified against exactly that: two
consecutive calls on one element, then undo/undo/redo/redo, land
where a merge predicts, not where two independent steps would.
- @b Wiring, @b labelling and @b folios: create a conductor between two
terminals (ConductorCreator, the same class the GUI's
drag-a-rectangle-over-terminals path uses, so the result inherits an
existing potential's properties and joins conductor auto-numbering),
change an element's label or any other information key
(ChangeElementInformationCommand, which also tells the project
database what changed), add a folio (QETProject::addNewDiagram(),
already undoable) and set its title (ChangeTitleBlockCommand). With
addElement() these are what make a script able to draw rather than
only rearrange: before them a script could place two symbols and had
no way to connect them.
Terminals are addressed by their @b index in Element::terminals(),
not by uuid, and elementTerminals() prints that indexing so a script
can see what it is about to wire. Terminal uuids look like the
obvious key and are not one: 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 -- so
it does not distinguish one placed coil's A1 from another's.
- @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
@@ -87,7 +110,11 @@ class Element;
import-collision case that would otherwise reach
QETProject::importElement()'s own ImportElementDialog::exec() and
refuses instead, rather than let a plain QDialog (not routed through
QetMessageBox) block a script the same way.
QetMessageBox) block a script the same way. addConductor() declines the
same way, for the same reason, when the two terminals belong to two
different existing potentials and ConductorCreator would therefore ask
which one's properties to inherit -- measured: with that check removed,
exactly that call never returns.
*/
class QetScriptApi : public QObject
{
@@ -128,7 +155,29 @@ class QetScriptApi : public QObject
Q_INVOKABLE QString addElement(int folioIndex, const QString &locationPath, double x, double y);
Q_INVOKABLE bool setElementPosition(int folioIndex, const QString &elementUuid, double x, double y);
Q_INVOKABLE bool moveElement(int folioIndex, const QString &elementUuid, double dx, double dy);
Q_INVOKABLE bool rotateElement(int folioIndex, const QString &elementUuid, double angle);
Q_INVOKABLE bool deleteElement(int folioIndex, const QString &elementUuid);
// -- address what is already there --
Q_INVOKABLE QStringList elementUuids(int folioIndex) const;
Q_INVOKABLE QString elementName(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE QStringList elementTerminals(int folioIndex, const QString &elementUuid) const;
// -- element information, through ChangeElementInformationCommand --
Q_INVOKABLE QString elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const;
Q_INVOKABLE bool setElementInfo(int folioIndex, const QString &elementUuid, const QString &key, const QString &value);
Q_INVOKABLE QString elementLabel(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE bool setElementLabel(int folioIndex, const QString &elementUuid, const QString &label);
// -- wire two terminals together --
Q_INVOKABLE bool addConductor(int folioIndex,
const QString &elementUuidA, int terminalIndexA,
const QString &elementUuidB, int terminalIndexB);
// -- folios --
Q_INVOKABLE int addFolio();
Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title);
Q_INVOKABLE bool undo();
Q_INVOKABLE bool redo();
Q_INVOKABLE bool canUndo() const;
@@ -148,6 +197,10 @@ class QetScriptApi : public QObject
private:
bool runFlag(const QString &flag, const QStringList &args);
Element *findElement(int folioIndex, const QString &elementUuid) const;
Terminal *findTerminal(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);
QETProject *m_project;
DiagramView *m_view;