Merge pull request #1354 from ispyisail/feat/1335-flip-elements

Add horizontal and vertical mirror for symbols on a folio (#1335)
This commit is contained in:
ispyisail
2026-10-07 21:30:18 +13:00
committed by GitHub
21 changed files with 976 additions and 43 deletions
+2
View File
@@ -899,6 +899,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/undocommand/groupitemscommand.h
${QET_DIR}/sources/undocommand/alignselectioncommand.cpp
${QET_DIR}/sources/undocommand/alignselectioncommand.h
${QET_DIR}/sources/undocommand/mirrorselectioncommand.cpp
${QET_DIR}/sources/undocommand/mirrorselectioncommand.h
${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp
${QET_DIR}/sources/undocommand/rotateselectioncommand.h
${QET_DIR}/sources/undocommand/promoteshapecommand.cpp
+12 -1
View File
@@ -682,13 +682,18 @@ def tool_diff(before: str, after: str) -> dict:
# the only thing a rotation changes, so without it a rotated symbol
# reads as untouched.
r["orientation"] = e.get("orientation", "0")
# A mirrored symbol carries mirror="horizontal", "vertical" or
# "both", about its own axes; a mirror changes nothing else unless
# the symbol's hotspot is off its centre.
r["mirror"] = e.get("mirror", "")
a_el[r["uuid"] or f"{i}:{r['x']},{r['y']}:{r['name']}"] = r
for i, e in _elements(_root(after)):
r = _element_row(i, e)
r["orientation"] = e.get("orientation", "0")
r["mirror"] = e.get("mirror", "")
b_el[r["uuid"] or f"{i}:{r['x']},{r['y']}:{r['name']}"] = r
moved, rotated, relabelled, changed_info = [], [], [], []
moved, rotated, mirrored, relabelled, changed_info = [], [], [], [], []
for k, a in a_el.items():
b = b_el.get(k)
if b is None:
@@ -696,6 +701,9 @@ def tool_diff(before: str, after: str) -> dict:
if a["orientation"] != b["orientation"]:
rotated.append({"uuid": k, "name": a["name"], "folio": a["folio"],
"orientation": [a["orientation"], b["orientation"]]})
if a["mirror"] != b["mirror"]:
mirrored.append({"uuid": k, "name": a["name"], "folio": a["folio"],
"mirror": [a["mirror"], b["mirror"]]})
if (a["x"], a["y"]) != (b["x"], b["y"]):
moved.append({
"uuid": k, "name": a["name"], "folio": a["folio"],
@@ -764,6 +772,7 @@ def tool_diff(before: str, after: str) -> dict:
"relabelled": relabelled[:50],
"info_changed": changed_info[:50],
"rotated": rotated[:50],
"mirrored": mirrored[:50],
},
"conductors": {
"before": len(a_co), "after": len(b_co),
@@ -1401,6 +1410,8 @@ OPS = {
("dx", "num"), ("dy", "num")]),
"rotate_element": ("rotateElement", [("folio", "folio"), ("element", "elmt"),
("angle", "num")]),
"mirror_element": ("mirrorElement", [("folio", "folio"), ("element", "elmt"),
("vertical", "bool")]),
"set_label": ("setElementLabel", [("folio", "folio"), ("element", "elmt"),
("label", "str")]),
"set_info": ("setElementInfo", [("folio", "folio"), ("element", "elmt"),
+15 -2
View File
@@ -379,6 +379,7 @@ class EditValidation(unittest.TestCase):
"set_position": el + [{"op": "set_position", "folio": "$f", "element": "$e", "x": 1, "y": 1}],
"move_element": el + [{"op": "move_element", "folio": "$f", "element": "$e", "dx": 1, "dy": 1}],
"rotate_element": el + [{"op": "rotate_element", "folio": "$f", "element": "$e", "angle": 90}],
"mirror_element": el + [{"op": "mirror_element", "folio": "$f", "element": "$e", "vertical": False}],
"set_label": el + [{"op": "set_label", "folio": "$f", "element": "$e", "label": "K"}],
"set_info": el + [{"op": "set_info", "folio": "$f", "element": "$e", "key": "k", "value": "v"}],
"add_conductor": two + [{"op": "add_conductor", "folio": "$f", "from": "$e", "from_terminal": 0,
@@ -2065,13 +2066,13 @@ class DiffContracts(unittest.TestCase):
"relabelled": [{"uuid": A, "name": "coil", "from": "K1", "to": "K2"}],
"info_changed": [{"uuid": A, "name": "coil", "from": {"label": "K1", "comment": "c"},
"to": {"label": "K2", "comment": "d"}}],
"rotated": []})
"rotated": [], "mirrored": []})
def test_an_unchanged_element_reports_nothing(self):
p = self.qet(self.folio(self.el(self.A, 1, 2, "K1", "c")))
e = m.tool_diff(p, p)["elements"]
self.assertEqual((e["moved"], e["relabelled"], e["info_changed"], e["added"], e["removed"],
e["rotated"]), ([], [], [], [], [], []))
e["rotated"], e["mirrored"]), ([], [], [], [], [], [], []))
def test_equal_angles_written_differently_are_the_same(self):
self.assertEqual([m._angle(v) for v in ("-270", "90", "-90", "270", "360", "0", "450", "12.5", "", "x")],
@@ -2098,6 +2099,18 @@ class DiffContracts(unittest.TestCase):
plain = self.qet(self.folio(self.el(self.A, 1, 2)))
self.assertEqual(m.tool_diff(plain, rot(0))["elements"]["rotated"], [])
def test_a_mirror_is_reported(self):
"""A mirror of a symbol changes its mirror attribute, which is absent
when the symbol is not mirrored."""
plain = self.qet(self.folio(self.el(self.A, 1, 2)))
mirrored = self.qet(self.folio(
self.el(self.A, 1, 2).replace('x="1"', 'mirror="horizontal" x="1"')))
d = m.tool_diff(plain, mirrored)
self.assertEqual(d["elements"]["mirrored"],
[{"uuid": self.A, "name": "coil", "folio": 1, "mirror": ["", "horizontal"]}])
self.assertEqual((d["elements"]["moved"], d["elements"]["rotated"]), ([], []))
self.assertEqual(m.tool_diff(plain, plain)["elements"]["mirrored"], [])
def test_conductors_section_exact(self):
A, B, C = self.A, self.B, self.C
els = self.el(A, 0, 0) + self.el(B, 0, 0) + self.el(C, 0, 0)
+62 -17
View File
@@ -38,6 +38,7 @@
#include "qetinformation.h"
#include "textlines.h"
#include <QFontMetricsF>
#include <QGraphicsSimpleTextItem>
#include <QSet>
#include <cmath>
@@ -48,14 +49,22 @@ namespace {
Draw the drawing of @a elmt's definition: its static texts, lines,
rectangles, circles, polygons and arcs, and its terminals when
@a draw_terminals, as if it were placed at @a elem_pos_x,
@a elem_pos_y and turned @a rotation_angle degrees. Used for every
symbol drawn in full, and once per block for a block's content.
@a elem_pos_y and turned @a rotation_angle degrees, with @a elmt's
mirrors when @a mirrored. Used for every symbol drawn in full, and
once per block for a block's content.
*/
void drawSymbol(const QString &file_path, Element *elmt,
qreal elem_pos_x, qreal elem_pos_y,
double rotation_angle, bool draw_terminals)
double rotation_angle, bool draw_terminals,
bool mirrored = false)
{
using namespace DxfExport;
//The mirrors of an element are about its own axes, before it is
//rotated, see Element::setMirror()
const bool mirror_h = mirrored && elmt -> hasHorizontalMirror();
const bool mirror_v = mirrored && elmt -> hasVerticalMirror();
const qreal mirror_x = mirror_h ? -1 : 1;
const qreal mirror_y = mirror_v ? -1 : 1;
ElementPictureFactory::primitives primitives = ElementPictureFactory::instance()->getPrimitives(elmt->location());
Createdxf::layer = Layer::SymbolTexts;
@@ -65,8 +74,26 @@ namespace {
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 (mirror_h || mirror_v)
{
//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;
@@ -91,21 +118,21 @@ namespace {
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);
}
@@ -114,7 +141,7 @@ namespace {
{
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);
@@ -133,6 +160,18 @@ namespace {
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 (mirror_h)
{
x = elem_pos_x - arc.at(0) - w;
startAngle = 180 - startAngle - spanAngle;
}
if (mirror_v)
{
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);
@@ -142,7 +181,7 @@ namespace {
// 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));
@@ -345,7 +384,7 @@ void DxfExport::write(Diagram *diagram, int width, int height,
QSet<QString> used;
for (Element *elmt : std::as_const(list_elements)) {
const QString key = elmt -> location().toString();
if (block_names.contains(key)
if (elmt -> isMirrored() || block_names.contains(key)
|| drawsNothing(elmt, properties.draw_terminals))
continue;
QString file_name = elmt -> location().fileName();
@@ -363,6 +402,14 @@ void DxfExport::write(Diagram *diagram, int width, int height,
block_models << elmt;
}
}
//A mirrored symbol is drawn in full: an INSERT with a negative
//scale would mirror its texts too, which QElectroTech keeps
//readable
const auto blockOf = [&block_names](Element *elmt) {
return elmt -> isMirrored()
? QString()
: block_names.value(elmt -> location().toString());
};
//With dxf_attributes, a symbol's own texts (its label, its
//function...) become attributes of its INSERT, written where the
@@ -384,9 +431,7 @@ void DxfExport::write(Diagram *diagram, int width, int height,
break;
auto *deti = qgraphicsitem_cast<DynamicElementTextItem *>(dti);
Element *elmt = deti ? deti -> parentElement() : nullptr;
const QString block = elmt
? block_names.value(elmt -> location().toString())
: QString();
const QString block = elmt ? blockOf(elmt) : QString();
if (block.isEmpty())
continue;
@@ -445,7 +490,7 @@ void DxfExport::write(Diagram *diagram, int width, int height,
//repeated; the label formula is how the label is made, not
//part data.
for (Element *elmt : std::as_const(list_elements)) {
const QString block = block_names.value(elmt -> location().toString());
const QString block = blockOf(elmt);
if (block.isEmpty())
continue;
const DiagramContext information = elmt -> elementInformations();
@@ -525,10 +570,10 @@ void DxfExport::write(Diagram *diagram, int width, int height,
const qreal elem_pos_x = elmt -> pos().x();
const qreal elem_pos_y = elmt -> pos().y();
const QString block = block_names.value(elmt -> location().toString());
const QString block = blockOf(elmt);
if (block.isEmpty()) {
drawSymbol(file_path, elmt, elem_pos_x, elem_pos_y,
rotation_angle, properties.draw_terminals);
rotation_angle, properties.draw_terminals, true);
} else {
Createdxf::layer = Layer::Symbols;
//QElectroTech turns a symbol clockwise, DXF counter-clockwise
+80 -4
View File
@@ -94,6 +94,40 @@ void ElementPictureFactory::getPictures(const ElementsLocation &location, QPictu
}
}
/**
@brief ElementPictureFactory::getMirroredPictures
Same as getPictures(), for an element mirrored on its folio
(Element::setMirror()). The drawing is the same; only the texts differ:
each one is drawn so that, once the element's mirror is applied, its box
lands mirrored but its letters still read normally.
@param location
@param horizontal : the element's left and right swap
@param vertical : the element's top and bottom swap
@param picture
@param low_picture
*/
void ElementPictureFactory::getMirroredPictures(const ElementsLocation &location,
bool horizontal,
bool vertical,
QPicture &picture,
QPicture &low_picture)
{
if(!location.exist() || !(horizontal || vertical)) {
return;
}
const QPair<QUuid, int> key(cacheKey(location),
(horizontal ? 1 : 0) | (vertical ? 2 : 0));
if(!m_mirrored_pictures_H.contains(key)
&& !build(location, nullptr, nullptr,
QTransform::fromScale(horizontal ? -1 : 1, vertical ? -1 : 1))) {
return;
}
picture = m_mirrored_pictures_H.value(key);
low_picture = m_mirrored_low_pictures_H.value(key);
}
/**
@brief ElementPictureFactory::dropCache
Forget the cached drawing of the element at @p location, so the next
@@ -113,6 +147,10 @@ void ElementPictureFactory::dropCache(const ElementsLocation &location)
const QUuid uuid = cacheKey(location);
m_pictures_H.remove(uuid);
m_low_pictures_H.remove(uuid);
for (int mirror = 1 ; mirror <= 3 ; ++mirror) {
m_mirrored_pictures_H.remove(qMakePair(uuid, mirror));
m_mirrored_low_pictures_H.remove(qMakePair(uuid, mirror));
}
m_pixmap_H.remove(uuid);
//The text items belong to the cache; nothing else holds them.
qDeleteAll(m_primitives_H.take(uuid).m_texts);
@@ -217,11 +255,16 @@ ElementPictureFactory::~ElementPictureFactory()
this function draw on it and don't store it.
if null, this function create a QPicture for normal and low zoom,
draw on it and store it in m_pictures_H and m_low_pictures_H
(m_mirrored_pictures_H and m_mirrored_low_pictures_H if @p mirror
is not the identity)
@param mirror : the mirror of an element to draw the drawing for, see
getMirroredPictures()
@return
*/
bool ElementPictureFactory::build(const ElementsLocation &location,
QPicture *picture,
QPicture *low_picture)
QPicture *low_picture,
const QTransform &mirror)
{
QDomElement dom = location.xml();
@@ -281,6 +324,8 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
tmp.setCosmetic(true);
low_painter.setPen(tmp);
m_build_mirror = mirror;
//scroll of the Children of the Definition: Parts of the Drawing
// Extract PLC master data for rendering plc_table parts
QDomElement plc_master_data;
@@ -333,9 +378,24 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
//End of the drawing
painter.end();
low_painter.end();
m_build_mirror = QTransform();
qDeleteAll(low_primitives.m_texts);
const auto uuid_ = cacheKey(location);
if (!mirror.isIdentity()) {
//The primitives are those of the drawing without mirror,
//already kept by the build of that one
qDeleteAll(primitives_.m_texts);
const QPair<QUuid, int> key(uuid_, (mirror.m11() < 0 ? 1 : 0)
| (mirror.m22() < 0 ? 2 : 0));
if (!picture) {
m_mirrored_pictures_H.insert(key, pic);
}
if (!low_picture) {
m_mirrored_low_pictures_H.insert(key, low_pic);
}
return true;
}
if (!picture) {
m_pictures_H.insert(uuid_, pic);
m_primitives_H.insert(uuid_, primitives_);
@@ -616,8 +676,6 @@ void ElementPictureFactory::parseText(const QDomElement &dom, QPainter &painter,
text_document.setPlainText(dom.attribute("text"));
painter.setTransform(QTransform(), false);
painter.translate(dom.attribute("x").toDouble(), dom.attribute("y").toDouble());
painter.rotate(dom.attribute("rotation", "0").toDouble());
/*
Moves the QPainter's coordinate system to render in the right place;
@@ -651,7 +709,25 @@ void ElementPictureFactory::parseText(const QDomElement &dom, QPainter &painter,
}
}
painter.translate(qpainter_offset);
QTransform text_transform;
text_transform.translate(dom.attribute("x").toDouble(), dom.attribute("y").toDouble());
text_transform.rotate(dom.attribute("rotation", "0").toDouble());
text_transform.translate(qpainter_offset.x(), qpainter_offset.y());
if (!m_build_mirror.isIdentity())
{
//The element is mirrored about its own axes when it is drawn.
//Mirror the text the same way first, about the centre of its
//box: the two mirrors cancel on the letters, which read
//normally, and the box still ends up where the element's
//mirror puts it.
const QRectF box(QPointF(0, 0), text_document.size());
const QPointF centre = text_transform.mapRect(box).center();
painter.setTransform(QTransform::fromTranslate(-centre.x(), -centre.y())
* m_build_mirror
* QTransform::fromTranslate(centre.x(), centre.y()));
}
painter.setTransform(text_transform, true);
// force the palette used to render the QTextDocument
QAbstractTextDocumentLayout::PaintContext ctx;
+12 -1
View File
@@ -21,6 +21,8 @@
#include <QMutex>
#include <QSharedPointer>
#include <QHash>
#include <QPair>
#include <QTransform>
class ElementsLocation;
class QPicture;
@@ -83,6 +85,8 @@ class ElementPictureFactory
}
void getPictures(const ElementsLocation &location, QPicture &picture, QPicture &low_picture);
void getMirroredPictures(const ElementsLocation &location, bool horizontal, bool vertical,
QPicture &picture, QPicture &low_picture);
QPixmap pixmap(const ElementsLocation &location);
ElementPictureFactory::primitives getPrimitives(const ElementsLocation &location);
void dropCache(const ElementsLocation &location);
@@ -94,7 +98,8 @@ class ElementPictureFactory
~ElementPictureFactory();
static QUuid cacheKey(const ElementsLocation &location);
bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr);
bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr,
const QTransform &mirror=QTransform());
void parseElement(const QDomElement &dom, QPainter &painter, primitives &prim) const;
void parseLine (const QDomElement &dom, QPainter &painter, primitives &prim) const;
void parseRect (const QDomElement &dom, QPainter &painter, primitives &prim) const;
@@ -108,6 +113,12 @@ class ElementPictureFactory
QHash<QUuid, QPicture> m_pictures_H;
QHash<QUuid, QPicture> m_low_pictures_H;
/// Drawings for mirrored elements, by element and mirror
/// (horizontal, vertical, both), see getMirroredPictures()
QHash<QPair<QUuid, int>, QPicture> m_mirrored_pictures_H;
QHash<QPair<QUuid, int>, QPicture> m_mirrored_low_pictures_H;
/// The mirror build() draws for, read by parseText()
QTransform m_build_mirror;
QHash<QUuid, QPixmap> m_pixmap_H;
QHash<QUuid, primitives> m_primitives_H;
static ElementPictureFactory* m_factory;
+24
View File
@@ -78,6 +78,7 @@
#include "utils/qetutils.h"
#include "undocommand/groupitemscommand.h"
#include "undocommand/alignselectioncommand.h"
#include "undocommand/mirrorselectioncommand.h"
#include "undocommand/rotateselectioncommand.h"
#include "undocommand/rotatetextscommand.h"
#include "diagram.h"
@@ -982,6 +983,8 @@ void QETDiagramEditor::setUpActions()
m_rotate_selection = m_selection_actions_group.addAction( QET::Icons::TransformRotate, tr("Pivoter") );
m_rotate_group_selection = m_selection_actions_group.addAction( QET::Icons::TransformRotate, tr("Pivoter le groupe") );
m_rotate_texts = m_selection_actions_group.addAction( QET::Icons::ObjectRotateRight, tr("Orienter les textes") );
m_mirror_horizontal = m_selection_actions_group.addAction( QET::Icons::ImageFlipHorizontal, tr("Miroir horizontal") );
m_mirror_vertical = m_selection_actions_group.addAction( QET::Icons::ImageFlipVertical, tr("Miroir vertical") );
m_find_element = m_selection_actions_group.addAction( QET::Icons::ZoomDraw, tr("Retrouver dans le panel") );
m_edit_selection = m_selection_actions_group.addAction( QET::Icons::ElementEdit, tr("Éditer l'item sélectionné") );
m_group_selected_texts = m_selection_actions_group.addAction( QET::Icons::textGroup, tr("Grouper les textes sélectionnés"));
@@ -992,6 +995,9 @@ void QETDiagramEditor::setUpActions()
ShortcutManager::instance().registerAction(m_rotate_selection, "diagrameditor.rotate_selection", tr("Éditeur de schémas"), Qt::Key_Space);
ShortcutManager::instance().registerAction(m_rotate_group_selection, "diagrameditor.rotate_group_selection", tr("Éditeur de schémas"), Qt::SHIFT | Qt::Key_Space);
ShortcutManager::instance().registerAction(m_rotate_texts, "diagrameditor.rotate_texts", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_Space);
//M and F, the keys of the same two actions in the element editor
ShortcutManager::instance().registerAction(m_mirror_horizontal, "diagrameditor.mirror_horizontal", tr("Éditeur de schémas"), Qt::Key_M);
ShortcutManager::instance().registerAction(m_mirror_vertical, "diagrameditor.mirror_vertical", tr("Éditeur de schémas"), Qt::Key_F);
ShortcutManager::instance().registerAction(m_find_element, "diagrameditor.find_element", tr("Éditeur de schémas"), QKeySequence());
ShortcutManager::instance().registerAction(m_group_selected_texts, "diagrameditor.group_selected_texts", tr("Éditeur de schémas"), QKeySequence());
ShortcutManager::instance().registerAction(m_edit_selection, "diagrameditor.edit_selection", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_E);
@@ -1085,12 +1091,16 @@ void QETDiagramEditor::setUpActions()
m_rotate_selection->setStatusTip( tr("Pivote les éléments et textes sélectionnés", "status bar tip"));
m_rotate_group_selection->setStatusTip( tr("Pivote la sélection comme un groupe autour de son centre, au lieu de chaque élément sur place", "status bar tip"));
m_rotate_texts ->setStatusTip( tr("Pivote les textes sélectionnés à un angle précis", "status bar tip"));
m_mirror_horizontal->setStatusTip( tr("Retourne les éléments sélectionnés de gauche à droite", "status bar tip"));
m_mirror_vertical ->setStatusTip( tr("Retourne les éléments sélectionnés de haut en bas", "status bar tip"));
m_find_element ->setStatusTip( tr("Retrouve l'élément sélectionné dans le panel", "status bar tip"));
m_delete_selection ->setData("delete_selection");
m_rotate_selection ->setData("rotate_selection");
m_rotate_group_selection->setData("rotate_group_selection");
m_rotate_texts ->setData("rotate_selected_text");
m_mirror_horizontal ->setData("mirror_horizontal");
m_mirror_vertical ->setData("mirror_vertical");
m_find_element ->setData("find_selected_element");
m_edit_selection ->setData("edit_selected_element");
m_group_selected_texts->setData("group_selected_texts");
@@ -2384,6 +2394,16 @@ void QETDiagramEditor::selectionGroupTriggered(QAction *action)
if(c->isValid())
diagram->undoStack().push(c);
}
else if (value == "mirror_horizontal" || value == "mirror_vertical")
{
auto *c = new MirrorSelectionCommand(
diagram,
value == "mirror_horizontal" ? Qt::Horizontal : Qt::Vertical);
if (c->isValid())
diagram->undoStack().push(c);
else
delete c;
}
else if (value == "rotate_group_selection")
{
RotateSelectionCommand *c = new RotateSelectionCommand(diagram, 90, nullptr, true);
@@ -2621,6 +2641,8 @@ void QETDiagramEditor::slot_updateComplexActions()
<< m_delete_selection
<< m_rotate_selection
<< m_rotate_group_selection
<< m_mirror_horizontal
<< m_mirror_vertical
<< m_edit_selection
<< m_group_selected_texts
<< m_group_selection
@@ -2644,6 +2666,8 @@ void QETDiagramEditor::slot_updateComplexActions()
// number of selected elements
int selected_elements_count = dc.count(DiagramContent::Elements);
m_find_element->setEnabled(selected_elements_count == 1);
m_mirror_horizontal->setEnabled(!ro && selected_elements_count);
m_mirror_vertical->setEnabled(!ro && selected_elements_count);
//Actions that need items (elements, conductors, texts...) selected, to be enabled
bool copiable_items = dc.hasCopiableItems();
+2
View File
@@ -271,6 +271,8 @@ class QETDiagramEditor : public QETMainWindow
*m_rotate_selection, ///< Rotate selected elements and text items by 90 degrees
*m_rotate_group_selection = nullptr, ///< Rotate the selection as a whole around its shared center, instead of each item in place
*m_rotate_texts, ///< Direct selected text items to a specific angle
*m_mirror_horizontal = nullptr, ///< Mirror the selected elements, left and right swap (#1335)
*m_mirror_vertical = nullptr, ///< Mirror the selected elements, top and bottom swap (#1335)
*m_find_element, ///< Find the selected element in the panel
*m_group_selected_texts = nullptr,
*m_group_selection = nullptr, ///< Group the selected items (#1070)
+7 -1
View File
@@ -124,6 +124,7 @@ void CrossRefItem::setUpConnection()
m_update_connection << connect(m_element, &Element::xChanged, this, &CrossRefItem::autoPos);
m_update_connection << connect(m_element, &Element::yChanged, this, &CrossRefItem::autoPos);
m_update_connection << connect(m_element, &Element::rotationChanged, this, &CrossRefItem::autoPos);
m_update_connection << connect(m_element, &Element::mirrorChanged, this, &CrossRefItem::autoPos);
set=true;
}
// For PLC masters, always set up connections for update notifications
@@ -288,6 +289,7 @@ void CrossRefItem::updateLabel()
QList<QPointF> positions = m_element->plcTablePositions();
QPointF pos = positions.isEmpty() ? QPointF(0, 0) : positions.first();
setPos(pos);
m_element->keepReadable(this);
// Populate m_hovered_contacts_map using drawAsPlcTable on a
// dummy painter (m_update_map=true).
@@ -338,6 +340,7 @@ void CrossRefItem::autoPos()
stackAtBottom();
else
QGIUtility::centerToParentBottom(this);
m_element->keepReadable(this);
}
/**
@@ -409,9 +412,12 @@ void CrossRefItem::stackAtBottom()
moved = true;
}
}
//From pos(), not scenePos(): on a mirrored element scenePos()
//also holds the mirror that keeps the cross readable
if (self.rect.bottom() != bottom)
xref->setPos(xref->parentItem()->mapFromScene(
xref->scenePos() + QPointF(0, self.rect.bottom() - bottom)));
xref->parentItem()->mapToScene(xref->pos())
+ QPointF(0, self.rect.bottom() - bottom)));
placed << self;
}
}
+205 -15
View File
@@ -36,10 +36,12 @@
#include "../ui/elementpropertieswidget.h"
#include "../undocommand/changeelementinformationcommand.h"
#include "../undocommand/setautonumcontextcommand.h"
#include "crossrefitem.h"
#include "dynamicelementtextitem.h"
#include "elementtextitemgroup.h"
#include "iostream"
#include <QAbstractTextDocumentLayout>
#include <QApplication>
#include <QCollator>
#include <QScreen>
@@ -161,6 +163,10 @@ Element::Element(
connect(this, &Element::rotationChanged, update_docked_conductors);
connect(this, &Element::xChanged, update_docked_conductors);
connect(this, &Element::yChanged, update_docked_conductors);
connect(this, &Element::mirrorChanged, update_docked_conductors);
//The mirror is about the element's own axis, so it follows the
//rotation, see applyMirrorTransform()
connect(this, &Element::rotationChanged, this, &Element::applyMirrorTransform);
}
/**
@@ -251,6 +257,152 @@ void Element::displayHelpLine(bool b)
t->drawHelpLine(b);
}
/**
@brief Element::setHorizontalMirror
Mirror this element about its own vertical axis (its left and right
swap), or put it back. See setMirror().
@param mirror
*/
void Element::setHorizontalMirror(bool mirror)
{
setMirror(mirror, m_vertical_mirror);
}
/**
@brief Element::setVerticalMirror
Mirror this element about its own horizontal axis (its top and bottom
swap), or put it back. See setMirror().
@param mirror
*/
void Element::setVerticalMirror(bool mirror)
{
setMirror(m_horizontal_mirror, mirror);
}
/**
@brief Element::setMirror
The mirrors are about the element's own axes, through its hotspot, and
are applied before the rotation: a mirrored element still turns
clockwise with "Pivoter", and orientation() keeps its meaning. On the
folio, a mirror of a turned element is the other mirror of the element
itself, see MirrorSelectionCommand.
The texts of the element, its cross reference and the texts drawn in
its symbol are moved to their mirrored place but keep reading normally.
@param horizontal : the left and right of the element swap
@param vertical : the top and bottom of the element swap
*/
void Element::setMirror(bool horizontal, bool vertical)
{
if (horizontal == m_horizontal_mirror && vertical == m_vertical_mirror)
return;
m_horizontal_mirror = horizontal;
m_vertical_mirror = vertical;
m_mirrored_picture = QPicture();
m_mirrored_low_zoom_picture = QPicture();
if (isMirrored())
ElementPictureFactory::instance()->getMirroredPictures(
m_location,
horizontal,
vertical,
m_mirrored_picture,
m_mirrored_low_zoom_picture);
applyMirrorTransform();
keepChildrenReadable();
update();
emit mirrorChanged();
}
/**
@brief Element::mirrorTransform
@return the mirrors of this element, about its own axes
*/
QTransform Element::mirrorTransform() const
{
return QTransform::fromScale(m_horizontal_mirror ? -1 : 1,
m_vertical_mirror ? -1 : 1);
}
/**
@brief Element::applyMirrorTransform
QGraphicsItem applies transform() after the rotation, so the mirrors of
the element's own axes (S, before rotating) are given here as the
mirrors about the rotated axes: R . S . R^-1, which is then applied
after R.
*/
void Element::applyMirrorTransform()
{
if (!isMirrored())
{
if (!transform().isIdentity())
setTransform(QTransform());
return;
}
//QTransform's product applies its left operand first
setTransform(QTransform().rotate(-rotation())
* mirrorTransform()
* QTransform().rotate(rotation()));
}
/**
@brief Element::keepReadable
Keep @p child, a text, a group of texts or a cross reference of this
element, reading normally when this element is mirrored.
The child is mirrored a second time, the same way, about the centre of
its own box: the box keeps its place in the element, so it lands
mirrored with the element, but the two mirrors cancel on what is drawn
inside it.
Does nothing for an item that is not a direct child of this element.
@param child
*/
void Element::keepReadable(QGraphicsItem *child) const
{
if (!child || child->parentItem() != this)
return;
if (!isMirrored())
{
if (!child->transform().isIdentity())
child->resetTransform();
return;
}
//The rotation part of the child's own transformation, which
//QGraphicsItem applies before transform()
const QPointF origin = child->transformOriginPoint();
QTransform rotation;
rotation.translate(origin.x(), origin.y());
rotation.rotate(child->rotation());
rotation.translate(-origin.x(), -origin.y());
const QPointF centre = rotation.mapRect(child->boundingRect()).center();
const QTransform mirror = QTransform::fromTranslate(-centre.x(), -centre.y())
* mirrorTransform()
* QTransform::fromTranslate(centre.x(), centre.y());
if (child->transform() != mirror)
child->setTransform(mirror);
}
/**
@brief Element::keepChildrenReadable
keepReadable() for every text, group of texts and cross reference
which is a direct child of this element.
*/
void Element::keepChildrenReadable() const
{
const QList<QGraphicsItem *> children = childItems();
for (QGraphicsItem *child : children)
{
if (child->type() == DynamicElementTextItem::Type
|| child->type() == CrossRefItem::Type
|| dynamic_cast<ElementTextItemGroup *>(child))
keepReadable(child);
}
}
/**
@brief Element::paint
@param painter
@@ -275,13 +427,16 @@ void Element::paint(
QBrush brush;
painter->setPen(pen);
painter->setBrush(brush);
const QPicture &picture = isMirrored() ? m_mirrored_picture : m_picture;
const QPicture &low_zoom_picture = isMirrored() ? m_mirrored_low_zoom_picture
: m_low_zoom_picture;
if (options && options->levelOfDetailFromTransform(painter->worldTransform()) < 0.5)
{
if (!m_low_zoom_picture.isNull())
painter->drawPicture(0, 0, m_low_zoom_picture);
if (!low_zoom_picture.isNull())
painter->drawPicture(0, 0, low_zoom_picture);
} else {
if (!m_picture.isNull())
painter->drawPicture(0, 0, m_picture);
if (!picture.isNull())
painter->drawPicture(0, 0, picture);
}
painter->restore(); //Restore the QPainter after use drawPicture
@@ -938,6 +1093,11 @@ bool Element::fromXml(QDomElement &e,
for(DynamicElementTextItem *deti : m_dynamic_text_list)
deti->m_block_alignment = false;
//Last, so that the texts and groups loaded above are kept readable
const QString mirror = e.attribute(QStringLiteral("mirror"));
setMirror(mirror == QLatin1String("horizontal") || mirror == QLatin1String("both"),
mirror == QLatin1String("vertical") || mirror == QLatin1String("both"));
m_state = QET::GIOK;
return(true);
}
@@ -987,6 +1147,13 @@ QDomElement Element::toXml(
element.setAttribute(QStringLiteral("y"), QString::number(pos().y()));
element.setAttribute(QStringLiteral("z"), QString::number(this->zValue()));
element.setAttribute(QStringLiteral("orientation"), QString::number(orientation()));
//Written only when set, so a project without a mirrored element
//saves exactly as before
if (isMirrored())
element.setAttribute(QStringLiteral("mirror"),
!m_vertical_mirror ? QStringLiteral("horizontal")
: !m_horizontal_mirror ? QStringLiteral("vertical")
: QStringLiteral("both"));
element.setAttribute(QStringLiteral("is_movable"), bool(is_movable_));
/* get the first id to use for the bounds of this element
@@ -1133,18 +1300,25 @@ QDomElement Element::toXml(
*/
void Element::addDynamicTextItem(DynamicElementTextItem *deti)
{
if (deti && !m_dynamic_text_list.contains(deti))
{
m_dynamic_text_list.append(deti);
deti->setParentItem(this);
emit textAdded(deti);
}
if (!deti || m_dynamic_text_list.contains(deti))
deti = new DynamicElementTextItem(this);
else
{
DynamicElementTextItem *text = new DynamicElementTextItem(this);
m_dynamic_text_list.append(text);
emit textAdded(text);
}
deti->setParentItem(this);
m_dynamic_text_list.append(deti);
//On a mirrored element, the mirror of the text depends on the size
//and the rotation of the text, see keepReadable()
connect(deti->document()->documentLayout(),
&QAbstractTextDocumentLayout::documentSizeChanged,
this, [this, deti]() {keepReadable(deti);});
connect(deti, &DynamicElementTextItem::rotationChanged,
this, [this, deti]() {keepReadable(deti);});
connect(deti, &DynamicElementTextItem::rotationPointCenterChanged,
this, [this, deti]() {keepReadable(deti);});
keepReadable(deti);
emit textAdded(deti);
}
/**
@@ -1156,10 +1330,18 @@ void Element::addDynamicTextItem(DynamicElementTextItem *deti)
*/
void Element::removeDynamicTextItem(DynamicElementTextItem *deti)
{
//Undo the connections and the mirror of addDynamicTextItem()
auto forget = [this, deti]() {
disconnect(deti->document()->documentLayout(), nullptr, this, nullptr);
disconnect(deti, nullptr, this, nullptr);
deti->resetTransform();
};
if (m_dynamic_text_list.contains(deti))
{
m_dynamic_text_list.removeOne(deti);
deti->setParentItem(nullptr);
forget();
emit textRemoved(deti);
return;
}
@@ -1171,6 +1353,7 @@ void Element::removeDynamicTextItem(DynamicElementTextItem *deti)
removeTextFromGroup(deti, group);
m_dynamic_text_list.removeOne(deti);
deti->setParentItem(nullptr);
forget();
emit textRemoved(deti);
return;
}
@@ -2003,6 +2186,13 @@ Element::ReloadPictureResult Element::reloadPicture()
m_picture = picture;
m_low_zoom_picture = low_zoom_picture;
if (isMirrored())
ElementPictureFactory::instance()->getMirroredPictures(
m_location,
m_horizontal_mirror,
m_vertical_mirror,
m_mirrored_picture,
m_mirrored_low_zoom_picture);
update();
return ReloadPictureResult::Reloaded;
}
+18
View File
@@ -46,6 +46,9 @@ class Element : public QetGraphicsItem
friend class DynamicElementTextItem;
Q_OBJECT
Q_PROPERTY(bool horizontalMirror READ hasHorizontalMirror WRITE setHorizontalMirror NOTIFY mirrorChanged)
Q_PROPERTY(bool verticalMirror READ hasVerticalMirror WRITE setVerticalMirror NOTIFY mirrorChanged)
public:
/**
@brief The kind enum
@@ -111,6 +114,7 @@ class Element : public QetGraphicsItem
void textRemovedFromGroup(
DynamicElementTextItem *text,
ElementTextItemGroup *group);
void mirrorChanged();
public slots:
void updateConductorTexts();
@@ -185,6 +189,13 @@ class Element : public QetGraphicsItem
int> &) const;
QUuid uuid() const;
int orientation() const;
bool hasHorizontalMirror() const {return m_horizontal_mirror;}
bool hasVerticalMirror() const {return m_vertical_mirror;}
bool isMirrored() const {return m_horizontal_mirror || m_vertical_mirror;}
void setHorizontalMirror(bool mirror);
void setVerticalMirror(bool mirror);
QTransform mirrorTransform() const;
void keepReadable(QGraphicsItem *child) const;
//METHODS related to texts
void addDynamicTextItem(DynamicElementTextItem *deti = nullptr);
@@ -296,10 +307,15 @@ class Element : public QetGraphicsItem
QList <Terminal *> m_terminals;
QPicture m_picture;
QPicture m_low_zoom_picture;
QPicture m_mirrored_picture;
QPicture m_mirrored_low_zoom_picture;
ElementData m_data;
QList<QPointF> m_plc_table_positions; // Positions of plc_table parts in the element definition
void drawPlcTable(QPainter *painter);
void setMirror(bool horizontal, bool vertical);
void applyMirrorTransform();
void keepChildrenReadable() const;
public:
/// Positions where the PLC IO table is drawn (from the .elmt file).
@@ -310,6 +326,8 @@ class Element : public QetGraphicsItem
QSize dimensions;
QPoint hotspot_coord;
bool m_mouse_over = false;
bool m_horizontal_mirror = false;
bool m_vertical_mirror = false;
QString m_prefix;
QList <DynamicElementTextItem *> m_dynamic_text_list;
QList <ElementTextItemGroup *> m_texts_group;
@@ -119,6 +119,12 @@ void ElementTextItemGroup::addToGroup(QGraphicsItem *item)
if((item->rotation() != rotation()) && !m_block_alignment_update)
item->setRotation(rotation());
//On a mirrored element, QGraphicsItemGroup::addToGroup() would
//fold the mirror that keeps the text and this group readable
//(Element::keepReadable()) into the text's transform. The group
//gets its own back from updateAlignment().
item->resetTransform();
resetTransform();
QGraphicsItemGroup::addToGroup(item);
updateAlignment();
@@ -153,6 +159,7 @@ void ElementTextItemGroup::addToGroup(QGraphicsItem *item)
*/
void ElementTextItemGroup::removeFromGroup(QGraphicsItem *item)
{
resetTransform(); //See addToGroup(), given back by updateAlignment()
QGraphicsItemGroup::removeFromGroup(item);
//the item transformation is not reset, we must do it ourselves,
// because for example if the group rotation is 45°
@@ -162,6 +169,8 @@ void ElementTextItemGroup::removeFromGroup(QGraphicsItem *item)
item->resetTransform();
item->setRotation(this->rotation());
item->setFlag(QGraphicsItem::ItemIsSelectable, true);
if (m_parent_element)
m_parent_element->keepReadable(item);
updateAlignment();
if(DynamicElementTextItem *deti = qgraphicsitem_cast<DynamicElementTextItem *>(item))
@@ -232,6 +241,10 @@ void ElementTextItemGroup::updateAlignment()
prepareGeometryChange();
QList <DynamicElementTextItem *> texts = this->texts();
//The mirror of a mirrored element's group depends on the size of
//the group: drop it while the texts are laid out, set it back after
resetTransform();
qreal rotation_ = rotation();
@@ -302,6 +315,8 @@ void ElementTextItemGroup::updateAlignment()
//Restore the rotation
setRotation(rotation_);
if (m_parent_element)
m_parent_element->keepReadable(this);
if(m_Xref_item)
m_Xref_item->autoPos();
@@ -357,6 +372,7 @@ void ElementTextItemGroup::setHoldToBottomPage(bool hold)
setFlag(QGraphicsItem::ItemIsMovable, false);
connect(m_parent_element, &Element::yChanged, this, &ElementTextItemGroup::autoPos);
connect(m_parent_element, &Element::rotationChanged, this, &ElementTextItemGroup::autoPos);
connect(m_parent_element, &Element::mirrorChanged, this, &ElementTextItemGroup::autoPos);
if(m_parent_element->linkType() == Element::Master)
{
//We use timer to let the time of the parent element
@@ -396,6 +412,8 @@ void ElementTextItemGroup::setHoldToBottomPage(bool hold)
this, &ElementTextItemGroup::autoPos);
disconnect(m_parent_element, &Element::rotationChanged,
this, &ElementTextItemGroup::autoPos);
disconnect(m_parent_element, &Element::mirrorChanged,
this, &ElementTextItemGroup::autoPos);
if(m_parent_element->linkType() == Element::Master)
{
disconnect(m_linked_changed_timer);
@@ -630,6 +648,10 @@ QRectF ElementTextItemGroup::boundingRect() const
void ElementTextItemGroup::setRotation(qreal angle)
{
QGraphicsItemGroup::setRotation(angle);
//On a mirrored element, the mirror that keeps this group readable
//is about the centre of its turned box
if (m_parent_element)
m_parent_element->keepReadable(this);
emit rotationChanged(angle);
}
@@ -75,6 +75,9 @@ bool centerToBottomDiagram (QGraphicsItem *item_to_center, Element *element_to_f
item_to_center -> setPos(0,0); //Due to a weird behavior or bug, before setting the new position and rotation,
item_to_center -> setRotation(0); //we must set the position and rotation to 0.
//Same for the mirror that keeps it readable on a mirrored element,
//given back below
item_to_center -> resetTransform();
item_to_center->setPos(item_to_center->mapFromScene(point));
@@ -87,6 +90,23 @@ bool centerToBottomDiagram (QGraphicsItem *item_to_center, Element *element_to_f
if(rot != 0) {
item_to_center->setRotation(item_to_center->rotation() - rot);
}
element_to_follow->keepReadable(item_to_center);
//On a mirrored element the item's box extends from its pos() the
//mirrored way, so it is not where the lines above meant. Put it
//there: centred on the element, its top at the same height.
if (element_to_follow->isMirrored() && item_to_center->parentItem())
{
const QRectF placed = item_to_center->sceneBoundingRect();
const QPointF wanted(
element_to_follow->sceneBoundingRect().center().x()
- placed.width() / 2,
border.bottom() - item_to_center->boundingRect().height() - offset);
QGraphicsItem *parent = item_to_center->parentItem();
item_to_center->setPos(parent->mapFromScene(
parent->mapToScene(item_to_center->pos())
+ wanted - placed.topLeft()));
}
return true;
}
+28 -2
View File
@@ -118,13 +118,20 @@ Qet::Orientation Terminal::orientation() const
// orientations actuelle et par defaut de l'element
// current and default element orientations
int ori_cur = elt -> orientation();
if (ori_cur == 0) return(d->m_orientation);
// The mirrors of an element are about its own axes, before it is
// rotated (Element::setMirror()): a horizontal one swaps east and
// west, a vertical one north and south
Qet::Orientation ori_elmt = d->m_orientation;
if (Qet::isHorizontal(ori_elmt) ? elt -> hasHorizontalMirror()
: elt -> hasVerticalMirror())
ori_elmt = (Qet::Orientation)((ori_elmt + 2) % 4);
if (ori_cur == 0) return(ori_elmt);
else {
// calcul l'angle de rotation implique par l'orientation de l'element parent
// angle de rotation de la borne sur la scene, divise par 90
// calculates the angle of rotation implied by the orientation of the parent
// element angle of rotation of the terminal on the scene, divided by 90
int angle = ori_cur + d->m_orientation;
int angle = ori_cur + ori_elmt;
while (angle >= 4) angle -= 4;
return((Qet::Orientation)angle);
}
@@ -299,6 +306,23 @@ void Terminal::paint(
QFontMetrics fm(d->m_label_font);
QSizeF text_size = fm.size(Qt::TextSingleLine, display_name);
// On a mirrored element, mirror the name a second time about
// the centre of its box, in the frame it is drawn in (turned
// by label_rotation): its box stays where the element's mirror
// puts it, but it reads normally (Element::keepReadable())
const Element *mirrored = qgraphicsitem_cast<Element *>(parentItem());
if (mirrored && !mirrored->isMirrored()) mirrored = nullptr;
auto keep_readable = [painter, mirrored](const QRectF &rect, qreal label_rotation) {
if (!mirrored) return;
const QTransform mirror = QTransform().rotate(label_rotation)
* mirrored->mirrorTransform()
* QTransform().rotate(-label_rotation);
const QPointF c = rect.center();
painter->setTransform(QTransform::fromTranslate(-c.x(), -c.y())
* mirror
* QTransform::fromTranslate(c.x(), c.y()), true);
};
if (!qFuzzyIsNull(d->m_label_rotation)) {
painter->save();
painter->translate(label_pos);
@@ -314,6 +338,7 @@ void Terminal::paint(
else if (d->m_label_valignment & Qt::AlignBottom) ry = -text_size.height();
QRectF text_rect(QPointF(rx, ry), text_size);
keep_readable(text_rect, d->m_label_rotation);
painter->drawText(text_rect, static_cast<int>(d->m_label_halignment | d->m_label_valignment), display_name);
painter->restore();
} else {
@@ -327,6 +352,7 @@ void Terminal::paint(
else if (d->m_label_valignment & Qt::AlignBottom) dy = -text_size.height();
QRectF text_rect(label_pos + QPointF(dx, dy), text_size);
keep_readable(text_rect, 0);
if (d->m_label_frame) {
painter->drawRect(text_rect.adjusted(-1, -1, 1, 1));
}
+2
View File
@@ -550,6 +550,8 @@ QStringList LiveServer::allowedCommands()
QStringLiteral("diagrameditor.zoom_fit"),
QStringLiteral("diagrameditor.zoom_reset"),
QStringLiteral("diagrameditor.rotate_selection"),
QStringLiteral("diagrameditor.mirror_horizontal"),
QStringLiteral("diagrameditor.mirror_vertical"),
QStringLiteral("diagrameditor.rotate_texts"),
QStringLiteral("diagrameditor.snap_selection_to_grid"),
QStringLiteral("diagrameditor.group_selection"),
+38
View File
@@ -61,6 +61,7 @@
#include "../qetgraphicsitem/dynamicelementtextitem.h"
#include "../qetgraphicsitem/independenttextitem.h"
#include "../qetgraphicsitem/qetshapeitem.h"
#include "../undocommand/mirrorselectioncommand.h"
#include "../undocommand/promoteshapecommand.h"
#include "../TerminalStrip/UndoCommand/addterminalstripcommand.h"
#include "../TerminalStrip/UndoCommand/addterminaltostripcommand.h"
@@ -833,6 +834,43 @@ bool QetScriptApi::rotateElement(int folioIndex, const QString &elementUuid, dou
return true;
}
/**
@brief QetScriptApi::mirrorElement
Mirror an element in place, as "Miroir horizontal" (left and right
swap) or, with @p vertical, "Miroir vertical" (top and bottom swap)
do on a selection. Mirroring twice the same way puts it back.
@return false if the element is not found or the project is read only
*/
bool QetScriptApi::mirrorElement(int folioIndex, const QString &elementUuid, bool vertical)
{
if (m_project && m_project->isReadOnly()) {
log(QStringLiteral("qet.mirrorElement: project is read-only"));
return false;
}
Element *element = findElement(folioIndex, elementUuid);
if (!element) return false;
m_project->undoStack()->push(new MirrorSelectionCommand(
{element}, vertical ? Qt::Vertical : Qt::Horizontal));
return true;
}
/**
@brief QetScriptApi::elementMirror
@return the mirrors of the element about its own axes, as saved in the
project (Element::setMirror()): "horizontal", "vertical", "both", or ""
if it is not mirrored or not found. On a turned element, a horizontal
mirror of the folio is a vertical mirror of the element.
*/
QString QetScriptApi::elementMirror(int folioIndex, const QString &elementUuid) const
{
const Element *element = findElement(folioIndex, elementUuid);
if (!element || !element->isMirrored()) return QString();
if (!element->hasVerticalMirror()) return QStringLiteral("horizontal");
if (!element->hasHorizontalMirror()) return QStringLiteral("vertical");
return QStringLiteral("both");
}
QStringList QetScriptApi::elementUuids(int folioIndex) const
{
QStringList uuids;
+2
View File
@@ -398,6 +398,8 @@ class QetScriptApi : public QObject
Q_INVOKABLE bool setElementPosition(int folioIndex, const QString &elementUuid, double x, double y);
Q_INVOKABLE bool moveElement(int folioIndex, const QString &elementUuid, double dx, double dy);
Q_INVOKABLE bool rotateElement(int folioIndex, const QString &elementUuid, double angle);
Q_INVOKABLE bool mirrorElement(int folioIndex, const QString &elementUuid, bool vertical = false);
Q_INVOKABLE QString elementMirror(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE bool deleteElement(int folioIndex, const QString &elementUuid);
// -- address what is already there --
@@ -0,0 +1,125 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "mirrorselectioncommand.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../diagram.h"
#include "../qetgraphicsitem/element.h"
/**
@brief MirrorSelectionCommand::MirrorSelectionCommand
Mirror the elements selected in @p diagram; the other selected items
are left as they are. See the other constructor.
@param diagram : the diagram whose selection is mirrored
@param orientation : Qt::Horizontal swaps left and right,
Qt::Vertical swaps top and bottom
@param parent : parent undo command
*/
MirrorSelectionCommand::MirrorSelectionCommand(Diagram *diagram,
Qt::Orientation orientation,
QUndoCommand *parent) :
MirrorSelectionCommand(selectedElements(diagram), orientation, parent)
{}
/**
@brief MirrorSelectionCommand::MirrorSelectionCommand
The mirrors of an element are about its own axes, before it is rotated
(Element::setMirror()). Turned by 0 or 180 degrees, the element's axes
are the folio's, so a horizontal mirror on the folio is the element's
own horizontal mirror; turned by 90 or 270 degrees, they are swapped,
so it is the element's vertical mirror. The rotation never changes.
Each element is mirrored in place: its centre stays where it was.
@param elements : the elements to mirror
@param orientation : Qt::Horizontal swaps left and right on the folio,
Qt::Vertical swaps top and bottom
@param parent : parent undo command
*/
MirrorSelectionCommand::MirrorSelectionCommand(const QList<Element *> &elements,
Qt::Orientation orientation,
QUndoCommand *parent) :
QUndoCommand(parent)
{
setText(orientation == Qt::Horizontal
? QObject::tr("Miroir horizontal")
: QObject::tr("Miroir vertical"));
for (Element *element : elements)
{
if (!element)
continue;
const bool turned = element->orientation() % 2;
const bool horizontal = (orientation == Qt::Horizontal) != turned;
bool new_horizontal = element->hasHorizontalMirror();
bool new_vertical = element->hasVerticalMirror();
if (horizontal)
{
new_horizontal = !new_horizontal;
new QPropertyUndoCommand(element, "horizontalMirror",
!new_horizontal, new_horizontal, this);
}
else
{
new_vertical = !new_vertical;
new QPropertyUndoCommand(element, "verticalMirror",
!new_vertical, new_vertical, this);
}
//The mirror is about the element's hotspot, which is often a
//corner of the symbol: move it so that it stays where it was,
//centre on centre, and on the grid
QPointF centre = element->boundingRect().center();
centre = QTransform::fromScale(new_horizontal ? -1 : 1, new_vertical ? -1 : 1)
.map(centre);
centre = QTransform().rotate(element->rotation()).map(centre);
const QPointF old_pos = element->pos();
const QPointF new_pos = Diagram::snapToGrid(
element->mapToScene(element->boundingRect().center()) - centre);
if (new_pos != old_pos)
new QPropertyUndoCommand(element, "pos", old_pos, new_pos, this);
}
}
/**
@brief MirrorSelectionCommand::selectedElements
@param diagram
@return the elements selected in @p diagram, none if it is read only
*/
QList<Element *> MirrorSelectionCommand::selectedElements(const Diagram *diagram)
{
QList<Element *> elements;
if (!diagram || diagram->isReadOnly())
return elements;
const QList<QGraphicsItem *> items = diagram->selectedItems();
for (QGraphicsItem *item : items)
if (Element *element = qgraphicsitem_cast<Element *>(item))
elements << element;
return elements;
}
/**
@brief MirrorSelectionCommand::isValid
@return true if at least one element is mirrored by this command
*/
bool MirrorSelectionCommand::isValid() const
{
return childCount() > 0;
}
@@ -0,0 +1,48 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#ifndef MIRRORSELECTIONCOMMAND_H
#define MIRRORSELECTIONCOMMAND_H
#include <QList>
#include <QUndoCommand>
class Diagram;
class Element;
/**
@brief The MirrorSelectionCommand class
Mirror elements in place, horizontally (left and right swap) or
vertically (top and bottom swap), on the folio. See Element::setMirror()
*/
class MirrorSelectionCommand : public QUndoCommand
{
public:
MirrorSelectionCommand(Diagram *diagram,
Qt::Orientation orientation,
QUndoCommand *parent = nullptr);
MirrorSelectionCommand(const QList<Element *> &elements,
Qt::Orientation orientation,
QUndoCommand *parent = nullptr);
bool isValid() const;
private:
static QList<Element *> selectedElements(const Diagram *diagram);
};
#endif // MIRRORSELECTIONCOMMAND_H
+13
View File
@@ -775,6 +775,19 @@ if(QET_HAS_SCRIPTING)
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# Mirroring a symbol on its folio (#1335): its terminals swap sides and
# face the other way, a second mirror puts it back, and the mirror is
# kept by a save. Through the real binary's --run on perceuse.qet.
add_executable(
tst_scriptmirror
tst_scriptmirror.cpp)
add_test(NAME tst_scriptmirror COMMAND tst_scriptmirror)
add_dependencies(tst_scriptmirror qelectrotech)
target_link_libraries(tst_scriptmirror PRIVATE Qt::Test Qt::Xml)
target_compile_definitions(tst_scriptmirror PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# Undoing a crop of a picture restores its crop rectangle too, so a
# project saved after the undo does not record the undone crop.
add_executable(
+239
View File
@@ -0,0 +1,239 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDomDocument>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTemporaryDir>
// Mirroring a symbol on its folio (#1335), through the real binary's --run
// on perceuse.qet: qet.mirrorElement() pushes the same undo command as
// Edit > Miroir horizontal / vertical. A horizontal mirror swaps the
// terminals left and right and turns the ones facing east to face west; a
// vertical one does the same top and bottom; mirroring twice the same way
// puts the symbol back exactly; and a save keeps the mirror.
class tst_scriptmirror : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
// Run @p script on @p project in a sandbox of its own and return the
// JSON object it logged.
QJsonObject run(const QString &script, const QString &project)
{
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write(script.toUtf8());
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--run"), path, project});
if (!proc.waitForFinished(120000)) return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 0)
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
}
return {};
}
static QString facing(const QJsonArray &terminals, int i)
{
return terminals.at(i).toObject().value(QStringLiteral("facing")).toString();
}
static double coord(const QJsonArray &terminals, int i, const char *axis)
{
return terminals.at(i).toObject().value(QLatin1String(axis)).toDouble();
}
// The facing of a terminal once mirrored: across a vertical line
// (horizontal mirror) east and west swap, across a horizontal one north
// and south do.
static QString mirrored(const QString &facing, bool vertical)
{
const QString swap = vertical ? QStringLiteral("ns") : QStringLiteral("ew");
const int i = swap.indexOf(facing);
return i < 0 ? facing : QString(swap.at(1 - i));
}
// @p after is @p before mirrored horizontally (or vertically): the
// terminals face the mirrored way, keep the other coordinate, and are
// mirrored across one same line, so x + x' (or y + y') is the same for
// all of them.
static void checkMirrored(const QJsonArray &before, const QJsonArray &after, bool vertical)
{
QCOMPARE(after.size(), before.size());
const char *across = vertical ? "y" : "x";
const char *along = vertical ? "x" : "y";
const double axis = coord(before, 0, across) + coord(after, 0, across);
const double shift = coord(after, 0, along) - coord(before, 0, along);
for (int i = 0; i < before.size(); ++i) {
QCOMPARE(facing(after, i), mirrored(facing(before, i), vertical));
QCOMPARE(coord(before, i, across) + coord(after, i, across), axis);
QCOMPARE(coord(after, i, along) - coord(before, i, along), shift);
}
}
static QString script(const QString &body)
{
return QStringLiteral(
"function terms(f, el) {\n"
" var r = [], n = qet.elementTerminals(f, el).length;\n"
" for (var i = 0; i < n; i++) r.push(qet.terminalPosition(f, el, i));\n"
" return r;\n"
"}\n") + body;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void mirrorBackAndSave()
{
const QString saved = m_dir.filePath(QStringLiteral("mirrored.qet"));
const QJsonObject r = run(script(QStringLiteral(
// a symbol with terminals at different x and different y, one
// of them facing east or west
"var pick = null;\n"
"for (var f = 0; f < qet.folioCount() && !pick; f++) {\n"
" var els = qet.elementUuids(f);\n"
" for (var e = 0; e < els.length && !pick; e++) {\n"
" var t = terms(f, els[e]);\n"
" if (t.length < 2) continue;\n"
" var side = t.some(function (p) { return p.facing === 'e' || p.facing === 'w'; });\n"
" var xs = t.some(function (p) { return p.x !== t[0].x; });\n"
" var ys = t.some(function (p) { return p.y !== t[0].y; });\n"
" if (side && xs && ys) pick = {f: f, e: els[e]};\n"
" }\n"
"}\n"
"var f = pick.f, el = pick.e, out = {element: el};\n"
"out.before = terms(f, el);\n"
"qet.mirrorElement(f, el, false);\n"
"out.h = terms(f, el); out.hMirror = qet.elementMirror(f, el);\n"
"qet.mirrorElement(f, el, false);\n"
"out.back = terms(f, el); out.backMirror = qet.elementMirror(f, el);\n"
"qet.mirrorElement(f, el, true);\n"
"out.v = terms(f, el); out.vMirror = qet.elementMirror(f, el);\n"
"qet.undo();\n"
"out.undone = terms(f, el);\n"
"qet.mirrorElement(f, el, true);\n"
"out.folio = f;\n"
"out.saved = qet.save(%1);\n"
"qet.log('PROBE ' + JSON.stringify(out));\n")
.arg(QString::fromUtf8(QJsonDocument(QJsonArray{saved}).toJson(QJsonDocument::Compact)).mid(1).chopped(1))),
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
const QJsonArray before = r.value(QStringLiteral("before")).toArray();
QVERIFY(before.size() >= 2);
QVERIFY(!r.value(QStringLiteral("hMirror")).toString().isEmpty());
checkMirrored(before, r.value(QStringLiteral("h")).toArray(), false);
QCOMPARE(r.value(QStringLiteral("backMirror")).toString(), QString());
QCOMPARE(r.value(QStringLiteral("back")).toArray(), before);
checkMirrored(before, r.value(QStringLiteral("v")).toArray(), true);
QVERIFY(r.value(QStringLiteral("vMirror")).toString()
!= r.value(QStringLiteral("hMirror")).toString());
QCOMPARE(r.value(QStringLiteral("undone")).toArray(), before);
//The save keeps the mirror, and only on that element
QVERIFY(r.value(QStringLiteral("saved")).toBool());
QFile file(saved);
QVERIFY(file.open(QIODevice::ReadOnly));
QDomDocument doc;
QVERIFY(bool(doc.setContent(&file)));
const QString element = r.value(QStringLiteral("element")).toString();
const QDomNodeList elements = doc.elementsByTagName(QStringLiteral("element"));
int mirrored_count = 0;
for (int i = 0; i < elements.size(); ++i) {
const QDomElement e = elements.at(i).toElement();
if (e.hasAttribute(QStringLiteral("mirror"))) {
++mirrored_count;
QCOMPARE(e.attribute(QStringLiteral("uuid")), element);
QCOMPARE(e.attribute(QStringLiteral("mirror")),
r.value(QStringLiteral("vMirror")).toString());
}
}
QCOMPARE(mirrored_count, 1);
const QJsonObject reloaded = run(script(QStringLiteral(
"var f = %1, el = '%2';\n"
"qet.log('PROBE ' + JSON.stringify({t: terms(f, el), m: qet.elementMirror(f, el)}));\n")
.arg(r.value(QStringLiteral("folio")).toInt()).arg(element)), saved);
QCOMPARE(reloaded.value(QStringLiteral("m")).toString(),
r.value(QStringLiteral("vMirror")).toString());
QCOMPARE(reloaded.value(QStringLiteral("t")).toArray(),
r.value(QStringLiteral("v")).toArray());
}
/**
On a turned symbol a mirror of the folio is the other mirror of the
symbol itself, and its rotation does not change: a label kept
upright does not swing round.
*/
void turnedElement_data()
{
QTest::addColumn<bool>("vertical");
QTest::newRow("horizontal") << false;
QTest::newRow("vertical") << true;
}
void turnedElement()
{
QFETCH(bool, vertical);
const QJsonObject r = run(script(QStringLiteral(
"var pick = null;\n"
"for (var f = 0; f < qet.folioCount() && !pick; f++) {\n"
" var els = qet.elementUuids(f);\n"
" for (var e = 0; e < els.length && !pick; e++) {\n"
" var t = terms(f, els[e]);\n"
" if (t.length >= 2 && t.some(function (p) { return p.x !== t[0].x; })\n"
" && t.some(function (p) { return p.y !== t[0].y; })) pick = {f: f, e: els[e]};\n"
" }\n"
"}\n"
"var f = pick.f, el = pick.e, out = {};\n"
"qet.rotateElement(f, el, 90);\n"
"out.before = terms(f, el);\n"
"qet.mirrorElement(f, el, %1);\n"
"out.after = terms(f, el); out.mirror = qet.elementMirror(f, el);\n"
"qet.log('PROBE ' + JSON.stringify(out));\n")
.arg(vertical ? QStringLiteral("true") : QStringLiteral("false"))),
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
checkMirrored(r.value(QStringLiteral("before")).toArray(),
r.value(QStringLiteral("after")).toArray(), vertical);
//turned by 90 degrees, the symbol's own axes are the other way
QCOMPARE(r.value(QStringLiteral("mirror")).toString(),
vertical ? QStringLiteral("horizontal") : QStringLiteral("vertical"));
}
};
QTEST_APPLESS_MAIN(tst_scriptmirror)
#include "tst_scriptmirror.moc"