Commit Graph

222 Commits

Author SHA1 Message Date
ispyisail 289e33d06e Merge remote-tracking branch 'upstream/master' into fix/keep-unloadable-items 2026-10-19 20:21:02 +13:00
ispyisail 0bcd0ca4b6 Keep the symbols and wires a folio cannot load, and save them back
Opening a project dropped, with only a debug line, a symbol whose
definition could not be found (missing from the project's collection and
from the library) and every wire on it; and a wire whose terminal could
not be matched, after the symbol's definition was redrawn. The next save
wrote the project without them. One save of a forum upload whose
collection was incomplete kept 1 of 219 symbols and none of its 299 wires.

A folio now keeps what it could not load as it was read
(Diagram::m_kept_symbols, m_kept_wires) and writes it back when it is
saved whole (Diagram::writeKept()):
- a symbol, its terminals given ids after those of the folio's symbols;
- a wire, an end found written from its terminal as Conductor::toXml()
  does, an end not found as the symbol and terminal uuids it names; for a
  wire saved by legacy ids, the terminal's uuid derived from where it was
  saved (Element::savedTerminalUuid(), out of matchMovedTerminals()), so
  it joins again once the definition has it there. A wire whose ends
  cannot be read, or an end naming no terminal of the file, is kept so
  that it joins nothing.
A symbol kept counts as waiting (projectDataBase::waitingSymbols()), so a
partner keeps its link to it. The open warning now lists the symbols not
loaded too, and says both are kept.

Once the definition is back, the symbol and its wires are built again:
741.qet with one transistor definition removed (8 symbols, 33 wires on
them) saves all 67 wires, and with the definition put back opens with
all 65 symbols and 67 wires, the same folio as before.

tst_resaveunchanged::unloadableItemsKept: fixtures/coil_contact_wired.qet
(coil, linked contact, two wires; built with the script API) with the
coil's definition removed, or the contact's terminals moved and given new
uuids: both symbols, wires and links saved, a second save the same, and
the coil's definition put back gives the undamaged project's symbols and
wires. Fails on master (1 symbol; 0 wires).

FINDINGS F127, F128.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:45:55 +13:00
ispyisail 1e988eadfd Merge pull request #1490 from Kellermorph/fix-textposition
Stop a folio report's text jumping when the report is not linked
2026-10-19 15:49:13 +13:00
ispyisail 171a597b12 Merge pull request #1546 from ispyisail/refactor/link-from-db-rows
Link symbols on folios opened later from the project database
2026-10-19 15:07:21 +13:00
ispyisail 4d61909fd1 Merge pull request #1545 from ispyisail/refactor/save-links-from-db
Fix removed folios leaving links to their symbols in the saved file
2026-10-19 15:07:03 +13:00
ispyisail 9a41e01187 Link a symbol built to its partners from the project database's rows
A symbol built after opening (lazy folios) links to the partners its
link rows name, and to the symbols whose rows name it; its file's list
only adds what the rows miss. The rows keep a partner on a folio not
built until it is built, in its saved place, so a symbol no longer keeps
a list of its own: the list read from the file is dropped once opening
has filled the database. pendingLinks(), waitingLinks(),
keepsSavedLinks(), linksToSave() and QETProject::waitingLinkTargets()
go; the callers that asked a symbol for its waiting partners ask the
database (waitingPartners()).

When its links change, a symbol's rows are written from its built
partners and the waiting partners its rows held, in their order
(rowsToWrite()), so a full rewrite of the table keeps them too.

A symbol built alone as a partner on a folio not built
(Diagram::buildSymbols()) counts as built: its rows are kept, it is not
waiting (waitingSymbols(), waitingPartners()), so saving does not build
its folio, and removing it does not remove the rows naming it. A row
naming a placed symbol that has not linked yet (its folio being built)
is kept too. At opening the database holds no link until its fill
(forgetLinks()); the symbols read their files' lists until then, and the
fill takes their order: a symbol's own rows, its list, then the rows
naming it. The symbols waiting on folios not built are cached and
updated as folios are built.

QET_CHECK_LINK_ROWS now checks the rows against the links: each built
partner in its order, no other row but to a folio not built.

tst_resaveunchanged::undoKeepsLinksToUnbuiltFolios: m_000 opened with
lazy folios, a contact, its folio or its coil's folio (not built)
removed, then undone: every link saved as before.

QETZ-DB-FIRST stage 2a-3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:47:41 +13:00
Laurent Trinques ddc71553e2 Terminal strip item: redraw the strip as soon as it changes
TerminalStripItem was connected to nothing: after applying the terminal
strip editor (width, catalog terminal, color, type...), undo or redo, the
strip drawn on the folio kept its old drawing until something else made
the scene repaint, and its bounding rect was not updated when the width of
the terminals changed.

- TerminalStrip::dataChanged() is emitted when the data of the strip is
  set, and when the data of the element of one of its terminals changes
  (Element::setElementData(), Element::setElementInformations()).
- TerminalStripItem connects to dataChanged(), orderChanged(),
  bridgeChanged() and bridgeColorChanged() of its strip, and updates its
  geometry and redraws itself.
