Compare commits

...

14 Commits

Author SHA1 Message Date
Laurent Trinques c71839a291 Merge pull request #869 from Kellermorph/fix-crossref-textfield
Clear stale xref data from PLC slave elementInformations on unlink and position change
2026-09-16 21:28:10 +02:00
Laurent Trinques d9a2539702 Merge pull request #890 from ispyisail/fix/734-conductor-self-retrace
Fix bugtracker #734: stop routing a bridge conductors don't need
2026-09-16 21:23:39 +02:00
Laurent Trinques 22c1ec9321 Merge pull request #891 from ispyisail/feature/162-js-scripting
Add JavaScript scripting: read, export, edit geometry, undo, navigate
2026-09-16 21:22:01 +02:00
ispyisail 4300a655bc Merge remote-tracking branch 'upstream/master' into feature/162-js-scripting
# Conflicts:
#	sources/qetdiagrameditor.cpp
#	sources/qetdiagrameditor.h
2026-09-17 06:50:35 +12:00
ispyisail 2ed85f6fc7 Address review: qRound-consistent guards, regression test
Addresses scorpio810's review of PR #890 (bugtracker #734):

- The four cas "3"/"4" bridge-skip guards now compare qRound()ed
  coordinates, matching how the bridge coordinate itself is computed --
  an exact != would miss a pair already grid-equal after rounding but
  off by a sub-pixel remainder, and still route a degenerate bridge for
  it. Verified no behavior change on the shipped corpus: per-file
  self-retrace counts are identical before/after (every coordinate
  there already lands exactly on-grid).
