A script laying out a drawing could read where a symbol is
(elementGeometry) but not where its terminals are, so it could not place
one symbol with a terminal exactly in line with another's -- the one
thing that makes the wire between them straight. And it could read a
wire's drawn path only through conductorSegments(), which names the wire
by a terminal and so refuses any terminal carrying two wires: 282 of the
3120 wires in the shipped examples.
terminalPosition(folio, element, terminal) returns where a wire docks on
the terminal, in folio coordinates, and which way it leaves (n/e/s/w,
the element's rotation included). conductorPath(folio, uuid) returns any
wire's drawn path as points, by its uuid.
tst_scriptlayoutreads checks the two against each other on every wire of
a fixture: each path starts and ends where its terminals' positions say,
and leaves each the way it faces; where conductorSegments() can name a
wire, both give the same points; and a quarter turn turns the facing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
XRefProperties read the stored cross-reference position with
QMetaEnum::keyToValue() and cast the result straight to
Qt::AlignmentFlag. An empty value gives -1. Earlier versions saved
xrefpos="" into projects and the settings file (18 of the 29 shipped
examples that carry cross-reference settings have it), and -1 matches
no branch of DynamicElementTextItem::setXref_item(), so the
cross-reference stayed at (0,0): the top-left corner of the label,
on top of it.
fromXml() fell back to AlignBottom only when the attribute was
missing, and fromSettings() only when the key was missing (#296), so
an empty value kept producing the bad position and was saved back
empty on every save.
Both now go through one helper that returns AlignBottom for an empty,
unknown, or not-offered value. The next save writes "AlignBottom", so
affected projects heal once resaved.
tst_xrefpos covers fromXml(), fromSettings() and the rewrite on save;
it fails 5 of 14 cases without this change. Exporting
examples/2612_ats_singlephase.qet to SVG before and after shows every
slave cross-reference moving from over its label to below it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELKpbGxqJd7EFiTUypBtVe
Reported on #1178. Two things kept --common-elements-dir from working for
--run and the command-line export:
- main() runs both before QETApp exists, and only QETApp::parseArguments()
applied the folder options, so they were ignored. The application of
those options is now QETApp::applyDirectoryArguments(), static, and the
two headless branches call it too. They also drop the options from the
arguments they pass on: the export reads its project and output by
position, so "--export-pdf --common-elements-dir=X p.qet out.pdf" took
the option for the project ("Project not found").
- commonElementsDir() read the saved collection path before the
command-line one, so a user's own setting beat a folder given for this
run. The command line now comes first, in the GUI too.
Tested with a --run script adding a symbol that exists only in a given
folder: master fails with or without a saved collection path; this
resolves it, before or after --run, and over a saved path to another
folder. Without the option it still fails, and a saved path to the
folder still works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELKpbGxqJd7EFiTUypBtVe
QPdfWriter::setDocumentId() is new in Qt 6.8, QTimeZone::UTC in 6.5 and
QHashSeed in 6.2, so the build failed on Qt 6.4 (Ubuntu 24.04). Qt before
6.8 also writes no /ModDate and no XMP metadata, and setDocumentDate()
then gave up and left the time of the export in the file. Those two are
now rewritten when present, and no document id is set where Qt writes
none.
tst_pdfreproducible also checks that the xref table still matches the
file after the dates are rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJw3u5AdKQgQVPug8CzuMQ
lupdate had added them, but nobody filled them in, so the English
interface showed these strings in French: align, group and ungroup,
arc and fillet, centre on cursor, paste at the original position, wire
hops, the collection prefixes, the materials list, saving and loading
settings, the terminal-name check in the symbol editor, recovery files,
the macro recorder's messages and a few more.
About 30 of them were pre-filled by lupdate from an identical string
elsewhere and left unfinished; some of those guesses were wrong (a
dialog's "Annuler" offered as "Undo"). All are reviewed here. Plural
messages get both English forms. lrelease now reports 3487 finished,
0 unfinished. Other languages are untouched.
Terms follow the existing English: sheet (folio), terminal (borne),
conductor (conducteur), parts list (nomenclature); "liste de matériaux"
is "materials list" so it is not confused with the parts list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELKpbGxqJd7EFiTUypBtVe
Reported on #1178 after a first try of live mode on macOS:
- The English interface showed the live-mode setting in French. The
strings were in lang/qet_en.ts but untranslated. This fills in English
for live mode, the script buttons, the macro recorder and the Connect
an AI assistant dialog (101 entries); other features' untranslated
English entries are left for their own change.
- The setting is on Configurer QElectroTech > Général > Projets, but the
start-up warning, the MCP server's error and its README all said
"Configurer > Général". All three now name the Projects tab, and the
README and the server name the English menus and buttons.
- qet_live_screenshot grabbed the whole viewport, so a tall window around
a landscape folio came back mostly empty. It now crops to the folio's
frame and title block where they are on screen (the whole viewport if
the folio is scrolled out of sight), and says so in cropped_to_folio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reported on #1178: with "route": "avoid", two contacts one above the
other, wired bottom terminal to top terminal 30 px apart or closer, got
a five-segment loop (at 20 px: v 80, h -20, v -140, h 20, v 80) that ran
down through the lower contact and back up past the upper one.
The cause was exitPoint(): it walked out of a terminal until clear of
every obstacle, so with another symbol in front it walked through that
symbol, and the search then had to come back. The same walk made a wire
between two symbols inside a frame (a cabinet drawn as one element)
leave through the frame's side, go round, and cross back in.
The router now knows each terminal's own symbol (Request::start_symbol,
end_symbol; applyRoute() fills them):
- two terminals facing each other on one line with nothing between
them are joined straight, however close;
- the exit walks through the margin around other symbols but never
through one; a terminal pointing straight into another symbol gets
"no-route" instead of a route through it;
- an obstacle drawn around either end's own symbol is left out.
Without the symbols (the old Request), routes are as before.
Rerouting every wire of four shipped examples (perceuse, affuteuse_250h,
Polonez, industrial; 1331 wires): master routes 121 of them through
another symbol, this none (10 pass through a second symbol lying
exactly on an end symbol's rectangle, which no route can avoid). 99
wires that master routed through a symbol now get "no-route" and keep
their path. The 1232 wires both route are 6 % shorter in total
(436,144 -> 410,608 units) with 11 % fewer bends (2256 -> 2010).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rename the `near` lambda in Conductor::setPathPoints() to `isNear`:
`near` and `far` are empty macros in the Windows headers (minwindef.h).
- Reword the log when setPathPoints() refuses a route. "does not join the
two terminals" named only one of its reasons; the router always returns
the two terminals and at least one exit point, so a refusal means the
route is not a run of horizontal and vertical segments between them.
- README: running along another wire is penalised, not forbidden, so two
wires can end up drawn on top of each other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The note the op returns reads "...; frame 1223.25 x 791.25 pt"; the README
quoted it as "PDF page".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two exports of one project differed in the creation and modification
dates (the time of the export, in local time) and in the document id
(random). With SOURCE_DATE_EPOCH set (reproducible-builds.org),
--export-pdf now uses the time it names, in UTC, and a document id
derived from the project: version 5 of the project uuid with the
SHA-256 of the project file, so it is the same for the same file and
changes when the file does. Qt has no setter for the dates, so
PdfLinks::setDocumentDate() rewrites them in the document information
and the XMP metadata after the file is written, fixing the XMP /Length
and shifting the xref table. Without the variable nothing changes.
Only --export-pdf reads it; the print window's PDF export does not, as
a person exporting by hand wants the real date.
qet_export (misc/qet-mcp) gains "reproducible" and "source_date_epoch".
It reports whether the build honoured the variable, since an older one
ignores it.
tst_pdfreproducible exports examples/741.qet four times and requires
identical bytes and the epoch's date; it fails with either this change
or the hash seed fix reverted. Projects with cross-reference links also
need their links in a fixed order, which is a separate fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main() set QT_HASH_SEED=0 so that hashes, and so the XML a project is
saved as, come out the same in every run. Qt reads that variable once,
when the first hash is made, and that happens before main(): printed
right after the qputenv(), QHashSeed::globalSeed() was a different
random number in every run. The setting never took effect.
QHashSeed::setDeterministicGlobalSeed() does what the variable was
meant to, for every hash made from then on. The variable is kept for
the processes QElectroTech starts.
One visible effect: the PDF engine writes its fonts in QHash order, so
--export-pdf of the same project gave different files from run to run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS, Qt names the settings subfolder after the organization domain
when one is set, so QElectroTech read <folder>/qelectrotech.org/ while
main.cpp, tst_settingsdir and qet_mcp.py all use <folder>/QElectroTech/.
Clear the domain when QET_SETTINGS_DIR is set, on macOS only. Nothing in
QElectroTech reads it; SingleApplication hashes it into its key, so these
runs no longer hand off to an open QElectroTech.
test_qet_mcp.py: find true on PATH (/usr/bin/true on macOS), and compare
the found binary with samefile() on case-insensitive file systems.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
injectCrossRefLinks() drew each link as it found it: the contacts of a
cross-reference come from a QMultiMap keyed by Element pointer, and the
scene's items in stacking order. Both change from run to run, so the same
project exported twice gave two different PDFs whose only difference was
the order of the link annotations.
The links are now collected and drawn sorted by their rectangle on the
page (top, left, bottom, right), with the target as the tie-break. The
links themselves are unchanged. The print window's PDF export goes
through the same function.
tst_pdflinkorder exports examples/industrial.qet five times in separate
processes and requires the same links in the same order. It fails on
the old code (lists differ at index 2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The preset code uses QPageSize and std::floor whether or not QtPdf was
found; it compiled only because other headers bring them in.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The router costed every grid edge against every symbol and every wire
segment on the folio. On the 191-symbol, 366-wire Polonez example that
is about 145 million tests and 700 ms per route, so a qet_edit routing
some 40 wires there ran into the scripting run's 30-second limit.
Each symbol and wire now visits only the grid edges it can touch, found
by binary search, and applies the same exact test to them as before, in
the same order. Rerouting every conductor of the 23 example projects
gives the same 11,018 saved segments as before, now 22 ms per route on
Polonez instead of 700.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
setFolioBorder(folio, "preset", "tabloid-landscape") picks the column and
row counts and whole-number sizes that fill the sheet best without going
over it, as one ChangeBorderCommand. The title block and headers are
measured from the folio, so it holds for any template on either edge.
Sizes stay whole because the folio properties panel edits them in whole
pixels. Among fills within 1.5 pt of the sheet, the size nearest the
current one wins: from a new folio tabloid landscape is 23 x 70 by 12 x 82,
not 9 x 179 by 5 x 197. A0-A5, letter, legal, tabloid/ledger, each
-portrait or -landscape; folioPresets() lists them, and folioBorder() reads
"width" and "height" of the frame and title block.
Every preset exports on its sheet; landscape ones rely on the previous
commit, which turns a wide page before matching it to a sheet.
The MCP server checks the name up front, probes for folioPresets(), and
the op's note says what was chosen and the frame's size in points.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>