Add "Export to SVG" to the element editor

Implements discussion #605. The diagram editor can already export a folio
to SVG; the element editor, where a single .elmt symbol is drawn, had no
export capability at all -- confirmed by grepping its header for "export"
before starting: nothing.

## Renders the live scene, not ElementPictureFactory's cache

The discussion proposed sourcing this from ElementPictureFactory's cached
per-element QPicture (m_pictures_H), the one used for the elements-panel
preview icons. Checked that cache's actual invalidation before building on
it: nothing in the editor ever tells it to drop an entry on edit, and it is
keyed by the element's on-disk uuid. So for any element already previewed
once in the panel, exporting from the cache would silently produce stale
content after any edit; for a brand-new, never-saved element, no entry
would exist at all. Neither is acceptable for a File > Export action a
user expects to reflect what's on screen right now.

Renders ElementScene directly instead, the same way
ExportDialog::generateSvg() already renders the live Diagram for the
diagram editor's own SVG export: no new drawing logic, only a new playback
target (QSvgGenerator instead of the screen), sized to the element's own
content bounds via the existing elementSceneGeometricRect() helper.

## Hotspot cross excluded from the export

ElementScene::drawForeground() draws the red origin/hotspot cross on every
render() call, unconditionally -- it's an editing aid, not part of the
element being drawn, and diagram editor's SVG export has no equivalent
problem since Diagram doesn't draw one. Added a settable
hotspotVisible flag, defaulting to true (the existing editing view is
completely unaffected) and turned off only for the duration of the export
render() call.

## Verified end-to-end via a real Xvfb session, not just a build

Opened a real shipped element (en_60617_05_06_04.elmt, "Phototransistor"),
exported it, and rendered the resulting SVG back to a bitmap with a small
QSvgRenderer-based harness -- pixel-identical in shape to the element as
shown in the editor. Confirmed the file is valid XML and contains no
red/#ff0000 stroke (the hotspot cross did not leak in).

Then the case the whole "render live, not cached" decision was about:
opened the same element, drew a new line with the line tool, and exported
again *without saving*. The new line is present in the exported SVG.
git status on the source .elmt file after both exports shows it completely
untouched -- the export is read-only and reflects live, unsaved editor
state, exactly the property a cache-based implementation would have gotten
wrong.

