diff --git a/sources/SearchAndReplace/searchandreplaceworker.cpp b/sources/SearchAndReplace/searchandreplaceworker.cpp index 3f4e17727..5f4b55998 100644 --- a/sources/SearchAndReplace/searchandreplaceworker.cpp +++ b/sources/SearchAndReplace/searchandreplaceworker.cpp @@ -226,9 +226,12 @@ void SearchAndReplaceWorker::replaceConductor(QList list) project_->undoStack()->beginMacro(QObject::tr("Search / replace conductors properties.")); for (Conductor *c : list) { - ConductorProperties cp = applyChange(c->properties(), m_conductor_properties); + //Captured before the pushes below: QPropertyUndoCommand applies + //on push, so reading it afterwards would give back @a cp. + const ConductorProperties old_properties = c->properties(); + ConductorProperties cp = applyChange(old_properties, m_conductor_properties); - if (cp != c->properties()) + if (cp != old_properties) { QSet conductors_list = c->relatedPotentialConductors(true); conductors_list << c; @@ -239,6 +242,26 @@ void SearchAndReplaceWorker::replaceConductor(QList list) new_value.setValue(cp); project_->undoStack()->push(new QPropertyUndoCommand(cc, "properties", old_value, new_value)); } + + //The other side of a function separated terminal element + //only receives the voltage / protocol, and only when this + //replacement really changed it: the comparison is this + //replacement's own before / after (@a old_properties vs + //@a cp), never the far wire's own value, which would make + //every replacement overwrite it (@see + //Conductor::functionSeparatedConductors). + for (Conductor *cc : c->functionSeparatedConductors(true)) + { + ConductorProperties separated_cp = cc->properties(); + separated_cp.applyTensionProtocolChange(old_properties, cp); + if (separated_cp == cc->properties()) + continue; + + QVariant old_value, new_value; + old_value.setValue(cc->properties()); + new_value.setValue(separated_cp); + project_->undoStack()->push(new QPropertyUndoCommand(cc, "properties", old_value, new_value)); + } } } project_->undoStack()->endMacro(); @@ -351,6 +374,25 @@ void SearchAndReplaceWorker::replaceAdvanced( new_value.setValue(new_properties); project_->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value)); } + + //The other side of a function separated terminal element + //only receives the voltage / protocol, and only when + //this replacement really changed it: the comparison is + //this replacement's own before / after, never the far + //wire's own value (@see + //Conductor::functionSeparatedConductors). + for (Conductor *c : conductor->functionSeparatedConductors(true)) + { + ConductorProperties separated_properties = c->properties(); + separated_properties.applyTensionProtocolChange(old_properties, new_properties); + if (separated_properties == c->properties()) + continue; + + QVariant old_value, new_value; + old_value.setValue(c->properties()); + new_value.setValue(separated_properties); + project_->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value)); + } } } } diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp index 8214d4eea..5c4d5188f 100644 --- a/sources/cli_export.cpp +++ b/sources/cli_export.cpp @@ -283,7 +283,8 @@ int exportPdf(QETProject &project, const QString &output, } int exportImages(QETProject &project, const QString &format, - const QString &out_dir, bool showTerminals = false) + const QString &out_dir, bool showTerminals = false, + bool svgSmallPictures = false) { const QList diagrams = project.diagrams(); if (diagrams.isEmpty()) { @@ -306,8 +307,10 @@ int exportImages(QETProject &project, const QString &format, gen.setViewBox(QRect(0, 0, r.width(), r.height())); gen.setTitle(diagram->title()); QPainter painter(&gen); + diagram->setSmallPictures(svgSmallPictures); renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()), showTerminals); + diagram->setSmallPictures(false); painter.end(); } else { // png QImage image(r.size(), QImage::Format_ARGB32); @@ -1012,6 +1015,7 @@ int run(const QStringList &args) const bool showTerminals = filtered.removeAll("--show-terminals") > 0; const bool dxfBlocks = filtered.removeAll("--dxf-blocks") > 0; const bool dxfAttributes = filtered.removeAll("--dxf-attributes") > 0; + const bool svgSmallPictures = filtered.removeAll("--svg-small-pictures") > 0; // --no-slaves and --no-junctions leave rows out of --export-bom. const bool includeSlaves = filtered.removeAll("--no-slaves") == 0; const bool includeJunctions = filtered.removeAll("--no-junctions") == 0; @@ -1083,7 +1087,8 @@ int run(const QStringList &args) return resaveProject(project, output); if (format == "settb") return setTitleBlock(project, output, rest.mid(2)); - return exportImages(project, format, output, showTerminals); + return exportImages(project, format, output, showTerminals, + svgSmallPictures); } } // namespace CLIExport diff --git a/sources/cli_export.h b/sources/cli_export.h index efb3ad59e..09f66826f 100644 --- a/sources/cli_export.h +++ b/sources/cli_export.h @@ -44,7 +44,7 @@ namespace CLIExport { Usage: qelectrotech --export-pdf [--show-terminals] qelectrotech --export-png [--show-terminals] - qelectrotech --export-svg [--show-terminals] + qelectrotech --export-svg [--show-terminals] [--svg-small-pictures] qelectrotech --export-dxf [--show-terminals] [--dxf-blocks] [--dxf-attributes] qelectrotech --export-cables qelectrotech --export-wires @@ -71,6 +71,9 @@ namespace CLIExport { --dxf-attributes: as --dxf-blocks, and a symbol's texts (its label, function...) are attributes of its INSERT. LibreCAD does not show attributes. + --svg-small-pictures: in an SVG, each picture at most four times the + size it is drawn at instead of its full resolution: a much + smaller file, still sharp when printed. cables: wiring list (one row per conductor) as CSV. wires: list of distinct wire numbers as CSV. bom: bill of materials (one row per element) as CSV. diff --git a/sources/conductormultiedit.h b/sources/conductormultiedit.h index a6553e0d1..baf6898e8 100644 --- a/sources/conductormultiedit.h +++ b/sources/conductormultiedit.h @@ -62,33 +62,80 @@ namespace ConductorMultiEdit @brief targets @param selected : the edited conductors @param apply_all : true to add every conductor of their potentials + @param function_separated : set to the conductors that share the + potential of an edited one only by crossing a function separated + terminal element -- conductors undo() then gives the voltage / + protocol only, and no other property. nullptr, the default, leaves + those conductors out of the returned list entirely. + + A conductor the user picked themselves is never part of it: the + selection is the edit reaching that conductor, so it takes the + full edit however remote it is from the other selected ones -- + picking a wire on each side of a separation and editing them is + exactly that. Nor is a conductor that belongs to a potential, on + any path, however far that potential is reached. @return each conductor an edit reaches, once */ template - QList targets(const QList &selected, bool apply_all) + QList targets(const QList &selected, bool apply_all, + QSet *function_separated = nullptr) { QList list; QSet seen; + //The explicitly picked ones, so a separation met while walking + //the list cannot demote a wire the user asked to edit. + QSet chosen; + for (T *conductor : selected) + if (conductor) + chosen.insert(conductor); + //What the potentials reach, and what only a function separation + //reaches. A conductor can be in both -- wired normally on one + //path and across a separation on another -- and then belongs to + //the potential. + QSet potential_reached, separated_reached; + for (T *conductor : selected) { - //Already reached through an earlier conductor's potential: - //its own potential is the same one. - if (!conductor || seen.contains(conductor)) + if (!conductor) continue; - seen.insert(conductor); - list << conductor; - if (!apply_all) + //Already reached through an earlier conductor's potential or + //separation: its own potential adds nothing new to walk over + //-- unless the user picked it, because a separation may have + //put it in the list before its potential was ever reached. + const bool fresh = !seen.contains(conductor); + if (fresh) + { + seen.insert(conductor); + list << conductor; + } + if (!apply_all || (!fresh && !chosen.contains(conductor))) continue; const QSet potential = conductor->relatedPotentialConductors(); for (T *other : potential) { + potential_reached.insert(other); if (!seen.contains(other)) { seen.insert(other); list << other; } } + if (function_separated) + { + const QSet separated = conductor->functionSeparatedConductors(); + for (T *other : separated) + { + separated_reached.insert(other); + if (!seen.contains(other)) + { + seen.insert(other); + list << other; + } + } + } } + if (function_separated) + *function_separated = separated_reached - potential_reached - chosen; return list; } @@ -97,13 +144,18 @@ namespace ConductorMultiEdit @param targets : the conductors to change, see targets() @param shown : the properties as the panel showed them @param edited : the properties after the user's edit + @param function_separated : the targets of targets() that only a + function separated terminal element links to the edited + conductors: they get the voltage / protocol and nothing else + (@see ConductorProperties::applyTensionProtocolChange) @return one undo step giving each target the fields that differ between shown and edited, or nullptr if no conductor changes */ template QUndoCommand *undo(const QList &targets, const ConductorProperties &shown, - const ConductorProperties &edited) + const ConductorProperties &edited, + const QSet &function_separated = QSet()) { if (edited == shown) return nullptr; @@ -114,7 +166,10 @@ namespace ConductorMultiEdit { const ConductorProperties old_properties = conductor->properties(); ConductorProperties properties = old_properties; - properties.applyChanges(shown, edited); + if (function_separated.contains(conductor)) + properties.applyTensionProtocolChange(shown, edited); + else + properties.applyChanges(shown, edited); if (properties == old_properties) continue; diff --git a/sources/conductorproperties.cpp b/sources/conductorproperties.cpp index d312941f3..5549cb327 100644 --- a/sources/conductorproperties.cpp +++ b/sources/conductorproperties.cpp @@ -851,6 +851,32 @@ void ConductorProperties::applyChanges(const ConductorProperties &before, singleLineProperties.setPhasesCount(slp_after.phasesCount()); } +/** + @brief ConductorProperties::applyTensionProtocolChange + Copy into this the voltage / protocol when it differs between before + and after, and leave every other attribute as it is. This is the one + property a function separated terminal element lets through + (@see Conductor::functionSeparatedConductors): the voltage / + protocol describes the electrical node itself, while the text, the + function, the wire color... describe one side of the separation and + stay local to it. + + Both arguments describe one edit -- what was shown to the user, and + what the user made of it -- so that only an edit which really + changed the voltage / protocol passes it on. Passing the far wire's + own properties as @a before instead would make every edit overwrite + its voltage: the two sides differ on that field just as often as + not, so the comparison would hold no matter what was edited. + @param before : the properties as they were shown to the user + @param after : the properties after the user's edit +*/ +void ConductorProperties::applyTensionProtocolChange(const ConductorProperties &before, + const ConductorProperties &after) +{ + if (before.m_tension_protocol != after.m_tension_protocol) + m_tension_protocol = after.m_tension_protocol; +} + /** @brief ConductorProperties::defaultProperties @return the default properties stored in the setting file diff --git a/sources/conductorproperties.h b/sources/conductorproperties.h index c9cb0df84..36c9ac9d6 100644 --- a/sources/conductorproperties.h +++ b/sources/conductorproperties.h @@ -139,6 +139,8 @@ class ConductorProperties void applyForEqualAttributes(QList list); void applyChanges(const ConductorProperties &before, const ConductorProperties &after); + void applyTensionProtocolChange(const ConductorProperties &before, + const ConductorProperties &after); static ConductorProperties defaultProperties(); diff --git a/sources/diagram.h b/sources/diagram.h index 233ba48d9..cddb4d77e 100644 --- a/sources/diagram.h +++ b/sources/diagram.h @@ -133,6 +133,7 @@ class Diagram : public QGraphicsScene bool draw_terminals_; bool draw_terminal_names_; bool draw_colored_conductors_; + bool m_small_pictures = false; QString m_conductors_autonum_name; DiagramEventInterface *m_event_interface; @@ -303,6 +304,11 @@ class Diagram : public QGraphicsScene void setDrawTerminalNames(bool); bool drawColoredConductors() const; void setDrawColoredConductors(bool); + /// While true, pictures draw a copy of a few times the size + /// they are drawn at, not their full resolution (SVG export, + /// ExportProperties::svg_small_pictures) + bool smallPictures() const {return m_small_pictures;} + void setSmallPictures(bool small) {m_small_pictures = small;} QString title() const; bool toPaintDevice(QPaintDevice &, int = -1, int = -1, diff --git a/sources/editor/ui/elementpropertieseditorwidget.ui b/sources/editor/ui/elementpropertieseditorwidget.ui index 13dee5dcd..138cd9dfa 100644 --- a/sources/editor/ui/elementpropertieseditorwidget.ui +++ b/sources/editor/ui/elementpropertieseditorwidget.ui @@ -255,7 +255,7 @@ margin: 5px; font-weight: bold; - Potential separation + Function separation diff --git a/sources/exportdialog.cpp b/sources/exportdialog.cpp index 42e2f8a67..f0996e4dd 100644 --- a/sources/exportdialog.cpp +++ b/sources/exportdialog.cpp @@ -417,12 +417,14 @@ void ExportDialog::generateSvg( // genere une QPicture a partir du schema QPicture picture; + diagram -> setSmallPictures(epw -> exportProperties().svg_small_pictures); diagram -> toPaintDevice( picture, width, height, keep_aspect_ratio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio ); + diagram -> setSmallPictures(false); // "joue" la QPicture sur un QSvgGenerator QSvgGenerator svg_engine; diff --git a/sources/exportproperties.cpp b/sources/exportproperties.cpp index cd53bc806..1c4cfc404 100644 --- a/sources/exportproperties.cpp +++ b/sources/exportproperties.cpp @@ -39,6 +39,7 @@ ExportProperties::ExportProperties() : draw_colored_conductors(true), dxf_blocks(false), dxf_attributes(false), + svg_small_pictures(false), exported_area(QET::BorderArea) { } @@ -83,6 +84,8 @@ void ExportProperties::toSettings(QSettings &settings, dxf_blocks); settings.setValue(prefix % "dxfattributes", dxf_attributes); + settings.setValue(prefix % "svgsmallpictures", + svg_small_pictures); settings.setValue(prefix % "area", QET::diagramAreaToString(exported_area)); } @@ -125,6 +128,8 @@ void ExportProperties::fromSettings(QSettings &settings, false).toBool(); dxf_attributes = settings.value(prefix % "dxfattributes", false).toBool(); + svg_small_pictures = settings.value(prefix % "svgsmallpictures", + false).toBool(); exported_area = QET::diagramAreaFromString( settings.value( diff --git a/sources/exportproperties.h b/sources/exportproperties.h index c8feca461..39ca00c32 100644 --- a/sources/exportproperties.h +++ b/sources/exportproperties.h @@ -52,6 +52,7 @@ class ExportProperties { bool draw_colored_conductors; ///< Whether to render conductors colors bool dxf_blocks; ///< DXF: each symbol as a block, placed with an INSERT bool dxf_attributes; ///< DXF, with dxf_blocks: a symbol's texts as attributes of its INSERT + bool svg_small_pictures; ///< SVG: pictures at most 4x the size they are drawn, not full resolution QET::DiagramArea exported_area; ///< Area of diagrams to be rendered }; #endif diff --git a/sources/exportpropertieswidget.cpp b/sources/exportpropertieswidget.cpp index e2c9ea70b..2b248ccc5 100644 --- a/sources/exportpropertieswidget.cpp +++ b/sources/exportpropertieswidget.cpp @@ -68,6 +68,7 @@ ExportProperties ExportPropertiesWidget::exportProperties() const export_properties.draw_colored_conductors = draw_colored_conductors -> isChecked(); export_properties.dxf_blocks = dxf_blocks -> isChecked(); export_properties.dxf_attributes = dxf_attributes -> isChecked(); + export_properties.svg_small_pictures = svg_small_pictures -> isChecked(); export_properties.exported_area = export_border -> isChecked() ? QET::BorderArea : QET::ElementsArea; return(export_properties); @@ -94,6 +95,7 @@ void ExportPropertiesWidget::setExportProperties(const ExportProperties &export_ dxf_blocks -> setChecked(export_properties.dxf_blocks); dxf_attributes -> setChecked(export_properties.dxf_attributes); dxf_attributes -> setEnabled(export_properties.dxf_blocks); + svg_small_pictures -> setChecked(export_properties.svg_small_pictures); if (export_properties.exported_area == QET::BorderArea) { export_border -> setChecked(true); @@ -125,6 +127,7 @@ void ExportPropertiesWidget::setPrintingMode(bool mode) { export_border -> setVisible(!mode); export_elements -> setVisible(!mode); draw_bg_transparent -> setVisible(!mode); + svg_small_pictures -> setVisible(!mode); } /** @@ -235,6 +238,11 @@ void ExportPropertiesWidget::build() dxf_attributes -> setToolTip(tr("A symbol's texts (label, function...) become attributes of its block. LibreCAD does not show attributes.")); optionshlayout -> addWidget(dxf_attributes, 5, 1); connect(dxf_blocks, &QCheckBox::toggled, dxf_attributes, &QCheckBox::setEnabled); + + // pictures not at full resolution in an SVG: a much smaller file + svg_small_pictures = new QCheckBox(tr("SVG: smaller pictures"), groupbox_options); + svg_small_pictures -> setToolTip(tr("Pictures are stored at four times the size they have on the folio instead of their full resolution: a much smaller file, still sharp when printed")); + optionshlayout -> addWidget(svg_small_pictures, 5, 0); vboxLayout -> addWidget(groupbox_options); @@ -252,6 +260,7 @@ void ExportPropertiesWidget::build() setTabOrder(draw_colored_conductors, draw_bg_transparent); setTabOrder(draw_bg_transparent, dxf_blocks); setTabOrder(dxf_blocks, dxf_attributes); + setTabOrder(draw_bg_transparent, svg_small_pictures); // connexion du bouton permettant le choix du repertoire connect(button_browse, &QPushButton::released, this, &ExportPropertiesWidget::slot_chooseADirectory); @@ -269,6 +278,7 @@ void ExportPropertiesWidget::build() connect(draw_colored_conductors, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged); connect(dxf_blocks, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged); connect(dxf_attributes, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged); + connect(svg_small_pictures, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged); #else connect(draw_grid, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); connect(draw_border, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); @@ -279,5 +289,6 @@ void ExportPropertiesWidget::build() connect(draw_colored_conductors, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); connect(dxf_blocks, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); connect(dxf_attributes, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); + connect(svg_small_pictures, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); #endif } diff --git a/sources/exportpropertieswidget.h b/sources/exportpropertieswidget.h index 5b96e4273..5d850cbe6 100644 --- a/sources/exportpropertieswidget.h +++ b/sources/exportpropertieswidget.h @@ -67,6 +67,7 @@ class ExportPropertiesWidget : public QWidget { QCheckBox *draw_colored_conductors; QCheckBox *dxf_blocks; QCheckBox *dxf_attributes; + QCheckBox *svg_small_pictures; QRadioButton *export_border; QRadioButton *export_elements; QButtonGroup *exported_content_choices; diff --git a/sources/properties/terminaldata.h b/sources/properties/terminaldata.h index 7d269b4cd..f43f15ba6 100644 --- a/sources/properties/terminaldata.h +++ b/sources/properties/terminaldata.h @@ -107,10 +107,11 @@ class TerminalData : public PropertiesInterface @brief m_potential An optional, symbol-author-chosen grouping identifier shared by terminals that belong to the same physical terminal within - a multi-terminal block. When potential_isolating is enabled + a multi-terminal block. When the function separation checkbox + (element information potential_isolating) is enabled on the parent element, terminals sharing a non-empty, matching m_potential stay linked to each other; terminals - with different (or empty) values become isolated from each + with different (or empty) values become separated from each other. Empty by default. */ QString m_potential; diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index f6d3b8ae4..278fbbc6c 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -2010,9 +2010,16 @@ void Conductor::displayedTextChanged() @param all_diagram : if true search in all diagram of the project, false search only in the parent diagram of this conductor @param t_list : a list of terminal already found for this potential. + @param ignore_function_separation : if true, a terminal element whose + function separation is enabled does not stop the search, and the + conductors of both its sides are returned as one potential. Only + functionSeparatedConductors() passes true. + @sa functionSeparatedConductors @return a QSet of conductor at the same potential. */ -QSet Conductor::relatedPotentialConductors(const bool all_diagram, QList *t_list) +QSet Conductor::relatedPotentialConductors(const bool all_diagram, + QList *t_list, + const bool ignore_function_separation) { bool declar_t_list = false; if (t_list == nullptr) @@ -2035,7 +2042,8 @@ QSet Conductor::relatedPotentialConductors(const bool all_diagram, //Get the other terminals of the parent element of @terminal, who share the same potential //This is use for element type "folio report" and "terminal element" - for (Terminal *t : relatedPotentialTerminal(terminal, all_diagram)) + for (Terminal *t : relatedPotentialTerminal(terminal, all_diagram, + ignore_function_separation)) { if (!t_list->contains(t)) { @@ -2047,7 +2055,8 @@ QSet Conductor::relatedPotentialConductors(const bool all_diagram, other_conductors_list_t.removeAll(this); //Get the conductors at the same potential for each conductors of other_conductors_list_t for (Conductor *c : other_conductors_list_t) { - other_conductors += c->relatedPotentialConductors(all_diagram, t_list); + other_conductors += c->relatedPotentialConductors(all_diagram, t_list, + ignore_function_separation); } other_conductors += QSet(other_conductors_list_t.begin(),other_conductors_list_t.end()); @@ -2060,6 +2069,36 @@ QSet Conductor::relatedPotentialConductors(const bool all_diagram, return(other_conductors); } +/** + @brief Conductor::functionSeparatedConductors + Return the conductors that belong to this conductor's potential only + by crossing a function separated terminal element -- a terminal + element whose function separation checkbox (element information + potential_isolating) is enabled, the checkbox the user ticks to stop + sharing a terminal's properties with the other side of the terminal. + + Those conductors are deliberately not part of + relatedPotentialConductors(): no property may be copied over them + by default. One does cross that separation, though: the voltage / + protocol, which describes the electrical node itself. Callers that + push it over the whole potential use this method to reach the other + side (@see ConductorProperties::applyTensionProtocolChange). + + It is simply the potential computed with the separations ignored, + minus the potential that respects them. + @param all_diagram : if true search in all diagram of the project, + false search only in the parent diagram of this conductor + @return a QSet of conductor, this conductor excluded +*/ +QSet Conductor::functionSeparatedConductors(const bool all_diagram) +{ + QSet separated = relatedPotentialConductors(all_diagram, nullptr, true); + separated.subtract(relatedPotentialConductors(all_diagram)); + separated.remove(this); + + return separated; +} + /** @brief Conductor::diagramEditor @return The parent diagram editor or nullptr; diff --git a/sources/qetgraphicsitem/conductor.h b/sources/qetgraphicsitem/conductor.h index 844e97787..b1327682b 100644 --- a/sources/qetgraphicsitem/conductor.h +++ b/sources/qetgraphicsitem/conductor.h @@ -151,7 +151,10 @@ class Conductor : public QGraphicsObject virtual void setHighlighted(Highlight); QSet relatedPotentialConductors( const bool all_diagram = true, - QList *t_list=nullptr); + QList *t_list=nullptr, + const bool ignore_function_separation = false); + QSet functionSeparatedConductors( + const bool all_diagram = true); QETDiagramEditor* diagramEditor() const; void editProperty (); diff --git a/sources/qetgraphicsitem/diagramimageitem.cpp b/sources/qetgraphicsitem/diagramimageitem.cpp index 4c5f05145..14c99b60d 100644 --- a/sources/qetgraphicsitem/diagramimageitem.cpp +++ b/sources/qetgraphicsitem/diagramimageitem.cpp @@ -167,7 +167,28 @@ void DiagramImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * return; } - DarkImageRendering::paintPixmap(painter, pixmap_, m_adapt_to_dark_theme); + // SVG export with smaller pictures: a copy four times the size the + // picture is drawn at, when that is smaller than the picture. An + // SVG stores what it is given, so a photo shown at a tenth of its + // size would otherwise be written whole. + const QTransform world = painter -> worldTransform(); + const qreal drawn_scale = std::sqrt(std::abs(world.determinant())); + const QSize small_size = (QSizeF(pixmap_.size()) * drawn_scale * 4).toSize(); + if (diagram() && diagram() -> smallPictures() && !pixmap_.isNull() + && small_size.width() < pixmap_.width() + && small_size.height() < pixmap_.height() + && !small_size.isEmpty()) + { + const QPixmap small = pixmap_.scaled(small_size, Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + painter -> save(); + painter -> scale(qreal(pixmap_.width()) / small.width(), + qreal(pixmap_.height()) / small.height()); + DarkImageRendering::paintPixmap(painter, small, m_adapt_to_dark_theme); + painter -> restore(); + } + else + DarkImageRendering::paintPixmap(painter, pixmap_, m_adapt_to_dark_theme); Q_UNUSED(option); Q_UNUSED(widget); diff --git a/sources/qetgraphicsitem/terminal.cpp b/sources/qetgraphicsitem/terminal.cpp index b232d252d..ed29d86d9 100644 --- a/sources/qetgraphicsitem/terminal.cpp +++ b/sources/qetgraphicsitem/terminal.cpp @@ -1082,10 +1082,19 @@ void Terminal::setMasterLabelIndex(int index) @param terminal : to start search @param all_diagram :if true return all related terminal, false return only terminal in the same diagram of t + @param ignore_function_separation : if true, a parent element whose + function separation (element information potential_isolating) is + enabled does not keep its terminals apart: they all count as one + potential, the way they do when the option is disabled. Only the + callers that need the conductors a separation hides -- so that the + one property still allowed to cross it, the voltage / protocol, can + be applied there (@see Conductor::functionSeparatedConductors) -- + pass true; every other caller keeps the default. @return the list of terminal at the same potential */ QList relatedPotentialTerminal ( - const Terminal *terminal, const bool all_diagram) + const Terminal *terminal, const bool all_diagram, + const bool ignore_function_separation) { // If terminal parent element is a folio report. if (all_diagram && terminal -> parentElement() -> linkType() & Element::AllReport) @@ -1118,14 +1127,18 @@ QList relatedPotentialTerminal ( // If terminal parent element is a Terminal element. else if (terminal -> parentElement() -> linkType() & Element::Terminale) { - // English: Check if the user activated the potential isolation checkbox for this terminal - if (QET::infoFlagIsTrue(terminal->parentElement()->elementInformations().value(QStringLiteral("potential_isolating")).toString())) { - // English: Potential is isolated -- but terminals sharing this - // terminal's own, non-empty potential-group identifier - // (@see TerminalData::m_potential, set per-terminal in the - // element editor) still belong to the same physical terminal + // English: Check if the user activated the function separation + // checkbox for this terminal (the checkbox that used to be called + // potential separation -- the stored flag is still + // "potential_isolating" so existing elements keep their setting). + if (!ignore_function_separation + && QET::infoFlagIsTrue(terminal->parentElement()->elementInformations().value(QStringLiteral("potential_isolating")).toString())) { + // English: The terminals are function separated -- but terminals + // sharing this terminal's own, non-empty potential-group + // identifier (@see TerminalData::m_potential, set per-terminal in + // the element editor) still belong to the same physical terminal // and stay linked to each other. An empty/unset potential on - // every terminal reproduces the previous, fully-isolated + // every terminal reproduces the previous, fully-separated // behavior exactly, so existing elements are unaffected. const QString this_potential = terminal->potential(); QList same_potential; diff --git a/sources/qetgraphicsitem/terminal.h b/sources/qetgraphicsitem/terminal.h index 832c61853..e70549054 100644 --- a/sources/qetgraphicsitem/terminal.h +++ b/sources/qetgraphicsitem/terminal.h @@ -168,6 +168,7 @@ inline int Terminal::conductorsCount() const } QList relatedPotentialTerminal (const Terminal *terminal, - const bool all_diagram = true); + const bool all_diagram = true, + const bool ignore_function_separation = false); #endif diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 7a43faf9c..caf4816af 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -49,6 +49,7 @@ #include "qetversion.h" #include "undocommand/adddiagramcommand.h" +#include #include #include #include @@ -417,11 +418,25 @@ QUuid QETProject::derivedUuid(const QByteArray &content) static const QUuid project_namespace( QStringLiteral("{c8c75719-0fea-4b1c-9f4b-2dd179fb2f0c}")); - QByteArray normalized(content); - normalized.replace('\r', QByteArray()); - return QUuid::createUuidV5(project_namespace, - QByteArrayLiteral("qet-project-legacy\n") - + normalized); + //Same result as QUuid::createUuidV5(project_namespace, + //"qet-project-legacy\n" + content without '\r'), hashed in place : + //copying the content twice took 2 s of opening a 263 MB project. + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(project_namespace.toRfc4122()); + hash.addData(QByteArrayView("qet-project-legacy\n")); + const QByteArrayView view(content); + qsizetype start = 0; + while (start < view.size()) { + qsizetype cr = view.indexOf('\r', start); + if (cr < 0) + cr = view.size(); + hash.addData(view.sliced(start, cr - start)); + start = cr + 1; + } + QByteArray bytes = hash.result().left(16); + bytes[6] = char((bytes[6] & 0x0F) | 0x50); // version 5 + bytes[8] = char((bytes[8] & 0x3F) | 0x80); // RFC 4122 variant + return QUuid::fromRfc4122(bytes); } /** diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index e8b7f70e5..3ffe17358 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -1239,6 +1239,10 @@ QString QetScriptApi::conductorProperty(int folioIndex, const QString &elementUu describes a potential and not one drawn segment. Setting it on one conductor and leaving the rest of the potential disagreeing would produce a file no GUI action could have produced. + + One thing stops at a terminal element with its function separation + enabled: every property but the voltage / protocol, which still + crosses it (@see Conductor::functionSeparatedConductors). @return true if anything was changed, or if it already held that value */ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUuid, @@ -1275,7 +1279,11 @@ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUu */ bool QetScriptApi::applyConductorProperties(Conductor *conductor, const ConductorProperties &properties) { - if (properties == conductor->properties()) return true; // already so + //Captured before the pushes below: QPropertyUndoCommand applies on + //push, so reading the properties afterwards would already give back + //@a properties. + const ConductorProperties before = conductor->properties(); + if (properties == before) return true; // already so QSet potential = conductor->relatedPotentialConductors(true); potential << conductor; @@ -1287,6 +1295,25 @@ bool QetScriptApi::applyConductorProperties(Conductor *conductor, const Conducto old_value.setValue(c->properties()); new_value.setValue(properties); m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value)); + } + //The other side of a function separated terminal element gets the + //voltage / protocol only: it is the one property that still + //describes the electrical node rather than one side of it. And it + //only arrives when this call really changed it -- before / after + //is the edit, not the far wire's own value, which differs from it + //just as often as not and would then be overwritten by every call, + //colour included. + for (Conductor *c : conductor->functionSeparatedConductors(true)) + { + ConductorProperties separated_properties = c->properties(); + separated_properties.applyTensionProtocolChange(before, properties); + if (separated_properties == c->properties()) + continue; + + QVariant old_value, new_value; + old_value.setValue(c->properties()); + new_value.setValue(separated_properties); + m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value)); } m_project->undoStack()->endMacro(); return true; diff --git a/sources/ui/conductorpropertiesdialog.cpp b/sources/ui/conductorpropertiesdialog.cpp index c28cf171a..c539bdf7c 100644 --- a/sources/ui/conductorpropertiesdialog.cpp +++ b/sources/ui/conductorpropertiesdialog.cpp @@ -88,6 +88,37 @@ void ConductorPropertiesDialog::PropertiesDialog(Conductor *conductor, } } + //A function separated terminal element (element information + //potential_isolating) keeps the two sides of the terminal apart: + //every property but one stays local to the side it was edited on. + //The voltage / protocol still crosses, because it describes the + //electrical node and not one side of it. + if (cpd.applyAll()) + { + //Only an edit that really changed the voltage / protocol passes + //it on, so the comparison is the edit itself -- what the dialog + //was filled with, against what the user left in it -- and never + //the far wire's own value, which differs from it just as often + //as not (a 24 V side against a 0 V one, say) and would then be + //overwritten by every edit, voltage included. + const ConductorProperties shown = conductor->properties(); + foreach (Conductor *separated_conductor, conductor->functionSeparatedConductors()) + { + ConductorProperties separated_properties = separated_conductor->properties(); + separated_properties.applyTensionProtocolChange( + shown, + cpd.properties()); + if (separated_properties == separated_conductor->properties()) + continue; + + QVariant separated_old_value, separated_new_value; + separated_old_value.setValue(separated_conductor->properties()); + separated_new_value.setValue(separated_properties); + new QPropertyUndoCommand (separated_conductor, "properties", + separated_old_value, separated_new_value, undo); + } + } + conductor->diagram()->undoStack().push(undo); } diff --git a/sources/ui/conductorpropertieseditorwidget.cpp b/sources/ui/conductorpropertieseditorwidget.cpp index f132971fa..8be71fdd1 100644 --- a/sources/ui/conductorpropertieseditorwidget.cpp +++ b/sources/ui/conductorpropertieseditorwidget.cpp @@ -346,7 +346,10 @@ void ConductorPropertiesEditorWidget::updateUi() When "apply to all" is ticked, every conductor on the same potential as an edited one is updated too, in the same undo step (one undo reverts them all), as the modal dialog does - (ConductorPropertiesDialog::PropertiesDialog). + (ConductorPropertiesDialog::PropertiesDialog). A terminal element with + its function separation enabled is not crossed by any of that -- its + other side keeps its own text, function, color... -- but the voltage / + protocol still crosses (@see Conductor::functionSeparatedConductors). */ QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const { @@ -360,10 +363,12 @@ QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const if (c) selected << c.data(); - return ConductorMultiEdit::undo( - ConductorMultiEdit::targets( - selected, m_apply_all_cb && m_apply_all_cb->isChecked()), - m_shown, edited); + QSet function_separated; + const QList targets = ConductorMultiEdit::targets( + selected, m_apply_all_cb && m_apply_all_cb->isChecked(), + &function_separated); + + return ConductorMultiEdit::undo(targets, m_shown, edited, function_separated); } /** diff --git a/sources/ui/elementinfowidget.cpp b/sources/ui/elementinfowidget.cpp index b8a05169b..018a3a4bd 100644 --- a/sources/ui/elementinfowidget.cpp +++ b/sources/ui/elementinfowidget.cpp @@ -313,8 +313,9 @@ void ElementInfoWidget::buildInterface() ui->scroll_vlayout->addStretch(); - // Existing potential isolating checkbox - m_potential_isolating_cb = new QCheckBox(tr("Potential separation"), this); + // Existing function separation checkbox (stored as the + // "potential_isolating" element information) + m_potential_isolating_cb = new QCheckBox(tr("Function separation"), this); m_potential_isolating_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;")); // English: Initialize and style the BOM exclusion checkbox diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index a113a8769..49164834e 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -271,6 +271,15 @@ qet_add_app_test(tst_rotateselectionimage tst_rotateselectionimage.cpp) # it selected and records no undo step (#1414); the key goes to the view. qet_add_app_test(tst_imagehandleescape tst_imagehandleescape.cpp) +# SVG export with smaller pictures (--svg-small-pictures, the export dialog's +# option): a picture drawn at a tenth of its size is stored at four times +# that, a small one whole; without the option every picture is whole. +qet_add_app_test(tst_svgsmallpictures tst_svgsmallpictures.cpp) +add_dependencies(tst_svgsmallpictures qelectrotech) +target_link_libraries(tst_svgsmallpictures PRIVATE Qt::Svg) +target_compile_definitions(tst_svgsmallpictures PRIVATE + "QET_TEST_BINARY_PATH=\"$\"") + # TextResizeHandles: the corner handles that change the width of a text box, # on a plain QGraphicsTextItem with the "textWidth" property QET texts have. add_executable( diff --git a/tests/qttest/fixtures/svg_small_pictures.qet b/tests/qttest/fixtures/svg_small_pictures.qet new file mode 100644 index 000000000..c47c4d7b0 --- /dev/null +++ b/tests/qttest/fixtures/svg_small_pictures.qet @@ -0,0 +1,41 @@ + + + 10/2/26 + 02-10-2026 + 2026-10-02 + svg_small_pictures + 18:39 + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + Imported elements + + + + diff --git a/tests/qttest/tst_conductormultiedit.cpp b/tests/qttest/tst_conductormultiedit.cpp index 9a6d36e92..757bc2544 100644 --- a/tests/qttest/tst_conductormultiedit.cpp +++ b/tests/qttest/tst_conductormultiedit.cpp @@ -29,6 +29,10 @@ class FakeWire : public QObject ConductorProperties properties() const { return m_properties; } void setProperties(const ConductorProperties &p) { m_properties = p; } QSet relatedPotentialConductors() const { return m_potential; } + // The wires of the other side of a function separated terminal + // element: same electrical node, but only the voltage / + // protocol crosses. + QSet functionSeparatedConductors() const { return m_separated; } QPointF pos() const { return m_pos; } // Joins wires into one potential; like the real one, a wire's @@ -41,10 +45,24 @@ class FakeWire : public QObject w->m_potential.insert(o); } + // Marks two groups as separated by a function separated + // terminal element, without joining their potentials. + static void separate(const QList &one, + const QList &other) + { + for (FakeWire *w : one) + for (FakeWire *o : other) + { + w->m_separated.insert(o); + o->m_separated.insert(w); + } + } + ConductorProperties m_properties; private: QSet m_potential; + QSet m_separated; QPointF m_pos; }; @@ -102,6 +120,143 @@ class tst_conductormultiedit : public QObject (QSet {&a, &b, &c, &d})); } + // A function separated terminal element: the other side of the + // terminal is reached too, but only the voltage / protocol + // crosses it -- not the function, not the number. + void functionSeparatedGetsVoltageProtocolOnly() + { + FakeWire a("1", "fnA", "", {}), b("2", "fnB", "", {}), + c("3", "fnC", "", {}); + FakeWire::join({&a, &b}); + FakeWire::separate({&a}, {&c}); + + QSet separated; + const Wires t = ConductorMultiEdit::targets(Wires {&a}, true, + &separated); + QCOMPARE(QSet(t.begin(), t.end()), + (QSet {&a, &b, &c})); + QCOMPARE(separated, (QSet {&c})); + // A caller that does not know about the rule gets the + // separated side left out, as before + QCOMPARE(ConductorMultiEdit::targets(Wires {&a}, true), + (Wires {&a, &b})); + + const ConductorProperties shown = a.properties(); + ConductorProperties after = edited(shown, + &ConductorProperties::m_function, "PWR"); + after.m_tension_protocol = "24V"; + + QUndoStack stack; + stack.push(ConductorMultiEdit::undo(t, shown, after, separated)); + QCOMPARE(stack.count(), 1); + + for (FakeWire *w : {&a, &b}) + { + QCOMPARE(w->m_properties.m_function, QStringLiteral("PWR")); + QCOMPARE(w->m_properties.m_tension_protocol, + QStringLiteral("24V")); + } + QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC")); + QCOMPARE(c.m_properties.text, QStringLiteral("3")); + QCOMPARE(c.m_properties.m_tension_protocol, QStringLiteral("24V")); + + stack.undo(); + QVERIFY(c.properties().m_tension_protocol.isEmpty()); + QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC")); + } + + // The explicitly picked side of a separation: selecting one + // wire on each side and editing them edits them both, in + // full -- however the selection happens to be walked (#review) + void selectedWireIsNeverSeparated() + { + FakeWire a("1", "fnA", "", {}), c("3", "fnC", "", {}); + FakeWire::separate({&a}, {&c}); + + const ConductorProperties original_a = a.properties(); + const ConductorProperties original_c = c.properties(); + const ConductorProperties after = edited(original_a, + &ConductorProperties::m_function, "PWR"); + + for (const Wires &selection : {Wires {&a, &c}, Wires {&c, &a}}) + { + //Each order starts from the same, unedited state + a.setProperties(original_a); + c.setProperties(original_c); + + QSet separated; + const Wires t = ConductorMultiEdit::targets(selection, true, + &separated); + //c is picked, so it is not "the far side" of anything + QCOMPARE(separated, (QSet {})); + + QUndoStack stack; + QUndoCommand *undo = ConductorMultiEdit::undo(t, original_a, + after, separated); + QVERIFY(undo); + stack.push(undo); + QCOMPARE(a.m_properties.m_function, QStringLiteral("PWR")); + QCOMPARE(c.m_properties.m_function, QStringLiteral("PWR")); + } + } + + // A wire that belongs to the potential as well -- on any path, + // however far that path runs -- takes the full edit, even when a + // separation also reaches it. The fake's potential is one hop + // deep, unlike the real one, so the shared potential is stated + // in full here. + void wireReachableBothWaysIsNotSeparated() + { + FakeWire a("1", "fnA", "", {}), b("2", "fnB", "", {}), + c("3", "fnC", "", {}); + FakeWire::join({&a, &b, &c}); + FakeWire::separate({&a}, {&c}); + + QSet separated; + const Wires t = ConductorMultiEdit::targets(Wires {&a}, true, + &separated); + QCOMPARE(QSet(t.begin(), t.end()), + (QSet {&a, &b, &c})); + QCOMPARE(separated, (QSet {})); + + const ConductorProperties shown = a.properties(); + QUndoStack stack; + stack.push(ConductorMultiEdit::undo(t, shown, edited(shown, + &ConductorProperties::m_function, "PWR"), separated)); + QCOMPARE(c.m_properties.m_function, QStringLiteral("PWR")); + } + + // The far side only takes the voltage when the edit changed it: + // editing the function of a 24 V wire must leave the 0 V one + // at 0 V, not drag it along (#review) + void separatedWireKeepsVoltageWhenNotEdited() + { + FakeWire a("1", "fnA", "", {}), c("3", "fnC", "", {}); + FakeWire::separate({&a}, {&c}); + a.m_properties.m_tension_protocol = "24V"; + c.m_properties.m_tension_protocol = "0V"; + + QSet separated; + const Wires t = ConductorMultiEdit::targets(Wires {&a}, true, + &separated); + const ConductorProperties shown = a.properties(); + QUndoStack stack; + + //Only the function changes: the far side changes nothing + stack.push(ConductorMultiEdit::undo(t, shown, edited(shown, + &ConductorProperties::m_function, "PWR"), separated)); + QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC")); + QCOMPARE(c.m_properties.m_tension_protocol, QStringLiteral("0V")); + + //Now the voltage changes too: it crosses, and only it + const ConductorProperties shown_now = a.properties(); + stack.push(ConductorMultiEdit::undo(t, shown_now, edited(shown_now, + &ConductorProperties::m_tension_protocol, "30V"), separated)); + QCOMPARE(c.m_properties.m_tension_protocol, QStringLiteral("30V")); + QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC")); + QCOMPARE(a.m_properties.m_tension_protocol, QStringLiteral("30V")); + } + // One edit of three wires: each takes the function, keeps its // own number and cable; one undo restores all three exactly void undoRedoThreeWires() diff --git a/tests/qttest/tst_svgsmallpictures.cpp b/tests/qttest/tst_svgsmallpictures.cpp new file mode 100644 index 000000000..5df692832 --- /dev/null +++ b/tests/qttest/tst_svgsmallpictures.cpp @@ -0,0 +1,143 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "qetapptest.h" +#include "qetgraphicsitem/diagramimageitem.h" + +#include +#include +#include +#include +#include +#include +#include + +/** + SVG export with "smaller pictures" (ExportProperties::svg_small_pictures, + --svg-small-pictures) stores each picture at four times the size it is + drawn at when that is smaller than the picture, and at full resolution + otherwise. fixtures/svg_small_pictures.qet has an 800x600 picture drawn + at a tenth of its size and a 40x30 one at full size. +*/ +class tst_svgsmallpictures : public QObject +{ + Q_OBJECT + + QString m_fixture; + + /// Pixel size of each picture embedded in @p svg, smallest first + static QList embeddedSizes(const QByteArray &svg) + { + static const QRegularExpression data( + QStringLiteral("data:image/png;base64,([A-Za-z0-9+/=\\s]+)")); + QList sizes; + auto it = data.globalMatch(QString::fromLatin1(svg)); + while (it.hasNext()) + sizes << QImage::fromData(QByteArray::fromBase64( + it.next().captured(1).toLatin1())).size(); + std::sort(sizes.begin(), sizes.end(), [](const QSize &a, const QSize &b) { + return a.width() < b.width(); + }); + return sizes; + } + + QByteArray exportWithCli(const QStringList &extra) + { + QTemporaryDir dir; + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), + QStringList{QStringLiteral("--export-svg"), m_fixture, dir.path()} + extra); + if (!proc.waitForFinished(60000) || proc.exitCode() != 0) + return {}; + const QStringList files = QDir(dir.path()).entryList({QStringLiteral("*.svg")}); + QFile svg(QDir(dir.path()).filePath(files.value(0))); + return svg.open(QIODevice::ReadOnly) ? svg.readAll() : QByteArray(); + } + + /// As ExportDialog::generateSvg(): the folio drawn into a QPicture, + /// played into the SVG generator + static QByteArray exportLikeTheDialog(Diagram *diagram, bool small) + { + const QSize size = diagram->imageSize(); + QPicture picture; + diagram->setSmallPictures(small); + diagram->toPaintDevice(picture, size.width(), size.height()); + diagram->setSmallPictures(false); + + QByteArray svg; + QBuffer buffer(&svg); + buffer.open(QIODevice::WriteOnly); + QSvgGenerator generator; + generator.setSize(size); + generator.setOutputDevice(&buffer); + QPainter painter(&generator); + picture.play(&painter); + painter.end(); + return svg; + } + +private slots: + void initTestCase() + { + m_fixture = QFINDTESTDATA("fixtures/svg_small_pictures.qet"); + QVERIFY2(!m_fixture.isEmpty(), "fixture project not found"); + QVERIFY(QetAppTest::editor()); + } + + void cliFullResolutionByDefault() + { + const QByteArray svg = exportWithCli({}); + QVERIFY2(!svg.isEmpty(), "--export-svg failed"); + QCOMPARE(embeddedSizes(svg), (QList{QSize(40, 30), QSize(800, 600)})); + } + + void cliSmallPictures() + { + const QByteArray svg = exportWithCli({QStringLiteral("--svg-small-pictures")}); + QVERIFY2(!svg.isEmpty(), "--export-svg --svg-small-pictures failed"); + // 800 x 600 drawn at 0.1, times 4; the 40 x 30 one is kept whole + QCOMPARE(embeddedSizes(svg), (QList{QSize(40, 30), QSize(320, 240)})); + } + + void dialogSmallPicturesThenFullAgain() + { + QFile file(m_fixture); + QVERIFY(file.open(QIODevice::ReadOnly)); + QDomDocument document; + QVERIFY(document.setContent(&file)); + const QDomElement image_xml = document.elementsByTagName( + QStringLiteral("image")).at(0).toElement(); + Diagram *diagram = QetAppTest::diagram(); + auto *picture = new DiagramImageItem(); + QVERIFY(picture->fromXml(image_xml)); + diagram->addItem(picture); + + QCOMPARE(embeddedSizes(exportLikeTheDialog(diagram, true)), + QList{QSize(320, 240)}); + // The option lasts for one export only + QVERIFY(!diagram->smallPictures()); + QCOMPARE(embeddedSizes(exportLikeTheDialog(diagram, false)), + QList{QSize(800, 600)}); + delete picture; + } +}; + +QET_APP_TEST_MAIN(tst_svgsmallpictures) +#include "tst_svgsmallpictures.moc"