2026-10-18 16:06:59 +02:00
ispyisail b473a07b7d Save a symbol's links from the project database
A symbol's <links_uuids> are now what the project database's link rows
list for it, in their order (projectDataBase::savedLinks()), instead of
a list the save built itself from the symbol's partners: QETZ-DB-FIRST
stage 2a, the save reads links from the database. A symbol the database
does not hold (saved outside a project) still lists them itself.

The rows leave out a partner the project no longer has, so removing a
folio with every folio built no longer saves links to its symbols, as
lazy folios already did (the links part of F113). Every other save is
unchanged.

tst_resaveunchanged removedFolioLeavesNoLinkToIt: affuteuse_250h with
every folio built, folio 3 removed: 4 links named symbols no longer in
the file, none now.

Needs #1522 (each symbol its own uuid) and #1523 (rows in saved order);
local only until both are merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XCdg2Er2vYhB8Vwa5G7fSR
2026-10-19 01:50:35 +13:00
ispyisail ee2a060b16 Write a symbol's link rows in the order it saves them, with its waiting partners
The project database's link table should say what each symbol saves:
QETZ-DB-FIRST stage 2a moves link reading and writing to it. Measured over
the corpus and 25 kinds of edit, its rows disagreed with the file for
three reasons; this fixes the one in the database:

