mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-07 20:34:14 +02:00
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:
@@ -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
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user