From 825a1a594d88f1e921d0d9fc17b455f6d50fd958 Mon Sep 17 00:00:00 2001 From: Jeff Patterson Date: Sun, 6 Sep 2026 06:48:45 -0500 Subject: [PATCH] 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 --- sources/editor/editorcommands.cpp | 56 +++++++++++++------ sources/editor/editorcommands.h | 14 +++++ sources/editor/graphicspart/partarc.cpp | 8 +-- sources/editor/graphicspart/partarc.h | 4 +- .../graphicspart/partdynamictextfield.cpp | 8 +-- .../graphicspart/partdynamictextfield.h | 4 +- sources/editor/graphicspart/partellipse.cpp | 8 +-- sources/editor/graphicspart/partellipse.h | 4 +- sources/editor/graphicspart/partline.cpp | 12 ++-- sources/editor/graphicspart/partline.h | 4 +- sources/editor/graphicspart/partpolygon.cpp | 8 +-- sources/editor/graphicspart/partpolygon.h | 4 +- sources/editor/graphicspart/partrectangle.cpp | 8 +-- sources/editor/graphicspart/partrectangle.h | 4 +- sources/editor/graphicspart/partterminal.cpp | 8 +-- sources/editor/graphicspart/partterminal.h | 4 +- sources/editor/graphicspart/parttext.cpp | 8 +-- sources/editor/graphicspart/parttext.h | 4 +- 18 files changed, 104 insertions(+), 66 deletions(-) diff --git a/sources/editor/editorcommands.cpp b/sources/editor/editorcommands.cpp index 7646ab88d..62bc2a013 100644 --- a/sources/editor/editorcommands.cpp +++ b/sources/editor/editorcommands.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #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 &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(item); - staticText->mirror(); + staticText->mirror(m_axis_x); } else if (item->type() == PartDynamicTextField::Type) { PartDynamicTextField* dyntext = qgraphicsitem_cast(item); - dyntext->mirror(); + dyntext->mirror(m_axis_x); } else if (item->type() == PartArc::Type) { PartArc* arc = qgraphicsitem_cast(item); - arc->mirror(); + arc->mirror(m_axis_x); } else if (item->type() == PartEllipse::Type) { PartEllipse* ellipse = qgraphicsitem_cast(item); - ellipse->mirror(); + ellipse->mirror(m_axis_x); } else if (item->type() == PartLine::Type) { PartLine* line = qgraphicsitem_cast(item); - line->mirror(); + line->mirror(m_axis_x); } else if (item->type() == PartPolygon::Type) { PartPolygon* poly = qgraphicsitem_cast(item); - poly->mirror(); + poly->mirror(m_axis_x); } else if (item->type() == PartRectangle::Type) { PartRectangle* rect = qgraphicsitem_cast(item); - rect->mirror(); + rect->mirror(m_axis_x); } else if (item->type() == PartTerminal::Type) { PartTerminal* term = qgraphicsitem_cast(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(item); - staticText->flip(); + staticText->flip(m_axis_y); } else if (item->type() == PartDynamicTextField::Type) { PartDynamicTextField* dyntext = qgraphicsitem_cast(item); - dyntext->flip(); + dyntext->flip(m_axis_y); } else if (item->type() == PartArc::Type) { PartArc* arc = qgraphicsitem_cast(item); - arc->flip(); + arc->flip(m_axis_y); } else if (item->type() == PartEllipse::Type) { PartEllipse* ellipse = qgraphicsitem_cast(item); - ellipse->flip(); + ellipse->flip(m_axis_y); } else if (item->type() == PartLine::Type) { PartLine* line = qgraphicsitem_cast(item); - line->flip(); + line->flip(m_axis_y); } else if (item->type() == PartPolygon::Type) { PartPolygon* poly = qgraphicsitem_cast(item); - poly->flip(); + poly->flip(m_axis_y); } else if (item->type() == PartRectangle::Type) { PartRectangle* rect = qgraphicsitem_cast(item); - rect->flip(); + rect->flip(m_axis_y); } else if (item->type() == PartTerminal::Type) { PartTerminal* term = qgraphicsitem_cast(item); - term->flip(); + term->flip(m_axis_y); } } } diff --git a/sources/editor/editorcommands.h b/sources/editor/editorcommands.h index b920b47d9..e5a811a83 100644 --- a/sources/editor/editorcommands.h +++ b/sources/editor/editorcommands.h @@ -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 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 m_items; + qreal m_axis_y = 0; }; #endif diff --git a/sources/editor/graphicspart/partarc.cpp b/sources/editor/graphicspart/partarc.cpp index 596f19f37..fb06bdb83 100644 --- a/sources/editor/graphicspart/partarc.cpp +++ b/sources/editor/graphicspart/partarc.cpp @@ -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(); diff --git a/sources/editor/graphicspart/partarc.h b/sources/editor/graphicspart/partarc.h index bc6f51301..cd0878120 100644 --- a/sources/editor/graphicspart/partarc.h +++ b/sources/editor/graphicspart/partarc.h @@ -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; diff --git a/sources/editor/graphicspart/partdynamictextfield.cpp b/sources/editor/graphicspart/partdynamictextfield.cpp index d04ac6f5b..c65c955df 100644 --- a/sources/editor/graphicspart/partdynamictextfield.cpp +++ b/sources/editor/graphicspart/partdynamictextfield.cpp @@ -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); } diff --git a/sources/editor/graphicspart/partdynamictextfield.h b/sources/editor/graphicspart/partdynamictextfield.h index 4fdf0d700..1b730639b 100644 --- a/sources/editor/graphicspart/partdynamictextfield.h +++ b/sources/editor/graphicspart/partdynamictextfield.h @@ -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: diff --git a/sources/editor/graphicspart/partellipse.cpp b/sources/editor/graphicspart/partellipse.cpp index 4cf3a4f10..dc19d6102 100644 --- a/sources/editor/graphicspart/partellipse.cpp +++ b/sources/editor/graphicspart/partellipse.cpp @@ -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(); diff --git a/sources/editor/graphicspart/partellipse.h b/sources/editor/graphicspart/partellipse.h index e566a1f16..477aa3e8c 100644 --- a/sources/editor/graphicspart/partellipse.h +++ b/sources/editor/graphicspart/partellipse.h @@ -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; diff --git a/sources/editor/graphicspart/partline.cpp b/sources/editor/graphicspart/partline.cpp index c0d86bc57..cf71406b1 100644 --- a/sources/editor/graphicspart/partline.cpp +++ b/sources/editor/graphicspart/partline.cpp @@ -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); diff --git a/sources/editor/graphicspart/partline.h b/sources/editor/graphicspart/partline.h index dfbb361d5..1e3082a53 100644 --- a/sources/editor/graphicspart/partline.h +++ b/sources/editor/graphicspart/partline.h @@ -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; diff --git a/sources/editor/graphicspart/partpolygon.cpp b/sources/editor/graphicspart/partpolygon.cpp index eceae2bb2..eb96837e4 100644 --- a/sources/editor/graphicspart/partpolygon.cpp +++ b/sources/editor/graphicspart/partpolygon.cpp @@ -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); diff --git a/sources/editor/graphicspart/partpolygon.h b/sources/editor/graphicspart/partpolygon.h index f8a67f18c..bee786be4 100644 --- a/sources/editor/graphicspart/partpolygon.h +++ b/sources/editor/graphicspart/partpolygon.h @@ -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; diff --git a/sources/editor/graphicspart/partrectangle.cpp b/sources/editor/graphicspart/partrectangle.cpp index 78fa726e3..e1955b1b9 100644 --- a/sources/editor/graphicspart/partrectangle.cpp +++ b/sources/editor/graphicspart/partrectangle.cpp @@ -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()); diff --git a/sources/editor/graphicspart/partrectangle.h b/sources/editor/graphicspart/partrectangle.h index 00ed2bc39..c2d259f77 100644 --- a/sources/editor/graphicspart/partrectangle.h +++ b/sources/editor/graphicspart/partrectangle.h @@ -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; diff --git a/sources/editor/graphicspart/partterminal.cpp b/sources/editor/graphicspart/partterminal.cpp index 194ae0aec..0b85ba026 100644 --- a/sources/editor/graphicspart/partterminal.cpp +++ b/sources/editor/graphicspart/partterminal.cpp @@ -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; diff --git a/sources/editor/graphicspart/partterminal.h b/sources/editor/graphicspart/partterminal.h index d0ba05da9..1ef413cd6 100644 --- a/sources/editor/graphicspart/partterminal.h +++ b/sources/editor/graphicspart/partterminal.h @@ -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(); diff --git a/sources/editor/graphicspart/parttext.cpp b/sources/editor/graphicspart/parttext.cpp index ae6d97277..47d3007af 100644 --- a/sources/editor/graphicspart/parttext.cpp +++ b/sources/editor/graphicspart/parttext.cpp @@ -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); } diff --git a/sources/editor/graphicspart/parttext.h b/sources/editor/graphicspart/parttext.h index 4ba66f7c8..29609440e 100644 --- a/sources/editor/graphicspart/parttext.h +++ b/sources/editor/graphicspart/parttext.h @@ -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;