- rows followed the order partners were built in, not the order the
  symbol saves them (11 symbols, mostly .qetz files where an edit built a
  partner's folio, with lazy folios);
- the rows were written by three pieces of code with three rules: the
  rebuild and the flush from the built partners then the waiting ones,
  the save from its own list.

Element::linksToSave() is now that one list: the order the symbol was
saved with, then partners linked since, a partner on a folio not built
included. toXml() writes it and the database's rows are written from it
(projectDataBase::insertLinkRows()), with their place in a new ord
column of the in-memory table (the .qetz file's own link table is not
touched). A built partner the project no longer has, on a removed folio,
still gets no row, as before.

The other two reasons are not here: symbols sharing a uuid (PR #1522)
and links saved to symbols on a removed folio (F113), which the save will
stop writing once it writes from the database (stage 2a-2).

QET_CHECK_LINK_ROWS=1 says at each save whether the rows are what the
symbols save, without building any folio. tst_resaveunchanged
linkRowsAfterLazyEdit moves a symbol of m_000 as a .qetz with lazy
folios: with the previous order, 2 symbols differ.

No saved byte changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XCdg2Er2vYhB8Vwa5G7fSR
2026-10-18 16:35:58 +13:00
Kellermorph 68914ee31b Stop a folio report's text jumping when the report is not linked
A report's texts that show a wire's properties (function, section...)
are only filled once the wires are read, and those are read after the
symbols: the fill comes from the 100 ms timer of
DynamicElementTextItem::conductorWasAdded(), by which time the project
state is Ok again and the alignment is live.

For a report whose partner is linked, that fill had already happened
inside the loading state (Element::initLink() -> linkedElementChanged()
-> reportChanged()), so the alignment was blocked and the text stayed
where the file put it. buildFolios() relies on that and says so. An
unlinked report, and one whose partner sits on a folio not built yet,
got nothing during loading: the first fill was the timer's one. It took
the rect of the still empty text for anchor and shifted the item by the
whole width of the text, on every open. Only the right, centred or
bottom aligned texts showed it -- the others have a null offset.

Fill those texts inside the loading state instead. Element::
updateConductorTexts(), already run when a wire is connected to a
terminal, now covers a report as well as a wire label, and
Diagram::refreshContents() runs it for every symbol while the project
is still parsing, right after initLink() has resolved the links. The
timer's fill that comes 100 ms later then finds the text already set,
and DynamicElementTextItem::setPlainText() returns on it unchanged, so
the alignment is never handed an empty rect to anchor. A wire that
appears, or a property that changes, while the project is open is
aligned as it was.

A report's "label" text is left to updateReportText(): it is the linked
folio's, not the symbol's own information.
2026-10-15 12:47:13 +02:00
ispyisail b085dcd5dd Pair terminals moved by one shift, and terminals sharing a place
The first pass refused a pair whenever a saved terminal had two live
candidates in reach. That happens when a whole drawing moves by half the
spacing of its terminals (pins 10 apart moved 5), and when two terminals
share a place. When one offset fits every unmatched terminal, as when a
whole drawing moved, that offset now settles all pairs; terminals sharing
a place pair in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:42:43 +13:00
ispyisail 786f8574d9 Reattach wires to a terminal that moved a little since the project was saved
A project saves, for every placed symbol, where each terminal docked, its
orientation and its number. When the symbol's definition is replaced by one
whose terminal sits a few units elsewhere, wires were matched by exact
position and so were dropped on the next open: legacy wires by Terminal::
fromXml(), uuid wires by the uuid derived from the terminal's new place.

Element::fromXml() now pairs each saved terminal that nothing matched
exactly with the live terminal of the same orientation whose docking point
is within 5 units in x and y, only when each is the other's only candidate.
A pair registers the saved number, and the uuid derived from the old place,
which Diagram::findTerminals() checks after the derived uuid of the current
place. Wire ends reattached this way are counted and logged once per folio.

tst_movedterminals covers a move of 3 and 5 units (number and derived
uuid), 6 units, two candidates, and an unmoved terminal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:26:30 +13:00
ispyisail 5c4fa8fadf Build folios only when needed, by default (stage D6)
Every project now opens without building its folios; a folio is built
when it is shown or something needs its items (QETProject::lazyFolios()).
QET_LAZY_FOLIOS is gone; QET_EAGER_FOLIOS=1 builds every folio on
opening, as before, for comparisons and troubleshooting. Comments that
named the old variable now say "lazy folios"; the tests switch modes
with the new one.

The first save of a .qet as .qetz still builds every folio: a .qet does
not say which program wrote it, so its folios are written again rather
than copied. A .qetz opens and saves without building.

ctest 96/96 by default and with QET_EAGER_FOLIOS=1; 342 corpus projects
save the same in all four arms and export the same. In the GUI
(qet-gui-dialog): a 136-folio .qetz opens with no folio built, shows
folios 1 to 4, saves building none, the save = the original but the time
spent in it; a .qet opens lazily, saves as a .qetz beside it after the
notice, the .qet unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail 244de05421 Change a symbol's information without building its folio (stage D5)
A script changing the information of a symbol on a folio not built, when
nothing else shows that information (no symbol built holds it, no
partner is linked to it, no label formula), changes it through the
project database: the store and the symbol's element info row, undoable
by uuid (ChangeStoredElementInformationCommand). Reading it back reads
the store. Building the symbol later takes the change
(projectDataBase::takeUnbuiltEdit(), in Element::fromXml()), the label
and texts following as for any change. Saving a .qetz writes the
changed information in the database in place of the rows the folio was
read with, its file copied, the folio still not built; a symbol whose
file holds no information block, or a .qet, builds the folio to save.

136-folio .qetz, "comment" set on the 3 513 symbols the database can
change: 0.9 s, no folio built, saved in 6.1 s, none built (27 s and 116
folios when saving built them). The same edits on the 4 039 symbols with
no partner and no formula, lazily and with every folio built: the two
saved projects are the same file. tst_lazyfolios: a script changes a
symbol not built, building none; saved as when built (fails when
building ignores the change, and when the .qetz writer does); undone;
the folio built afterwards shows it. ctest 96/96 eager and lazy; 342
corpus projects save the same in all four arms and export the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:44 +13:00
ispyisail b891b6354a Show a folio without building its partners' folios (stage D5b)
Showing a folio built it and every folio holding a partner of its
symbols. Now a partner is built alone, as a symbol, into its folio's
scene (Diagram::buildSymbols()), and reused when that folio is built.

- A linked folio report built alone had no wires when it was linked,
  so its texts showing a wire's properties stayed empty: after any
  build, linked reports on the built folios and their partners look at
  their wires again, as opening does. Unlinked reports keep their texts,
  as opening leaves them.
- A report whose partner was built alone builds the partner's folio
  when its potential is followed.
- tableChainFolios() parsed every unbuilt folio's file. The index now
  lists the drawn tables and the table each continues (folio_table),
  and a .qetz opened from it uses that list. An index without it falls
  back to the folio files.
- A .qetz's project.sqlite is opened once per project
  (QetContainerDb::FolioReader), not once per folio built.

nuri (136 folios), opened fresh, one folio shown: median 1104 -> 184 ms,
each show builds one folio; industrial 234 -> 87 ms.

tst_lazyfolios: a folio whose table continues another is shown first
from a .qetz (fails when the index's chains are ignored). ctest 96/96
eager and lazy; 342-project corpus: master, lazy, .qetz lazy and .qetz
kept all the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail 2f94724df3 Save a .qetz without building partners: symbols save the links they wait for (stage D5a)
A built symbol whose partner is on a folio not built yet (QET_LAZY_FOLIOS)
now saves that link too, in the order it was saved with
(Element::toXml(); QETProject::waitingLinkTargets() keeps it to partners
the project has, so a link to a symbol gone is dropped as it always was).
With that, and with no partner's text in a .qetz's folio files (D3c),
saving a .qetz no longer builds the folios of the partners built folios
wait for; table chains still are (a table saves the table before it), and
a .qet saved as read (QET_LAZY_SAVE_KEPT) still builds them, its folio
files holding partners' texts.

industrial.qetz: an untouched save builds no folio (47 of 47 stay not
built; 19 before), after everyday edits on its busiest folio 29 stay not
built (7 before).

tst_lazyfolios compares the links an edited .qetz saved without building
holds, symbol by symbol in their order, with those the same edits saved
with every folio built -- it fails when waiting links are not saved (the
drawing alone cannot tell: links are kept on both sides, and a symbol
reread gets its partner's back, in another order). Corpus, 342 projects:
a .qetz saved without building, read and saved as .qet = eager = master.
ctest 96/96 with and without QET_LAZY_FOLIOS=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail 00e200b504 Remove references to notes that are not in this repository
Comments added with the lazy folios cited planning notes kept outside the
QElectroTech repository, which a reader of the source cannot open. The
sentences now stand on their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:14 +13:00
Laurent Trinques 77e2a9bfc5 Speed up opening big projects with lazy folios 2026-10-13 11:01:21 +02:00
ispyisail 5441e2ae76 Keep a coil's contacts in the order the file saved them
On opening a project, a coil and its contacts are linked in the order
the folios' items are visited. When contacts are on an earlier folio
than the coil they link first, so the coil's list followed the folio
order instead of the file's, and contacts on one folio swapped places
from run to run. Saving the same project twice could then give two
different files.

After linking, Element::initLink() now puts the linked elements back in
the order the file listed them. Elements linked but not in that list
keep their place after it.

Test: tst_resaveunchanged::savedLinkOrderKept reverses a coil's two
contacts in m_000.qet and checks a resave keeps that order; it fails
without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Wga9B92BNurDEtxxtCDxf
2026-10-10 16:54:26 +13:00
ispyisail 80c8e297ec Regenerate the English source conversion on master 8b5a09f3fc
Generated by tools/qet-en-source/regenerate.sh, not edited by hand. The
tree is the fresh conversion; this commit only joins it to the PR's
history so the branch is never force-pushed.
2026-10-09 05:05:49 +13:00
Beat Hangartner 1023b38f8e Sort elements and terminals by position with a strict weak ordering
Two position comparisons handed to std::sort were not strict weak
orderings, which std::sort requires; with the wrong kind of comparator
the sort is undefined behaviour (libstdc++ can read past the range,
MSVC debug builds assert "invalid comparator").

- comparPos(), used when renumbering the elements of a project, ended
  with "<=" on x and y, so two elements at the same position (pasted at
  the origin, placed by a script, stacked symbols) were each "before"
  the other.
- The terminal numbering dialog compared positions with a 1 px
  tolerance ("within 1 px counts as aligned, then compare the other
  axis"), which is not transitive: terminals at x = 2, 1.1 and 0.2 give
  a < b, b < c and c < a.

Move the comparisons into positionorder.h, a header-only helper:
xThenY()/yThenX() with "<", and roundedXThenY()/roundedYThenX(), which
round the positions to whole pixels first so that items a fraction of a
pixel apart still count as aligned, as the tolerance meant to, while
staying transitive. comparPos() keeps its folio and row-letter stages
and calls xThenY() for the last one.

No file-format change. Elements at distinct positions sort exactly as
before; the terminal order changes only for terminals less than a pixel
apart that straddle a half-pixel boundary.

Tests: tst_positionorder checks each order on a grid of awkward
positions by brute force (irreflexive, asymmetric, transitive, with a
transitive equivalence), the three-terminal cycle, twenty items at one
position, and that std::sort leaves the list sorted and intact. The
helper is new, so the test cannot fail on master; the call sites are
the two replacements in the diff.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
2026-10-08 06:03:31 +02:00
ispyisail f5962ecd06 Make English the source language of the interface text
Every tr() source string in the code and the forms is now the English
text, and French is a translation (qet_fr.ts) like the other languages.
Generated, not hand-edited: master ef795a21e5 converted by
qet-en-source (harness 4c4ddc9).

3144 messages get an English source, 665 already had one, 0 left French; 8 shared-wording pairs disambiguated.
C++: 2904 literals in 233 files. Forms: 809 strings in 65 files. All 34 .ts files re-keyed; qet_fr.ts filled.
lupdate check: PASS. What each of 38 languages shows, old vs new: same 118266, English instead of French 38433, comment fallback 51, differences 0.
2026-10-08 12:50:13 +13:00
Beat Hangartner e3a1a67c66 Recognise seqhf_1 when loading sequential numbers saved the old way
Before the <sequentialNumbers> element, an element's or conductor's
sequential numbers were saved as the attributes sequ_1, sequf_1,
seqt_1, seqtf_1, seqh_1 and seqhf_1. The readers check for one of them
to take the old route, and all three lists had the same slip: sequf_1
twice, seqhf_1 never (Element::fromXml, Conductor::fromXml, and
readSequence() in the project database). A file whose only old
sequence was the hundred-folio one took the new route, found no
<sequentialNumbers>, and lost it; the database built its labels from
an empty sequence instead of refusing the fast path as it does for the
other five attributes.

Name seqhf_1 in the three lists.

No file-format change: nothing is written differently, and a file with
any of the other five attributes loads exactly as before.

Tests: tst_legacysequentialattributes runs the binary's --resave on a
fixture whose element and conductor carry only seqhf_1 (and a second
pair carrying sequ_1 as a control) and reads the <sequentialNumbers>
written back. The two seqhf_1 cases fail on master.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
2026-10-07 23:38:34 +02:00
ispyisail 592cbdbf0b Keep the texts of turned symbols horizontal, per project (#1335)
A project can now keep the texts drawn in its symbols, and the names of
their terminals, horizontal when a symbol is turned: Project properties >
General, "Garder horizontaux les textes des symboles pivotés". The box of
each text turns with the symbol; the text does not, and reads as it does
in the symbol itself.

It is a project setting, saved as <symbol_texts upright="true"/> and
only when on. A new project starts with it on; a project saved without
it (every existing one) reads with it off, so it looks and saves exactly
as before, and looks the same on every computer. The MCP server's new
projects start with it on too.

It builds on the mirror of #1354, which already redraws the texts of a
mirrored symbol readable: what the symbol does to its texts is now its
mirrors and, with the setting on, its turn (Element::symbolTextsTransform()).
ElementPictureFactory caches one drawing per such transform, terminal
names undo it the same way, and the DXF export places the texts alike.

The fields of a symbol (label, comment...) already keep their angle with
"Garder la rotation visuelle" and are left as they are.

Known limit: two texts stacked in a symbol end up side by side when it is
turned, and can overlap when kept horizontal; the setting can be turned
off for such a project.

Test: tst_uprightsymboltexts turns the symbols of a folio and checks the
angle of each motor's "M" in the DXF, with the setting off and on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:43:04 +13:00
ispyisail a3b1468b0f Mirror: terminal names, reload drawings, turned text groups, a leak
Found by review of #1354:
- Terminal names (shown on the folio) read backwards on a mirrored
  symbol: Terminal::paint() now mirrors them back about the centre of
  their box, in the frame they are drawn in.
- "Reload the drawings of the symbols" left mirrored symbols with the old
  drawing: reloadPicture() refreshes the mirrored pictures too.
- Turning a group of texts on a mirrored symbol left the group's
  readable mirror about the centre of its old box until the next layout:
  ElementTextItemGroup::setRotation() reapplies it, as texts already do.
- The mirrored build of a symbol's drawing leaked the texts of its
  primitives, which only the drawing without mirror keeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:13:27 +13:00
ispyisail a5b5d49b98 Add horizontal and vertical mirror for symbols on a folio (#1335)
Edit > "Miroir horizontal" (M) and "Miroir vertical" (F), also in the
folio's right-click menu, mirror the selected symbols in place. The keys
are the ones the element editor uses for the same two actions.

An element keeps two mirrors about its own axes, applied before its
rotation, and saves them as mirror="horizontal|vertical|both" on its
<element> (written only when set, so other projects save byte for byte
as before). On a symbol turned by 90 or 270 degrees, a mirror of the
folio is the other mirror of the symbol itself, so the rotation never
changes: a label kept upright does not swing round, and "Pivoter" still
turns a mirrored symbol clockwise.

- Terminals face the mirrored way (Terminal::orientation()), so wires
  follow.
- The symbol stays where it was: its centre is kept, on the grid, since
  the hotspot is often a corner.
- Texts read normally. The element's texts, text groups and cross
  reference are mirrored a second time about the centre of their own box
  (Element::keepReadable()), and ElementPictureFactory draws the texts
  of the symbol itself the same way, in a cached picture per mirror.
  Groups and cross references held at the bottom of the folio stay
  centred under their element.
- DXF export mirrors the symbol's lines, arcs and texts.
- Scripting: qet.mirrorElement(folio, uuid, vertical) and
  qet.elementMirror(folio, uuid); live mode may run both menu commands;
  the MCP server gets a mirror_element op and qet_diff reports mirrors.

Not done: the parts of a PLC table drawn at run time
(Element::drawPlcTable()) are not kept readable on a mirrored PLC; the
project database has no column for it, as it has none for the rotation.

Test: tst_scriptmirror mirrors a symbol through --run, checks every
terminal's side and facing, the round trip, undo, a save and reload,
and a symbol turned by 90 degrees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:05:17 +13:00
Andre Rummler d76f3528ae AutoNum: a lot of changes
Schemas now get saved with a UUID and do not have to be looked up according to the form. They can be renamed and it gets reflected in existing elements. The GUI for
editing them is now much easier, e.g. forms can be reshuffled. A formula can be applied. Many options now also for conductors and sheets.
2026-10-03 18:09:33 +02:00
ispyisail 20e134f2bd Add a wires-per-terminal limit, with an application master switch
Discussion #1158: a real terminal takes one or two wires (four with
double ferrules), a folio report is a virtual point that carries one, but
QElectroTech lets any number of wires be connected anywhere.

Project properties > General gets a "Conducteurs par borne" group:
- the most wires a terminal may take (0, the default, is no limit);
- "one wire per folio report".
When set, a new wire that would go past the limit is refused: the
terminal shows red while dragging, as for any refused link, and a tooltip
says why on release. Auto-connect on placing, moving or multi-pasting a
symbol skips full terminals, and qet.addConductor() refuses with its own
message. Wires already drawn, opening, pasting and undo are never
refused.

Settings > General gets a master switch, on by default, that turns every
rule of this feature off in every project; the project group is greyed
with a note while it is off.

The rules live in wiringrules.cpp. The project setting is saved as
<wiring_rules .../> only when a rule is on, so a project that never used
it saves exactly as before; an older version drops it, which means "off".

tst_wiringrules covers the rules, the save round trip and, through the
real binary, that a wire past the limit is refused unless the master
switch is off (checked to fail with the limit check removed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
2026-10-03 21:42:12 +13: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 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
Laurent Trinques f10275cb22 Merge pull request #997 from ispyisail/revive/659-preserve-links
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m15s
Preserve master/slave links when pasting or duplicating a folio
2026-09-23 18:40:34 +02:00
Kellermorph 3457a88e81 Open the PLC master properties dialog at three times its width
Element::editProperty() gave PropertiesEditorDialog no size of its own,
so the dialog fell back to its sizeHint, which is sized for the compact
general-purpose editors it usually hosts. A PLC master instead shows a
six-column IO table that grows horizontally, and the dialog came out
too narrow to read those columns in.

For a master whose type is PLC, resize the dialog to three times its
natural width before exec(), keeping the natural height. The width is
clamped to the available screen so it cannot run off the display, and
every other element type opens exactly as before.
2026-09-23 10:28:00 +02:00
ispyisail de9b3eae06 Preserve master/slave links when pasting or duplicating a folio
Reviving #659, closed 2026-09-10 purely to clear a review backlog
(#630), not on merit. Rebuilt fresh against current master rather than
merged from the old branch (elementspanelwidget.cpp had drifted enough
that a textual merge risked silently losing content, as it did earlier
in this same session for a different revival). Builds discussion #607.

Cutting/copying a linked group of elements -- a relay coil with its
contacts, a PLC master with its slave I/O elements -- dropped the
master/slave link entirely. Traced end to end: Element::toXml() writes
each partner's uuid into <link_uuid>, Element::fromXml() reads it back
into a deferred, unresolved buffer (tmp_uuids_link), and the only code
that ever resolves that buffer is initLink(QETProject *) -- called
only from Diagram::refreshContents(), itself only called from full
project load and macro-block insertion. Neither DiagramView::paste()
nor ElementsPanelWidget::duplicateDiagram() ever call it, so
tmp_uuids_link is populated correctly and never resolved: the link is
silently dropped. duplicateDiagram() already knew this and worked
around it by calling clearPendingLinks() -- correct to not link back
to a stale source, but it meant folio duplication never preserved a
link either.

Added Element::initLink(const QList<Element *> &candidates) --
resolves against a caller-supplied list instead of a project-wide
search. The scoping is the subtle part: right after the XML round-trip
and before uuids are renewed, a pasted/duplicated element's
tmp_uuids_link still holds its source's original partner uuid, which
at that exact moment still equals the not-yet-renewed uuid of that
partner's own copy, if it was carried along in the same batch.
Resolving only within the batch is what stops a linked pair pasted
together from matching an original element left elsewhere that
happens to still carry that same soon-to-be-replaced uuid. If only one
half of a linked group is in the batch, its entry finds no match and
is dropped -- the same "leave it unlinked" outcome as before.

Wired into PasteDiagramCommand::redo(), before the existing newUuid()
loop and gated by the same first_redo flag. Wired into
duplicateDiagram() the same way, replacing its clearPendingLinks()
call (initLink() clears tmp_uuids_link internally, matched or not).

Verified live -- the original PR's own test plan left both of these
unchecked, so this closes that gap rather than repeating it. Built a
project with a linked PLC master/slave pair (qet-mcp's link_elements),
then drove the real interaction under Xvfb:

  Ctrl+A, Ctrl+C, Ctrl+V:
    originals   95ad58fc <-> e728632c   (unchanged)
    pasted      513e6bf8 <-> 29aa60b4   (linked to each other)

  Right-click folio > "Copier et coller":
    originals   95ad58fc <-> e728632c   (unchanged)
    duplicated  0a33ccb4 <-> 3264fe66   (linked to each other)

Neither copy links back to an original or comes in unlinked. Qt 6.10.2,
ctest 13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:37:45 +12:00
ispyisail 29d16c3337 Fix #591's resize handles: reachable only via Shift/right-click, wrong position
Two bugs reported by @arummler on #591 after merge:

  "It works but to select the text field one has to right click on
  it...I think there are competing handlers or something."
  "the drag elements should be on the border of the box. In the
  moment they appear directly left and right from the text."

Both reproduced headlessly (scripts/qet-gui-dialog.sh) against a fresh
build of current master and root-caused before touching anything.

Selection: DynamicElementTextItem::mousePressEvent() forwards a plain
click (no Shift) straight to parentElement()->mousePressEvent(), by
design and pre-existing -- it's what lets dragging a symbol by its own
label move the whole symbol rather than just the label. That's correct
and untouched here. But it means a plain click leaves the *parent*
selected, not the text, and #591's handles were wired only to the
text's own ItemSelectedHasChanged -- so they were only reachable via
Shift+click or a right-click's context menu (which happens to select
the item under the cursor for its own context menu, unrelated to the
Shift path), neither of which anyone reaches for to resize a text.
Confirmed with screenshots at each step, including that Shift+click
already reached the existing (if misplaced) handles correctly.

Fix: DynamicElementTextItem::refreshResizeHandlesVisibility() shows the
handles when either the text itself or its parent element is selected,
and Element gets an itemChange() override (it had none) that calls it
on each of its own texts when the element's own selection changes. Both
sides driven from itemChange(), Qt's own hook for exactly this and the
same one already used for the text's own selection.

First attempt drove this from paint() instead, since the PR's own
updateResizeHandlesPos() already runs there. That crashed reproducibly
(SIGABRT) on deselecting a text: paint() runs while QGraphicsScene
iterates its item list to draw it, and addResizeHandles()/
removeResizeHandles() mutate that list via QGraphicsScene::addItem()/
removeItem(), which cannot safely happen mid-iteration. Caught it with
the same headless repro before it went anywhere near a PR, moved the
logic to itemChange(), and re-ran the full sequence -- select, resize,
undo, deselect, twice through -- clean.

Position: updateResizeHandlesPos() placed the handles on frameRect(),
which is a box sized to the text's natural (idealWidth()) content and
then re-centred inside boundingRect() -- it does not grow with
textWidth(). Once a text has been widened, frameRect() stays tight
around the glyphs while boundingRect() -- the box QGraphicsView actually
outlines as the selection, and the box a user drags relative to -- grows
around it, leaving the handles stranded well inside the visible
selection border. Fix: position them on boundingRect() instead, which
does track textWidth(); confirmed by widening a text and checking the
handle lands exactly on the new edge rather than partway across it.

Verified headlessly end to end on the original report's own element
("motor off" on grafcet.qet, folio 1): a single plain left-click (no
Shift, no right-click) now shows both handles at the true box border;
dragging resizes correctly and the handle tracks the growing edge;
Ctrl+Z restores the -1 auto-width sentinel and the handles stay at the
reverted position; clicking away removes them; repeated twice with no
crash. Qt 6.10.2, ctest 12/12, no new warnings in either changed file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 07:30:39 +12:00
Andre Rummler 54e61d482d Fix: "group rotate" of elements detached the conductors as those were not rotated. 2026-09-18 12:35:08 +02:00
Laurent Trinques 9004db1d99 Reload element drawings: keep old drawing on failure, skip elements whose geometry changed (#802)
- Element::reloadPicture() now returns a ReloadPictureResult and never
  clears the current drawing before a successful rebuild: a missing or
  unreadable definition leaves the element as it was instead of blank.
- Elements whose size, hotspot or terminals (added, removed or moved)
  differ from the new definition are not redrawn: the new drawing would
  no longer match their bounding rect and live terminals.
- The action lists those elements and warns that they must be removed
  and re-inserted, which deletes the conductors already connected to
  them.
- Status tip states the action is not undoable.
2026-09-16 21:35:17 +12:00
ispyisail 43d27a9563 Add "Reload element drawings" to refresh placed elements (#802)
A placed element is drawn once from its definition at construction --
buildFromXml() only turns terminal/input/dynamic_text tags into live
child objects, every other primitive (line, rect, ellipse, polygon,
arc, text) is pre-rendered into a QPicture by ElementPictureFactory,
cached forever under the element's uuid with no invalidation path
anywhere in the codebase. Edit and save a symbol's drawing and every
already-placed instance keeps showing the old one until the project
is closed and reopened.

Fix, scoped to what is safe to do without ever risking a conductor or
a dynamic text's per-instance state:

- ElementPictureFactory::dropCache(location) forgets the cached
  drawing for one location, so the next fetch rebuilds it from the
  definition's current content.
- Element::reloadPicture() re-fetches and repaints one instance.
- Projet > "Recharger les dessins des éléments": walks every diagram,
  drops each distinct location's cache once, then reloads every placed
  instance.

Deliberately does not touch terminals or dynamic texts -- a definition
whose terminal positions moved still needs the existing remove-and-
reinsert workflow, since terminals are what conductors are attached to
and a wrong guess there would silently misconnect wires.

Verified: build clean, ctest 6/6. Triggered the new action on a real,
densely-wired project (76 elements) via exact keyboard-menu navigation
cross-checked against the menu's own addAction order -- ran to
completion, correct confirmation dialog, no crash, diagram unchanged
and uncorrupted afterward. Could not complete a live edit-and-watch-
it-update trace: opening the element editor on a selected item via
GUI automation was unreliable in this environment (same class of
friction as PR #888), and this sandbox has no file-based (common://)
element to mutate on disk as a shortcut -- every example project
embeds its elements. The mechanism itself is traced correct:
ElementsLocation::xml() for an embed:// location reads the project's
live in-memory collection DOM on every call, so a dropped cache
rebuilds from whatever was most recently saved.

Refs #802 (own analysis comment, 2026-08-31).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 16:24:44 +12:00
ispyisail 9afba7091c Merge branch 'master' into fix-nonfinite-coordinate-validation 2026-09-12 23:07:47 +12:00
ispyisail c69e5747a1 Add position-lock checkbox to element properties (issue #801)
Elements already inherited QetGraphicsItem::isMovable()/setMovable() --
the same mechanism images and drawn shapes use for their "lock position"
checkbox -- but nothing exposed it in the element properties panel, and
Element::toXml()/fromXml() never persisted it.

- ElementPropertiesWidget::generalWidget(): add a "Verrouiller la
  position" checkbox mirroring ShapeGraphicsItemPropertiesWidget's
  m_lock_pos_cb, toggling the element's inherited setMovable().
- Element::toXml()/fromXml(): persist is_movable, same attribute name
  and default-true behavior as DiagramImageItem/QetShapeItem.

Verified via headless --resave round-trip: is_movable="0" survives
load -> save unchanged, existing elements without the attribute default
to movable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdWpDp3TrPKbHnv7YUcNJj
2026-09-01 07:16:32 +12:00
ispyisail 68e507a522 Reject non-finite element/terminal coordinates on load (fixes #781, #782)
QString::toDouble() reports a successful conversion for "nan"/"inf"/
"-inf" -- confirmed directly -- so the existing conv_ok checks in
Element::valideXml() and Terminal::valideXml() never caught a
non-finite x/y. A NaN-positioned element reaching the scene can hang
QGraphicsScene::addItem() forever: an existing conductor's itemChange()
runs a collision test (calculateTextItemPosition() ->
QGraphicsItem::collidesWithPath()) whose underlying QPathClipper spins
without terminating when fed a NaN-valued QPainterPath, since NaN
breaks the ordering comparisons the clipping algorithm's termination
depends on (#781). Short of that, a non-finite value that doesn't
happen to trigger a collision test simply gets written straight back
out on save with nothing to stop it (#782).

Element::valideXml() and Terminal::valideXml() now also check
qIsFinite() on the parsed x/y, rejecting the whole item the same way a
missing attribute already does. DynamicElementTextItem::fromXml() has
no such reject-the-item gate (void return, no caller check), so its x/y
are clamped to 0 instead -- the same fallback the attribute lookup
already uses when x/y is missing entirely.

Verified against both original findings' exact repro steps:
- #781: Habitat-Unifilaire.qet with x="nan" on one element -- hung
  (SIGTERM'd by a 25s timeout) on an unfixed build, resaves cleanly
  (exit 0) on this one.
- #782: grafcet.qet with y="nan" on a dynamic_elmt_text -- the value
  passed straight through to the resaved file on an unfixed build;
  clamped to 0 on this one. The specific field is now stable (y="0" on
  two consecutive resaves) where it read "nan" both times before.
  (grafcet.qet has an unrelated, already-known, unmerged fix
  (PR #779) for element/terminal-order non-determinism, so a whole-file
  diff across resaves still differs for reasons unconnected to this
  change -- checked the specific once-NaN field in isolation instead.)
- Also checked -inf on affuteuse_250h.qet: same rejection, same result.

Full qet-dbcheck.py sweep of the unmutated example corpus (23
projects), 0 regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:23:05 +12:00
Kellermorph 2f6fb7808a fix 2026-08-22 09:52:01 +02:00
Laurent Trinques 06b20c483e Merge pull request #645 from ispyisail/feature-autonum-undo
Cover auto-numbering counter changes with undo/redo (discussion #608)
2026-08-06 08:49:33 +02:00
Kellermorph 4f9474a05f PLC Fix scroll sync, data persistence, font defaults, copy/paste, and layout fixes 2026-08-06 07:33:39 +02:00
ispyisail 2e0fe44174 Cover auto-numbering counter changes with undo/redo
Placing an auto-numbered element or conductor advances a shared
NumerotationContext counter (QETProject::addConductorAutoNum/
addElementAutoNum) as a side effect that sat entirely outside the undo
stack. Undoing the placement removed the visible number but left the
counter advanced, so every undo of an auto-numbered placement silently
burned a number, with no way to get it back short of a manual reset.

Adds SetAutoNumContextCommand, a small QUndoCommand storing the old/new
NumerotationContext and calling the matching add*AutoNum() setter on
undo()/redo() -- the same shape QPropertyUndoCommand already uses next
to it in ConductorAutoNumerotation::applyText().

Wires it into the two conductor call sites (the static newProperties(),
and numerateNewConductor(), both in ConductorAutoNumerotation) and the
element call site (Element::setUpFormula(), called from
DiagramEventAddElement::addElement() when a new element is dropped onto
a diagram). setUpFormula() now takes an optional parent QUndoCommand;
addElement() calls it before pushing its own undo_object so the counter
change lands in the same undo macro as the element's placement -- one
Ctrl+Z reverts both together, instead of leaving the counter adrift.

The project-properties config dialog's own add*AutoNum() calls (editing
the numbering rule itself, not a side effect of placing something) are
deliberately left untouched, as are the load-time folio-sequential
bookkeeping calls in Diagram::loadElmtFolioSeq()/loadCndFolioSeq() and
the bulk folio-renumbering passes in QETProject -- none of those run as
part of an undoable user gesture.

Implements the scope proposed in discussion #608.
2026-08-03 13:19:51 +12:00
Kellermorph e91bab14fe Update 2026-08-01 14:26:39 +02:00
Kellermorph 4c1f82f814 Fix-PLC-Manager 2026-07-31 21:02:07 +02:00
Kellermorph b3a8ae898e PLC Manager 2026-07-27 22:16:52 +02:00
Kellermorph b025bd205d Implement slave contact groups — label transfer, terminal assignment and UI fixes 2026-07-17 09:47:32 +02:00
Kellermorph 4e0c075575 Fix grid and guide startup behavior and update german translation 2026-06-16 17:41:03 +02:00
Kellermorph f416c2a97e New element: Line definition 2026-05-21 20:47:44 +02:00