Add horizontal and vertical mirror for symbols on a folio (#1335)

Edit > "Miroir horizontal" (M) and "Miroir vertical" (F), also in the
folio's right-click menu, mirror the selected symbols in place. The keys
are the ones the element editor uses for the same two actions.

An element keeps two mirrors about its own axes, applied before its
rotation, and saves them as mirror="horizontal|vertical|both" on its
<element> (written only when set, so other projects save byte for byte
as before). On a symbol turned by 90 or 270 degrees, a mirror of the
folio is the other mirror of the symbol itself, so the rotation never
changes: a label kept upright does not swing round, and "Pivoter" still
turns a mirrored symbol clockwise.

- Terminals face the mirrored way (Terminal::orientation()), so wires
  follow.
- The symbol stays where it was: its centre is kept, on the grid, since
  the hotspot is often a corner.
- Texts read normally. The element's texts, text groups and cross
  reference are mirrored a second time about the centre of their own box
  (Element::keepReadable()), and ElementPictureFactory draws the texts
  of the symbol itself the same way, in a cached picture per mirror.
  Groups and cross references held at the bottom of the folio stay
  centred under their element.
- DXF export mirrors the symbol's lines, arcs and texts.
- Scripting: qet.mirrorElement(folio, uuid, vertical) and
  qet.elementMirror(folio, uuid); live mode may run both menu commands;
  the MCP server gets a mirror_element op and qet_diff reports mirrors.

Not done: the parts of a PLC table drawn at run time
(Element::drawPlcTable()) are not kept readable on a mirrored PLC; the
project database has no column for it, as it has none for the rotation.

Test: tst_scriptmirror mirrors a symbol through --run, checks every
terminal's side and facing, the round trip, undo, a save and reload,
and a symbol turned by 90 degrees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-07 11:01:27 +13:00
parent 0708338bce
commit a5b5d49b98
21 changed files with 923 additions and 33 deletions
+42 -7
View File
@@ -37,6 +37,7 @@
#include "qetgraphicsitem/terminal.h"
#include "textlines.h"
#include <QFontMetricsF>
#include <QGraphicsSimpleTextItem>
#include <QSet>
#include <cmath>
@@ -178,6 +179,10 @@ void DxfExport::write(Diagram *diagram, int width, int height,
foreach(Element *elmt, list_elements)
{
double rotation_angle = elmt -> orientation() * 90;
//The mirrors of an element are about its own axes, before it is
//rotated, see Element::setMirror()
const qreal mirror_x = elmt -> hasHorizontalMirror() ? -1 : 1;
const qreal mirror_y = elmt -> hasVerticalMirror() ? -1 : 1;
qreal elem_pos_x = elmt -> pos().x();
qreal elem_pos_y = elmt -> pos().y();// - (diagram -> margin / 2);
@@ -191,8 +196,26 @@ void DxfExport::write(Diagram *diagram, int width, int height,
if (fontSize < 0)
fontSize = text->font().pixelSize();
qreal x = elem_pos_x + text->pos().x();
qreal y = elem_pos_y + text->pos().y();
QPointF text_pos = text->pos();
if (elmt -> isMirrored())
{
//The text keeps reading normally: only its box is
//mirrored, about its own centre, as ElementPictureFactory
//draws it on the folio
QTransform box_transform;
box_transform.translate(text->pos().x(), text->pos().y());
box_transform.rotate(text->rotation());
const QFontMetricsF metrics(text->font());
const QRectF box(0, -metrics.ascent(),
text->boundingRect().width(),
text->boundingRect().height());
const QPointF centre = box_transform.mapRect(box).center();
text_pos += QPointF(centre.x() * (mirror_x - 1),
centre.y() * (mirror_y - 1));
}
qreal x = elem_pos_x + text_pos.x();
qreal y = elem_pos_y + text_pos.y();
qreal angle = text -> rotation() + rotation_angle;
qreal angler = angle * M_PI/180;
@@ -217,21 +240,21 @@ void DxfExport::write(Diagram *diagram, int width, int height,
Createdxf::layer = Layer::Symbols;
for (QLineF line : primitives.m_lines)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle);
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QLineF l = t.map(line);
Createdxf::drawLine(file_path, l, 0);
}
for (QRectF rect : primitives.m_rectangles)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle);
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QRectF r = t.mapRect(rect);
Createdxf::drawRectangle(file_path,r,0);
}
for (QRectF circle_rect : primitives.m_circles)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle);
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QPointF c = t.map(QPointF(circle_rect.center().x(),circle_rect.center().y()));
Createdxf::drawCircle(file_path,c,circle_rect.width()/2,0);
}
@@ -240,7 +263,7 @@ void DxfExport::write(Diagram *diagram, int width, int height,
{
if (polygon.size() == 0)
continue;
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle);
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QPolygonF poly = t.map(polygon);
if(poly.isClosed())
Createdxf::drawPolygon(file_path,poly,0);
@@ -259,6 +282,18 @@ void DxfExport::write(Diagram *diagram, int width, int height,
qreal h = arc.at(3);
qreal startAngle = arc.at(4);
qreal spanAngle = arc .at(5);
//In a mirror the arc starts where it ended: an angle a
//becomes 180 - a in a horizontal one, -a in a vertical one
if (elmt -> hasHorizontalMirror())
{
x = elem_pos_x - arc.at(0) - w;
startAngle = 180 - startAngle - spanAngle;
}
if (elmt -> hasVerticalMirror())
{
y = elem_pos_y - arc.at(1) - h;
startAngle = -startAngle - spanAngle;
}
QRectF r(x,y,w,h);
QPointF hotspot(elem_pos_x,elem_pos_y);
Createdxf::drawArcEllipse(file_path, r, startAngle, spanAngle, hotspot, rotation_angle, 0);
@@ -268,7 +303,7 @@ void DxfExport::write(Diagram *diagram, int width, int height,
// Draw terminals
QList<Terminal *> list_terminals = elmt->terminals();
QColor col("red");
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle);
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
foreach(Terminal *tp, list_terminals) {
QPointF c = t.map(QPointF(tp->dock_elmt_.x(),tp->dock_elmt_.y()));
Createdxf::drawCircle(file_path,c,3.0,Createdxf::dxfColor(col));