- Added tests/qttest/tst_conductorselfretrace.cpp, fixture
  qet_bug_repro_resaved.qet (the report's own canonical reproduction):
  exports it via the built binary's --export-svg and asserts no
  conductor path is self-retracing. Confirmed it actually catches the
  regression, not just passes vacuously -- reverted conductor.cpp to
  master and reran: fails, 1 self-retracing path found.

Not changed in code: the cas "4" descending-branch dead-code note (kept
for symmetry, as already agreed) and the schema_unifilaire_voltaique2.qet
trade-off (flagged for the reviewer's own visual check).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 05:08:14 +12:00
ispyisail e62d43d024 Revert .ts changes, translations will be updated separately
update_translations dropped 3 .ui files' <location> entries this run
(graphicstablepropertieseditor.ui, inditextpropertieswidget.ui,
projectdbmodelpropertieswidget.ui), marking ~38-44 existing translations
per language as vanished -- lrelease drops those, so merging would have
lost real, in-use translations in about twenty languages. Cause not
yet identified.

scorpio810 will run lupdate separately after merge, so the 37 new
scripting strings get picked up without this loss and without
conflicting with translation work in progress.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 04:45:29 +12:00
ispyisail 4a83521306 Fix scripting review findings: import, save, backups, timeout
Addresses scorpio810's review of PR #891:

- addElement() now imports the element into the project's own embedded
  collection first (QETProject::importElement()), same as the
  drag-from-collection-panel path -- otherwise the saved .qet referenced
  a definition outside the project, missing on another machine. Also
  found and fixed while wiring this up: ElementsLocation::setPath()
  forces any path to embed:// once a non-null project is passed, so the
  unconditional ElementsLocation(locationPath, m_project) this method
  used before silently broke every common://custom:// call. Refuses
  on an import-collision case that would otherwise reach
  QETProject::importElement()'s own modal ImportElementDialog, with
  nobody there to answer it in a script.
- addElement()/setElementPosition()/moveElement()/deleteElement() refuse
  on a read-only project, matching their GUI equivalents.
- save() goes through QETProject::write() when no path is given (read
  -only handling, saveddate/savedtime, QSaveFile via writeXmlFile()) and
  QET::writeXmlFile() directly for an explicit output path, instead of a
  plain QFile that skipped all of that.
- Interactive "Run Script...": exports briefly disable project backups
  around the temporary QETProject CLIExport::run() opens on the same
  file, so it doesn't race the real open project's own KAutoSaveFile.
  Restored right after -- the headless --run path is untouched, since it
  depends on backups staying off for the whole run.
- A watchdog thread now calls QJSEngine::setInterrupted() after 30s,
  so a runaway script (`while(true){}`) can't freeze the GUI or hang a
  CI job forever. First version used sleep_for() and blocked every run,
  fast ones included, for the full 30s on join() -- caught by testing a
  one-line script, fixed with wait_for() on a condition variable so a
  script that finishes early wakes the watchdog immediately.
- Interactive script errors now also show a QetMessageBox, not just
  stderr (invisible on Windows).
- Ran update_translations (lupdate) to pick up the 37 strings this
  feature had not yet added to the .ts files.

Not applied: wrapping the whole script run in one undo macro. It would
make the script's own qet.undo()/qet.redo() calls silent no-ops for the
run's duration -- QUndoStack ignores undo()/redo() while a macro is
open -- which would break that already-shipped, explicitly requested
capability to get one convenience Ctrl+Z instead.

Verified: Qt 6.10.2, builds clean, ctest 6/6. Ran each fix against a
real project: addElement() on a common:// path now succeeds and the
saved file embeds the definition (embed://import/...); read-only
project refuses addElement(); save("") and save(otherpath) both
produce a correctly embedded file; `while(true){}` under --run is
interrupted at 30s where it previously hung forever, and a normal
script now exits in ~0.3s instead of blocking for the full timeout
budget.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 22:28:06 +12:00
Kellermorph a7f406cd44 Merge remote-tracking branch 'origin/master' into fix-crossref-textfield 2026-09-16 11:49:55 +02:00
Kellermorph dad6206386 Address review feedback: parentGroup guard, ELMT_XREF constant, indentation
- Add !parentGroup() guard to stale xref removal in updateXref()
  to prevent deleting xref data that ElementTextItemGroup::updateXref()
  just stored for a grouped text item.
- Replace QStringLiteral("xref") with QETInformation::ELMT_XREF
  in linkelementcommand.cpp and masterelement.cpp.
- Fix indentation in plclinkwidget.cpp.
2026-09-16 11:49:49 +02:00
ispyisail 0920e82188 Add geometry editing, undo integration, and navigation to scripting
Extends the scripting surface from the previous commit with exactly
the three things explicitly scoped out there, per follow-up direction:
editing geometry, undo integration, and driving the GUI -- the last
one narrowed to select/zoom/message after discussion, since "invoke
any menu action by name" would let a script trigger a modal
QDialog::exec() with nobody there to dismiss it, the same hang class
investigated for bugtracker #882.

## New capabilities

- addElement/setElementPosition/moveElement/deleteElement, through the
  same undo commands the GUI itself uses: AddGraphicsObjectCommand
  (the same one drag-from-collection-panel placement uses),
  QPropertyUndoCommand on the standard `pos` property, and
  DeleteQGraphicsItemCommand (refuses a non-deletable terminal, same
  as the Delete key).
- undo/redo/canUndo/canRedo against the project's real QUndoStack --
  the same one QETDiagramEditor's Ctrl+Z is wired to via
  undo_group.activeStack(), not a parallel mechanism.
- selectElement/deselectAll (scene state, no view required -- works
  headless), zoomFit/zoomToContent/zoomReset (need the active
  DiagramView, so false headless where there is nothing to zoom), and
  showMessage (a modal QET::QetMessageBox::information -- safe headless
  because non-interactive mode is already on for the whole process
  before any script runs).

## Two real bugs caught by testing this, not assumed away

1. save() was still going through the same reopen-from-disk path as
   every export method: it opened a *second*, unmodified copy of the
   project from its file on disk and rewrote that. addElement() and
   friends operate on the live in-memory project, so nothing they did
   ever reached the saved file -- an element counted correctly in
   memory and then silently vanished from the output. Fixed by having
   save() write m_project->toXml() directly, the only method that
   touches the live instance rather than a fresh copy of the file.

2. A script calling setElementPosition() then moveElement() on the
   same element produced a saved position that didn't match either
   call, and undo/redo didn't step through them independently. Traced
   to QPropertyUndoCommand::mergeWith() (pre-existing, not new):
   consecutive commands on the same object+property merge when their
   text() also matches, and both calls build the identical "Déplacer
   %1" text for a given element -- exactly the same collapsing
   dragging an item repeatedly gets. Not a bug in the new code; a
   wrong assumption in the first test. Re-verified against the
   correct, merge-aware expectation: add -> merged move -> undo (back
   to first position) -> undo (element removed) -> redo (element back)
   -> redo (merged move reapplied) landed at the exact predicted final
   position, read back from the saved XML.

## Verified

Qt6, build clean from a fresh reconfigure, ctest 6/6.

- Headless: addElement returns a real uuid and the count updates;
  select/set-position/move all report correctly; the merge-aware
  undo/redo/save round trip above, confirmed against the saved file's
  actual XML, not just in-memory counters.
- Corpus: the existing read-model smoke script re-run against all 24
  shipped example projects on the fixed binary, 0 failures.
- zoomFit correctly returns false headless (no view to act on),
  confirming the "narrowed GUI-driving" scope holds in code, not just
  in the doc comment.
- GUI: running the add-only script via "Exécuter un script..." marked
  the project [modifié] in the title bar, the same change-tracking
  path a manual edit goes through -- consistent with the undo command
  actually being pushed onto the project's real stack rather than some
  side channel invisible to the rest of the application.

Refs #162.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 20:20:58 +12:00
ispyisail eba258f6cd Add JavaScript scripting: --run and "Run Script..." (bugtracker #162)
Following up on my own comments there: a deliberately small, mostly
read-only scripting surface, exposed to scripts as a single global
`qet` object (QetScriptApi) built on QJSEngine rather than an embedded
Python interpreter -- no new toolchain to package (QJSEngine ships in
every Qt SDK QET already targets, via the Qml module), no GIL, no
version pinning, automatic reflection of the QObject-derived core
classes' own methods with no hand-written binding layer.

## What a script can do

- Read the model: project title, file path, folio count/titles,
  element/conductor counts per folio.
- Trigger the same operations the --export-* CLI flags already do
  (pdf/png/svg/cables/wires/bom/wiring/nets/links/info), plus
  set-titleblock and save -- thin wrappers around CLIExport::run(),
  reusing its already-tested logic rather than duplicating it.

Deliberately NOT in this version: creating or editing diagram
geometry, undo integration, driving the GUI. All explicitly out of
scope per the discussion on #162.

## Two entry points, both built and tested

- `qelectrotech --run script.js project.qet` -- headless/CI.
- Projet > "Exécuter un script..." -- an interactive macro against the
  currently open project. Export/save calls act on the project's file
  on disk (see QetScriptApi's class comment for why), so unsaved GUI
  edits aren't visible to the script; save first if that matters.

## Optional dependency, not a hard requirement

Qt::Qml is probed the same way QtPdf already is in this codebase:
QUIET, non-fatal, behind a QET_HAS_SCRIPTING compile definition. A
build without it compiles and links identically; the CLI flag and
menu action are simply absent (main.cpp) or compile to a clear
"not available" stderr message rather than silently disappearing
(qetscripting.cpp), matching the existing QtPdf pattern rather than
introducing a new one.

One real bug caught building this, not assumed away: my first pass
conditionally excluded the new source files from QET_SRC_FILES behind
`if(QET_HAS_SCRIPTING)` inside qet_compilation_vars.cmake -- but that
file is included before QET_HAS_SCRIPTING is set in the top-level
CMakeLists.txt, so the variable didn't exist yet at that point and the
files were silently never compiled, only caught by an undefined-symbol
link error. Fixed by following the QtPdf file's own precedent:
compile the files unconditionally, guard their Qt::Qml-dependent
content internally instead.

## Verified

Qt6, build clean, ctest 6/6.

- Headless: a script reading project/folio/element/conductor counts,
  calling exportInfo() and exportPdf() against a real project --
  correct JSON, a real single-page PDF confirmed with `file`.
  Error paths: a thrown script exception reports file:line:message and
  exit 1; missing script/project arguments exit 2 (matching
  CLIExport's own usage-error convention); a missing project file is
  reported and does not hang.
- Corpus: the same read-model script run against all 24 shipped
  example projects, 0 failures.
- GUI: "Exécuter un script..." opens a real file dialog filtered to
  *.js, running the picked script against the live open project
  produced the exact expected JSON export file, and the application
  was still fully responsive afterward.

Refs #162.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 19:44:14 +12:00
ispyisail abd5e8978a Fix bugtracker #734: stop routing a bridge conductors don't need
generateConductorPath()'s cas "3"/"4" branches each insert a two-point
bridge along one axis, at a coordinate on the other axis computed as
the midpoint of depart/arrivee and then snapped to the routing grid by
a loop that walks it strictly downward until it divides evenly.

When depart and arrivee already agree on the axis the bridge would run
along, no bridge is needed at all -- the midpoint starts on the
correct, already-shared value. But the snap can still walk it off that
value, or, when it happens to already sit on the grid, the two bridge
points just duplicate depart and arrivee outright. Either way the
conductor renders with an unnecessary out-and-back excursion or a
small looping detour at a join that needed neither.

Fix: skip the bridge in exactly that degenerate case, per branch, on
the axis that branch actually bridges on. descendant and montant are
not mirror images of each other in which axis each cas guards on --
each site says so.

Verified against the report's own canonical reproduction
(qet_bug_repro_resaved.qet from the linked gist): 1 self-retracing
path -> 0. Full corpus of 24 shipped example projects (stored
segments, as shipped, not regenerated): 74 -> 65 self-retracing
conductor paths, zero regressions -- only two files changed, both
improved.

One disclosed trade-off, found while checking for regressions: in
schema_unifilaire_voltaique2.qet, 8 terminal pairs closer together
than twice the extension length (docked stubs already cross before
any bridge is considered) go from an existing small rectangular-loop
artifact to a straight out-and-back retrace covering the full gap --
same count of defective paths (8 -> 8), a different shape, still no
connectivity change either way. Not chased further; a real fix for
that narrower "crossed stubs" case is a separate piece of work.

The shipped affuteuse_250h.qet's own 12 self-retracing paths (verified
by stripping all 185 stored <segment> elements and forcing full
regeneration) are unchanged by this fix -- they are a structurally
different point-count signature (a 4-point back-and-forth reachable
from cas "1"/"2", not the cas "3"/"4" grid-snap bridge this fixes),
consistent with what the issue thread already flagged as a separate,
undiagnosed mechanism.

Refs #734 (own root-cause comment, 2026-08-13).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 17:17:03 +12:00
Kellermorph 9bfe18807f Fix undo not restoring PLC slave fields and stale connections on position change
Two additional bugs found after the initial xref cleanup:

1. Undo after unlink did not restore PLC variables (PLC_TYPE,
   PLC_ADDRESS, etc.) because PlcLinkWidget::on_m_unlink_pb_clicked()
   never stored the current group index in the LinkElementCommand.
   makeLink() only populates PLC fields when group_idx >= 0, so
   the slave was re-linked but appeared empty. Fixed by reading the
   group index from the master via groupIndexForElement() and calling
   setGroupIndex() before unlinking.

2. m_update_slave_Xref_connection was only cleared inside the
   if(m_slave_Xref_item) block in the updateXref() cleanup path.
   For AlignHCenter (Text field) position, no m_slave_Xref_item
   is ever created — the connections are stored in
   m_update_slave_Xref_connection but never cleaned up on unlink.
   On re-link, the old stale entries prevented new connections from
   being established. Fixed by clearing the list unconditionally
   in the cleanup path.
2026-09-14 16:19:37 +02:00
Kellermorph 00e75604b0 Clear stale xref data from PLC slave elementInformations on unlink and position change 2026-09-14 12:04:17 +02:00
18 changed files with 2242 additions and 22 deletions
+16
View File
@@ -109,6 +109,22 @@ if(QT_VERSION_MAJOR GREATER_EQUAL 6)
endif()
endif()
# JavaScript scripting (bugtracker #162: `--run script.js`, and later a
# "Run Script..." menu action) needs QJSEngine, in the Qml module. As with
# QtPdf above, this is not fatal when missing: some minimal Qt6 packagings
# may not ship it, and scripting is optional functionality nothing else in
# the application depends on. When it's missing the feature is silently
# disabled - see the QET_HAS_SCRIPTING guard in qetscripting.*.
set(QET_HAS_SCRIPTING FALSE)
find_package(Qt${QT_VERSION_MAJOR} QUIET COMPONENTS Qml)
if(TARGET Qt${QT_VERSION_MAJOR}::Qml)
set(QET_HAS_SCRIPTING TRUE)
list(APPEND QET_PRIVATE_LIBRARIES Qt::Qml)
add_compile_definitions(QET_HAS_SCRIPTING)
else()
message(STATUS "Qt Qml module not available: JavaScript scripting (--run) disabled")
endif()
find_package(SQLite3 REQUIRED)
# CMake < 4.3 only creates the SQLite::SQLite3 target (no SQLite3::SQLite3
+14
View File
@@ -851,6 +851,20 @@ if(QT_VERSION_MAJOR GREATER_EQUAL 6)
)
endif()
# JavaScript scripting (bugtracker #162). Unconditionally in the source
# list, like the QtPdf files above: this file is included before the
# QET_HAS_SCRIPTING probe runs in the top-level CMakeLists.txt, so the
# variable isn't set yet here. Same pattern as QtPdf: always compiled, the
# actual Qt::Qml dependent code is behind #ifdef QET_HAS_SCRIPTING inside
# qetscripting.cpp/qetscriptapi.cpp themselves, compiling to a harmless
# stub when the module wasn't found.
list(APPEND QET_SRC_FILES
${QET_DIR}/sources/scripting/qetscriptapi.cpp
${QET_DIR}/sources/scripting/qetscriptapi.h
${QET_DIR}/sources/scripting/qetscripting.cpp
${QET_DIR}/sources/scripting/qetscripting.h
)
set(TS_FILES
${QET_DIR}/lang/qet_ar.ts
${QET_DIR}/lang/qet_ca.ts
+14
View File
@@ -16,6 +16,9 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "cli_export.h"
#ifdef QET_HAS_SCRIPTING
#include "scripting/qetscripting.h"
#endif
#include "logging/eventloopwatchdog.h"
#include "logging/qetlogger.h"
#include "machine_info.h"
@@ -135,6 +138,17 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
QET::QetMessageBox::setNonInteractive(true);
return CLIExport::run(export_app.arguments());
}
#ifdef QET_HAS_SCRIPTING
// Headless scripting: --run <script.js> <project.qet> (bugtracker
// #162). Same reasoning as the export branch above for running
// before SingleApplication and answering message boxes headlessly.
if (QetScripting::isRunRequest(raw_args)) {
QApplication script_app(argc, argv);
QETProject::setBackupEnabled(false);
QET::QetMessageBox::setNonInteractive(true);
return QetScripting::run(script_app.arguments());
}
#endif
}
// Resolve the logger's state (log directory, session filename, open
+44
View File
@@ -16,6 +16,9 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "qetdiagrameditor.h"
#ifdef QET_HAS_SCRIPTING
#include "scripting/qetscripting.h"
#endif
#include <QCoreApplication>
#include "ElementsCollection/elementscollectionwidget.h"
#include "QWidgetAnimation/qwidgetanimation.h"
@@ -558,6 +561,15 @@ void QETDiagramEditor::setUpActions()
" sans avoir à fermer et rouvrir le projet (action non annulable)"));
connect(m_reload_element_drawings, &QAction::triggered, this, &QETDiagramEditor::slot_reloadElementDrawings);
#ifdef QET_HAS_SCRIPTING
// Run a JavaScript macro against the current project (bugtracker #162).
m_run_script = new QAction(tr("Exécuter un script..."), this);
m_run_script->setStatusTip(
tr("Exécute un script JavaScript sur le projet courant (voir qet.*"
" dans le script pour l'API disponible)"));
connect(m_run_script, &QAction::triggered, this, &QETDiagramEditor::slot_runScript);
#endif
#ifdef QET_EXPORT_PROJECT_DB
m_export_project_db = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter la base de donnée interne du projet"), this);
connect(m_export_project_db, &QAction::triggered, [this]() {
@@ -1012,6 +1024,9 @@ void QETDiagramEditor::setUpMenu()
menu_project -> addAction(m_project_wiring_list_view);
menu_project -> addAction(m_terminal_numbering);
menu_project -> addAction(m_reload_element_drawings);
#ifdef QET_HAS_SCRIPTING
menu_project -> addAction(m_run_script);
#endif
#ifdef QET_EXPORT_PROJECT_DB
menu_project -> addSeparator();
menu_project -> addAction(m_export_project_db);
@@ -1867,6 +1882,9 @@ void QETDiagramEditor::slot_updateActions()
m_project_wiring_list_view -> setEnabled(opened_project);
m_terminal_numbering -> setEnabled(editable_project);
m_reload_element_drawings -> setEnabled(opened_project);
#ifdef QET_HAS_SCRIPTING
m_run_script -> setEnabled(opened_project);
#endif
#ifdef QET_EXPORT_PROJECT_DB
m_export_project_db -> setEnabled(editable_project);
#endif
@@ -3075,3 +3093,29 @@ void QETDiagramEditor::slot_reloadElementDrawings() {
box.setDetailedText(geometry_changed.join(QLatin1Char('\n')));
box.exec();
}
#ifdef QET_HAS_SCRIPTING
/**
@brief QETDiagramEditor::slot_runScript
Run a JavaScript macro against the current project (bugtracker #162).
See QetScriptApi for what a script can do -- read-mostly: folio/element/
conductor counts and the same export operations the --export-* CLI
flags provide. Export/save calls inside the script act on this
project's file on disk, so unsaved edits in the open editor are not
visible to the script; save first if that matters.
*/
void QETDiagramEditor::slot_runScript() {
QETProject *project = currentProject();
if (!project) return;
const QString script_path = QFileDialog::getOpenFileName(
this,
tr("Exécuter un script"),
QString(),
tr("Scripts JavaScript (*.js);;Tous les fichiers (*)")
);
if (script_path.isEmpty()) return;
QetScripting::runOnProject(script_path, project, currentDiagramView());
}
#endif
+6
View File
@@ -137,6 +137,9 @@ class QETDiagramEditor : public QETMainWindow
void editProjectProperties(QETProject *);
void slot_terminalNumbering();
void slot_reloadElementDrawings();
#ifdef QET_HAS_SCRIPTING
void slot_runScript();
#endif
void editDiagramProperties(DiagramView *);
void editDiagramProperties(Diagram *);
void addDiagramToProject(QETProject *);
@@ -213,6 +216,9 @@ class QETDiagramEditor : public QETMainWindow
*m_project_wiring_list_view, ///< Action to show the wiring list read from the project database
*m_terminal_numbering, ///< Action to launch terminal numbering
*m_reload_element_drawings, ///< Action to redraw every placed element from its current definition
#ifdef QET_HAS_SCRIPTING
*m_run_script, ///< Action to run a JavaScript macro against the current project
#endif
*m_export_project_db, ///Export to file the internal database of the current project
*m_tile_window, ///< Show MDI subwindows as tile
*m_cascade_window, ///< Show MDI subwindows as cascade
+59 -20
View File
@@ -407,20 +407,48 @@ void Conductor::generateConductorPath(const QPointF &p1, Qet::Orientation o1, co
// commence le vrai trajet
// starts the real path
//
// Each cas "3"/"4" below inserts a two-point bridge along one axis, at a
// coordinate on the OTHER axis computed as a midpoint and then snapped
// to the grid by a loop that walks it strictly downward until it
// divides evenly. Whenever depart and arrivee already agree on the axis
// the bridge would run along, that bridge is unneeded -- the midpoint
// starts on the correct value already, but the snap can still walk it
// off, or, if it happens to already be on-grid, the two bridge points
// just duplicate depart and arrivee outright. Either way the result is
// a short out-and-back excursion, or a longer looping detour, at a join
// that needed no bridge in the first place (bugtracker #734). Skip the
// bridge in exactly that case; depart and arrivee already form the
// direct run. Which axis each cas guards on is noted at each site below
// -- descendant and montant are not mirror images of each other here.
if (depart.y() < arrivee.y()) {
// trajet descendant
if ((ori_depart == Qet::North && (ori_arrivee == Qet::South || ori_arrivee == Qet::West)) || (ori_depart == Qet::East && ori_arrivee == Qet::West)) {
// cas "3"
int ligne_inter_x = qRound(depart.x() + arrivee.x()) / 2;
while (ligne_inter_x % Diagram::xGrid) -- ligne_inter_x;
points << QPointF(ligne_inter_x, depart.y());
points << QPointF(ligne_inter_x, arrivee.y());
// cas "3": bridge is vertical, at a shared x between depart.y()
// and arrivee.y() -- unnecessary exactly when depart and
// arrivee already share an x. Compared qRound()ed, the same
// rounding the bridge coordinate itself is computed with below:
// an exact != would miss a pair that's already grid-equal after
// rounding but off by a sub-pixel remainder, and still route a
// degenerate bridge for it.
if (qRound(depart.x()) != qRound(arrivee.x())) {
int ligne_inter_x = qRound(depart.x() + arrivee.x()) / 2;
while (ligne_inter_x % Diagram::xGrid) -- ligne_inter_x;
points << QPointF(ligne_inter_x, depart.y());
points << QPointF(ligne_inter_x, arrivee.y());
}
} else if ((ori_depart == Qet::South && (ori_arrivee == Qet::North || ori_arrivee == Qet::East)) || (ori_depart == Qet::West && ori_arrivee == Qet::East)) {
// cas "4"
int ligne_inter_y = qRound(depart.y() + arrivee.y()) / 2;
while (ligne_inter_y % Diagram::yGrid) -- ligne_inter_y;
points << QPointF(depart.x(), ligne_inter_y);
points << QPointF(arrivee.x(), ligne_inter_y);
// cas "4": bridge is horizontal, at a shared y between
// depart.x() and arrivee.x() -- unnecessary exactly when depart
// and arrivee already share a y. (Always true in this branch,
// since "descendant" requires depart.y() < arrivee.y() strictly
// -- kept for symmetry with the "montant" branch below.)
if (qRound(depart.y()) != qRound(arrivee.y())) {
int ligne_inter_y = qRound(depart.y() + arrivee.y()) / 2;
while (ligne_inter_y % Diagram::yGrid) -- ligne_inter_y;
points << QPointF(depart.x(), ligne_inter_y);
points << QPointF(arrivee.x(), ligne_inter_y);
}
} else if ((ori_depart == Qet::North || ori_depart == Qet::East) && (ori_arrivee == Qet::North || ori_arrivee == Qet::East)) {
points << QPointF(arrivee.x(), depart.y()); // cas "2"
} else {
@@ -429,17 +457,28 @@ void Conductor::generateConductorPath(const QPointF &p1, Qet::Orientation o1, co
} else {
// trajet montant
if ((ori_depart == Qet::West && (ori_arrivee == Qet::East || ori_arrivee == Qet::South)) || (ori_depart == Qet::North && ori_arrivee == Qet::South)) {
// cas "3"
int ligne_inter_y = qRound(depart.y() + arrivee.y()) / 2;
while (ligne_inter_y % Diagram::yGrid) -- ligne_inter_y;
points << QPointF(depart.x(), ligne_inter_y);
points << QPointF(arrivee.x(), ligne_inter_y);
// cas "3": bridge is horizontal, at a shared y between
// depart.x() and arrivee.x() -- unnecessary exactly when depart
// and arrivee already share a y (the West->East case
// diagnosed for #734: two stubs extended onto the same y run
// straight into each other, no bridge needed). Compared
// qRound()ed, same as the other three guards.
if (qRound(depart.y()) != qRound(arrivee.y())) {
int ligne_inter_y = qRound(depart.y() + arrivee.y()) / 2;
while (ligne_inter_y % Diagram::yGrid) -- ligne_inter_y;
points << QPointF(depart.x(), ligne_inter_y);
points << QPointF(arrivee.x(), ligne_inter_y);
}
} else if ((ori_depart == Qet::East && (ori_arrivee == Qet::West || ori_arrivee == Qet::North)) || (ori_depart == Qet::South && ori_arrivee == Qet::North)) {
// cas "4"
int ligne_inter_x = qRound(depart.x() + arrivee.x()) / 2;
while (ligne_inter_x % Diagram::xGrid) -- ligne_inter_x;
points << QPointF(ligne_inter_x, depart.y());
points << QPointF(ligne_inter_x, arrivee.y());
// cas "4": bridge is vertical, at a shared x between depart.y()
// and arrivee.y() -- unnecessary exactly when depart and
// arrivee already share an x.
if (qRound(depart.x()) != qRound(arrivee.x())) {
int ligne_inter_x = qRound(depart.x() + arrivee.x()) / 2;
while (ligne_inter_x % Diagram::xGrid) -- ligne_inter_x;
points << QPointF(ligne_inter_x, depart.y());
points << QPointF(ligne_inter_x, arrivee.y());
}
} else if ((ori_depart == Qet::West || ori_depart == Qet::North) && (ori_arrivee == Qet::West || ori_arrivee == Qet::North)) {
points << QPointF(depart.x(), arrivee.y()); // cas "2"
} else {
@@ -764,6 +764,8 @@ QVariant DynamicElementTextItem::itemChange(QGraphicsItem::GraphicsItemChange ch
if(m_parent_element.data()->linkType() == Element::Slave)
{
connect(m_parent_element.data(), &Element::linkedElementChanged, this, &DynamicElementTextItem::masterChanged);
if(m_parent_element.data()->diagram())
connect(m_parent_element.data()->diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref);
//The parent is already linked, wa call master changed for init the connection
if(!m_parent_element.data()->linkedElements().isEmpty())
masterChanged();
@@ -1468,11 +1470,12 @@ void DynamicElementTextItem::updateXref()
m_Xref_item = nullptr;
}
m_update_slave_Xref_connection.clear();
if(m_slave_Xref_item)
{
delete m_slave_Xref_item;
m_slave_Xref_item = nullptr;
m_update_slave_Xref_connection.clear();
//If position changed to Champ de texte, store xref in element info
if(m_parent_element->linkType() == Element::Slave &&
@@ -1497,6 +1500,19 @@ void DynamicElementTextItem::updateXref()
}
}
}
//Remove stale "xref" from elementInformations when no longer needed
if(m_parent_element->linkType() == Element::Slave &&
m_text_from == ElementInfo && m_info_name == "xref" &&
!parentGroup())
{
DiagramContext dc = m_parent_element->elementInformations();
if(!dc.value("xref").toString().isEmpty())
{
dc.remove("xref");
m_parent_element->setElementInformations(dc);
}
}
}
void DynamicElementTextItem::setPlainText(const QString &text)
@@ -59,6 +59,11 @@ ElementTextItemGroup::ElementTextItemGroup(const QString &name,
&Element::linkedElementChanged,
this,
&ElementTextItemGroup::updateXref);
if(parent->diagram())
connect(parent->diagram()->project(),
&QETProject::XRefPropertiesChanged,
this,
&ElementTextItemGroup::updateXref);
}
ElementTextItemGroup::~ElementTextItemGroup()
@@ -868,11 +873,12 @@ void ElementTextItemGroup::updateXref()
delete m_Xref_item;
m_Xref_item = nullptr;
}
m_update_slave_Xref_connection.clear();
if(m_slave_Xref_item)
{
delete m_slave_Xref_item;
m_slave_Xref_item = nullptr;
m_update_slave_Xref_connection.clear();
//If position changed to Champ de texte, store xref in element info
if(m_parent_element->linkType() == Element::Slave &&
@@ -898,6 +904,25 @@ void ElementTextItemGroup::updateXref()
}
}
}
//Remove stale "xref" from elementInformations when no longer needed
if(m_parent_element->linkType() == Element::Slave &&
m_parent_element->diagram())
{
for(DynamicElementTextItem *deti : texts())
{
if(deti->textFrom() == DynamicElementTextItem::ElementInfo && deti->infoName() == "xref")
{
DiagramContext dc = m_parent_element->elementInformations();
if(!dc.value("xref").toString().isEmpty())
{
dc.remove("xref");
m_parent_element->setElementInformations(dc);
}
break;
}
}
}
}
void ElementTextItemGroup::adjustSlaveXrefPos(int slave_offset)
@@ -111,6 +111,7 @@ void MasterElement::unlinkElement(Element *elmt)
ctx.remove(QETInformation::ELMT_PLC_COMMENT);
ctx.remove(QETInformation::ELMT_PLC_CROSSREF);
ctx.remove(QETInformation::ELMT_LABEL);
ctx.remove(QETInformation::ELMT_XREF);
elmt->setElementInformations(ctx);
setGroupIndexForElement(elmt, -1);
+499
View File
@@ -0,0 +1,499 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "qetscriptapi.h"
#include "../ElementsCollection/elementslocation.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../cli_export.h"
#include "../diagram.h"
#include "../diagramcontent.h"
#include "../diagramview.h"
#include "../factory/elementfactory.h"
#include "../qet.h"
#include "../qetgraphicsitem/element.h"
#include "../qetmessagebox.h"
#include "../qetproject.h"
#include "../qetresult.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include "../undocommand/deleteqgraphicsitemcommand.h"
#include <QTextStream>
#include <QUndoCommand>
QetScriptApi::QetScriptApi(QETProject *project, DiagramView *view, QObject *parent) :
QObject(parent),
m_project(project),
m_view(view)
{
}
QString QetScriptApi::projectTitle() const
{
return m_project ? m_project->title() : QString();
}
QString QetScriptApi::filePath() const
{
return m_project ? m_project->filePath() : QString();
}
int QetScriptApi::folioCount() const
{
return m_project ? m_project->diagrams().count() : 0;
}
QString QetScriptApi::folioTitle(int index) const
{
if (!m_project) return QString();
const QList<Diagram *> diagrams = m_project->diagrams();
if (index < 0 || index >= diagrams.count()) return QString();
return diagrams.at(index)->title();
}
int QetScriptApi::elementCount(int folioIndex) const
{
if (!m_project) return 0;
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return 0;
DiagramContent content(diagrams.at(folioIndex), false);
return content.m_elements.count();
}
int QetScriptApi::conductorCount(int folioIndex) const
{
if (!m_project) return 0;
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return 0;
DiagramContent content(diagrams.at(folioIndex), false);
return content.conductors(DiagramContent::AnyConductor).count();
}
/**
@brief QetScriptApi::runFlag
Build a CLIExport::run() argument list from this project's own file
path plus @p args, and run it. Reopens the project from disk -- see the
class comment for why that trade-off was made.
@param flag one of the --export-* / --resave / --set-titleblock flags
@param args the flag's own positional arguments (output path, etc.)
@return true if CLIExport::run() returned 0 (success)
*/
bool QetScriptApi::runFlag(const QString &flag, const QStringList &args)
{
if (!m_project) {
log(QStringLiteral("qet.%1: no project").arg(flag));
return false;
}
const QString path = m_project->filePath();
if (path.isEmpty()) {
log(QStringLiteral("qet.%1: project has no file path -- save it first").arg(flag));
return false;
}
QStringList full_args;
full_args << flag << path << args;
if (m_view)
{
// Interactive "Run Script...": CLIExport::run() opens a second,
// temporary QETProject on the same file the GUI already has open.
// Backups are only disabled on the headless --run path (main.cpp);
// here that second, short-lived project would otherwise manage its
// own KAutoSaveFile for the same path as the user's real, already
// open project, racing it and risking a stale-restore prompt next
// launch. Disable backups for just this one export and restore
// them right after -- the headless path must never see this
// change, since it depends on backups staying off for the whole
// run (see the crash note next to the other setBackupEnabled(false)
// call in main.cpp).
QETProject::setBackupEnabled(false);
const int result = CLIExport::run(full_args);
QETProject::setBackupEnabled(true);
return result == 0;
}
return CLIExport::run(full_args) == 0;
}
bool QetScriptApi::exportPdf(const QString &output, bool showTerminals)
{
QStringList args{output};
if (showTerminals) args << QStringLiteral("--show-terminals");
return runFlag(QStringLiteral("--export-pdf"), args);
}
bool QetScriptApi::exportPng(const QString &outDir, bool showTerminals)
{
QStringList args{outDir};
if (showTerminals) args << QStringLiteral("--show-terminals");
return runFlag(QStringLiteral("--export-png"), args);
}
bool QetScriptApi::exportSvg(const QString &outDir, bool showTerminals)
{
QStringList args{outDir};
if (showTerminals) args << QStringLiteral("--show-terminals");
return runFlag(QStringLiteral("--export-svg"), args);
}
bool QetScriptApi::exportCables(const QString &output)
{
return runFlag(QStringLiteral("--export-cables"), {output});
}
bool QetScriptApi::exportWires(const QString &output)
{
return runFlag(QStringLiteral("--export-wires"), {output});
}
bool QetScriptApi::exportBom(const QString &output)
{
return runFlag(QStringLiteral("--export-bom"), {output});
}
bool QetScriptApi::exportWiring(const QString &output)
{
return runFlag(QStringLiteral("--export-wiring"), {output});
}
bool QetScriptApi::exportNets(const QString &output)
{
return runFlag(QStringLiteral("--export-nets"), {output});
}
bool QetScriptApi::exportLinks(const QString &output)
{
return runFlag(QStringLiteral("--export-links"), {output});
}
bool QetScriptApi::exportInfo(const QString &output)
{
return runFlag(QStringLiteral("--info"), output.isEmpty() ? QStringList{} : QStringList{output});
}
bool QetScriptApi::setTitleBlock(const QString &output, const QStringList &assignments)
{
QStringList args{output};
args << assignments;
return runFlag(QStringLiteral("--set-titleblock"), args);
}
/**
@brief QetScriptApi::save
Write this project's own current in-memory state -- unlike every export
method above, this does NOT go through runFlag()/CLIExport::run(): that
reopens the project fresh from its file on disk, which would never see
any addElement()/setElementPosition()/moveElement()/deleteElement() this
script made, only silently rewrite the file exactly as it already was.
Caught by testing this against a script that added an element and
called save(): the element counted correctly in memory but the saved
file didn't have it, since the old implementation opened an unrelated,
unmodified second copy of the project to write.
With no output path, this goes through QETProject::write() -- the same
path "Enregistrer" uses -- so it honours read-only mode the same way,
updates saveddate/savedtime, and clears the modified flag. A plain
QFile write used to skip all of that and could tear the file on an
interruption, where write() goes through QET::writeXmlFile()'s
QSaveFile. With an explicit output path this is a save to a different
file, so it goes through QET::writeXmlFile() directly without touching
the project's own filePath() or read-only state, the same way "Save As"
targeting a writable location is allowed even for a project opened
read-only.
@param output path to write to; the project's own file path if empty
@return false if there is no output path to use, or the write failed
*/
bool QetScriptApi::save(const QString &output)
{
if (!m_project) return false;
if (output.isEmpty())
{
if (m_project->filePath().isEmpty()) {
log(QStringLiteral("qet.save: no output path given and the project has none of its own -- pass one"));
return false;
}
const QETResult result = m_project->write();
if (!result.isOk()) {
log(QStringLiteral("qet.save: %1").arg(result.errorMessage()));
return false;
}
return true;
}
QDomDocument xml_doc(m_project->toXml());
QString error_message;
if (!QET::writeXmlFile(xml_doc, output, &error_message)) {
log(QStringLiteral("qet.save: %1").arg(error_message));
return false;
}
return true;
}
void QetScriptApi::log(const QString &message)
{
QTextStream(stderr) << message << "\n";
}
/**
@brief QetScriptApi::findElement
@param folioIndex
@param elementUuid as returned by addElement(), or read from the
element's own uuid attribute
@return the matching element on that folio, or nullptr
*/
Element *QetScriptApi::findElement(int folioIndex, const QString &elementUuid) const
{
if (!m_project) return nullptr;
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return nullptr;
DiagramContent content(diagrams.at(folioIndex), false);
for (Element *elmt : std::as_const(content.m_elements)) {
if (elmt->uuid().toString() == elementUuid)
return elmt;
}
return nullptr;
}
/**
@brief QetScriptApi::addElement
Place a new element on a folio, through the same AddGraphicsObjectCommand
the interactive drag-from-collection-panel path uses (see
DiagramEventAddElement::addElement()) -- so Ctrl+Z undoes it exactly as
it would a manually dropped element.
Like that path, the element is first imported into the project's own
embedded collection (QETProject::importElement()): building straight
from a common://custom:// location without embedding it left the saved
.qet referencing a definition outside the project, missing on any
machine that doesn't have that same collection installed.
importElement() can, on a name collision with a different, already
embedded element, pop a modal dialog asking the user to choose -- with
nobody there to answer it in a script, headless or interactive, that is
exactly the hang class this whole API is built to avoid (see the class
comment). Detected and refused before it can happen, rather than risked.
@param folioIndex
@param locationPath an element collection path, e.g.
"embed://some/path.elmt" or "common://10_electric/...elmt"
@param x @param y target position, in the diagram's own coordinates
@return the new element's uuid (empty string on failure -- read-only
project, bad folio index, an import collision, or the location could
not be resolved/built)
*/
QString QetScriptApi::addElement(int folioIndex, const QString &locationPath, double x, double y)
{
if (!m_project) return QString();
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.addElement: project is read-only"));
return QString();
}
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
Diagram *diagram = diagrams.at(folioIndex);
// ElementsLocation::setPath() forces ANY path to embed:// as soon as a
// non-null project is given, common://custom:// included -- passing
// m_project unconditionally here (as the pre-fix code did) silently
// turned every non-embed:: locationPath into a lookup for an embedded
// element that was never there. Caught by actually running this
// against a common:// path: "does not resolve to an element" even
// though the file plainly exists.
ElementsLocation location = locationPath.startsWith(QStringLiteral("embed://"))
? ElementsLocation(locationPath, m_project)
: ElementsLocation(locationPath);
if (!location.isElement() || !location.exist()) {
log(QStringLiteral("qet.addElement: '%1' does not resolve to an element").arg(locationPath));
return QString();
}
ElementsLocation import_location = location;
if (!(location.isProject() && location.project() == m_project))
{
const QString import_path = location.isFileSystem()
? QStringLiteral("import/") + location.collectionPath(false)
: location.collectionPath(false);
const ElementsLocation existing(import_path, m_project);
if (existing.exist() && existing.uuid() != location.uuid()) {
log(QStringLiteral("qet.addElement: '%1' would collide with a different element "
"already embedded under the same name -- refusing rather than "
"risk the interactive import-conflict dialog").arg(locationPath));
return QString();
}
import_location = m_project->importElement(location);
if (!import_location.exist()) {
log(QStringLiteral("qet.addElement: could not import '%1' into the project").arg(locationPath));
return QString();
}
}
int state = 0;
Element *element = ElementFactory::Instance()->createElement(import_location, nullptr, &state);
if (state) {
delete element;
log(QStringLiteral("qet.addElement: could not build element from '%1'").arg(locationPath));
return QString();
}
const QPointF pos(x, y);
element->setPos(pos);
diagram->addItem(element);
auto *undo_group = new QUndoCommand(QObject::tr("Ajouter %1").arg(element->name()));
new AddGraphicsObjectCommand(element, diagram, pos, undo_group);
diagram->undoStack().push(undo_group);
return element->uuid().toString();
}
bool QetScriptApi::setElementPosition(int folioIndex, const QString &elementUuid, double x, double y)
{
if (m_project && m_project->isReadOnly()) {
log(QStringLiteral("qet.setElementPosition: project is read-only"));
return false;
}
Element *element = findElement(folioIndex, elementUuid);
if (!element) return false;
const QVariant old_value = element->pos();
const QVariant new_value = QPointF(x, y);
if (old_value == new_value) return true; // already there; nothing to push
auto *cmd = new QPropertyUndoCommand(element, "pos", old_value, new_value);
cmd->setText(QObject::tr("Déplacer %1").arg(element->name()));
m_project->undoStack()->push(cmd);
return true;
}
bool QetScriptApi::moveElement(int folioIndex, const QString &elementUuid, double dx, double dy)
{
Element *element = findElement(folioIndex, elementUuid);
if (!element) return false;
const QPointF p = element->pos();
return setElementPosition(folioIndex, elementUuid, p.x() + dx, p.y() + dy);
}
/**
@brief QetScriptApi::deleteElement
Through DeleteQGraphicsItemCommand -- the same command the Delete key
uses -- so any conductors attached to the element's terminals are
cleaned up the same way, not left dangling.
*/
bool QetScriptApi::deleteElement(int folioIndex, const QString &elementUuid)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.deleteElement: project is read-only"));
return false;
}
Element *element = findElement(folioIndex, elementUuid);
if (!element) return false;
Diagram *diagram = m_project->diagrams().at(folioIndex);
DiagramContent content;
content.m_elements << element;
if (DeleteQGraphicsItemCommand::hasNonDeletableTerminal(content)) {
log(QStringLiteral("qet.deleteElement: %1 has a non-deletable terminal (linked master/slave?), refusing").arg(elementUuid));
return false;
}
auto *cmd = new DeleteQGraphicsItemCommand(diagram, content);
diagram->undoStack().push(cmd);
return true;
}
bool QetScriptApi::undo()
{
if (!m_project || !m_project->undoStack()->canUndo()) return false;
m_project->undoStack()->undo();
return true;
}
bool QetScriptApi::redo()
{
if (!m_project || !m_project->undoStack()->canRedo()) return false;
m_project->undoStack()->redo();
return true;
}
bool QetScriptApi::canUndo() const
{
return m_project && m_project->undoStack()->canUndo();
}
bool QetScriptApi::canRedo() const
{
return m_project && m_project->undoStack()->canRedo();
}
bool QetScriptApi::selectElement(const QString &elementUuid)
{
if (!m_project) return false;
for (Diagram *diagram : m_project->diagrams()) {
DiagramContent content(diagram, false);
for (Element *elmt : std::as_const(content.m_elements)) {
if (elmt->uuid().toString() == elementUuid) {
elmt->setSelected(true);
return true;
}
}
}
return false;
}
void QetScriptApi::deselectAll(int folioIndex)
{
if (!m_project) return;
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return;
diagrams.at(folioIndex)->clearSelection();
}
bool QetScriptApi::zoomFit()
{
if (!m_view) return false;
m_view->zoomFit();
return true;
}
bool QetScriptApi::zoomToContent()
{
if (!m_view) return false;
m_view->zoomContent();
return true;
}
bool QetScriptApi::zoomReset()
{
if (!m_view) return false;
m_view->zoomReset();
return true;
}
/**
@brief QetScriptApi::showMessage
A modal QET::QetMessageBox::information() -- safe to call headless
because non-interactive mode is already set for the whole process
before any script runs (main.cpp), which is exactly the mechanism
that keeps --export-* and --resave from hanging on a warning dialog.
Interactively, a real person is there to see and dismiss it.
*/
void QetScriptApi::showMessage(const QString &text)
{
QET::QetMessageBox::information(nullptr, QObject::tr("Script"), text);
}
+156
View File
@@ -0,0 +1,156 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef QET_SCRIPT_API_H
#define QET_SCRIPT_API_H
#include <QObject>
#include <QString>
#include <QStringList>
class QETProject;
class DiagramView;
class Element;
/**
@brief The QetScriptApi class
The object a script sees as `qet` (bugtracker #162): batch/CI work and
human-written macros against an open project.
@b Scope. Three groups of capability, each added at a different point in
the discussion and each drawing its own line:
- @b Reading the model and @b exporting: folio/element/conductor counts,
and the same export operations the `--export-*` CLI flags provide.
Every export method is a thin wrapper around CLIExport::run() -- the
same, already-tested code path those flags use -- built from the
project's own file path rather than the live instance. That keeps this
file free of any dependency on cli_export.cpp's internals, at the cost
of re-opening the project from disk per call, which means @b none of
them ever see edits this script made with the methods below: only
save() writes the live instance. A script that edits then exports
@b must call save() first, or the export reflects the file as it was
before the script ran. Verified the hard way: an early version of this
file had save() go through the same reopen-from-disk path as the
exports, so it silently wrote back the unmodified original -- an
addElement() call counted correctly in memory and then vanished from
the saved file.
- @b Editing geometry, through the same undo commands the GUI itself
uses (AddGraphicsObjectCommand for placement, QPropertyUndoCommand on
the standard `pos` property for moves, DeleteQGraphicsItemCommand for
removal) -- Ctrl+Z undoes a script's edits exactly as it would the
equivalent manual ones, because they are, mechanically, the same
commands on the same stack. All four refuse on a read-only project,
same as their GUI equivalents check Diagram::isReadOnly() before
editing. addElement() also imports the element into the project's
own embedded collection first (QETProject::importElement()), same
as the drag-from-collection-panel path -- without it, the saved
file referenced a definition outside the project and went missing
on a machine without that same collection installed. One
consequence worth knowing, not a bug:
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
consecutive calls on one element, then undo/undo/redo/redo, land
where a merge predicts, not where two independent steps would.
- @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
returning false without one, since there is nothing to zoom.
Explicitly @b not in scope: driving arbitrary GUI actions or dialogs. A
script that could invoke any QAction by name could just as easily
trigger one that opens a modal QDialog::exec() with nobody there to
dismiss it -- exactly the hang class investigated for bugtracker #882.
Every method here is either non-blocking by construction or, for
messages, safe under QET::QetMessageBox's existing non-interactive mode
(already active for headless runs). Nothing here opens a dialog the
caller has to wait on -- including addElement(), which detects an
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.
*/
class QetScriptApi : public QObject
{
Q_OBJECT
public:
/**
@param project the project this API acts on
@param view the active DiagramView, when run interactively via
"Run Script..."; nullptr for the headless --run entry point.
Only the zoom methods use it -- everything else works either way.
*/
explicit QetScriptApi(QETProject *project, DiagramView *view = nullptr, QObject *parent = nullptr);
// -- read the model --
Q_INVOKABLE QString projectTitle() const;
Q_INVOKABLE QString filePath() const;
Q_INVOKABLE int folioCount() const;
Q_INVOKABLE QString folioTitle(int index) const;
Q_INVOKABLE int elementCount(int folioIndex) const;
Q_INVOKABLE int conductorCount(int folioIndex) const;
// -- export / save: thin wrappers around the --export-* CLI paths --
Q_INVOKABLE bool exportPdf(const QString &output, bool showTerminals = false);
Q_INVOKABLE bool exportPng(const QString &outDir, bool showTerminals = false);
Q_INVOKABLE bool exportSvg(const QString &outDir, bool showTerminals = false);
Q_INVOKABLE bool exportCables(const QString &output);
Q_INVOKABLE bool exportWires(const QString &output);
Q_INVOKABLE bool exportBom(const QString &output);
Q_INVOKABLE bool exportWiring(const QString &output);
Q_INVOKABLE bool exportNets(const QString &output);
Q_INVOKABLE bool exportLinks(const QString &output);
Q_INVOKABLE bool exportInfo(const QString &output);
Q_INVOKABLE bool setTitleBlock(const QString &output, const QStringList &assignments);
Q_INVOKABLE bool save(const QString &output);
// -- edit geometry, through the real undo commands --
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 deleteElement(int folioIndex, const QString &elementUuid);
Q_INVOKABLE bool undo();
Q_INVOKABLE bool redo();
Q_INVOKABLE bool canUndo() const;
Q_INVOKABLE bool canRedo() const;
// -- navigate and message --
Q_INVOKABLE bool selectElement(const QString &elementUuid);
Q_INVOKABLE void deselectAll(int folioIndex);
Q_INVOKABLE bool zoomFit();
Q_INVOKABLE bool zoomToContent();
Q_INVOKABLE bool zoomReset();
Q_INVOKABLE void showMessage(const QString &text);
// -- logging: a script has no console of its own --
Q_INVOKABLE void log(const QString &message);
private:
bool runFlag(const QString &flag, const QStringList &args);
Element *findElement(int folioIndex, const QString &elementUuid) const;
QETProject *m_project;
DiagramView *m_view;
};
#endif // QET_SCRIPT_API_H
+174
View File
@@ -0,0 +1,174 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "qetscripting.h"
#include "qetscriptapi.h"
#include "../qetmessagebox.h"
#include "../qetproject.h"
#include <QFile>
#include <QFileInfo>
#include <QObject>
#include <QTextStream>
#ifdef QET_HAS_SCRIPTING
#include <QJSEngine>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
#endif
namespace {
QTextStream out(stdout);
QTextStream err(stderr);
}
namespace QetScripting {
bool isRunRequest(const QStringList &args)
{
return args.contains(QStringLiteral("--run"));
}
#ifdef QET_HAS_SCRIPTING
int run(const QStringList &args)
{
const int idx = args.indexOf(QStringLiteral("--run"));
const QString script_path = args.value(idx + 1);
const QString project_path = args.value(idx + 2);
if (script_path.isEmpty() || project_path.isEmpty()) {
err << "Usage: qelectrotech --run <script.js> <project.qet>\n";
return 2;
}
if (!QFileInfo::exists(script_path)) {
err << "Script not found: " << script_path << "\n";
return 2;
}
if (!QFileInfo::exists(project_path)) {
err << "Project not found: " << project_path << "\n";
return 2;
}
QETProject project(project_path);
if (project.state() != QETProject::Ok) {
err << "Failed to open project: " << project_path
<< " (state " << project.state() << ")\n";
return 1;
}
return runOnProject(script_path, &project, nullptr) ? 0 : 1;
}
namespace {
// A runaway script (an infinite loop, or just a very slow one) would
// otherwise freeze the GUI forever, or hang a CI job running --run with
// no way out. QJSEngine::setInterrupted() is documented callable from
// another thread; the engine polls it during evaluation and returns an
// error QJSValue, which the normal error-reporting path below already
// handles.
constexpr int kScriptTimeoutMs = 30000;
}
bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view)
{
QFile file(scriptPath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
err << "Cannot open script: " << scriptPath << "\n";
return false;
}
const QString source = QString::fromUtf8(file.readAll());
file.close();
QJSEngine engine;
auto *api = new QetScriptApi(project, view, &engine);
QJSValue qet_value = engine.newQObject(api);
// newQObject() takes ownership by default (QJSEngine::JavaScriptOwnership),
// which would delete api as soon as the engine's GC decides to -- api's
// real owner is the engine itself via the parent-child relationship set
// above, so keep the engine, not the GC, in charge of its lifetime.
engine.setObjectOwnership(api, QJSEngine::CppOwnership);
engine.globalObject().setProperty(QStringLiteral("qet"), qet_value);
// wait_for(), not sleep_for(): a script that finishes well inside the
// timeout must let the watchdog thread wake immediately, not force
// every run -- including a fast, successful one -- to block on join()
// for the full budget. Caught by testing this against a one-line
// script: it took the full 30 seconds to exit before this fix.
bool finished = false;
std::mutex mtx;
std::condition_variable cv;
std::thread watchdog([&]() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait_for(lock, std::chrono::milliseconds(kScriptTimeoutMs), [&finished]{ return finished; });
if (!finished) {
engine.setInterrupted(true);
}
});
QJSValue result = engine.evaluate(source, scriptPath);
{
std::lock_guard<std::mutex> lock(mtx);
finished = true;
}
cv.notify_one();
watchdog.join();
if (result.isError()) {
const QString message = QStringLiteral("Script error: %1:%2: %3")
.arg(scriptPath)
.arg(result.property(QStringLiteral("lineNumber")).toInt())
.arg(result.toString());
err << message << "\n";
// Interactive "Run Script...": stderr is invisible to a user who
// launched the GUI normally (nowhere on Windows, easy to miss
// everywhere else). Headless --run has no GUI to show this in, and
// no session for it to block.
if (view) {
QET::QetMessageBox::critical(nullptr, QObject::tr("Script"), message);
}
return false;
}
return true;
}
#else // !QET_HAS_SCRIPTING
// Qt::Qml was not found at configure time (see the QET_HAS_SCRIPTING probe
// in the top-level CMakeLists.txt). Compile to a clear, non-silent failure
// rather than omitting these symbols -- the same "always compiled, the
// disabled path says why" shape as the QtPdf feature guard.
int run(const QStringList &)
{
err << "This build of QElectroTech was compiled without the Qt Qml "
"module, so JavaScript scripting (--run) is not available.\n";
return 1;
}
bool runOnProject(const QString &, QETProject *, DiagramView *)
{
err << "This build of QElectroTech was compiled without the Qt Qml "
"module, so JavaScript scripting is not available.\n";
return false;
}
#endif // QET_HAS_SCRIPTING
}
+65
View File
@@ -0,0 +1,65 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef QET_SCRIPTING_H
#define QET_SCRIPTING_H
#include <QStringList>
class QETProject;
class DiagramView;
/**
@brief JavaScript scripting entry points (bugtracker #162).
A script sees a single global, `qet` (see QetScriptApi): reading the
model, exporting, editing geometry through the real undo commands, and
a narrow set of navigation/messaging calls. See QetScriptApi's class
comment for the exact scope and why each group of capability stops
where it does.
*/
namespace QetScripting {
/**
@brief True if @p args is a `--run <script.js> <project.qet>`
invocation.
*/
bool isRunRequest(const QStringList &args);
/**
@brief Run the script named in @p args against the project also
named there, headless.
Usage: qelectrotech --run <script.js> <project.qet>
@return process exit code: 0 on success, 1 if the project failed to
open or the script threw, 2 on a usage error.
*/
int run(const QStringList &args);
/**
@brief Run @p scriptPath against an already-open @p project (the
"Run Script..." GUI macro path). Errors go to stderr; there is no
modal reporting in this first version.
@param view the active DiagramView, so the script's zoom methods
have something to act on; nullptr from the headless entry point,
where they become no-ops (see QetScriptApi).
@return true if the script ran without throwing.
*/
bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view = nullptr);
}
#endif // QET_SCRIPTING_H
+14
View File
@@ -393,6 +393,20 @@ void PlcLinkWidget::on_m_unlink_pb_clicked()
return;
LinkElementCommand *undo = new LinkElementCommand(m_element);
// Store the current group index so makeLink() can
// restore PLC variables and terminal labels on undo.
for (Element *elmt : m_element->linkedElements())
{
if (elmt->linkType() == Element::Master)
{
int idx = elmt->groupIndexForElement(m_element);
if (idx >= 0)
undo->setGroupIndex(idx);
break;
}
}
undo->unlinkAll();
m_element->diagram()->undoStack().push(undo);
@@ -254,6 +254,7 @@ void LinkElementCommand::undo()
ctx.remove(QETInformation::ELMT_PLC_COMMENT);
ctx.remove(QETInformation::ELMT_PLC_CROSSREF);
ctx.remove(QETInformation::ELMT_LABEL);
ctx.remove(QETInformation::ELMT_XREF);
m_element->setElementInformations(ctx);
foreach(Element *elmt, m_element->linkedElements())
@@ -291,6 +292,7 @@ void LinkElementCommand::undo()
ctx.remove(QETInformation::ELMT_PLC_COMMENT);
ctx.remove(QETInformation::ELMT_PLC_CROSSREF);
ctx.remove(QETInformation::ELMT_LABEL);
ctx.remove(QETInformation::ELMT_XREF);
slave->setElementInformations(ctx);
}
@@ -622,6 +624,7 @@ void LinkElementCommand::makeLink(const QList<Element *> &element_list)
ctx.remove(QETInformation::ELMT_PLC_COMMENT);
ctx.remove(QETInformation::ELMT_PLC_CROSSREF);
ctx.remove(QETInformation::ELMT_LABEL);
ctx.remove(QETInformation::ELMT_XREF);
slave->setElementInformations(ctx);
}
m_element->unlinkElement(elmt);
+17
View File
@@ -129,3 +129,20 @@ add_executable(
add_test(NAME tst_menubarkeyboard COMMAND tst_menubarkeyboard)
target_include_directories(tst_menubarkeyboard PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_menubarkeyboard PRIVATE Qt::Test Qt::Widgets)
# Bugtracker #734 regression: exports fixtures/qet_bug_repro_resaved.qet
# (the report's own canonical reproduction) via the built qelectrotech
# binary's --export-svg and checks no conductor path is self-retracing.
# Runs the real binary rather than linking conductor.cpp directly, since
# Conductor::generateConductorPath() pulls in the full graphics-item
# dependency graph (Diagram, Element, Terminal...) that the other,
# deliberately header-only tests above avoid.
add_executable(
tst_conductorselfretrace
tst_conductorselfretrace.cpp)
add_test(NAME tst_conductorselfretrace COMMAND tst_conductorselfretrace)
add_dependencies(tst_conductorselfretrace qelectrotech)
target_include_directories(tst_conductorselfretrace PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_conductorselfretrace PRIVATE Qt::Test)
target_compile_definitions(tst_conductorselfretrace PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
@@ -0,0 +1,999 @@
<project title="ENA006 E-101 QET TRIAL V2 - PILOT ARTIFACT" version="0.100.1">
<titleblocktemplates>
<titleblocktemplate name="ENA006_D">
<information>ENA006 SLD probe title block generated.</information>
<logos/>
<grid cols="t5%;t9%;t44%;t6%;t12%;t12%;t6%;t6%;" rows="26;26;26;40;34;34;">
<field row="0" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="3" valign="center">
<value>
<translation lang="en">Z</translation>
</value>
<label>
<translation lang="en">REV</translation>
</label>
</field>
<field row="0" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="4" valign="center">
<value>
<translation lang="en">2026-08-11</translation>
</value>
<label/>
</field>
<field row="0" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="5" valign="center">
<value>
<translation lang="en">E-110: G4 CLOSED - LEVITON B3277-YB SIDE-MOUNT SPD + 3P 20 A CB-SPD</translation>
</value>
<label/>
</field>
<field row="0" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="6" valign="center">
<value>
<translation lang="en">CC</translation>
</value>
<label>
<translation lang="en">BY</translation>
</label>
</field>
<field row="0" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="7" valign="center">
<value>
<translation lang="en">PRELIMINARY</translation>
</value>
<label/>
</field>
<field row="0" fontsize="9" hadjust="true" displaylabel="true" name="" align="left" col="2" valign="center">
<value>
<translation lang="en">NOT FOR CONSTRUCTION</translation>
</value>
<label/>
</field>
<field row="1" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="3" valign="center">
<value>
<translation lang="en">Y</translation>
</value>
<label>
<translation lang="en">REV</translation>
</label>
</field>
<field row="1" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="4" valign="center">
<value>
<translation lang="en">2026-08-11</translation>
</value>
<label/>
</field>
<field row="1" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="5" valign="center">
<value>
<translation lang="en">BREAKER AUX/POSITION DELETED - s60 GOVERNS; G6 CLOSED-DEL; A-5 RETIRED</translation>
</value>
<label/>
</field>
<field row="1" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="6" valign="center">
<value>
<translation lang="en">CC</translation>
</value>
<label>
<translation lang="en">BY</translation>
</label>
</field>
<field row="1" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="7" valign="center">
<value/>
<label/>
</field>
<field row="1" fontsize="9" hadjust="true" displaylabel="true" name="" align="left" col="2" valign="center">
<value/>
<label/>
</field>
<field row="2" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="3" valign="center">
<value>
<translation lang="en">X</translation>
</value>
<label>
<translation lang="en">REV</translation>
</label>
</field>
<field row="2" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="4" valign="center">
<value>
<translation lang="en">2026-08-11</translation>
</value>
<label/>
</field>
<field row="2" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="5" valign="center">
<value>
<translation lang="en">COUNCIL REVIEW - WRAP/REFS/T3/TEE; KEY-2 PAWL; G14 ACCESS; H-OTD</translation>
</value>
<label/>
</field>
<field row="2" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="6" valign="center">
<value>
<translation lang="en">CC</translation>
</value>
<label>
<translation lang="en">BY</translation>
</label>
</field>
<field row="2" fontsize="8" hadjust="true" displaylabel="true" name="" align="left" col="7" valign="center">
<value/>
<label/>
</field>
<field row="2" fontsize="9" hadjust="true" displaylabel="true" name="" align="left" col="2" valign="center">
<value/>
<label/>
</field>
<field row="3" fontsize="12" hadjust="true" displaylabel="true" name="" align="left" col="2" valign="center">
<value>
<translation lang="en">MEDIUM VOLTAGE POWER ELECTRONICS LABORATORY UNIVERSITY OF CALGARY, SCHULICH SCHOOL OF ENGINEERING</translation>
</value>
<label/>
</field>
<field row="3" fontsize="10" hadjust="true" displaylabel="true" name="" align="left" col="4" valign="center">
<value>
<translation lang="en">%plant</translation>
</value>
<label>
<translation lang="en">PROJECT NO.</translation>
</label>
</field>
<field row="3" fontsize="10" hadjust="true" displaylabel="true" name="" align="left" col="5" valign="center">
<value>
<translation lang="en">%locmach</translation>
</value>
<label>
<translation lang="en">DRAWING NO.</translation>
</label>
</field>
<field row="3" fontsize="10" hadjust="true" displaylabel="true" name="" align="left" col="6" valign="center">
<value>
<translation lang="en">%indexrev</translation>
</value>
<label>
<translation lang="en">REV</translation>
</label>
</field>
<field row="3" fontsize="10" hadjust="true" displaylabel="true" name="" align="left" col="7" valign="center">
<value>
<translation lang="en">%folio</translation>
</value>
<label>
<translation lang="en">SHEET</translation>
</label>
</field>
<field row="4" fontsize="11" hadjust="true" displaylabel="true" name="" align="left" col="2" valign="center">
<value>
<translation lang="en">%title</translation>
</value>
<label>
<translation lang="en">TITLE</translation>
</label>
</field>
<field row="4" fontsize="9" hadjust="true" displaylabel="true" name="" align="left" col="4" valign="center">
<value>
<translation lang="en">%author</translation>
</value>
<label>
<translation lang="en">DRAWN</translation>
</label>
</field>
<field row="4" fontsize="9" hadjust="true" displaylabel="true" name="" align="left" col="5" valign="center">
<value>
<translation lang="en">%date</translation>
</value>
<label>
<translation lang="en">DATE</translation>
</label>
</field>
<field row="4" fontsize="9" hadjust="true" displaylabel="true" name="" align="left" col="6" valign="center">
<value>
<translation lang="en">NTS</translation>
</value>
<label>
<translation lang="en">SCALE</translation>
</label>
</field>
<field row="4" fontsize="9" hadjust="true" displaylabel="true" name="" align="left" col="7" valign="center">
<value>
<translation lang="en">D</translation>
</value>
<label>
<translation lang="en">SIZE</translation>
</label>
</field>
<field row="5" fontsize="11" hadjust="true" displaylabel="true" name="" align="left" col="2" valign="center">
<value>
<translation lang="en">ENA006 MV TEST FACILITY ENG A 006</translation>
</value>
<label>
<translation lang="en">PROJECT</translation>
</label>
</field>
<field row="5" fontsize="9" hadjust="true" displaylabel="true" name="" align="left" col="4" valign="center">
<value>
<translation lang="en">PILOT ARTIFACT</translation>
</value>
<label/>
</field>
<field row="5" fontsize="9" hadjust="true" displaylabel="true" name="" align="left" col="5" valign="center">
<value>
<translation lang="en">NOT A DELIVERABLE</translation>
</value>
<label/>
</field>
</grid>
</titleblocktemplate>
</titleblocktemplates>
<properties>
<property name="saveddate" show="1">8/12/2026</property>
<property name="saveddate-eu" show="1">12-08-2026</property>
<property name="saveddate-us" show="1">2026-08-12</property>
<property name="savedfilename" show="1">qet_bug_repro</property>
<property name="savedfilepath" show="1">C:/Users/phili/.claude/skills/cabinet-drawings/pilot_qet/out/qet_bug_repro.qet</property>
<property name="savedtime" show="1">19:09</property>
</properties>
<usage enabled="true" time_spent="0"/>
<newdiagrams>
<border cols="17" rows="8" displaycols="true" rowsize="80" colsize="60" displayrows="true"/>
<inset date="null" displayAt="bottom" title="" version="" locmach="" plant="" auto_page_num="" folio="%id/%total" filename="" author="" indexrev=""/>
<conductors text_color="#000000" bus="" type="multi" conductor_section="" color2="#000000" num="_" cable="" formula="" condsize="1" conductor_color="" displaytext="1" tension_protocol="" vertirotatetext="270" bicolor="false" numsize="7" function="" horizrotatetext="0" horizontal-alignment="AlignBottom" onetextperfolio="1" vertical-alignment="AlignRight" dash-size="1"/>
<report label="%f-%l%c"/>
<xrefs>
<xref offset="0" powerprefix="" type="commutator" slave_label="(%f-%l%c)" xrefpos="" delayprefix="" switchprefix="" showterminalname="true" master_label="%f-%l%c" displayhas="cross" showpowerctc="false" snapto="label"/>
<xref offset="0" powerprefix="" type="plc" slave_label="(%f-%l%c)" xrefpos="" delayprefix="" switchprefix="" showterminalname="true" master_label="%f-%l%c" displayhas="cross" showpowerctc="true" snapto="label"/>
<xref offset="0" powerprefix="" type="coil" slave_label="(%f-%l%c)" xrefpos="AlignBottom" delayprefix="" switchprefix="" showterminalname="true" master_label="%f-%l%c" displayhas="contacts" showpowerctc="false" snapto="bottom"/>
<xref offset="0" powerprefix="" type="protection" slave_label="(%f-%l%c)" xrefpos="AlignBottom" delayprefix="" switchprefix="" showterminalname="true" master_label="%f-%l%c" displayhas="contacts" showpowerctc="false" snapto="label"/>
</xrefs>
<conductors_autonums freeze_new_conductors="false" auto_break_conductors="false" current_autonum=""/>
<folio_autonums/>
<element_autonums freeze_new_elements="false" current_autonum=""/>
<guides/>
</newdiagrams>
<diagram freezeNewConductor="false" locmach="ENA006-E-101" titleblocktemplate="ENA006_D" cols="8" version="0.100.1-dev" colsize="405.333" freezeNewElement="false" title="SINGLE LINE DIAGRAM - QET TRIAL V2 LAYOUT ENGINE (PILOT ARTIFACT)" displaycols="true" rowsize="317.444" plant="ENA006" height="1924.66" order="1" displayAt="bottom" folio="%id OF %total" displayrows="true" indexrev="TRIAL2" rows="6" auto_page_num="" titleblocktemplateCollection="embedded" author="CC" date="null" filename="">
<defaultconductor text_color="#000000" bus="" type="multi" conductor_section="" color2="#000000" num="_" cable="" formula="" condsize="1" conductor_color="" displaytext="1" tension_protocol="" vertirotatetext="270" bicolor="false" numsize="7" function="" horizrotatetext="0" horizontal-alignment="AlignBottom" onetextperfolio="0" vertical-alignment="AlignRight" dash-size="1"/>
<elements>
<element orientation="0" freezeLabel="false" type="embed://import/probe/v2_busstub_n.elmt" z="10" uuid="{5f92eb56-c4f7-58b0-9bab-214799bb6e7e}" prefix="" y="513" x="353">
<terminals>
<terminal orientation="0" y="-12" x="0" id="0"/>
<terminal orientation="3" y="-5" x="4" id="1"/>
<terminal orientation="1" y="-5" x="-4" id="2"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{a29619d6-a146-5d01-bcde-c4a6a81b57b1}" y="-30.9" rotation="0" Valignment="AlignTop" x="9.3" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,75,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>PANEL A - 480Y/277 V, 3PH, 4W, 225 A BUS</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{7c64eaac-f664-5ae1-a721-928c337105fc}" y="7.5" rotation="0" Valignment="AlignTop" x="9.3" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,7.5,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>175 A MAIN</text>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element orientation="0" freezeLabel="false" type="embed://import/probe/v2_busstub_n.elmt" z="10" uuid="{dc1e8bdf-ce98-5f8e-8e72-8cbe654d0062}" prefix="" y="1131" x="652">
<terminals>
<terminal orientation="0" y="-12" x="0" id="3"/>
<terminal orientation="3" y="-5" x="4" id="4"/>
<terminal orientation="1" y="-5" x="-4" id="5"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{56a90a13-8ed9-568b-83b7-27fc276992f0}" y="-30.4" rotation="0" Valignment="AlignTop" x="609.1" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,75,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>TRANSFER BUS - 200 A, 3W + PE [CABINET C]</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{7f9d5ad8-a301-5d77-8768-2df60792821e}" y="8" rotation="0" Valignment="AlignTop" x="9.9" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,7.5,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>160 A CONT. TEST ENVELOPE</text>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element orientation="0" freezeLabel="false" type="embed://import/probe/v2_transformer_2w_primary-delta_secondary-wye_g.elmt" z="10" uuid="{3e82a25a-b2e8-554e-b726-14279f81ca38}" prefix="" y="383" x="353">
<terminals>
<terminal orientation="0" y="-28.4" x="0" id="6"/>
<terminal orientation="2" y="28.4" x="0" id="7"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{cceb7d8e-68a5-5609-a4cf-a327123cf7e3}" y="-29" rotation="0" Valignment="AlignTop" x="-79.7" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,75,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>T-MAIN</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{3d038d23-8843-5c24-8a54-e887be8e6492}" y="-16.1" rotation="0" Valignment="AlignTop" x="-159.9" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>112.5 kVA DRY-TYPE</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{fa675a16-6eee-5576-b864-0dc6d79724f8}" y="-3.1" rotation="0" Valignment="AlignTop" x="-147" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>600 V / 480Y-277 V</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{c08d8155-1d1f-5a40-99e0-dd62e15a57c7}" y="9.8" rotation="0" Valignment="AlignTop" x="-178.2" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>SEPARATELY DERIVED</text>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element orientation="0" freezeLabel="false" type="embed://import/probe/v2_circuit_breaker.elmt" z="10" uuid="{d723fac8-9c4d-50d5-9faf-f288341bac82}" prefix="" y="469" x="353">
<terminals>
<terminal orientation="0" y="-17.6" x="0" id="8"/>
<terminal orientation="2" y="17.6" x="0" id="9"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{612c44b6-fb6d-5a30-a2e5-5c90600c18d5}" y="-22.2" rotation="0" Valignment="AlignTop" x="-91.2" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,75,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>CB-MAIN</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{88aca51f-ce9b-5487-97fb-d8f4d8296ba5}" y="-9.2" rotation="0" Valignment="AlignTop" x="-107.8" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>175 A I-LINE</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{2575990d-7f9a-5b3e-bce3-7afed6143305}" y="3.8" rotation="0" Valignment="AlignTop" x="-89.9" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>MAIN [A]</text>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element orientation="0" freezeLabel="false" type="embed://import/probe/v2_fuse.elmt" z="10" uuid="{38720887-d879-5b09-aa98-919009b0a730}" prefix="" y="886" x="652">
<terminals>
<terminal orientation="0" y="-17.6" x="0" id="10"/>
<terminal orientation="2" y="17.6" x="0" id="11"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{e377eab9-5ef0-54ac-906e-6779ea5d4ea6}" y="-22.3" rotation="0" Valignment="AlignTop" x="-73" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,75,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>F-VAR</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{07378af4-d36b-5eee-af3d-40786c613279}" y="-9.3" rotation="0" Valignment="AlignTop" x="-116.4" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>40 A OUTPUT</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{231acebe-5726-58e5-887d-cc9e8b84ebaf}" y="3.6" rotation="0" Valignment="AlignTop" x="-100.1" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>FUSES [B]</text>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element orientation="0" freezeLabel="false" type="embed://import/probe/v2_tee_l.elmt" z="10" uuid="{7955ded1-b35b-5c90-b974-c18cb71d64c6}" prefix="" y="949" x="652">
<terminals>
<terminal orientation="0" y="-6" x="0" id="12"/>
<terminal orientation="3" y="0" x="-6" id="13"/>
<terminal orientation="2" y="6" x="0" id="14"/>
</terminals>
<inputs/>
<dynamic_texts/>
<texts_groups/>
</element>
<element orientation="0" freezeLabel="false" type="embed://import/probe/v2_box_h-8_text-UV_w-10_tape.elmt" z="10" uuid="{c4bf7ba7-98c7-5102-95ee-1f13f9cb4a05}" prefix="" y="949" x="620">
<terminals>
<terminal orientation="1" y="0" x="-4" id="15"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{b334fd8e-f0ee-5bb4-b252-0084b3768b85}" y="-10.5" rotation="0" Valignment="AlignTop" x="-30.3" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,7.5,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>UV</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{518e1dec-7575-5833-ad0e-557efd1d34af}" y="-28" rotation="0" Valignment="AlignTop" x="-84.4" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,75,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>R-UV</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{2e342bb8-ac55-5c62-9ebf-dd90208e1437}" y="-15.1" rotation="0" Valignment="AlignTop" x="-160.5" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>3x RM35UA13MW</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{f2e9069b-e9e4-5ca3-9312-eb3449397d5f}" y="-2.1" rotation="0" Valignment="AlignTop" x="-176.1" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>WIPER-UV L-L AT T3</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{edea9c4d-09de-5ccf-a93d-e3774155b95c}" y="10.8" rotation="0" Valignment="AlignTop" x="-160.1" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>FUSE LOAD SIDE</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{c0cd4483-629b-52b6-9e87-d9baa0122c4e}" y="23.8" rotation="0" Valignment="AlignTop" x="-142.3" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>(E-111/E-112-2)</text>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element orientation="0" freezeLabel="false" type="embed://import/probe/v2_overload.elmt" z="10" uuid="{4e742e7e-3ea9-5554-8d60-7a7e4b271b5d}" prefix="" y="1012" x="652">
<terminals>
<terminal orientation="0" y="-17.6" x="0" id="16"/>
<terminal orientation="2" y="17.6" x="0" id="17"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{9f36bc48-0f43-5daa-bb6e-3eac08431eaa}" y="-8.9" rotation="0" Valignment="AlignTop" x="-9.9" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,5.25,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>OL</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{50e7912e-5bf6-5db2-8ad3-bee08580c145}" y="-22.3" rotation="0" Valignment="AlignTop" x="-75.4" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,75,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>OL-BR</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{358d6c87-6730-51c8-9245-3e11b7aafe62}" y="-9.3" rotation="0" Valignment="AlignTop" x="-138.2" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>3RR2443 BRUSH</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{bed2dfb3-889f-5c3c-89f3-a06d90047855}" y="3.6" rotation="0" Valignment="AlignTop" x="-118.6" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>MONITOR [C]</text>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element orientation="0" freezeLabel="false" type="embed://import/probe/v2_no_contact.elmt" z="10" uuid="{15e1997c-4eb6-50df-8629-22efc1524f2f}" prefix="" y="1088" x="652">
<terminals>
<terminal orientation="0" y="-17.6" x="0" id="18"/>
<terminal orientation="2" y="17.6" x="0" id="19"/>
</terminals>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{5b2149d4-31b6-5078-b88c-0f35725a2ae8}" y="-22.7" rotation="0" Valignment="AlignTop" x="-62.7" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,75,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>Kvar</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{4f702d8c-dba1-59d9-a882-79e637c82963}" y="-9.7" rotation="0" Valignment="AlignTop" x="-90.5" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>LC1D50A</text>
</dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" uuid="{2f7647d6-0815-56bc-9a1c-9dee9987bec2}" y="3.2" rotation="0" Valignment="AlignTop" x="-121.4" frame="false" text_width="-1" rotation_point_center="false" text_from="UserText" font="Arial,9.75,-1,5,50,0,0,0,0,0,Regular" keep_visual_rotation="true">
<text>+ MIRROR [C]</text>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
<conductors>
<conductor text_color="#000000" terminalname2="top" freezeLabel="false" terminalname1="bottom" bus="" type="multi" element2_label="" conductor_section="" element2="{d723fac8-9c4d-50d5-9faf-f288341bac82}" color2="#000000" element1="{3e82a25a-b2e8-554e-b726-14279f81ca38}" num="" x="0" cable="" element2_name="v2_circuit_breaker" formula="" conductor_color="" condsize="1" element1_name="v2_transformer_2w_primary-delta_secondary-wye_g" displaytext="0" tension_protocol="" vertirotatetext="270" bicolor="false" numsize="7" function="" horizrotatetext="0" element1_label="" horizontal-alignment="AlignBottom" onetextperfolio="0" terminal1="{1a09bcec-0e22-5e69-a82d-85f20fd2944c}" terminal2="{67a1679e-e95b-581e-a26e-b47ce16e1ca2}" vertical-alignment="AlignRight" dash-size="1" y="0">
<sequentialNumbers/>
</conductor>
<conductor text_color="#000000" terminalname2="tap" freezeLabel="false" terminalname1="bottom" bus="" type="multi" element2_label="" conductor_section="" element2="{5f92eb56-c4f7-58b0-9bab-214799bb6e7e}" color2="#000000" element1="{d723fac8-9c4d-50d5-9faf-f288341bac82}" num="" x="0" cable="" element2_name="v2_busstub_n" formula="" conductor_color="" condsize="1" element1_name="v2_circuit_breaker" displaytext="0" tension_protocol="" vertirotatetext="270" bicolor="false" numsize="7" function="" horizrotatetext="0" element1_label="" horizontal-alignment="AlignBottom" onetextperfolio="0" terminal1="{b238d746-e1b0-5bc0-bdf6-690650239460}" terminal2="{515aaabf-a848-5922-8e0e-30b9442a6004}" vertical-alignment="AlignRight" dash-size="1" y="0">
<sequentialNumbers/>
</conductor>
<conductor text_color="#000000" terminalname2="t" freezeLabel="false" terminalname1="bottom" bus="" type="multi" element2_label="" conductor_section="" element2="{7955ded1-b35b-5c90-b974-c18cb71d64c6}" color2="#000000" element1="{38720887-d879-5b09-aa98-919009b0a730}" num="" x="0" cable="" element2_name="v2_tee_l" formula="" conductor_color="" condsize="1" element1_name="v2_fuse" displaytext="0" tension_protocol="" vertirotatetext="270" bicolor="false" numsize="7" function="" horizrotatetext="0" element1_label="" horizontal-alignment="AlignBottom" onetextperfolio="0" terminal1="{64651853-d240-5db8-88d9-6adb7340a161}" terminal2="{93dbb9de-6acb-50b2-b2bc-8dc78b7bd469}" vertical-alignment="AlignRight" dash-size="1" y="0">
<sequentialNumbers/>
</conductor>
<conductor text_color="#000000" terminalname2="top" freezeLabel="false" terminalname1="l" bus="" type="multi" element2_label="" conductor_section="" element2="{c4bf7ba7-98c7-5102-95ee-1f13f9cb4a05}" color2="#000000" element1="{7955ded1-b35b-5c90-b974-c18cb71d64c6}" num="" x="0" cable="" element2_name="v2_box_h-8_text-UV_w-10_tape" formula="" conductor_color="" condsize="1" element1_name="v2_tee_l" displaytext="0" tension_protocol="" vertirotatetext="270" bicolor="false" numsize="7" function="" horizrotatetext="0" element1_label="" horizontal-alignment="AlignBottom" onetextperfolio="0" terminal1="{be1f5b40-75e4-5797-a01a-0782e7979dde}" terminal2="{42c69da2-a77c-5fcf-92b4-88ae9fbc950b}" vertical-alignment="AlignRight" dash-size="1" y="0">
<sequentialNumbers/>
</conductor>
<conductor text_color="#000000" terminalname2="top" freezeLabel="false" terminalname1="b" bus="" type="multi" element2_label="" conductor_section="" element2="{4e742e7e-3ea9-5554-8d60-7a7e4b271b5d}" color2="#000000" element1="{7955ded1-b35b-5c90-b974-c18cb71d64c6}" num="" x="0" cable="" element2_name="v2_overload" formula="" conductor_color="" condsize="1" element1_name="v2_tee_l" displaytext="0" tension_protocol="" vertirotatetext="270" bicolor="false" numsize="7" function="" horizrotatetext="0" element1_label="" horizontal-alignment="AlignBottom" onetextperfolio="0" terminal1="{9d70e09e-c3a2-536f-876a-98bc14839f3d}" terminal2="{d548c004-98ea-5594-9621-ca07f35a11e9}" vertical-alignment="AlignRight" dash-size="1" y="0">
<sequentialNumbers/>
</conductor>
<conductor text_color="#000000" terminalname2="top" freezeLabel="false" terminalname1="bottom" bus="" type="multi" element2_label="" conductor_section="" element2="{15e1997c-4eb6-50df-8629-22efc1524f2f}" color2="#000000" element1="{4e742e7e-3ea9-5554-8d60-7a7e4b271b5d}" num="" x="0" cable="" element2_name="v2_no_contact" formula="" conductor_color="" condsize="1" element1_name="v2_overload" displaytext="0" tension_protocol="" vertirotatetext="270" bicolor="false" numsize="7" function="" horizrotatetext="0" element1_label="" horizontal-alignment="AlignBottom" onetextperfolio="0" terminal1="{b6645803-91cb-5b4e-8323-c6ebc88e20a7}" terminal2="{1b663411-841b-50df-94b6-25953716cb9f}" vertical-alignment="AlignRight" dash-size="1" y="0">
<sequentialNumbers/>
</conductor>
<conductor text_color="#000000" terminalname2="tap" freezeLabel="false" terminalname1="bottom" bus="" type="multi" element2_label="" conductor_section="" element2="{dc1e8bdf-ce98-5f8e-8e72-8cbe654d0062}" color2="#000000" element1="{15e1997c-4eb6-50df-8629-22efc1524f2f}" num="" x="0" cable="" element2_name="v2_busstub_n" formula="" conductor_color="" condsize="1" element1_name="v2_no_contact" displaytext="0" tension_protocol="" vertirotatetext="270" bicolor="false" numsize="7" function="" horizrotatetext="0" element1_label="" horizontal-alignment="AlignBottom" onetextperfolio="0" terminal1="{c9545907-50fe-500b-a6c7-7e0cd3936950}" terminal2="{515aaabf-a848-5922-8e0e-30b9442a6004}" vertical-alignment="AlignRight" dash-size="1" y="0">
<sequentialNumbers/>
</conductor>
</conductors>
</diagram>
<collection>
<category name="import">
<category name="probe">
<element name="v2_busstub_n.elmt">
<definition width="30" hotspot_y="21" version="0.100.0" type="element" link_type="simple" hotspot_x="15" height="42">
<uuid uuid="{df96c31a-3d4c-54eb-93b7-1acf2aef049e}"/>
<names>
<name lang="en">v2_busstub_n</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0" y2="0" x1="0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-16" length1="1.5" length2="1.5"/>
<circle diameter="6" style="line-style:normal;line-weight:normal;filling:black;color:black" antialias="true" y="-3" x="-3"/>
<terminal orientation="n" type="Generic" uuid="{515aaabf-a848-5922-8e0e-30b9442a6004}" name="tap" y="-16" x="0"/>
<terminal orientation="w" type="Generic" uuid="{311e59cb-6c46-5771-adad-8a92536c7bc3}" name="l" y="-5" x="0"/>
<terminal orientation="e" type="Generic" uuid="{a7f2014d-5dfa-531b-af05-b852a70fbbb1}" name="r" y="-5" x="0"/>
</description>
</definition>
</element>
<element name="v2_busstub_s.elmt">
<definition width="30" hotspot_y="21" version="0.100.0" type="element" link_type="simple" hotspot_x="15" height="42">
<uuid uuid="{912d25d2-aebf-5ca0-bf50-9369ce8450d0}"/>
<names>
<name lang="en">v2_busstub_s</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0" y2="0" x1="0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="16" length1="1.5" length2="1.5"/>
<circle diameter="6" style="line-style:normal;line-weight:normal;filling:black;color:black" antialias="true" y="-3" x="-3"/>
<terminal orientation="s" type="Generic" uuid="{c857ddef-ca9e-592f-8e78-2e3d9bb81eb2}" name="tap" y="16" x="0"/>
<terminal orientation="w" type="Generic" uuid="{824c3447-6b41-5e1a-ad28-152e9d929e32}" name="l" y="-5" x="0"/>
<terminal orientation="e" type="Generic" uuid="{b0922151-dd45-5869-8ac9-c3e6be1627b3}" name="r" y="-5" x="0"/>
</description>
</definition>
</element>
<element name="v2_utility.elmt">
<definition width="22" hotspot_y="34" version="0.100.0" type="element" link_type="simple" hotspot_x="11" height="39">
<uuid uuid="{ebc54f89-5896-5c12-82a1-3c7da232dd83}"/>
<names>
<name lang="en">v2_utility</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="-10.1" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-28.8" length1="1.5" length2="1.5"/>
<polygon x3="5.8" x4="0.0" x2="-5.8" y2="-10.1" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="0.0" antialias="true" closed="true" y1="0.0" y3="-10.1"/>
<terminal orientation="n" type="Generic" uuid="{d5db23a8-edae-5913-878b-fb6774275a6b}" name="top" y="-28.8" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{f6554699-2407-53b9-a55f-7058e13f61e6}" name="bottom" y="0.0" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_disconnect.elmt">
<definition width="25" hotspot_y="27" version="0.100.0" type="element" link_type="simple" hotspot_x="17" height="54">
<uuid uuid="{942cc615-f1ff-59da-94aa-fcc1453762c4}"/>
<names>
<name lang="en">v2_disconnect</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="-9.7" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-21.6" length1="1.5" length2="1.5"/>
<circle diameter="5.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" y="-9.7" x="-2.5"/>
<line end2="none" x2="-11.5" y2="5.8" x1="-1.8" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-5.4" length1="1.5" length2="1.5"/>
<circle diameter="5.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" y="4.7" x="-2.5"/>
<line end2="none" x2="0.0" y2="21.6" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="9.7" length1="1.5" length2="1.5"/>
<terminal orientation="n" type="Generic" uuid="{3bde8f44-4ba9-583c-8d2e-e2d33fbb8355}" name="top" y="-21.6" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{cea02d4e-770c-5661-bc06-58ed4e421587}" name="bottom" y="21.6" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_transformer_2w_primary-delta_secondary-wye_g.elmt">
<definition width="52" hotspot_y="38" version="0.100.0" type="element" link_type="simple" hotspot_x="18" height="76">
<uuid uuid="{2326ecb2-de00-5fcd-93e7-2f1e68cf7a01}"/>
<names>
<name lang="en">v2_transformer_2w_primary-delta_secondary-wye_g</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<circle diameter="25.9" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" y="-21.3" x="-13.0"/>
<circle diameter="25.9" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" y="-4.7" x="-13.0"/>
<line end2="none" x2="0.0" y2="-21.2" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-32.4" length1="1.5" length2="1.5"/>
<line end2="none" x2="0.0" y2="32.4" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="21.2" length1="1.5" length2="1.5"/>
<polygon x3="4.7" x4="0.0" x2="-4.7" y2="-7.4" x1="0.0" style="line-style:normal;line-weight:thin;filling:none;color:black" y4="-15.5" antialias="true" closed="true" y1="-15.5" y3="-7.4"/>
<line end2="none" x2="0.0" y2="1.4" x1="0.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="6.8" length1="1.5" length2="1.5"/>
<line end2="none" x2="-4.7" y2="9.5" x1="0.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="6.8" length1="1.5" length2="1.5"/>
<line end2="none" x2="4.7" y2="9.5" x1="0.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="6.8" length1="1.5" length2="1.5"/>
<line end2="none" x2="23.0" y2="8.3" x1="13.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="8.3" length1="1.5" length2="1.5"/>
<line end2="none" x2="23.0" y2="14.0" x1="23.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="8.3" length1="1.5" length2="1.5"/>
<line end2="none" x2="28.4" y2="14.0" x1="17.6" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="14.0" length1="1.5" length2="1.5"/>
<line end2="none" x2="26.3" y2="16.6" x1="19.8" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="16.6" length1="1.5" length2="1.5"/>
<line end2="none" x2="24.5" y2="19.1" x1="21.6" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="19.1" length1="1.5" length2="1.5"/>
<terminal orientation="n" type="Generic" uuid="{e2460619-0e0a-5db9-b5e3-1c3e04baeb55}" name="top" y="-32.4" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{1a09bcec-0e22-5e69-a82d-85f20fd2944c}" name="bottom" y="32.4" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_circuit_breaker.elmt">
<definition width="26" hotspot_y="27" version="0.100.0" type="element" link_type="simple" hotspot_x="13" height="54">
<uuid uuid="{401afc3c-0271-519c-9515-d2d9287616f1}"/>
<names>
<name lang="en">v2_circuit_breaker</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="-7.2" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-21.6" length1="1.5" length2="1.5"/>
<line end2="none" x2="0.0" y2="21.6" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="7.2" length1="1.5" length2="1.5"/>
<arc width="14.4" start="270" angle="180" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" y="-7.2" x="-7.2" height="14.4"/>
<terminal orientation="n" type="Generic" uuid="{67a1679e-e95b-581e-a26e-b47ce16e1ca2}" name="top" y="-21.6" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{b238d746-e1b0-5bc0-bdf6-690650239460}" name="bottom" y="21.6" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_tee_l.elmt">
<definition width="30" hotspot_y="15" version="0.100.0" type="element" link_type="simple" hotspot_x="15" height="30">
<uuid uuid="{286fe5c5-6217-51d3-811a-83060121dca4}"/>
<names>
<name lang="en">v2_tee_l</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<circle diameter="6" style="line-style:normal;line-weight:normal;filling:black;color:black" antialias="true" y="-3" x="-3"/>
<line end2="none" x2="0" y2="10" x1="0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-10" length1="1.5" length2="1.5"/>
<terminal orientation="n" type="Generic" uuid="{93dbb9de-6acb-50b2-b2bc-8dc78b7bd469}" name="t" y="-10" x="0"/>
<terminal orientation="s" type="Generic" uuid="{9d70e09e-c3a2-536f-876a-98bc14839f3d}" name="b" y="10" x="0"/>
<terminal orientation="w" type="Generic" uuid="{be1f5b40-75e4-5797-a01a-0782e7979dde}" name="l" y="0" x="-10"/>
</description>
</definition>
</element>
<element name="v2_spd_lv_tape.elmt">
<definition width="40" hotspot_y="5" version="0.100.0" type="element" link_type="simple" hotspot_x="20" height="53">
<uuid uuid="{877441ae-9bef-5065-9d9d-1f599b72686c}"/>
<names>
<name lang="en">v2_spd_lv_tape</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="7.2" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="0.0" length1="1.5" length2="1.5"/>
<polygon x3="14.4" x4="-14.4" x2="14.4" y2="28.8" y5="28.8" x1="-14.4" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="7.2" antialias="true" x5="-14.4" closed="true" y1="28.8" y3="7.2"/>
<line end2="none" x2="0.0" y2="34.6" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="28.8" length1="1.5" length2="1.5"/>
<line end2="none" x2="9.0" y2="34.6" x1="-9.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="34.6" length1="1.5" length2="1.5"/>
<line end2="none" x2="5.8" y2="38.5" x1="-5.8" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="38.5" length1="1.5" length2="1.5"/>
<line end2="none" x2="2.5" y2="42.5" x1="-2.5" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="42.5" length1="1.5" length2="1.5"/>
<terminal orientation="e" type="Generic" uuid="{e19c76b6-5c57-53ff-a5b8-fa51a5adce05}" name="top" y="0.0" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_box_h-8_text-LS_w-10.elmt">
<definition width="46" hotspot_y="20" version="0.100.0" type="element" link_type="simple" hotspot_x="23" height="40">
<uuid uuid="{2440327c-529e-55c8-8e69-ac8aab4395ca}"/>
<names>
<name lang="en">v2_box_h-8_text-LS_w-10</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="18.0" x4="-18.0" x2="18.0" y2="14.4" y5="14.4" x1="-18.0" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-14.4" antialias="true" x5="-18.0" closed="true" y1="14.4" y3="-14.4"/>
<terminal orientation="n" type="Generic" uuid="{78a377a5-1507-514d-aa70-83390dc5de01}" name="top" y="-14.4" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{e10499a4-3979-5cdb-8aa8-be9260e5454e}" name="bottom" y="14.4" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_variac.elmt">
<definition width="44" hotspot_y="31" version="0.100.0" type="element" link_type="simple" hotspot_x="22" height="62">
<uuid uuid="{ff2da9fb-a168-59fc-9e3f-d33f3ceb72bf}"/>
<names>
<name lang="en">v2_variac</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="-16.2" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-25.2" length1="1.5" length2="1.5"/>
<circle diameter="32.4" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" y="-16.2" x="-16.2"/>
<line end2="none" x2="9.4" y2="-9.4" x1="-12.2" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="12.2" length1="1.5" length2="1.5"/>
<polygon x3="10.1" x4="12.2" x2="4.3" y2="-10.1" x1="12.2" style="line-style:normal;line-weight:normal;filling:black;color:black" y4="-12.2" antialias="true" closed="true" y1="-12.2" y3="-4.3"/>
<line end2="none" x2="0.0" y2="25.2" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="16.2" length1="1.5" length2="1.5"/>
<terminal orientation="n" type="Generic" uuid="{b1ea261f-cf22-55fb-814d-e9fe9b7fdd2e}" name="top" y="-25.2" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{1c865d9a-c587-583f-8e62-82cbc705b697}" name="bottom" y="25.2" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_fuse.elmt">
<definition width="22" hotspot_y="27" version="0.100.0" type="element" link_type="simple" hotspot_x="11" height="54">
<uuid uuid="{579e40bc-8ee5-53f4-a8e2-515e5c8d7f01}"/>
<names>
<name lang="en">v2_fuse</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="21.6" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-21.6" length1="1.5" length2="1.5"/>
<polygon x3="5.8" x4="-5.8" x2="5.8" y2="12.6" y5="12.6" x1="-5.8" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-12.6" antialias="true" x5="-5.8" closed="true" y1="12.6" y3="-12.6"/>
<terminal orientation="n" type="Generic" uuid="{312cffce-9a2d-52ad-9632-8147f6460507}" name="top" y="-21.6" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{64651853-d240-5db8-88d9-6adb7340a161}" name="bottom" y="21.6" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_box_h-8_text-UV_w-10_tape.elmt">
<definition width="46" hotspot_y="20" version="0.100.0" type="element" link_type="simple" hotspot_x="41" height="40">
<uuid uuid="{b99557f0-f555-58bc-b009-b268d75e5ed5}"/>
<names>
<name lang="en">v2_box_h-8_text-UV_w-10_tape</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="0.0" x4="-36.0" x2="0.0" y2="14.4" y5="14.4" x1="-36.0" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-14.4" antialias="true" x5="-36.0" closed="true" y1="14.4" y3="-14.4"/>
<terminal orientation="e" type="Generic" uuid="{42c69da2-a77c-5fcf-92b4-88ae9fbc950b}" name="top" y="0.0" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_overload.elmt">
<definition width="28" hotspot_y="27" version="0.100.0" type="element" link_type="simple" hotspot_x="14" height="54">
<uuid uuid="{999b9a70-b5a7-59e9-8d3d-9068eff86fa2}"/>
<names>
<name lang="en">v2_overload</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="-10.8" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-21.6" length1="1.5" length2="1.5"/>
<polygon x3="9.0" x4="-9.0" x2="9.0" y2="10.8" y5="10.8" x1="-9.0" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-10.8" antialias="true" x5="-9.0" closed="true" y1="10.8" y3="-10.8"/>
<line end2="none" x2="0.0" y2="21.6" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="10.8" length1="1.5" length2="1.5"/>
<terminal orientation="n" type="Generic" uuid="{d548c004-98ea-5594-9621-ca07f35a11e9}" name="top" y="-21.6" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{b6645803-91cb-5b4e-8323-c6ebc88e20a7}" name="bottom" y="21.6" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_no_contact.elmt">
<definition width="21" hotspot_y="27" version="0.100.0" type="element" link_type="simple" hotspot_x="16" height="54">
<uuid uuid="{d32835d2-8cc4-57c9-a729-c3d3b51ddc35}"/>
<names>
<name lang="en">v2_no_contact</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="-7.2" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-21.6" length1="1.5" length2="1.5"/>
<line end2="none" x2="-10.1" y2="5.8" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-7.2" length1="1.5" length2="1.5"/>
<line end2="none" x2="0.0" y2="21.6" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="7.2" length1="1.5" length2="1.5"/>
<terminal orientation="n" type="Generic" uuid="{1b663411-841b-50df-94b6-25953716cb9f}" name="top" y="-21.6" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{c9545907-50fe-500b-a6c7-7e0cd3936950}" name="bottom" y="21.6" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_resistor.elmt">
<definition width="22" hotspot_y="27" version="0.100.0" type="element" link_type="simple" hotspot_x="11" height="54">
<uuid uuid="{2e9f77ec-9ae3-5532-b671-00e5afd1141c}"/>
<names>
<name lang="en">v2_resistor</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="-15.1" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-21.6" length1="1.5" length2="1.5"/>
<polygon x5="-5.0" closed="false" x1="0.0" x3="-5.0" y8="15.1" x6="5.0" y4="-2.5" x8="0.0" y6="7.6" y1="-15.1" x2="5.0" antialias="true" y2="-12.6" y7="12.6" x4="5.0" y5="2.5" y3="-7.6" x7="-5.0" style="line-style:normal;line-weight:normal;filling:none;color:black"/>
<line end2="none" x2="0.0" y2="21.6" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="15.1" length1="1.5" length2="1.5"/>
<terminal orientation="n" type="Generic" uuid="{e859bac3-df77-5cd9-b4d7-f154517e3fca}" name="top" y="-21.6" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{298e3cbf-3348-5fc5-9e6e-202a511076c3}" name="bottom" y="21.6" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_box_h-11_text-ITECH_w-18.elmt">
<definition width="76" hotspot_y="25" version="0.100.0" type="element" link_type="simple" hotspot_x="38" height="50">
<uuid uuid="{ddb30ced-f3d9-51ac-a311-463bacdeef79}"/>
<names>
<name lang="en">v2_box_h-11_text-ITECH_w-18</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="32.4" x4="-32.4" x2="32.4" y2="19.8" y5="19.8" x1="-32.4" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-19.8" antialias="true" x5="-32.4" closed="true" y1="19.8" y3="-19.8"/>
<terminal orientation="n" type="Generic" uuid="{8fc34f12-ff87-5ef6-ae13-2aba4fa88cf3}" name="top" y="-19.8" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{648b75fb-363e-5683-9cfd-154ff31b8aac}" name="bottom" y="19.8" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_rectifier.elmt">
<definition width="62" hotspot_y="31" version="0.100.0" type="element" link_type="simple" hotspot_x="31" height="62">
<uuid uuid="{0bf53093-0ffd-57e3-8176-1ce40ff82dc8}"/>
<names>
<name lang="en">v2_rectifier</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="25.2" x4="-25.2" x2="25.2" y2="25.2" y5="25.2" x1="-25.2" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-25.2" antialias="true" x5="-25.2" closed="true" y1="25.2" y3="-25.2"/>
<line end2="none" x2="-25.2" y2="25.2" x1="25.2" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="-25.2" length1="1.5" length2="1.5"/>
<terminal orientation="n" type="Generic" uuid="{f5eb6733-ca45-5c2a-b2fa-c894a4958a7c}" name="top" y="-25.2" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{2f2037de-cb82-5686-8995-63202aac686a}" name="bottom" y="25.2" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_load_arrow.elmt">
<definition width="22" hotspot_y="5" version="0.100.0" type="element" link_type="simple" hotspot_x="11" height="39">
<uuid uuid="{3f4716d9-e690-5e20-a12f-22ee04663e2e}"/>
<names>
<name lang="en">v2_load_arrow</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="18.7" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="0.0" length1="1.5" length2="1.5"/>
<polygon x3="5.4" x4="0.0" x2="-5.4" y2="18.7" x1="0.0" style="line-style:normal;line-weight:normal;filling:black;color:black" y4="28.8" antialias="true" closed="true" y1="28.8" y3="18.7"/>
<terminal orientation="n" type="Generic" uuid="{429c4d81-a73c-59e0-b403-6ed5a242eceb}" name="top" y="0.0" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_box_h-9_text-s60_w-12.elmt">
<definition width="54" hotspot_y="22" version="0.100.0" type="element" link_type="simple" hotspot_x="27" height="44">
<uuid uuid="{a271cad0-2c9e-5e19-a32f-2aeb4fefc998}"/>
<names>
<name lang="en">v2_box_h-9_text-s60_w-12</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="21.6" x4="-21.6" x2="21.6" y2="16.2" y5="16.2" x1="-21.6" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-16.2" antialias="true" x5="-21.6" closed="true" y1="16.2" y3="-16.2"/>
<terminal orientation="n" type="Generic" uuid="{a1a38578-8eee-59f9-8706-85f507594626}" name="top" y="-16.2" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{0c62b4ef-92b3-561d-9a19-14ae2cb3d253}" name="bottom" y="16.2" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_earth_switch_tape.elmt">
<definition width="31" hotspot_y="5" version="0.100.0" type="element" link_type="simple" hotspot_x="17" height="49">
<uuid uuid="{93598103-5108-5555-a4b6-f5d5f09ab28c}"/>
<names>
<name lang="en">v2_earth_switch_tape</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="4.7" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="0.0" length1="1.5" length2="1.5"/>
<circle diameter="5.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" y="4.7" x="-2.5"/>
<line end2="none" x2="-11.5" y2="20.2" x1="-1.8" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="9.0" length1="1.5" length2="1.5"/>
<circle diameter="5.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" y="20.2" x="-2.5"/>
<line end2="none" x2="0.0" y2="30.2" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="25.2" length1="1.5" length2="1.5"/>
<line end2="none" x2="9.0" y2="30.2" x1="-9.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="30.2" length1="1.5" length2="1.5"/>
<line end2="none" x2="5.8" y2="34.2" x1="-5.8" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="34.2" length1="1.5" length2="1.5"/>
<line end2="none" x2="2.5" y2="38.2" x1="-2.5" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="38.2" length1="1.5" length2="1.5"/>
<terminal orientation="e" type="Generic" uuid="{7077f93d-13f7-55a5-b8d9-1a094d5a6849}" name="top" y="0.0" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_transformer_2w_neutral-out_primary-delta_secondary-wye.elmt">
<definition width="55" hotspot_y="38" version="0.100.0" type="element" link_type="simple" hotspot_x="18" height="76">
<uuid uuid="{187f5a17-9334-518b-80f7-f38927279a93}"/>
<names>
<name lang="en">v2_transformer_2w_neutral-out_primary-delta_secondary-wye</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<circle diameter="25.9" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" y="-21.3" x="-13.0"/>
<circle diameter="25.9" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" y="-4.7" x="-13.0"/>
<line end2="none" x2="0.0" y2="-21.2" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-32.4" length1="1.5" length2="1.5"/>
<line end2="none" x2="0.0" y2="32.4" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="21.2" length1="1.5" length2="1.5"/>
<polygon x3="4.7" x4="0.0" x2="-4.7" y2="-7.4" x1="0.0" style="line-style:normal;line-weight:thin;filling:none;color:black" y4="-15.5" antialias="true" closed="true" y1="-15.5" y3="-7.4"/>
<line end2="none" x2="0.0" y2="1.4" x1="0.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="6.8" length1="1.5" length2="1.5"/>
<line end2="none" x2="-4.7" y2="9.5" x1="0.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="6.8" length1="1.5" length2="1.5"/>
<line end2="none" x2="4.7" y2="9.5" x1="0.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="6.8" length1="1.5" length2="1.5"/>
<line end2="none" x2="26.6" y2="8.3" x1="13.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="8.3" length1="1.5" length2="1.5"/>
<circle diameter="5.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" y="5.8" x="26.7"/>
<terminal orientation="e" type="Generic" uuid="{4dfabc6c-a669-5279-9acf-f6230bd01778}" name="neutral" y="8.3" x="31.7"/>
<terminal orientation="n" type="Generic" uuid="{69ea8a10-dd86-548f-a83d-a3e9194731f1}" name="top" y="-32.4" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{362a4c97-c89a-5d2b-954a-18cfd0d6b5bd}" name="bottom" y="32.4" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_relay_func-50G-51G.elmt">
<definition width="58" hotspot_y="29" version="0.100.0" type="element" link_type="simple" hotspot_x="29" height="58">
<uuid uuid="{4f677421-7f77-5214-8610-0847bc6eac85}"/>
<names>
<name lang="en">v2_relay_func-50G-51G</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<circle diameter="46.3" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" y="-23.2" x="-23.2"/>
<terminal orientation="n" type="Generic" uuid="{91149297-5666-5761-9d70-6cb816961f31}" name="top" y="-22.5" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{400d8d37-d581-5c84-b15e-5fa4354b3a62}" name="bottom" y="22.5" x="0.0"/>
<terminal orientation="e" type="Generic" uuid="{5ae6572d-a2ce-5f5a-b6a0-1115a16e44d8}" name="left" y="0.0" x="-22.5"/>
<terminal orientation="e" type="Generic" uuid="{29122bc9-05c3-5901-bd8b-a1e4fb037d0a}" name="right" y="0.0" x="22.5"/>
</description>
</definition>
</element>
<element name="v2_box_dashed-True_h-9_text-MVGND_w-15.elmt">
<definition width="64" hotspot_y="22" version="0.100.0" type="element" link_type="simple" hotspot_x="32" height="44">
<uuid uuid="{65bfd39f-8716-584b-96be-d6afa60c0c4a}"/>
<names>
<name lang="en">v2_box_dashed-True_h-9_text-MVGND_w-15</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="27.0" x4="-27.0" x2="27.0" y2="16.2" y5="16.2" x1="-27.0" style="line-style:dashed;line-weight:thin;filling:none;color:black" y4="-16.2" antialias="true" x5="-27.0" closed="true" y1="16.2" y3="-16.2"/>
<terminal orientation="n" type="Generic" uuid="{4ba9f9da-dd94-59df-85e1-3e73e87fe13e}" name="top" y="-16.2" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{5e39c327-6af7-59cb-a312-c164b98a1c31}" name="bottom" y="16.2" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_box_h-12_text-DUT_w-20.elmt">
<definition width="82" hotspot_y="27" version="0.100.0" type="element" link_type="simple" hotspot_x="41" height="54">
<uuid uuid="{25121bf1-49d1-5667-b84d-9eae0cf16d2c}"/>
<names>
<name lang="en">v2_box_h-12_text-DUT_w-20</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="36.0" x4="-36.0" x2="36.0" y2="21.6" y5="21.6" x1="-36.0" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-21.6" antialias="true" x5="-36.0" closed="true" y1="21.6" y3="-21.6"/>
<terminal orientation="n" type="Generic" uuid="{f1d9e05b-632d-5639-be9e-84b059822dca}" name="top" y="-21.6" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{c2ab760f-9b43-5376-825c-dee311b74019}" name="bottom" y="21.6" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_box_h-8_text-TF_w-10_tape.elmt">
<definition width="46" hotspot_y="20" version="0.100.0" type="element" link_type="simple" hotspot_x="41" height="40">
<uuid uuid="{0f275246-d56b-559f-ae8a-153ae0dcb852}"/>
<names>
<name lang="en">v2_box_h-8_text-TF_w-10_tape</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="0.0" x4="-36.0" x2="0.0" y2="14.4" y5="14.4" x1="-36.0" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-14.4" antialias="true" x5="-36.0" closed="true" y1="14.4" y3="-14.4"/>
<terminal orientation="e" type="Generic" uuid="{3fbc81a4-f120-5c14-b47b-92ea67e39d18}" name="top" y="0.0" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_ct.elmt">
<definition width="40" hotspot_y="27" version="0.100.0" type="element" link_type="simple" hotspot_x="15" height="54">
<uuid uuid="{4bb6d5d3-6285-5f64-b67c-a1a425ae6f8e}"/>
<names>
<name lang="en">v2_ct</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="21.6" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="-21.6" length1="1.5" length2="1.5"/>
<circle diameter="18.7" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" y="-9.4" x="-9.4"/>
<line end2="none" x2="19.8" y2="0.0" x1="9.4" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" end1="none" y1="0.0" length1="1.5" length2="1.5"/>
<circle diameter="3.6" style="line-style:normal;line-weight:normal;filling:black;color:black" antialias="true" y="-12.6" x="-7.9"/>
<terminal orientation="n" type="Generic" uuid="{dcd46bb2-a7a6-5354-b3cc-e2e435c7f2ef}" name="top" y="-21.6" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{f1b2e211-545d-5970-aeb7-385c9fd15317}" name="bottom" y="21.6" x="0.0"/>
<terminal orientation="e" type="Generic" uuid="{1c0399b8-9331-5eb3-b768-8e7e04eb69a0}" name="sec" y="0.0" x="19.8"/>
</description>
</definition>
</element>
<element name="v2_meter_letter-MFM.elmt">
<definition width="40" hotspot_y="20" version="0.100.0" type="element" link_type="simple" hotspot_x="20" height="40">
<uuid uuid="{b29bdb1d-8a5e-5631-8507-56b3b7f1fd5d}"/>
<names>
<name lang="en">v2_meter_letter-MFM</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<circle diameter="28.5" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" y="-14.3" x="-14.3"/>
<terminal orientation="n" type="Generic" uuid="{fc97451c-aed9-5e30-9042-acd18402f128}" name="top" y="-12.6" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{f56989af-7ed7-54ef-9941-da5c4f216072}" name="bottom" y="12.6" x="0.0"/>
<terminal orientation="e" type="Generic" uuid="{489fd3b1-eb92-5653-93bf-e2e35e9e9006}" name="left" y="0.0" x="-12.6"/>
<terminal orientation="e" type="Generic" uuid="{fcc85842-e95a-5e76-95fc-ede8f83f6cf6}" name="right" y="0.0" x="12.6"/>
</description>
</definition>
</element>
<element name="v2_box_h-10_text-RCPT_w-18.elmt">
<definition width="76" hotspot_y="23" version="0.100.0" type="element" link_type="simple" hotspot_x="38" height="46">
<uuid uuid="{fcd3f9a1-9c6c-51d7-93b2-deac226c423a}"/>
<names>
<name lang="en">v2_box_h-10_text-RCPT_w-18</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="32.4" x4="-32.4" x2="32.4" y2="18.0" y5="18.0" x1="-32.4" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-18.0" antialias="true" x5="-32.4" closed="true" y1="18.0" y3="-18.0"/>
<terminal orientation="n" type="Generic" uuid="{eae7d899-17b3-5245-9f2a-cbb1a5919e23}" name="top" y="-18.0" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{35edf270-c9b7-5c66-a6b7-eed22f54f630}" name="bottom" y="18.0" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_earth_switch.elmt">
<definition width="31" hotspot_y="5" version="0.100.0" type="element" link_type="simple" hotspot_x="17" height="49">
<uuid uuid="{d0ce1034-213c-5a38-b921-62f51c7bdfdb}"/>
<names>
<name lang="en">v2_earth_switch</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<line end2="none" x2="0.0" y2="4.7" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="0.0" length1="1.5" length2="1.5"/>
<circle diameter="5.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" y="4.7" x="-2.5"/>
<line end2="none" x2="-11.5" y2="20.2" x1="-1.8" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="9.0" length1="1.5" length2="1.5"/>
<circle diameter="5.0" style="line-style:normal;line-weight:thin;filling:none;color:black" antialias="true" y="20.2" x="-2.5"/>
<line end2="none" x2="0.0" y2="30.2" x1="0.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="25.2" length1="1.5" length2="1.5"/>
<line end2="none" x2="9.0" y2="30.2" x1="-9.0" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="30.2" length1="1.5" length2="1.5"/>
<line end2="none" x2="5.8" y2="34.2" x1="-5.8" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="34.2" length1="1.5" length2="1.5"/>
<line end2="none" x2="2.5" y2="38.2" x1="-2.5" style="line-style:normal;line-weight:normal;filling:none;color:black" antialias="true" end1="none" y1="38.2" length1="1.5" length2="1.5"/>
<terminal orientation="n" type="Generic" uuid="{67fffc8f-7e79-59d0-97e3-0e6aefa527d5}" name="top" y="0.0" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_box_h-10_text-LVPNL_w-18.elmt">
<definition width="76" hotspot_y="23" version="0.100.0" type="element" link_type="simple" hotspot_x="38" height="46">
<uuid uuid="{33d0e3bc-adaa-58c2-8176-4003444084d4}"/>
<names>
<name lang="en">v2_box_h-10_text-LVPNL_w-18</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="32.4" x4="-32.4" x2="32.4" y2="18.0" y5="18.0" x1="-32.4" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-18.0" antialias="true" x5="-32.4" closed="true" y1="18.0" y3="-18.0"/>
<terminal orientation="n" type="Generic" uuid="{e8b0e37b-ae0c-59e1-a773-027b9af4e24d}" name="top" y="-18.0" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{83f1dd0e-48fc-5f4b-bcf0-a355902c6f73}" name="bottom" y="18.0" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_box_dashed-True_h-9_text-AVT_w-12_tape.elmt">
<definition width="54" hotspot_y="22" version="0.100.0" type="element" link_type="simple" hotspot_x="49" height="44">
<uuid uuid="{7f6386d4-07ed-501b-ab67-6060234bd3bb}"/>
<names>
<name lang="en">v2_box_dashed-True_h-9_text-AVT_w-12_tape</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="0.0" x4="-43.2" x2="0.0" y2="16.2" y5="16.2" x1="-43.2" style="line-style:dashed;line-weight:thin;filling:none;color:black" y4="-16.2" antialias="true" x5="-43.2" closed="true" y1="16.2" y3="-16.2"/>
<terminal orientation="e" type="Generic" uuid="{87e8ccab-af69-57f9-a376-8aa611282d2f}" name="top" y="0.0" x="0.0"/>
</description>
</definition>
</element>
<element name="v2_box_h-11_text-DUT_w-18.elmt">
<definition width="76" hotspot_y="25" version="0.100.0" type="element" link_type="simple" hotspot_x="38" height="50">
<uuid uuid="{8268913e-3f82-5876-9766-27b136126ad8}"/>
<names>
<name lang="en">v2_box_h-11_text-DUT_w-18</name>
</names>
<kindInformations/>
<informations>ENA006 SLD probe element original NEMA/IEEE-styled geometry. PILOT ARTIFACT.</informations>
<description>
<polygon x3="32.4" x4="-32.4" x2="32.4" y2="19.8" y5="19.8" x1="-32.4" style="line-style:normal;line-weight:normal;filling:none;color:black" y4="-19.8" antialias="true" x5="-32.4" closed="true" y1="19.8" y3="-19.8"/>
<terminal orientation="n" type="Generic" uuid="{b5f577a1-ac22-5d6e-b109-1d9ac351b5f8}" name="top" y="-19.8" x="0.0"/>
<terminal orientation="s" type="Generic" uuid="{5b69e1c7-b417-5784-a874-5502eb581c22}" name="bottom" y="19.8" x="0.0"/>
</description>
</definition>
</element>
</category>
</category>
</collection>
</project>
+118
View File
@@ -0,0 +1,118 @@
#include <QtTest>
#include <QDir>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTemporaryDir>
namespace {
struct Point {
double x = 0.0;
double y = 0.0;
bool operator==(const Point &other) const { return x == other.x && y == other.y; }
};
// Mirrors count_retraces.py (qelectrotech-docker), which this project's own
// regression checks for bugtracker #734 already use: restricted to the
// conductor path shape (4-6 points, every segment axis-aligned, not closed)
// so legitimate closed element-graphics shapes (rectangles, polygons) are
// never mistaken for a self-retracing conductor.
QList<Point> parsePoints(const QString &d)
{
QList<Point> points;
static const QRegularExpression re(QStringLiteral("[ML]\\s*(-?\\d+\\.?\\d*)[ ,](-?\\d+\\.?\\d*)"));
auto it = re.globalMatch(d);
while (it.hasNext()) {
const QRegularExpressionMatch m = it.next();
points << Point{m.captured(1).toDouble(), m.captured(2).toDouble()};
}
return points;
}
bool isConductorShaped(const QList<Point> &points)
{
if (points.size() < 4 || points.size() > 6) return false;
if (points.first() == points.last()) return false; // closed shape, not a conductor
for (int i = 0; i + 1 < points.size(); ++i) {
if (points.at(i).x != points.at(i + 1).x && points.at(i).y != points.at(i + 1).y)
return false; // not axis-aligned
}
return true;
}
bool isSelfRetracing(const QList<Point> &points)
{
// (a) skip-one out-and-back: ... A B A ...
for (int i = 0; i + 2 < points.size(); ++i) {
if (points.at(i) == points.at(i + 2)) return true;
}
// (b) immediate duplicate vertex: ... A A ...
for (int i = 0; i + 1 < points.size(); ++i) {
if (points.at(i) == points.at(i + 1)) return true;
}
// (c) 6-point crossed-stub loop: depart0 == arrivee AND depart == arrivee0
if (points.size() == 6 && points.at(0) == points.at(4) && points.at(1) == points.at(5)) return true;
return false;
}
} // namespace
// Bugtracker #734: Conductor::generateConductorPath()'s cas "3"/"4" grid-
// snap bridge used to route an unnecessary out-and-back excursion, or a
// small looping detour, when depart and arrivee already shared the axis the
// bridge would run along. This fixture (the report's own canonical
// reproduction, "qet_bug_repro_resaved.qet") is known to trigger it.
class tst_conductorselfretrace : public QObject
{
Q_OBJECT
private slots:
void noSelfRetracingPaths()
{
const QString binary = QStringLiteral(QET_TEST_BINARY_PATH);
QVERIFY2(QFile::exists(binary), qPrintable(QStringLiteral("qelectrotech binary not found at '%1'").arg(binary)));
const QString fixture = QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet");
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
QTemporaryDir out_dir;
QVERIFY(out_dir.isValid());
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(binary, {QStringLiteral("--export-svg"), fixture, out_dir.path()});
QVERIFY2(proc.waitForFinished(30000), "export --export-svg timed out or failed to start");
QCOMPARE(proc.exitCode(), 0);
QDir dir(out_dir.path());
const QStringList svgs = dir.entryList({QStringLiteral("*.svg")}, QDir::Files);
QVERIFY2(!svgs.isEmpty(), "no SVG exported");
static const QRegularExpression path_re(QStringLiteral("<path[^>]*\\bd=\"([^\"]+)\""));
int retraces = 0;
for (const QString &name : svgs) {
QFile file(dir.filePath(name));
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(file.readAll());
auto it = path_re.globalMatch(content);
while (it.hasNext()) {
const QRegularExpressionMatch m = it.next();
const QList<Point> points = parsePoints(m.captured(1));
if (isConductorShaped(points) && isSelfRetracing(points)) {
++retraces;
}
}
}
QCOMPARE(retraces, 0);
}
};
QTEST_APPLESS_MAIN(tst_conductorselfretrace)
#include "tst_conductorselfretrace.moc"