Compare commits

...

2 Commits

Author SHA1 Message Date
Laurent Trinques 3abf187688 Merge pull request #813 from jp2images/fix-element-editor-mirror-in-place
Element editor: mirror and flip the selection in place instead of across the origin
2026-09-06 14:38:15 +02:00
Jeff Patterson 825a1a594d Fix element editor Mirror and Flip to reflect the selection in place
Mirror (M) and Flip (F) in the element editor reflected the selected
parts across the element origin: every part's mirror() and flip()
negated the scene x or y coordinate. A part drawn to the right of the
origin landed the same distance to the left, so the selection jumped
to the other side of the canvas instead of turning around.

MirrorElementsCommand and FlipElementsCommand now compute the united
scene bounding rectangle of the selected items and reflect across the
vertical or horizontal line through its center. The center is snapped
to the nearest half of the diagram grid, so points that were on the
grid stay on the grid after the reflection. Terminals in particular
keep their grid alignment.

Each part's mirror() and flip() takes the axis coordinate as a
parameter with a default of 0, so the previous behavior remains
available to any other caller. The command stores the axis when it is
created and undo reapplies the same reflection, which is its own
inverse, so the existing undo path is unchanged.

Parts covered: PartArc, PartDynamicTextField, PartEllipse, PartLine,
PartPolygon, PartRectangle, PartTerminal, PartText.

