A selection of several parts is moved by ElementPrimitiveDecorator, which
snapped to a fixed 1-unit step at every zoom. That is the "selection is
big" half of #112: below 100 % one part landed on the 10 grid but a
multi-part selection did not, and at 100-400 % it ignored the grid too.
It now snaps to the editor's current grid, as a single part does; Ctrl
still gives the fine 1-unit step. Resizing and arrow-key moves are
unchanged.
Also from review: add rows just below each zoom threshold to
tst_elementviewgrid (each catches a moved threshold the old rows missed),
and use one grid_step in drawBackground() instead of identical x/y copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AssignVariables::replaceVariable() is a fixed list of replacements. Every
auxiliary family is listed for blocks 1 to 4 except the machine
manufacturer reference, which stopped at the plain
%{machine_manufacturer_reference}. A text using one of the four
auxiliary forms printed the variable itself on the folio and in exports.
Add the four, next to their manufacturer-reference neighbours.
Test: tst_resaveunchanged resaves a project with one terminal whose
texts combine each machine reference variable with its neighbour, and
checks the saved texts carry the values. It fails without the fix (the
variable is saved as typed) and passes with it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Below 100 % ElementView::drawBackground() hid the grid and also set the
snap step to 1 unit, so anything dragged while zoomed out landed off the
grid with nothing on screen to show it. A large symbol opens fitted to
the window below 100 %, which is why the report also says "when the
selection is big".
Keep hiding the grid below 100 % (its dots would merge into a grey
wash), but snap every 10 units, as at 100 %. The zoom-to-grid rule moves
into a header-only ElementViewGrid so it can be tested without an
editor window, like textgrid.h.
Checked in the editor: a one-rectangle symbol zoomed out to about 0.6x,
its edge dragged by (15, 10) px and saved: master moves it by (28, 18),
this branch by (30, 20). tst_elementviewgrid fails with the old step 1
and passes now; ctest 45/45.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A contact (slave) not linked to a coil has no element to take its label
from: DynamicElementTextItem::elementUseForInfo() returns null. Since
8985babfe (#978) the composite-text branch of updateLabel() called
element->actualLabel() without checking, so a text built from %{label}
on such a contact crashed QElectroTech while the project was loading
(QETProject::refresh -> Diagram::refreshContents). The project could not
be opened again.
Check the element as the ElementInfo branch above already does; with no
element, %{label} shows empty, as it did before 8985babfe.
Test: tst_resaveunchanged opens a blank project holding one unlinked
contact with a %{label} text. It fails without the fix (the --resave
crashes) and passes with it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Projet > Scripts > Gérer les scripts… lists the stored scripts with their
icons, and the files that get no button with the reason. For each one it
edits the name, icon (a file copied next to the script, a theme icon, or
the initials), tooltip, shortcut, when it is enabled, and the script
itself; "Tester" saves it and runs it on the current project, one Ctrl+Z
to undo; "Supprimer" deletes it with its icon if no other script uses it.
It only reads and writes the files in the scripts folder, so a script
written here, by hand or by an assistant over the qet MCP server is the
same thing, and the folder's watcher turns each into a button.
ScriptHeader gains compose(), bodyOf() and idFor(), header-only and
tested: what the manager writes reads back as what was typed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every .js file in the "scripts" folder of the user's data folder that
starts with a // ==QETScript== header becomes a command: in Projet >
Scripts, as a button on a new Scripts toolbar, and, because it is
registered with ShortcutManager as diagrameditor.script.<file name>, in
the shortcut settings, the shortcut bar (S) and command search. The
header gives its name, icon (a file next to the script or a theme icon;
a tile with its initials otherwise), tooltip, default shortcut and when
it is enabled (always, with a selection, with a conductor selected).
The folder is watched, so a script added, edited or deleted while QET is
open appears, changes or goes without a restart. A file with a header
that cannot be used gets no button; the Scripts menu lists it with the
reason. The menu also opens the folder, and holds "Exécuter un script…".
A click runs the script on the current project as one undo step named
after it, and asks to switch scripting on first, like "Exécuter un
script…" does: scripting stays off by default.
ShortcutManager::unregisterAction() takes a command out of the lists
when its script is deleted, and lets it come back under a new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read from the meta-object, so the list is the one the running build has:
for someone writing a script, and for an assistant that has to write one
without the source at hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A script started from the editor had no way to know which folio is on
screen, and each qet.* call was its own undo step, so a script that adds
twenty items needed twenty Ctrl+Z to take back.
qet.currentFolio() returns the folio shown in the editor; through --run,
which has no view, the first folio, or -1 when there is none.
A run from the editor is now one undo macro, named after the script. A
run that changed nothing leaves no empty entry behind. qet.undo() and
qet.redo() inside a grouped run say why they cannot work (QUndoStack
ignores them inside a macro) instead of failing silently. --run is
unchanged: one step per call, so scripts that call qet.undo() keep
working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1205:
- The scene gives no order for its selection, so the "first" wire whose
values the panel shows could change between two selections of the
same wires. The wires are now sorted top to bottom, then left to right.
- A text field the selected wires do not agree on (function, section,
cable...) is shown blank with the hint "Plusieurs valeurs". Typing any
value there, including the first wire's own, now differs from what was
shown, so it reaches every wire: 1.5 mm² on a 1.5 and a 2.5 wire works.
- The conductor text is locked while several wires are selected: the same
number on all of them would make duplicates. "Apply to all conductors
of this potential" with one wire selected still sets the number on the
whole potential.
- One edit of N wires emitted N signals, each reloading the panel. They
are now gathered into one reload, which matters since a reload reads
every selected wire to find the mixed fields. Selections and targets
are de-duplicated with sets, and a wire reached through an earlier
wire's potential is not walked again. An edit that changes nothing
returns before any potential is walked.
The rules move to sources/conductormultiedit.h, templates over the wire
type, so tst_conductormultiedit can check them against a fake wire:
order, targets with and without potentials, one undo step and its redo
for three wires, and the mixed fields. tst_conductorapplychanges now
gives the other wire the opposite value in every boolean field, so
copying one by mistake fails the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With "Show the properties of a selected conductor in the Selection
properties panel" switched on, editing a wire there could change things
the user never touched:
- Enter in a field (Function, Section...) is not used by the line edit,
so it reaches the checkable "Multifilaire" group box around it, which
takes it as a click. The wire was switched to single-line, gaining
ground, neutral and phase symbols. The modal dialog never shows this
because its OK button takes Enter first.
- Every edit wrote back the whole set of properties as the widget holds
them, so a value the widget cannot show exactly was rewritten: a dash
size of 1 became 2.
The panel now swallows Enter at its checkable group boxes, and writes
only the fields that differ from what it showed, through the new
ConductorProperties::applyChanges(). "Apply to all conductors of this
potential" uses the same rule, so the rest of the potential keeps its
own values too.
tst_conductorapplychanges checks each field on its own: change that one
field, and a wire whose every field differs takes it and keeps the rest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where two wires cross without being connected, a project can now draw a
small arc (a hop) on one of them, instead of the user inserting a jump
symbol and splitting the wire. It is a project setting, off by default:
Project properties > Général > "Croisements de conducteurs", with no
hops, hops on horizontal wires, or hops on vertical wires. Choosing the
orientation rather than following drawing order keeps a project
consistent.
Only the drawing changes: no element is added and no wire is split. A
wire that ends or bends on another one is a junction and never hops,
and a crossing too close to the end of a segment for the arc to fit is
drawn plainly. PDF, SVG and image export and printing show the hops,
since they paint through Conductor::paint(); DXF export, which writes
the segments itself, does not.
The setting is saved as <wire_crossings hop="..."/> under the project
root, next to <usage>, and only when hops are on, so a project that
never used them saves exactly as before.
The geometry is in wirehops.cpp, free of any graphics item, and tested
on its own. Conductor::paintedPath() feeds it the conductors of the
folio from a snapshot of their scene points, rebuilt only when a
conductor changes shape, moves, or enters or leaves a folio; each hop
path is cached the same way. Asking the scene for the conductors in a
rect instead strokes every candidate's shape, which made a 366-wire
folio render 0.6 s slower; with the snapshot the difference is within
measurement noise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each wire already carries a cable name (Conductor properties -> Cable),
saved in the project, but no export showed it. The wiring list CSV
(Project menu, and --export-cables) now writes it as a last column, so
the list can be sorted or filtered by cable in a spreadsheet.
It goes last so existing columns keep their positions for anyone reading
the file by column number. When the two halves of a wire drawn through a
folio report are joined into one row, their cable names are merged the
same way as colour and section: written once if equal, both if not.
Asked for in issue #405.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MCP server gives each QElectroTech run a private HOME and writes the
element collection path for it (elements_dir) into
~/.config/QElectroTech/QElectroTech.conf there. Only Linux reads that
file: Qt keeps settings in the registry on Windows and in the system
preferences on macOS. So on those systems elements_dir did nothing, and
every run read the user's own settings (#1178).
When QET_SETTINGS_DIR names a folder, QElectroTech now keeps its
settings in <folder>/QElectroTech/QElectroTech.ini on every system. It is
set in main() before the first setting is read. Without the variable
nothing changes.
The server sets it for each run and writes the collection path to the
.ini as well as the .conf, so an older QElectroTech keeps working on
Linux. The path is written with forward slashes: Qt reads a backslash in
these files as an escape. The README notes that elements_dir needs this
on Windows and macOS, and that long arguments go on stdin with "-"
(Windows refuses a command line over 32,767 characters).
tst_settingsdir: a script places a symbol that only the folder's settings
can resolve, while the usual settings file points at an empty collection.
It fails on master and passes here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Windows crash report held the header and the log and nothing else: no
exception, no address, no stack (issue #1179, two reports that could not
be traced). It now also has:
- the exception, with its name ("access violation", "stack overflow"...)
- where it happened, as module+offset, and for an access violation the
address that was read or written
- a backtrace, one module+offset per frame, walked with the unwind
tables every x64 image carries (RtlLookupFunctionEntry and
RtlVirtualUnwind): no dbghelp, no symbols, no allocation. addr2line
on the same build turns each offset into a function and line.
The dump is written by a reporter thread started at install(), not on
the crashing thread, which may have no stack left or hold the loader
lock. The crashing thread waits for it for at most ten seconds.
Two kinds of crash wrote no report at all before and now do:
- abort() (std::terminate(), a failed assert): no SEH exception, so
SIGABRT is handled as well.
- qFatal(): Qt ends it with TerminateProcess() on Windows, so the
message handler now asks for the dump once the message is logged.
Elsewhere reportFatal() does nothing; SIGABRT follows there.
Tested with a cross-built RelWithDebInfo build under Wine 11, crashing
on purpose from inside the event loop: a null pointer, a call through a
null pointer, a stack overflow, abort() and qFatal(). Each report
resolved to the line that crashed. Linux reports are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wire drawn to a folio report waited, keyed by its report, for the wire on
the linked report, in a QMap whose insert() replaces: a second wire on the
same report pushed the first out of the list, and a pair with several wires
on a side was joined by whichever came first. On the shipped examples,
re-saved as the export reads them, 130 of 3,189 wires were missing from the
list (industrial 86, m_000 41, affuteuse_250h 3).
All the report halves are now collected first, and two are joined only
when each report of a linked pair has exactly one wire. With several on a
side the diagram does not say which terminal is wired to which, so each
wire gets its own row ending at the report, as an unmatched half already
did. The result no longer depends on the order the folios are saved in.
The Page column printed each folio's number template ("%id/%total") and
the rows were sorted by it. It now prints the number the folio shows, and
the rows are sorted by the folio's position in the project.
tst_wiringlistexport checks a fixture with one report pair of each kind,
and the page column on every example.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text properties showed and edited the top-left corner of a text,
whatever its alignment, and changing the alignment did not move the
text. A right-aligned label next to a symbol therefore needed a
different X for every text length to keep the same gap (#1155).
Position X/Y is now the anchor point: the point of the text chosen by
the alignment (right edge for "right", middle for "centre"...). Typing a
position puts that point there, and changing the alignment moves the
text so that the new anchor point is on the same X/Y. It is the same
point that already stays fixed when the text changes.
Applies to the element text properties on a folio and to the text
field properties in the symbol editor. The saved x/y is still the
top-left corner, so projects and symbols load and save unchanged and
look the same; only the numbers shown for texts that are not top-left
aligned change.
The geometry is in textanchor.h, shared by DiagramTextItem and
PartDynamicTextField, and tested in tst_textanchor.
Not changed: static texts of the symbol editor (PartText), whose
position is tied to the font baseline; texts inside a text group, which
have no position or alignment of their own; rotation still pivots on
the top-left corner (or the centre), not on the anchor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discussion #598, reviving PR #654. The crash-recovery backup written
every 20 minutes went to a single file. If the project was already in a
bad state when a backup ran, that bad state replaced the only recovery
copy.
QETProject now writes the backups in turn to three KAutoSaveFile slots
(BackupGenerations), so one bad write only replaces the oldest
snapshot. Scope is crash recovery only; the opt-in autosave is
unchanged.
After a crash, the recovery prompt groups the snapshots by project and
offers one row per project with a list to pick the snapshot to reopen,
newest selected by default. The snapshots not picked are deleted.
Ported onto current master: writeBackup() keeps the "skip if nothing
changed" check (bugtracker #273) and offerBackupFiles() keeps its place
after the stale-file filter and before the crash report (#901).
Tested with the backup interval shortened to 4 s (test build only):
after three changes, master holds one recovery file, overwritten each
time; this branch holds three, with 4, 5 and 6 folios. After killing
QET, the prompt lists the project; picking the oldest snapshot reopens
4 folios, the default reopens 6. Twice each. ctest 34/34 with and
without KDE Frameworks; the carried-over KAutoSaveFile test passes in
the nokde build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revives #640, closed only to clear a review backlog. Two entries in
the Configuration menu: "Enregistrer la configuration sous..." saves
the settings to a .conf file, "Charger une configuration..." replaces
them with one. The file is always ini format, so a profile made on one
platform loads on the others.
Changes from #640:
- Window sizes, positions and dock layouts (".../geometry",
".../state") and the recent-files lists stay out of a profile and
are kept on load: they describe this computer, not a preference.
- A saved file carries a format marker; loading refuses files that
QElectroTech did not save.
- Loading closes QElectroTech and asks the user to start it again,
instead of starting a second copy: a second copy started while this
one is still running hands its arguments over and exits (main.cpp).
- The colour dialog's custom colours are reloaded from the profile
before quitting, since ~QETApp() saves them over the loaded ones.
- Both actions are registered with ShortcutManager, so they appear in
command search and can be given a shortcut.
- The copy rules live in the header-only utils/configprofile.h and
are unit tested (tst_configprofile).
Loading still replaces rather than merges: a setting the profile does
not have is removed, or switching profiles would keep settings from the
previous one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discussion #1157. IEC 61666 §4.1 requires each terminal to be
identified unambiguously within its object, so two terminals of one
element must not share a name.
On save, the element editor now:
- refuses to save when two terminals share a name, lists the names
("N ×3") and selects those terminals;
- warns, and still saves, when a terminal has no name. Folio reports,
conductor definitions and thumbnails are skipped.
Names are compared after trimming surrounding spaces, case sensitive.
Settings > General > Editor has a new checkbox, on by default, that
turns both checks off (elementeditor/check-terminal-names).
--check-elements applies the same rule: repeated names are a FAIL,
missing names a WARN. It ignores the setting, since it is an explicit
check. On the shipped collection this reports 82 FAILs, the elements
that repeat a terminal name today.
The rule lives in the header-only editor/terminalnamecheck.h, shared
by both, and is unit tested by tst_terminalnamecheck.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The first fill from the document stored the label and wire text saved in
the file. QElectroTech saves what it last worked out, not what the formula
gives now: K%total-%id saved as K1-1 on a 3-folio project is K3-1 on the
folios, and the same for a wire's formula. So the document fill disagreed
with the folio fill on any file whose folios had been added or moved.
AssignVariables now works from a FormulaContext: the folio's number,
index, total, plant, location, title-block and project variables, and the
element's grid cell and prefix or the wire's four properties. The Diagram
overload fills one from a built folio, the database fills one from the
file, and both call the same evaluation.
Also brought in line with Diagram::fromXml(): an element Element::valideXml()
rejects is skipped with its wires; a folio where two elements number their
terminals alike (Element::fromXml() refuses one, by geometry), a frozen
formula wire text, and the older sequential-number attributes are left to
the folios. Each folio's border now starts from the defaults a new Diagram
has, not the previous folio's values.
tst_databasefromdocument: formulasAreWorkedOut (every variable kind, stale
saved values), unbuiltElementIsLeftOut, clashingTerminalIdsFallBack. Each
fails with its fix removed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The project database -- behind parts lists, summary tables and the wiring
list -- was filled by walking the built folios: every element and
conductor object of every scene. Filling it from the document the project
was just read from instead is what the database needs before a project
could be opened without building every folio (qelectrotech-docker
DB-FROM-XML-SCOPE.md).
updateDB(document), called by readProjectXml(), fills diagram,
diagram_info, element, element_info, terminal and conductor from the
document, through the code the folios use: a BorderTitleBlock read from
each folio's XML gives the title-block values and each element's grid
cell, the embedded collection gives each definition, and the binders are
shared with the live path. Shapes, texts and pictures still come from the
folios (their boxes need fonts and pens).
It falls back to the folios, saying why in the log, when the document
does not carry what that needs: an item without a saved uuid (older files
-- the folios derive them on load), a conductor naming its ends the older
way, a %autonum folio number, a terminal showing its master's label, a
missing or unbuildable definition. A symbol label computed from a formula
is the one saved in the file, which QElectroTech writes as it computes it
on every save. QET_DATABASE_FROM_FOLIOS=1 forces the folio path.
tst_databasefromdocument saves every example once, then fills both ways
and requires identical tables (24/24, filled from the document each
time), and checks that an older file falls back with its reason. Red when
the document path is made to write a wrong grid cell. Database phase of
loading unchanged: industrial.qet 0.146 s vs 0.160 s, Polonez 0.040 s vs
0.038 s (median of 5).
QETProject::projectWideProperties() is split out of
updateDiagramsFolioData() so both fills use the same title-block context.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The configuration names "python" or "python3", or the Python the Windows
installer can add. When none of those can run, the assistant fails with
an error far from the cause, so the dialog now says so in bold, with what
to do: tick "Python pour l'assistant IA" in the installer or install
Python (Windows), or install Python 3 from the system's packages.
Windows 10 and 11 put a python.exe in WindowsApps that only opens the
Microsoft Store, so a PATH search alone reports Python where there is
none. A result there is reported as possibly that shortcut. Backslashes
are converted explicitly, not with QDir::fromNativeSeparators(), which
leaves them alone off Windows and so could not be tested here.
tst_aiassistantsetup: 17 cases (8 new: the bundled Python is taken
without looking on PATH; none, python.org, the Store shortcut with either
separator, and a Linux folder named WindowsApps). Each of the 4 new rules
was removed in turn and the test failed. ctest 32/32. In the GUI, the
same install layout shows the warning with no python3 on PATH and no
warning with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
diagram_info.date -- and project_summary_view.date, which the summary
table dialog queries -- held the folio date read back from the title
block's text: QLocale::system().toDate() of the string
updateDiagramContextForTitleBlock() writes in the locale's short format.
Where that format has a two-digit year (en_US "M/d/yy") Qt reads the year
back as 19xx, so 741.qet's 2010-09-21 became 1910-09-21, and so did every
dated folio of the 24 examples (109 of 109).
bindDiagramInfoValues() now binds the folio's own date,
BorderTitleBlock::date(), the value that text was made from. A folio set
to show the current date or none gives the same date as before.
tst_dbfoliodate runs --run on examples/741.qet under LC_ALL=en_US.UTF-8
and expects 2010-09-21 from project_summary_view: 1910-09-21 without this
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dialog showing the configuration an AI assistant needs to start
QElectroTech's MCP server (misc/qet-mcp), with this installation's paths
filled in: pick the assistant (Claude Desktop, Claude Code, GitHub
Copilot in VS Code, Cursor, Gemini CLI, Codex CLI, LM Studio), the
drawings folder the assistant may use, and whether it may edit (off by
default). It says where the text goes, warns that text inside a project
from someone else can steer an assistant, and copies the text. It writes
nothing and starts nothing.
AiAssistantSetup finds the server beside the running program, in the two
places the packages put it (<root>/mcp beside <root>/bin on Windows,
<prefix>/share/qelectrotech/mcp beside <prefix>/bin otherwise), and the
Python the Windows installer can add. When there is none, the dialog says
the server is not installed with this version and links the guide.
The configuration is written with QJsonDocument, so Windows paths are
escaped correctly; Codex CLI gets TOML.
tst_aiassistantsetup (9 cases): both layouts, no server, the bundled
Python on Windows only, valid JSON for every JSON client with paths that
parse back unchanged, the "servers" key for VS Code, "type" only where
wanted, editing off unless allowed, escaped TOML. Each of 8 rules was
removed in turn and the test failed. ctest 32/32.
Checked in the GUI (Xvfb): Help menu opens it; from a build tree it
reports the server missing and disables Copy; from an install layout it
fills the paths, and the text Copy put on the clipboard, used unchanged
as a Claude Code configuration, ran a real qet_project_info call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the previous commit: the line was built with chained arg()
calls, so a "%3" in a terminal's name was replaced by the conductor
count -- as before -- and now a "%4" by the uuid as well. One
multi-argument arg() substitutes each placeholder of the pattern once.
percentInNameKept renames a terminal of perceuse.qet "x%3y%4": listed as
"x1y{uuid}" before, "x%3y%4" now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The conductor calls name a wire end as element uuid + terminal index.
The index is the terminal's place in Element::terminals(), a sort by
position that is undefined between two terminals at the same point, and
the documentation ruled terminal uuids out as "empty for most of the
installed base". Since #1118 every terminal of an opened project has one.
- elementTerminals() ends each line with the terminal's uuid
(Terminal::stableUuid()); the text before it is unchanged.
- terminalIndex(folio, elementUuid, terminalUuid) returns the index the
calls take, or -1 if the element or terminal is not there, or if two of
the element's terminals carry that uuid.
- The class documentation says what does address a terminal: its uuid
together with its element's.
tst_scriptterminaluuid runs --run on perceuse.qet (552 elements, two
terminals at one point in some): every terminal listed with a uuid,
distinct within its element, found again at its own index; -1 for an
unknown or malformed uuid, an unknown element and a bad folio. Red when
terminalIndex() returns the wrong index. qet-mcp suite 253/253.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A symbol file whose terminals have no uuid got a random one for each
terminal when opened in the element editor (PartTerminal's constructor),
written on save. Every copy of the same old symbol therefore ended up
with different terminal uuids, none of them the one a project gives the
same terminals on opening (TerminalUuids::fillMissing()).
ElementScene::loadContent() now reads a copy of the definition filled by
TerminalUuids::fillMissingInDefinition(), the same rule as a project,
including the next occurrence for the second of two terminals at one
point. Terminals that have a uuid keep it; a paste still renews them all
(PastePartsCommand).
Checked in the editor: 6es7_212-1ae40-0xb0__p3.elmt (no terminal uuids
in the collection) and tm3saf5r_layout.elmt with its uuids stripped (two
terminals at one point), opened, nudged back and forth, saved: this build
writes exactly the derived values (computed independently in Python),
the previous one random ones. Select all, copy, paste, save: 6 terminals,
6 distinct uuids.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the previous commit:
- The load fallback compared a saved uuid with occurrence 0 only, so a
wire on the second of two terminals at one point of a symbol was lost
once the definition was replaced, and one on the first could go to
either of the pair (Element::m_terminals is sorted, not in definition
order). Element::parseTerminal() now records each terminal's rank among
the terminals of the definition at the same point, derivedUuid() uses
it, and fillMissing() starts from the same rank.
derivedUuidFoundAfterReplacement runs on perceuse.qet and industrial.qet
too: 154/156 and 670/671 wires without the rank, all with it.
qet-mcp: the first save of an older project now rewrites its wires from
the numbered form to the uuid form, and qet_diff keyed the two forms
differently, so an untouched resave showed every wire removed and added
(4 failures in test_qet_mcp.py). A uuid end is now resolved to the same
key as a numbered one: the terminal's definition position, moved to where
the wire docks, is the placed symbol's <terminal> record.
test_conductor_key_same_in_both_forms fails without it; 253/253 pass on
this build and on the previous stage's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Most symbols stored in older projects have no uuid on their terminals
(706 of the 900 in the 24 examples), so a terminal's identity is worked
out from where it sits in its symbol on every load (stableUuid()). That
is only sound while nothing keyed on it is kept between loads.
- On opening a project, every terminal of its embedded symbols without a
uuid gets that same derived value (TerminalUuids::fillMissing(), from
XmlElementCollection's loading constructor, before any folio is
built). The next save writes it, and the wires on it in the form that
names terminals by uuid, which QElectroTech reads since 0.8.0.
- The recipe moves to TerminalUuids::derived(), which stableUuid() now
calls, so the two cannot drift apart. A second terminal at the same
point of a symbol gets the next occurrence, and no value is given
twice within a symbol.
- findTerminal(): a wire whose terminal uuid is not found is matched to
the terminal whose derived value it is, so a saved uuid still finds its
terminal after the symbol's definition was replaced by one whose
terminals carry other uuids.
The project database's terminal and conductor tables are identical before
and after on all 24 examples except the 3 terminals that share a point
with another in their symbol (industrial.qet 1, perceuse.qet 2), which
now have an identity of their own. Every example keeps every wire through
a resave, and a second save changes nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the previous commit:
- keep() could give an old uuid to a new terminal while another terminal
of the new definition already carried it (a moved terminal), leaving
two terminals with one uuid. A terminal carrying any old uuid is now
left alone, and an old uuid already in use is never handed out.
- copyDirectory() replaced a whole category of the embedded collection
(drag a folder onto the project's folder of the same name) without
carrying terminal uuids over. keepInDirectory() walks both trees by
name and calls keep() on each symbol.
- The "wire(s) not loaded" log line repeated the folio's list on every
paste; it is now written only when a folio is loaded.
tst_terminaluuids: 3 new cases, each red on the previous keep().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wire whose terminals have uuids is saved against them, and on load it
is reattached to a terminal with that uuid or dropped, with only a qDebug
line. Re-importing a changed symbol and choosing "replace" swapped the
project's definition for one whose terminal uuids differ; the placed
symbols kept the old ones until the project was reopened, so every wire
on them was lost at the next open, silently, and gone for good at the
next save.
- XmlElementCollection::copyElement(), where an embedded definition is
overwritten, carries each old terminal uuid onto the new terminal at
the same place and orientation (TerminalUuids::keep()). Terminals that
moved, and new ones, keep their own.
- Diagram::fromXml() records wires it could not reattach, logs them, and
the editor lists them in one warning after opening a project.
Measured on 2612_ats_singlephase.qet with the stored splice's terminal
uuids made to differ from the collection's: replace, save, reopen loads
34 of 131 wires on master, 131 with this change (GUI, both arms).
tst_terminaluuids covers keep() and runs the real loader.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1109 (scorpio810):
- tst_resaveunchanged now resaves every .qet in examples/ twice instead
of two of them (24 rows, ~40 s). Dropping the load-time trim now also
fails affuteuse_250h.qet, which the two-file version missed.
- New case singleSpaceValueKept: a title-block property set to one
space survives two saves (#973), and an accented property comes back
unchanged. Fails if qetproject.cpp stops parsing with
PreserveSpacingOnlyNodes.
- New tst_diagramcontext: the QDom reader (projects) and the pugixml
reader (element definitions in the collection) return the same value
for plain, stray-spaced, accented and non-Latin text. Fails if the
pugixml path decodes as Latin-1 or either reader stops trimming.
The pugixml reader still reads a single-space value as "": pugixml drops
whitespace-only text unless parse_ws_pcdata is set, as it did before
this PR. That reader only sees element definitions, never a project, so
#973's title-block values do not go through it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>