Built clean, no new warnings.
This commit is contained in:
ispyisail
2026-08-02 23:06:12 +12:00
parent ec70bdc0a3
commit 8a72230916
5 changed files with 105 additions and 0 deletions
+4
View File
@@ -317,6 +317,10 @@ void ElementScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
*/
void ElementScene::drawForeground(QPainter *p, const QRectF &)
{
if (!m_hotspot_visible) {
return;
}
p -> save();
// desactive tout antialiasing, sauf pour le texte
+10
View File
@@ -91,6 +91,8 @@ class ElementScene : public QGraphicsScene
QPointer<CustomElementGraphicPart> m_single_selected_item;
bool m_hotspot_visible = true;
// methods
public:
ElementData elementData();
@@ -133,6 +135,14 @@ class ElementScene : public QGraphicsScene
void addItems(QVector<QGraphicsItem *> items);
void removeItems(QVector<QGraphicsItem *> items);
/// Whether drawForeground() draws the red hotspot cross. On by
/// default (the normal editing view); turned off around a
/// render() call meant to capture the element's actual content
/// only, e.g. exporting to SVG -- the cross is an editing aid,
/// not part of the drawn symbol.
void setHotspotVisible(bool visible) {m_hotspot_visible = visible;}
bool hotspotVisible() const {return m_hotspot_visible;}
protected:
void mouseMoveEvent (QGraphicsSceneMouseEvent *) override;
void mousePressEvent (QGraphicsSceneMouseEvent *) override;
+80
View File
@@ -53,6 +53,8 @@
#include <QSettings>
#include <QActionGroup>
#include <QFileDialog>
#include <QSvgGenerator>
/**
* @brief QETElementEditor::QETElementEditor
@@ -1377,6 +1379,84 @@ bool QETElementEditor::on_m_save_as_file_action_triggered()
return false;
}
/**
@brief QETElementEditor::on_m_export_svg_action_triggered
Export the element currently open in this editor to a standalone SVG
file.
Renders the live ElementScene directly, the same way
ExportDialog::generateSvg() renders the live Diagram for the diagram
editor's own SVG export -- not ElementPictureFactory's cached picture.
That cache is keyed by the element's saved-to-disk uuid and is never
invalidated on edit (nothing in this editor ever tells it to), so it
would silently export stale content for any element already previewed
once in the elements panel, and nothing at all for one that has never
been saved. Rendering the scene directly has neither problem and
always reflects exactly what is currently on screen, saved or not.
@return true if the file was written
*/
bool QETElementEditor::on_m_export_svg_action_triggered()
{
QString fn = QFileDialog::getSaveFileName(
this,
tr("Exporter en SVG", "dialog title"),
m_file_name.isEmpty() ? QETApp::customElementsDir() : QDir(m_file_name).absolutePath(),
tr("Image SVG (*.svg)", "filetypes allowed when exporting an element to SVG"));
if (fn.isEmpty()) {
return false;
}
if (!fn.endsWith(".svg", Qt::CaseInsensitive)) {
fn += ".svg";
}
QFile file(fn);
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::critical(this, tr("Échec de l'export"),
tr("Impossible d'écrire dans le fichier « %1 ».").arg(fn));
return false;
}
//Margin-less bounding rect of the element's own drawn content
//(lines, rects, terminals, text...), excluding the origin cross
//and other editor-only decoration -- see
//ElementScene::elementSceneGeometricRect()'s own doc comment.
//Falls back to itemsBoundingRect() for the rare element made up
//only of item types that helper deliberately excludes.
QRectF source_rect = m_elmt_scene->elementSceneGeometricRect();
if (source_rect.isEmpty()) {
source_rect = m_elmt_scene->itemsBoundingRect();
}
constexpr qreal margin = 5.0;
source_rect.adjust(-margin, -margin, margin, margin);
QSize target_size = source_rect.size().toSize();
if (target_size.isEmpty()) {
target_size = QSize(1, 1);
}
QSvgGenerator svg_engine;
svg_engine.setSize(target_size);
svg_engine.setViewBox(QRect(QPoint(0, 0), target_size));
svg_engine.setOutputDevice(&file);
QPainter svg_painter(&svg_engine);
svg_painter.setRenderHint(QPainter::Antialiasing, true);
svg_painter.setRenderHint(QPainter::TextAntialiasing, true);
//The hotspot cross is ElementScene::drawForeground()'s editing aid,
//drawn unconditionally on every render() call including this one
//unless told not to -- it is not part of the element being
//exported.
m_elmt_scene->setHotspotVisible(false);
m_elmt_scene->render(&svg_painter, QRectF(QPointF(0, 0), target_size), source_rect);
m_elmt_scene->setHotspotVisible(true);
svg_painter.end();
return true;
}
void QETElementEditor::on_m_reload_action_triggered()
{
//If user already edit the element, ask confirmation to reload
+1
View File
@@ -87,6 +87,7 @@ class QETElementEditor : public QMainWindow
void on_m_open_action_triggered();
void on_m_open_from_file_action_triggered();
bool on_m_save_as_file_action_triggered();
bool on_m_export_svg_action_triggered();
void on_m_reload_action_triggered();
void on_m_quit_action_triggered();
void on_m_deselect_all_action_triggered();
+10
View File
@@ -40,6 +40,7 @@
<addaction name="m_save_action"/>
<addaction name="m_save_as_action"/>
<addaction name="m_save_as_file_action"/>
<addaction name="m_export_svg_action"/>
<addaction name="separator"/>
<addaction name="m_reload_action"/>
<addaction name="separator"/>
@@ -262,6 +263,15 @@
<string>Enregistrer dans un fichier</string>
</property>
</action>
<action name="m_export_svg_action">
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/document-export.png</normaloff>:/ico/22x22/document-export.png</iconset>
</property>
<property name="text">
<string>Exporter en SVG</string>
</property>
</action>
<action name="m_reload_action">
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">