Fixes #812
2026-09-06 06:48:45 -05:00
18 changed files with 104 additions and 66 deletions
+40 -16
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "editorcommands.h"
#include "../diagram.h"
/**
@brief ElementEditionCommand::ElementEditionCommand
@@ -666,10 +667,32 @@ void RotateFineElementsCommand::redo()
}
/**
@brief selectionCenter
@param items
@return the center of the united scene bounding rect of items,
snapped to the nearest half of the diagram grid. Mirroring across a
half-grid line keeps points that were on the grid on the grid.
*/
static QPointF selectionCenter(const QList<QGraphicsItem *> &items)
{
QRectF bounding;
for (auto *item : items) {
bounding = bounding.united(item->sceneBoundingRect());
}
QPointF center = bounding.center();
const qreal half_x = Diagram::xGrid / 2.0;
const qreal half_y = Diagram::yGrid / 2.0;
center.setX(qRound(center.x() / half_x) * half_x);
center.setY(qRound(center.y() / half_y) * half_y);
return center;
}
MirrorElementsCommand::MirrorElementsCommand(ElementScene *scene, QUndoCommand *parent) :
ElementEditionCommand(QObject::tr("Miroir de sélection", "undo caption"), scene, nullptr, parent)
{
m_items = scene->selectedItems();
m_axis_x = selectionCenter(m_items).x();
}
/**
@@ -680,28 +703,28 @@ void MirrorElementsCommand::redo()
foreach (auto *item, m_items) {
if (item->type() == PartText::Type) {
PartText* staticText = qgraphicsitem_cast<PartText*>(item);
staticText->mirror();
staticText->mirror(m_axis_x);
} else if (item->type() == PartDynamicTextField::Type) {
PartDynamicTextField* dyntext = qgraphicsitem_cast<PartDynamicTextField*>(item);
dyntext->mirror();
dyntext->mirror(m_axis_x);
} else if (item->type() == PartArc::Type) {
PartArc* arc = qgraphicsitem_cast<PartArc*>(item);
arc->mirror();
arc->mirror(m_axis_x);
} else if (item->type() == PartEllipse::Type) {
PartEllipse* ellipse = qgraphicsitem_cast<PartEllipse*>(item);
ellipse->mirror();
ellipse->mirror(m_axis_x);
} else if (item->type() == PartLine::Type) {
PartLine* line = qgraphicsitem_cast<PartLine*>(item);
line->mirror();
line->mirror(m_axis_x);
} else if (item->type() == PartPolygon::Type) {
PartPolygon* poly = qgraphicsitem_cast<PartPolygon*>(item);
poly->mirror();
poly->mirror(m_axis_x);
} else if (item->type() == PartRectangle::Type) {
PartRectangle* rect = qgraphicsitem_cast<PartRectangle*>(item);
rect->mirror();
rect->mirror(m_axis_x);
} else if (item->type() == PartTerminal::Type) {
PartTerminal* term = qgraphicsitem_cast<PartTerminal*>(item);
term->mirror();
term->mirror(m_axis_x);
}
}
}
@@ -718,6 +741,7 @@ FlipElementsCommand::FlipElementsCommand(ElementScene *scene, QUndoCommand *pare
ElementEditionCommand(QObject::tr("Retourner la sélection", "undo caption"), scene, nullptr, parent)
{
m_items = scene->selectedItems();
m_axis_y = selectionCenter(m_items).y();
}
/**
@@ -728,28 +752,28 @@ void FlipElementsCommand::redo()
foreach (auto *item, m_items) {
if (item->type() == PartText::Type) {
PartText* staticText = qgraphicsitem_cast<PartText*>(item);
staticText->flip();
staticText->flip(m_axis_y);
} else if (item->type() == PartDynamicTextField::Type) {
PartDynamicTextField* dyntext = qgraphicsitem_cast<PartDynamicTextField*>(item);
dyntext->flip();
dyntext->flip(m_axis_y);
} else if (item->type() == PartArc::Type) {
PartArc* arc = qgraphicsitem_cast<PartArc*>(item);
arc->flip();
arc->flip(m_axis_y);
} else if (item->type() == PartEllipse::Type) {
PartEllipse* ellipse = qgraphicsitem_cast<PartEllipse*>(item);
ellipse->flip();
ellipse->flip(m_axis_y);
} else if (item->type() == PartLine::Type) {
PartLine* line = qgraphicsitem_cast<PartLine*>(item);
line->flip();
line->flip(m_axis_y);
} else if (item->type() == PartPolygon::Type) {
PartPolygon* poly = qgraphicsitem_cast<PartPolygon*>(item);
poly->flip();
poly->flip(m_axis_y);
} else if (item->type() == PartRectangle::Type) {
PartRectangle* rect = qgraphicsitem_cast<PartRectangle*>(item);
rect->flip();
rect->flip(m_axis_y);
} else if (item->type() == PartTerminal::Type) {
PartTerminal* term = qgraphicsitem_cast<PartTerminal*>(item);
term->flip();
term->flip(m_axis_y);
}
}
}
+14
View File
@@ -279,6 +279,12 @@ private:
};
/**
@brief The MirrorElementsCommand class
Mirror the selected parts horizontally (left <-> right) across the
vertical line through the center of the selection, so the selection
keeps its place in the scene.
*/
class MirrorElementsCommand : public ElementEditionCommand
{
public:
@@ -288,8 +294,15 @@ public:
private:
ElementScene *m_scene =nullptr;
QList<QGraphicsItem*> m_items;
qreal m_axis_x = 0;
};
/**
@brief The FlipElementsCommand class
Flip the selected parts vertically (top <-> bottom) across the
horizontal line through the center of the selection, so the selection
keeps its place in the scene.
*/
class FlipElementsCommand : public ElementEditionCommand
{
public:
@@ -299,6 +312,7 @@ public:
private:
ElementScene *m_scene =nullptr;
QList<QGraphicsItem*> m_items;
qreal m_axis_y = 0;
};
#endif
+4 -4
View File
@@ -197,13 +197,13 @@ qreal PartArc::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartArc::flip() {
void PartArc::flip(qreal axis_y) {
m_start_angle = (-1) * m_start_angle;
m_span_angle = (-1) * m_span_angle;
while (m_start_angle < 0) { m_start_angle += (360*16); }
while (m_start_angle >= (360*16)) { m_start_angle -= (360*16); }
auto p1 = mapToScene(m_rect.x(),m_rect.y());
p1.setY(((-1.0) * p1.y()) - m_rect.height());
p1.setY(2 * axis_y - p1.y() - m_rect.height());
p1 = mapFromScene(p1.x(),p1.y());
m_rect = QRectF(m_rect.x(), p1.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
@@ -211,13 +211,13 @@ void PartArc::flip() {
emit rectChanged();
}
void PartArc::mirror() {
void PartArc::mirror(qreal axis_x) {
m_start_angle = (180.0 * 16) - m_start_angle;
m_span_angle = (-1) * m_span_angle;
while (m_start_angle < 0) { m_start_angle += (360*16); }
while (m_start_angle >= (360*16)) { m_start_angle -= (360*16); }
auto p1 = mapToScene(m_rect.x(),m_rect.y());
p1.setX(((-1.0) * p1.x()) - m_rect.width());
p1.setX(2 * axis_x - p1.x() - m_rect.width());
p1 = mapFromScene(p1.x(), p1.y());
m_rect = QRectF(p1.x(), m_rect.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
+2 -2
View File
@@ -62,8 +62,8 @@ class PartArc : public AbstractPartEllipse
QRectF sceneGeometricRect() const override;
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
@@ -74,19 +74,19 @@ void PartDynamicTextField::setRotation(qreal angle) {
setPos(QTransform().rotate(diffAngle).map(pos()));
}
void PartDynamicTextField::mirror() {
void PartDynamicTextField::mirror(qreal axis_x) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle(360-rotation(), true));
// then see, where we need to re-position depending on the angle!
qreal rot = qRound(QET::correctAngle(rotation(), true));
qreal c = qCos(qDegreesToRadians(rot));
qreal s = qSin(qDegreesToRadians(rot));
qreal x = (-1) * pos().x() - c * boundingRect().width();
qreal x = 2 * axis_x - pos().x() - c * boundingRect().width();
qreal y = pos().y() - s * boundingRect().width();
setPos(x, y);
}
void PartDynamicTextField::flip() {
void PartDynamicTextField::flip(qreal axis_y) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle(360-rotation(), true));
// then see, where we need to re-position depending on the angle!
@@ -94,7 +94,7 @@ void PartDynamicTextField::flip() {
qreal c = qCos(qDegreesToRadians(rot));
qreal s = qSin(qDegreesToRadians(rot));
qreal x = pos().x() + s * boundingRect().height();
qreal y = (-1) * pos().y() - c * boundingRect().height();
qreal y = 2 * axis_y - pos().y() - c * boundingRect().height();
setPos(x, y);
}
@@ -106,8 +106,8 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
bool rotationPointCenter() const;
void setRotation(qreal angle);
void mirror();
void flip();
void mirror(qreal axis_x = 0);
void flip(qreal axis_y = 0);
protected:
+4 -4
View File
@@ -256,9 +256,9 @@ qreal PartEllipse::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartEllipse::flip() {
void PartEllipse::flip(qreal axis_y) {
auto p1 = mapToScene(m_rect.x(), m_rect.y());
p1.setY(((-1.0) * p1.y()) - m_rect.height());
p1.setY(2 * axis_y - p1.y() - m_rect.height());
p1 = mapFromScene(p1.x(), p1.y());
m_rect = QRectF(p1.x(), p1.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
@@ -266,9 +266,9 @@ void PartEllipse::flip() {
emit rectChanged();
}
void PartEllipse::mirror() {
void PartEllipse::mirror(qreal axis_x) {
auto p1 = mapToScene(m_rect.x(), m_rect.y());
p1.setX(((-1.0) * p1.x()) - m_rect.width());
p1.setX(2 * axis_x - p1.x() - m_rect.width());
p1 = mapFromScene(p1.x(), p1.y());
m_rect = QRectF(p1.x(), p1.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
+2 -2
View File
@@ -62,8 +62,8 @@ class PartEllipse : public AbstractPartEllipse
void setRect(const QRectF &rect) override {AbstractPartEllipse::setRect(rect); adjustHandlerPos();}
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
+6 -6
View File
@@ -590,11 +590,11 @@ qreal PartLine::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartLine::flip() {
void PartLine::flip(qreal axis_y) {
auto p1 = mapToScene(m_line.p1());
auto p2 = mapToScene(m_line.p2());
p1 = QPointF(p1.x(), (-1) * p1.y());
p2 = QPointF(p2.x(), (-1) * p2.y());
p1 = QPointF(p1.x(), 2 * axis_y - p1.y());
p2 = QPointF(p2.x(), 2 * axis_y - p2.y());
m_line.setP1(mapFromScene(p1));
m_line.setP2(mapFromScene(p2));
setLine(m_line);
@@ -603,11 +603,11 @@ void PartLine::flip() {
emit lineChanged();
}
void PartLine::mirror() {
void PartLine::mirror(qreal axis_x) {
auto p1 = mapToScene(m_line.p1());
auto p2 = mapToScene(m_line.p2());
p1 = QPointF((-1) * p1.x(), p1.y());
p2 = QPointF((-1) * p2.x(), p2.y());
p1 = QPointF(2 * axis_x - p1.x(), p1.y());
p2 = QPointF(2 * axis_x - p2.x(), p2.y());
m_line.setP1(mapFromScene(p1));
m_line.setP2(mapFromScene(p2));
setLine(m_line);
+2 -2
View File
@@ -96,8 +96,8 @@ class PartLine : public CustomElementGraphicPart
void setSecondEndLength(const qreal &l);
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
+4 -4
View File
@@ -307,10 +307,10 @@ qreal PartPolygon::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartPolygon::flip() {
void PartPolygon::flip(qreal axis_y) {
for (auto &pt : m_polygon) {
pt = mapToScene(pt.x(), pt.y());
pt = QPointF(pt.x(), (-1) * pt.y());
pt = QPointF(pt.x(), 2 * axis_y - pt.y());
pt = mapFromScene(pt.x(), pt.y());
}
setPolygon(m_polygon);
@@ -319,10 +319,10 @@ void PartPolygon::flip() {
emit polygonChanged();
}
void PartPolygon::mirror() {
void PartPolygon::mirror(qreal axis_x) {
for (auto &pt : m_polygon) {
pt = mapToScene(pt.x(), pt.y());
pt = QPointF((-1) * pt.x(), pt.y());
pt = QPointF(2 * axis_x - pt.x(), pt.y());
pt = mapFromScene(pt.x(), pt.y());
}
setPolygon(m_polygon);
+2 -2
View File
@@ -89,8 +89,8 @@ class PartPolygon : public CustomElementGraphicPart
void setRotation (qreal angle);
qreal rotation () const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
@@ -187,11 +187,11 @@ qreal PartRectangle::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartRectangle::flip() {
void PartRectangle::flip(qreal axis_y) {
auto height = m_rect.height();
auto p1 = mapToScene(m_rect.x(),m_rect.y());
qreal x = p1.x();
qreal y = ((-1.0) * p1.y()) - height;
qreal y = 2 * axis_y - p1.y() - height;
p1 = mapFromScene(x, y);
m_rect.setX(p1.x());
m_rect.setY(p1.y());
@@ -201,10 +201,10 @@ void PartRectangle::flip() {
emit rectChanged();
}
void PartRectangle::mirror() {
void PartRectangle::mirror(qreal axis_x) {
auto width = m_rect.width();
auto p1 = mapToScene(m_rect.x(),m_rect.y());
qreal x = ((-1.0) * p1.x()) - width;
qreal x = 2 * axis_x - p1.x() - width;
qreal y = p1.y();
p1 = mapFromScene(x, y);
m_rect.setX(p1.x());
+2 -2
View File
@@ -73,8 +73,8 @@ class PartRectangle : public CustomElementGraphicPart
void setYRadius(qreal Y);
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
QRectF sceneGeometricRect() const override;
virtual QPointF sceneTopLeft() const;
+4 -4
View File
@@ -293,9 +293,9 @@ qreal PartTerminal::rotation() const {
@brief PartTerminal::flip
turn part upside down
*/
void PartTerminal::flip() {
void PartTerminal::flip(qreal axis_y) {
d->m_pos.setX( pos().x());
d->m_pos.setY((-1.0) * pos().y());
d->m_pos.setY(2 * axis_y - pos().y());
switch (d->m_orientation) {
case Qet::North : setOrientation(Qet::South);
break;
@@ -314,8 +314,8 @@ void PartTerminal::flip() {
@brief PartTerminal::mirror
turn part from left to right
*/
void PartTerminal::mirror() {
d->m_pos.setX((-1.0) * pos().x());
void PartTerminal::mirror(qreal axis_x) {
d->m_pos.setX(2 * axis_x - pos().x());
d->m_pos.setY( pos().y());
switch (d->m_orientation) {
case Qet::North : break;
+2 -2
View File
@@ -96,8 +96,8 @@ class PartTerminal : public CustomElementGraphicPart
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void nextOrientation();
void previousOrientation();
+4 -4
View File
@@ -75,7 +75,7 @@ void PartText::setRotation(qreal angle) {
setPos(QTransform().rotate(diffAngle).map(pos()));
}
void PartText::mirror() {
void PartText::mirror(qreal axis_x) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle((360-rotation()), true));
// then see, where we need to re-position depending on text, font ...
@@ -86,12 +86,12 @@ void PartText::mirror() {
qreal c = qCos(qDegreesToRadians(rot));
qreal s = qSin(qDegreesToRadians(rot));
// Now: Move!
qreal x = (-1) * pos().x() - c * (textwidth);
qreal x = 2 * axis_x - pos().x() - c * (textwidth);
qreal y = pos().y() - s * (textwidth);
setPos(x, y);
}
void PartText::flip() {
void PartText::flip(qreal axis_y) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle((360-rotation()), true));
// then see, where we need to re-position depending on text, font ...
@@ -103,7 +103,7 @@ void PartText::flip() {
qreal s = qSin(qDegreesToRadians(rot));
// Now: Move!
qreal x = pos().x() - s * (textheight);
qreal y = (-1) * pos().y() + c * (textheight);
qreal y = 2 * axis_y - pos().y() + c * (textheight);
setPos(x, y);
}
+2 -2
View File
@@ -64,8 +64,8 @@ class PartText : public QGraphicsTextItem, public CustomElementPart {
void fromXml(const QDomElement &) override;
const QDomElement toXml(QDomDocument &) const override;
void setRotation(qreal angle);
void mirror();
void flip();
void mirror(qreal axis_x = 0);
void flip(qreal axis_y = 0);
bool isUseless() const override;
QRectF sceneGeometricRect() const override;
void startUserTransformation(const QRectF &) override;