Commit Graph

9789 Commits

Author SHA1 Message Date
ispyisail f58bf7502e qet-mcp: qet_edit names a terminal by its uuid
"terminal", "from_terminal" and "to_terminal" take the terminal's uuid
(as qet_element_info lists it) in place of its index. It names a
terminal of the op's own element -- for add_conductor, of that end's
element, "$name" references included -- and is turned at run time into
the index the call takes by qet.terminalIndex(); if the element has no
such terminal the op fails with a note. Unlike the index, a sort by
position, it is defined between two terminals at the same point. The
lookup is required only when a uuid is used, so index-only edits still
run on older builds.

README: the terminal uuids; wires of older projects now get a lasting
uuid (#1107) instead of staying unnamed.

Tests: script generation for every terminal-taking op, "$name" and folio
uuid resolution, the index still passed through; through the binary,
bobine_ka_a_remanence (file order A2, A1; index order A1, A2) wired A2 to
A1 by uuid lands on exactly those ends, and an unknown uuid stops the run
with its note. 255/255 against a build with qet.terminalIndex(); a build
without it is reported as missing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:00:16 +13:00
ispyisail 3112727ee7 elementTerminals(): keep a "%3" or "%4" in a terminal's name as written
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>
2026-09-29 11:00:16 +13:00
ispyisail 9fdd6ac333 Scripting: find a terminal by its uuid
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>
2026-09-29 11:00:16 +13:00
Laurent Trinques b411a08340 git submodule update --remote elements 2026-09-28 23:34:05 +02:00
Laurent Trinques d3ca8483cf Merge pull request #1122 from ispyisail/feat/editor-derived-terminal-uuids
Give old symbols' terminals their lasting uuid in the symbol editor
2026-09-28 23:30:27 +02:00
Laurent Trinques 3df6d450fe Merge pull request #1121 from ispyisail/fix/titleblock-custom-vars-1113
Fix #1113: title-block variables shown by name and missing in Replace
2026-09-28 23:28:52 +02:00
Laurent Trinques a00e9439b7 Merge pull request #1120 from ispyisail/fix/1117-collection-icon-after-save
Fix #1117: element icon disappears from the collection after saving
2026-09-28 23:18:59 +02:00
ispyisail 22f1455868 Symbol editor: imported elements get new terminal uuids
Review of the previous commit: "Import scaled element" and "Import DXF"
add a definition to an open symbol through OpenElmtCommand, which, unlike
a paste, kept the imported terminals' uuids. With derived uuids, an old
symbol imported into itself (or into another with a terminal at the same
point) gave two terminals one uuid; importing any symbol whose terminals
already carried uuids did the same before this series. OpenElmtCommand now
renews the imported terminals' uuids, as PastePartsCommand does.

Checked in the editor with a stand-in scaler at scale 1: open
6es7_212-1ae40-0xb0__p3.elmt, import it into itself, save -- 6 terminals,
3 distinct uuids without this commit, 6 with it, the original 3 keeping
their derived values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:08:39 +13:00
ispyisail 2815dbaf47 Symbol editor: give old terminals their derived uuid, not a random one
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>
2026-09-29 10:08:39 +13:00
Laurent Trinques 90084ecf40 Merge pull request #1119 from Kellermorph/fix-material
Fix material list reading, selection and article fields
2026-09-28 23:04:32 +02:00
ispyisail 08bce54a2f Fix #1113: custom title-block variables in Find/Replace and unset placeholders
Two problems with a custom variable newly added to a title-block template:

1. Find/Replace > Folio > Custom showed an empty table, so the user had to
   know and type each variable name. It now lists every custom variable the
   folios already carry plus those their templates use, with empty values.
   An empty value now means "leave unchanged", like every field of the main
   tab; only the variables actually filled in are written to the folios.

2. The title block showed the variable's own name ("%doc-type") until the
   folio's properties were opened, because interpreteVariables() only
   replaces names present in the context. Placeholders in the template text
   that no key resolves now render blank, as auto-added unset ones already
   did since #973. Only the template's own text is considered, so a value
   that contains "%something" is never touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:59:21 +13:00
ispyisail 34a2ceff70 Fix #1117: element icon vanishes from the collection after saving
Saving an element from the element editor calls
ElementsCollectionWidget::locationWasSaved(), which runs clearData() on
the panel item: the icon is set to a null QIcon. The icon only comes
back through FileElementCollectionItem::setUpIcon(), but since the
#633 recursion guard that returns for good once m_icon_initialized is
set, and nothing ever reset it. The row stayed without an icon until
the whole collection was reloaded.

#1008 refreshed the picture caches on save, but no one asked them for
the new picture, so it could not fix this. Resetting the flag in
clearData() lets the next paint rebuild the icon, which then comes
from those refreshed caches, so the panel shows the new drawing.

The flag is reset after the base clearData(): its setIcon() emits
dataChanged(), which re-enters setUpIcon() and must still return early.
setUpIcon() sets the flag before its own setIcon(), so the #633 guard
is unchanged.

The same reset should also bring back a folder's icon after editing its
properties (editDirectory() calls clearData() too); read, not tested.
Project collection items were never affected: their setUpIcon() guards
on icon().isNull().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:55:10 +13:00
Laurent Trinques df2395e5c6 Merge pull request #1118 from ispyisail/feat/fill-terminal-uuids
Give terminals in older projects a lasting uuid
2026-09-28 22:42:22 +02:00
Kellermorph 8b54ea400b Fix material list reading, selection and article fields
Follow-up to #1088, covering its review comments and one behaviour
change found while testing the feature:

- MaterialList::load() names the columns from the machine key line when
  the file has one, instead of from the translated label line above it.
  A catalogue written in another language fills the element fields
  again, and saving no longer replaces the key line with labels.
- The entry picked after "New entry" is found by comparing the columns
  one by one instead of comparing the two maps as a whole: the entry
  form leaves the empty columns out, so the record never matched the
  line that had just been written and the search was cleared for
  nothing. The search is now only given up when it really hides the new
  line.
- Applying a catalogue entry pushes an undo command only when the live
  edit is on. In the properties window, where there is none, the fields
  wait for "Apply", so "Cancel" gives the element its own values back
  instead of leaving the picked part in place.
- Cells a file holds past the header are kept in MaterialRecord::extra
  and written back, so appending an article never shortens a line.
- An empty cell of a column describing the article itself (MaterialList::
  isArticleBound: description, designation, manufacturer, order number,
  supplier, model, ratings, dimensions, auxiliary block) clears the
  field, so an element never keeps the manufacturer of the part picked
  before. An empty cell of any other column (function, comment, notes,
  plant, location, quantity, unity), and any column the file does not
  hold at all, leaves the field alone.
- Comments left where the review asked for them: the corner button
  lookup, the ';' separator fallback, the search filter cost.
2026-09-28 22:38:41 +02:00
Laurent Trinques 56e69d5956 Merge pull request #1116 from ispyisail/fix/replace-symbol-keeps-wires
Fix wires lost after replacing a symbol already in the project
2026-09-28 22:30:09 +02:00
ispyisail eb3b10a48e Tell apart two terminals at one point; qet_diff: same wire in both forms
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>
2026-09-29 09:10:38 +13:00
ispyisail 4e6f59e011 Give terminals in older projects a lasting uuid
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>
2026-09-29 08:50:27 +13:00
ispyisail 079d085a87 Keep terminal uuids when a whole category is replaced; never duplicate one
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>
2026-09-29 08:11:15 +13:00
ispyisail e6ac117f9b Keep wires attached when a project's copy of a symbol is replaced
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>
2026-09-29 08:11:15 +13:00
Laurent Trinques 9cbf26dcc9 Merge pull request #1115 from ispyisail/feat/qet-mcp-edit-folio-uuid
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m10s
Let qet_edit name a folio by its uuid
2026-09-28 20:54:04 +02:00
Laurent Trinques 4bf02cd936 Merge branch 'master' into feat/qet-mcp-edit-folio-uuid 2026-09-28 20:53:51 +02:00
Laurent Trinques 9fe30c9081 Merge pull request #1114 from ispyisail/feat/script-folio-uuid
Add scripting calls to find a folio by its uuid
2026-09-28 20:28:58 +02:00
Laurent Trinques 045d2edc27 Merge pull request #1108 from ispyisail/feat/qet-mcp-conductor-uuid
Let qet_edit name a wire by its uuid
2026-09-28 20:27:05 +02:00
Laurent Trinques c1332adce5 Merge pull request #1107 from ispyisail/feat/lasting-wire-uuid
Give wires in older projects a lasting uuid, from what they connect
2026-09-28 20:19:03 +02:00
Laurent Trinques 59f0b0b3f8 Merge pull request #1106 from ispyisail/feat/script-conductor-uuid
Add scripting calls to list wires by uuid and find their ends
2026-09-28 20:10:53 +02:00
ispyisail a4d8f19d9f Merge branch 'feat/script-conductor-uuid' into feat/qet-mcp-conductor-uuid 2026-09-29 06:34:16 +13:00
ispyisail 97510f9639 Merge remote-tracking branch 'upstream/master' into feat/lasting-wire-uuid
# Conflicts:
#	tests/qttest/CMakeLists.txt
2026-09-29 06:34:16 +13:00
ispyisail 604c3b1cd7 Merge remote-tracking branch 'upstream/master' into feat/script-conductor-uuid
# Conflicts:
#	tests/qttest/CMakeLists.txt
2026-09-29 06:34:16 +13:00
Laurent Trinques bc5ef180b6 Merge pull request #1105 from ispyisail/feat/stable-symbol-uuid
Give symbols in older projects the same uuid every time they open
2026-09-28 16:42:25 +02:00
Laurent Trinques cdcff93190 Update CMakeLists.txt 2026-09-28 16:42:05 +02:00
Laurent Trinques 69a468af63 Merge branch 'master' into feat/stable-symbol-uuid 2026-09-28 16:33:24 +02:00
Laurent Trinques f981ec7dc8 Merge pull request #1111 from ispyisail/feat/1070-rotate-whole-group
Rotate a selected group as one piece
2026-09-28 14:55:04 +02:00
Laurent Trinques 7456a48681 Merge pull request #1110 from ispyisail/feat/1070-pick-one-member
Click again on an item of a selected group to select it alone
2026-09-28 14:54:27 +02:00
Laurent Trinques 5a09d67f64 Merge pull request #1109 from ispyisail/fix/second-save-changes-nothing
Fix a just-saved project changing again on its next save
2026-09-28 14:52:54 +02:00
ispyisail c7e5c0c3b3 qet-mcp: qet_edit names a folio by its uuid
qet_edit takes a folio as its index, and the index shifts when an earlier
op in the same run adds, inserts or removes a folio: remove folio 0, then
edit "folio 2", and the edit lands on the folio that was 3. Every other
item qet_edit addresses (elements, texts, shapes, pictures, tables, symbol
text fields) can already be named by uuid; folios could not.

- "folio" and "to_folio" take the folio's uuid as well as its index,
  turned into the current index at run time by qet.folioIndex(). The
  lookup is required only when a uuid is used, so an index-only edit still
  runs on a build without it.
- qet_project_info lists each folio's uuid. A folio saved without one
  (132 of the 133 in the shipped examples) shows it empty until the
  project is saved once, and an empty "folio" says so.

Tests: the generated script for a uuid folio, on its own and inside a
table lookup and link_elements' to_folio; and a real run that removes
folio 0 and then retitles the old folio 2 by uuid, on a file saved without
folio uuids. The run fails against a build without qet.folioIndex().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:00:27 +13:00
ispyisail b99f5551d2 Scripting: look up a folio by its uuid
Every scripting call names a folio by its index, and the index shifts when
a folio is added, removed or moved: a script that removes folio 0 and then
edits "folio 2" edits the wrong one. Texts, shapes, pictures, tables and
symbol text fields already have a uuid lookup for the same reason; folios
had none.

- qet.folioUuid(index): the folio's uuid, or "".
- qet.folioIndex(uuid): the folio's current index, or -1.

A folio saved without a uuid (132 of the 133 in the shipped examples) is
given one on load, derived from the file, so it is the same on every load
and is written on the next save. No two folios share one: a clash is
renewed on load.

Tests in misc/qet-mcp's integration suite, which drives these through
--run: a folio followed across the removal of the one before it, on a file
saved without folio uuids, and a new folio's uuid found in the saved file.
Both fail against a build without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:56:08 +13:00
ispyisail e25ae3cbfb Test the second save on every example, a single-space value and accents
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>
2026-09-28 23:29:51 +13:00
ispyisail e7745afd18 qet-mcp: answer review on naming a conductor by uuid
- An end conductorEnds() reports as "?" is never picked.
- An empty "conductor" says why: an older project's conductors have no
  saved uuid, so qet_conductors reports it empty.
- The op description and README say set_conductor still changes the
  whole potential when given a uuid, and that older projects' conductors
  are named by element + terminal until #1103.
- test_conductor_by_uuid covers move_conductor_segment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:25:05 +13:00
ispyisail af6c2f0dde Merge branch 'feat/script-conductor-uuid' into feat/qet-mcp-conductor-uuid 2026-09-28 23:21:51 +13:00
ispyisail 3d85fc013d Scripting: keep conductors()'s doc comment on conductors()
The anonymous namespace sat between the comment and the function, so
Doxygen attached the comment to describeEnd().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:21:38 +13:00
ispyisail a8f940505d Rotate a selected group as one piece
Discussion #1070 proposed that rotate, like move, copy and delete, works on
the whole group once one of its items is clicked. Rotate (Space) turned
each member on its own spot instead, so rotating a group pulled it apart:
two grouped texts side by side ended up each turned in place, no longer
side by side.

When the selection is exactly one whole group -- wires aside, which follow
their symbols -- Rotate now turns it as one piece around its centre, as
"Pivoter le groupe" (Shift+Space) already does
(ItemGroups::soleWholeGroup()). Any other selection, including a single
member picked out of its group, rotates as before.

In the GUI, on two grouped texts selected by one click: Space on master
leaves both where they were, turned; here it gives exactly what
Shift+Space gives on both (both texts swung around the group's centre).
tst_itemgroups: 4 new checks; without the whole-group condition, a
picked member counts as a group and fails. ctest 24/24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:39:03 +13:00
ispyisail 6ed41bbfd0 Click again on a member of a selected group to pick it on its own
Clicking an item of a group selects the whole group (#1070). Clicking
again on one of its items is meant to select just that item, to edit it
on its own -- what discussion #1070 proposed -- but the second click
selected the whole group again: Qt left only the clicked item selected on
release, and the group completion pulled the others back in.

A press on a member of a group that is selected whole now notes that
member (ItemGroups::memberToPick(), which also finds the member when the
click lands on a symbol's own text). If the click ends without a drag and
Qt has left only that member selected, the selection stays so. A drag
still moves the whole group; Ctrl+click keeps its meaning; a group of one
is not picked from.

In the GUI, on two grouped texts: one click then Delete removes both
(master and this); click, click again, Delete removes only the clicked
text here, both on master; dragging after one click moves both texts by
the same amount on both. tst_itemgroups: 5 new checks; removing the
whole-group or the group-of-one condition fails one each. ctest 24/24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:33:28 +13:00
ispyisail 4dcb54a3a2 Merge branch 'feat/script-conductor-uuid' into feat/qet-mcp-conductor-uuid 2026-09-28 22:04:33 +13:00
ispyisail 9db6084387 Build tst_scriptconductoruuid only where --run exists
The test runs a script through --run, which a build without the Qt Qml
module does not have (QET_HAS_SCRIPTING). Linux CI installs no Qml package,
so the binary took the script for a project to open and the test waited
out its 60 s. The test is now built only when scripting is: a configure
with Qt6Qml disabled lists 24 tests, without it; with Qml it runs and
passes as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:04:30 +13:00
ispyisail 75450d7102 Make saving a just-saved project change nothing
Saving a project that had just been saved changed it again in 18 of the
24 example projects, so a project kept in version control showed changes
nobody made. Both causes were cleanup done on save but not on load:

- Symbol information whose values were all empty was written as an empty
  <elementInformations/> block (DiagramContext::toXml() skips empty
  values, Element::toXml() wrote the block anyway). The next load read it
  as no information and the next save dropped it. The block is now written
  only when something went into it.
- Information values were trimmed on save but not on load, so a label with
  stray spaces (" PRISE") kept them in memory and in its displayed copy
  until the project was opened again. The same rule, kept in one place,
  now applies when reading: stray whitespace around real content trimmed,
  a value that is only whitespace kept (#973).

All 24 examples now save identically a second time (master: 6), and each
one's first save is byte-for-byte what master wrote only on its second.
A title-block property set to a single space keeps it through two saves.

tst_resaveunchanged runs --resave twice on Projet_vierge.qet and
m_000.qet; both fail without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:59:02 +13:00
ispyisail b3a4e30010 Give wires saved without a uuid a lasting one, from what they connect
A wire saved without a uuid got a random one on every load, never saved
(#754): it had no identity from one session to the next, so a script
could only name it as "the wire on terminal N of symbol X", and a
comparison of two versions could not tell a moved wire from a new one.

When a folio is loaded, such a wire now gets a UUID v5 derived from its
two ends -- the symbol and terminal at each, sorted so the direction it
was drawn in does not matter -- and it is written on save. Never its
place in the file or its folio's index: inserting or moving a folio, or
saving the wires in another order, does not change it. Once saved the
uuid no longer depends on the ends, so re-connecting the wire keeps it;
QETProject::derivedItemUuid() never hands out a uuid the file already
carries, so a wire later drawn on the ends it left gets another one.
Wires that have a uuid keep it; a paste still renews them.

The 24 example projects: 3,189 wires, none with a uuid before, all 3,189
after one save, none lost, no uuid used twice in any project; two saves
of the same file are identical, and a second save keeps every wire's
uuid. Discussion #1103 has the measurements behind the recipe.

tst_derivedwireuuid runs --resave on a fixture naming ends by uuid and on
examples/tremie_vibrante.qet (ends by terminal number): every wire gets a
distinct uuid, the same on every load, read back after a save, kept per
wire when a folio is inserted, the wires are reordered or a wire is drawn
the other way, and a newcomer on a re-connected wire's old ends gets
another uuid. Without this change 14 of the 18 fail; with the uuid taken
from folio index and file order instead, the folio-insert and reorder
tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:13:02 +13:00
ispyisail e12410e439 Never derive a uuid the file already carries
A symbol keeps its derived uuid once saved, even when it is moved. A
symbol saved without a uuid that later turns up on the spot it left -- a
hand edit, an older version, another tool writing the file -- derived the
same uuid, and the project had two symbols with one identity.

readDiagramsXml() now collects every symbol and wire uuid the file
carries, on any folio, before a folio loads; derivedItemUuid() moves to
the next counter value while a candidate is among them. The result still
depends on the file alone. Renamed from derivedUuid(), which QETProject
already has for the project's own uuid.

tst_derivedsymboluuid: newcomerOnAMovedSymbolsSpotGetsAnotherUuid fails
with the check switched off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:13:02 +13:00
ispyisail 0721b42e21 Give symbols saved without a uuid the same one on every load
A symbol saved without a uuid got a random one from Element::fromXml()
on every load, and the next save wrote it out: two loads of the same file
gave the same symbol two identities, and anything pointing at it by uuid
(a script, a comparison of two versions, a wire's identity) could not
follow it from one session to the next.

When a folio is loaded, such a symbol now gets a UUID v5 derived from
what it is and where it sits: its type, its position on the folio and its
orientation. Never the folio's index, so inserting or moving a folio does
not change it. Identical symbols stacked on one spot, or a copied folio,
are told apart by a counter kept per project (QETProject::derivedUuid()),
in load order among those symbols alone. A paste still renews uuids.

Symbols that have a uuid in the file keep it. All 24 example projects
already have one for every symbol, so they are unchanged; with the
symbols' uuids stripped, each saves byte-for-byte the same twice (master:
different every time).

tst_derivedsymboluuid runs --resave on a fixture with its uuids stripped:
same uuids on every load, same after a folio is inserted in front, saved
uuids kept, stacked copies differ. The first two fail without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:13:02 +13:00
ispyisail 9b967707a0 qet-mcp: qet_edit names a conductor by its uuid
set_conductor, move_conductor_segment and delete_conductor named a
conductor by one of its terminals, which had to carry exactly one
conductor: where two meet at a terminal, neither could be named from it.

Each now also takes "conductor": "{uuid}" (as qet_conductors reports it)
in place of element + terminal. The generated script asks
qet.conductorEnds() for the conductor's two ends and passes the one whose
terminal carries only that conductor. Where both ends are shared, or no
conductor has that uuid, the op fails and its "note" says which. The
lookup is required only when a uuid is used; giving both forms is an
error.

Tests: the script generated for each form and the argument errors; on a
folio where one terminal carries two conductors, deleting either by uuid
leaves exactly the other; an unknown uuid is reported. With the end chosen
without checking its terminal carries only that conductor, the terminal
test fails. 248/248 with a build carrying qet.conductorEnds().

Stacked on the conductorUuids()/conductorEnds() scripting change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:12:55 +13:00
ispyisail 353dbd3317 Scripting: list a folio's conductors by uuid and find their ends
A script could name a conductor only by one of its ends, "the conductor on
terminal N of element X", which fails where two conductors meet at one
terminal and cannot follow a conductor that is re-connected.

qet.conductorUuids(folio) lists the folio's conductor uuids, in the order
qet.conductors() lists them. qet.conductorEnds(folio, uuid) returns that
conductor's two ends as "{element uuid} terminal N" -- the form
conductors() prints and the conductor calls take -- or an empty list if
the folio has no such conductor. The end formatting conductors() already
did is shared rather than copied.

Conductors of older projects have no saved uuid yet, so theirs change
from one load to the next until that is settled (discussion #1103); new
conductors keep theirs.

tst_scriptconductoruuid runs a script through --run on a fixture: every
conductor has a distinct uuid, and its ends match the conductors() line
at the same position; an unknown uuid, a malformed one and a folio that
does not exist give empty lists. It fails with the two ends swapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:07:16 +13:00