Three more read-only command-line tools for verifying connectivity and
cross-reference intelligence (useful for import / migration pipelines):
qelectrotech --export-nets <project.qet> <output.json>
qelectrotech --export-links <project.qet> <output.csv>
qelectrotech --resave <project.qet> <output.qet>
- --export-nets walks Conductor::relatedPotentialConductors() to group
every electrically-connected terminal into a net (potential), following
folio reports and terminal blocks across all folios. Output is JSON:
per net, the wire number and the list of {element, terminal, folio}.
This is the connectivity ground truth.
- --export-links reports each linkable element (master/slave/report/
terminal), its link type and the elements it links to, flagging
masters/slaves with no link as UNRESOLVED. Verifies coil<->contact
cross-references. Verified on examples/industrial.qet: 436 linkable
(76 master, 41 slave, ...), 37 unresolved.
- --resave loads the project and writes its XML back out, so an external
diff can reveal markup QET silently normalises on load
(tolerated-but-invalid XML). Round-trip verified: the re-saved project
reloads with identical diagram/element/conductor counts.
Extends the headless command-line interface with three read-only tools
aimed at validating projects and element libraries (useful for batch
import / migration pipelines):
qelectrotech --info <project.qet> [output.json]
qelectrotech --export-bom <project.qet> <output.csv>
qelectrotech --check-elements <element.elmt | directory>
- --info dumps a structural summary as JSON straight from QET's loaded
model: per-diagram element / conductor counts, page size, and the
number of unconnected ("free") terminals, plus project totals. Because
it uses the real loader it reports what the editor actually sees.
- --export-bom writes a bill of materials (one row per element) as CSV,
querying the project's own element_nomenclature_view (the same source
as the GUI BOM export). updateDB() is called first so the database is
populated in a headless run.
- --check-elements validates one .elmt file, or every .elmt under a
directory (recursively), against the element schema: XML well-formed,
root <definition type="element">, a usable bounding box, and terminal
count. Reports OK / WARN / FAIL per file and a summary; exit code is
non-zero if any file fails. Verified against the full bundled
collection (8483 elements): 0 false failures, agreeing with QET's own
loader (e.g. a negative-height element it tolerates is a WARN, not a
FAIL).
run() is restructured to handle the differing argument arity (info takes
an optional output, check-elements takes a path rather than a project).
renderDiagram() had a no-op stub: was_drawing_grid was set to false and
Q_UNUSED'd, so the editor grid still leaked into exported PDF/PNG/SVG.
Toggle Diagram::setDisplayGrid(false) around the render and restore the
previous state afterwards. Fixes all three export formats (they share
renderDiagram).
Reported by scorpio810 on #483.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the headless command-line export with two CSV outputs:
qelectrotech --export-cables <project.qet> <output.csv> wiring list
qelectrotech --export-wires <project.qet> <output.csv> wire numbers
- --export-cables reuses WiringListExport (one row per conductor).
- --export-wires reuses ConductorNumExport::wiresNum() (distinct wire numbers).
WiringListExport::toCsv() mixed CSV generation with the file dialog and
writing. Extracted the generation into a new const method toCsvString()
that returns the CSV; toCsv() now calls it and writes the result. This
makes the wiring list usable headlessly with no behavioural change to the
GUI export.
Addresses part of the CLI export requests (#162, #309): @pkess specifically
asked to "export all connections as a list".
Implements the long-requested batch/headless export
(bugtracker #171, GitHub #309): render a project's diagrams to files
without opening the GUI.
qelectrotech --export-pdf <project.qet> <output.pdf> one multi-page PDF
qelectrotech --export-png <project.qet> <output_dir> one PNG per diagram
qelectrotech --export-svg <project.qet> <output_dir> one SVG per diagram
main.cpp detects an export request before SingleApplication is created (so the
arguments are not forwarded to a running instance), spins up a plain
QApplication for rendering, and exits with the export's status code.
Rendering reuses Diagram::render() over
BorderTitleBlock::borderAndTitleBlockRect(), the same geometry the GUI
print/export path uses, so output matches the editor. Image files are named
NN_Title.<ext>.
New files: sources/cli_export.{h,cpp}, registered in
cmake/qet_compilation_vars.cmake.
The comments describing the terminal_names layout were inherited from a
previous version and no longer matched the actual assignment order:
terminal_names << nc_name << no_name << common_name;
i.e. [0]=NC, [1]=NO, [2]=Common
Update all affected comments to reflect the current storage order.
On macOS arm64 (Apple Silicon, Sequoia), exporting a PDF via
QPrintPreviewWidget leaves a black screen with only the mouse cursor
visible. Cmd+Tab restores the display; the exported PDF itself is
correct and clickable cross-reference links work fine.
Root cause
----------
requestPaint() is a slot connected to QPrintPreviewWidget::paintRequested.
Inside this slot the code was calling painter.end() manually, then
pdfConvertUriToGoTo(). On macOS arm64 the Qt5 paint cycle backed by
Metal/CALayer is asynchronous: closing the QPainter from *within* the
paintRequested slot interrupts the compositor before it has flushed the
backing store. The window goes black and never repaints because the
close() that follows immediately destroys it.
On x86_64 / older macOS (raster/CoreGraphics backend) the paint cycle is
synchronous, so the same code happened to work.
Fix
---
1. Remove the manual painter.end() and pdfConvertUriToGoTo() call from
requestPaint(). The QPainter is stack-allocated; it destructs normally
when the slot returns, which is the correct moment to flush the PDF.
2. In print(), capture the output file name before m_preview->print(),
then defer both pdfConvertUriToGoTo() and this->close() to the next
event-loop iteration via QTimer::singleShot(0, ...). This gives the
Metal compositor one full event-loop turn to finish compositing the
backing store before the window is torn down.
The fix is a no-op on all other platforms: QTimer::singleShot(0) posts
an event that fires in the very next iteration, so there is no perceptible
delay.
Tested
------
- macOS Sequoia 15.x, Apple M-series, Qt 5.15.x (arm64): black screen gone
- macOS 10.15 x86_64 VM, Qt 5.15.x: no regression
- Linux/Debian Qt 5.15.x: no regression
- PDF cross-reference links and GoTo/FitR destinations: unaffected
Fixes: black screen after PDF export on macOS arm64
directly to the related component on its folio, framing the target element.
When a project is exported to PDF, every cross-reference becomes an internal
link. Four kinds are covered:
- **Master → contact**: the contact list on a coil/relay (`CrossRefItem`)
- **Folio report → report**: report element labels (`DynamicElementTextItem`)
- **Slave → master**: the `(folio-position)` reference shown on a slave
(both standalone `DynamicElementTextItem` and grouped `ElementTextItemGroup`)
Clicking a link navigates **inside** the open document (no new viewer
instance) and zooms to frame the target element.
1. **Injection** (`printDiagram`, only when the paint engine is a `QPdfEngine`):
link rectangles are added with `QPdfEngine::drawHyperlink()`. The scene→page
mapping is rebuilt to match exactly what `QGraphicsScene::render()` does
(top-left anchored, `KeepAspectRatio`, **no centering**), and rectangles are
passed in device pixels — `pageMatrix()` already applies the 72/resolution
scale and Y-flip internally.
2. Each link URL encodes the target page and the target element's rectangle, in
PDF points on its own page: `#page=N&fitr=L_B_R_T`.
3. **Post-processing** (`pdfConvertUriToGoTo`, run after the painter is closed):
the `/S /URI` annotations are rewritten to native `/S /GoTo` actions with a
`/D [pageObj 0 R /FitR L B R T]` destination, and the xref table is rebuilt.
Pages are enumerated from the `/Pages /Kids` tree (reliable), not by scanning
for `/Type /Page` in raw bytes.
- `sources/print/projectprintwindow.{cpp,h}` — injection + post-processing
- `sources/qetgraphicsitem/crossrefitem.{cpp,h}` — `hoveredContactsMap()` accessor; store text rect for hit area
- `sources/qetgraphicsitem/dynamicelementtextitem.h` — `slaveXrefItem()` / `masterElement()` accessors
- `sources/qetgraphicsitem/elementtextitemgroup.h` — `slaveXrefItem()` accessor
- `qelectrotech.pro`, `cmake/qet_compilation_vars.cmake` — enable Qt gui-private headers (`<private/qpdf_p.h>`)
- **Fit-to-page mode only.** Links are not injected in tiled mode (multiple
pages per folio), which would require a per-tile transform.
- Uses Qt private API (`QPdfEngine::drawHyperlink`), stable since Qt 4 but not
part of the public API; the build links against `gui-private`.
- Page-tree enumeration assumes the flat `/Kids` array Qt produces (no nested
page trees).
- The frame zoom is controlled by two constants in `destRectPdf` (`pad`,
`minSide`) and can be tuned.
- Tested on Qt5; the `/Kids` parsing and `pageMatrix` behaviour are identical on
Qt6.
Fix a use-after-free crash (SIGSEGV in QRegion::begin, Qt5Gui+0x49af60)
confirmed by analysis of 19 coredumps. The crash was triggered when the
scene viewport clip region was freed during zoom/resize events while
QPicture::play() replayed drawPolyline commands through the scene painter.
Qt's raster engine then dereferenced a stale QRegionData pointer.
Root cause: CrossRefItem used three nested QPicture objects (m_drawing,
m_hdr_no_ctc, m_hdr_nc_ctc). The nested drawPicture() calls amplified
the use-after-free risk on any repaint event.
Fix: remove all QPicture from CrossRefItem entirely.
- updateLabel() now uses a QImage-backed dummy painter to compute
m_bounding_rect, m_shape_path and m_hovered_contacts_map geometry.
A bool m_update_map flag prevents the map from being overwritten
during paint().
- paint() calls drawAsCross()/drawAsContacts() directly on the scene
painter — no QPicture::play() anywhere in the class.
- buildHeaderContact() now draws NO/NC symbols directly onto the painter
instead of recording them into QPicture members.
Also fix mouseDoubleClickEvent: the element under the click is now found
directly from m_hovered_contacts_map using the event position, rather
than relying on m_hovered_contact which could be reset by hoverMoveEvent
between the two clicks of a double-click.
Also remove setBold(true) on terminal name labels: the Qt PDF/SVG/print
engine rendered bold at 4pt as extremely thick glyphs, making exports
unreadable. Normal weight at 4pt is correct and legible on all backends.
Fixes: SIGSEGV in CrossRefItem::paint() on zoom/resize
Fixes: double-click navigation unreliable on Xref contact symbols
Fixes: terminal name labels unreadable in PDF/SVG/print export
Add a new boolean property 'showTerminalName' (default: true) to
XRefProperties, with full persistence in XML and QSettings.
A new checkbox "Afficher les numéros de bornes dans les Xrefs" is
added to the XRefPropertiesWidget in the main display group (not in
the cross-only group), so it is active in both Cross and Contacts modes.
When unchecked, terminal names are hidden in all three rendering paths:
- drawContact() (Contacts mode: NO/NC/SW symbols)
- fillCrossRef() (Cross mode: NO and NC columns)
- setUpCrossBoundingRect() (Cross mode: bounding rect sizing)
Backward compatible: existing project files without the attribute
default to showTerminalName=true (no visual change).
Files changed:
sources/properties/xrefproperties.h
sources/properties/xrefproperties.cpp
sources/ui/xrefpropertieswidget.ui
sources/ui/xrefpropertieswidget.cpp
sources/qetgraphicsitem/crossrefitem.cpp
The previous sort used QString::toInt() to order terminal names,
which returns 0 for any string containing a non-numeric prefix
(e.g. "R1", "R2", "L1", "L2"...). This caused undefined sort order
and incorrect pole pairing in the Xref contact mirror.
Example: a 4-pole NC power contact with terminals R1..R8 was
displaying R1/R3, R5/R7, R2/R4, R6/R8 instead of the correct
R1/R2, R3/R4, R5/R6, R7/R8.
Fix: extract the trailing numeric part of each terminal name and
compare prefixes separately. If both names share the same prefix
and both have a trailing number, sort numerically on that number;
otherwise fall back to full string comparison.
This covers all naming conventions: "1"/"2"/"3", "R1"/"R2"/"R3",
"L1"/"L2"/"L3", etc.
Applied in both drawContact() and setUpCrossBoundingRect().
Three bugs were causing a crash (SIGSEGV in QRegion::begin) when
a power NC slave element was placed on a folio, even before linking
it to a master element.
1. m_drawing (QPicture) was never reset between updateLabel() calls.
QPicture accumulates paint commands — calling qp.begin() on an
existing QPicture appends to it rather than replacing its content.
After several updates (load, move, hover...) the picture became
corrupted and crashed on play().
Fix: reset m_drawing = QPicture() at the start of updateLabel().
2. m_drawed_contacts was only initialized to 0 in drawAsContacts(),
but not in drawAsCross(). When drawing in Cross mode, fillCrossRef()
called drawContact() with an uninitialized m_drawed_contacts value,
producing a garbage offset. The NC contact symbol uses drawPolyline()
with a sub-pixel Y coordinate (offset+2.5); with a random offset Qt
generated an invalid QRegion and crashed.
This explains why NC contacts crashed but NO contacts did not: the
NO symbol only uses drawLine() which is more tolerant of bad coords.
Fix: add m_drawed_contacts = 0 at the start of drawAsCross().
3. setUpCrossBoundingRect() used QRectF() (null rect) as the reference
rect for painter.boundingRect(), which can return invalid dimensions.
Additionally, height was accumulated incorrectly: united() + setHeight()
doubled the height at each iteration, causing an exponentially growing
bounding rect with multiple contacts.
Fix: use QRectF(0, 0, 500, 20) as reference rect and accumulate
height and width independently.