mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-07 20:34:14 +02:00
Merge pull request #1355 from ispyisail/feat/upright-symbol-texts
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m54s
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m54s
Add an option to keep texts of turned symbols horizontal
This commit is contained in:
@@ -3567,7 +3567,11 @@ def tool_project_new(binary: str, output: str, title: str = "Untitled",
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix="qet-mcp-new-") as tmp:
|
||||
skeleton = Path(tmp) / "skeleton.qet"
|
||||
skeleton.write_text('<project version="0.100.0" title=%s>\n</project>\n'
|
||||
# <symbol_texts>: a project made in QElectroTech starts with the texts
|
||||
# of its turned symbols kept horizontal; one read from a file without
|
||||
# it does not, so say it here as QElectroTech would have
|
||||
skeleton.write_text('<project version="0.100.0" title=%s>\n'
|
||||
' <symbol_texts upright="true"/>\n</project>\n'
|
||||
% quoteattr(title), encoding="utf-8")
|
||||
result = _run_qet(binary, [str(skeleton)], timeout=timeout,
|
||||
elements_dir=elements_dir, script="\n".join(script), tail=200_000)
|
||||
|
||||
@@ -4299,6 +4299,13 @@ class Integration(unittest.TestCase):
|
||||
self.assertEqual(r["project"]["title"], 'A "b" & <c>')
|
||||
self.assertEqual([f["title"] for f in r["project"]["folios"]], ["Power", "Control"])
|
||||
|
||||
def test_new_project_keeps_symbol_texts_horizontal(self):
|
||||
"""Like a project made in QElectroTech, and kept by its save."""
|
||||
r = m.tool_project_new(BINARY, self.sb.p("u.qet"), title="U", folios=1)
|
||||
self.assertTrue(r["ok"])
|
||||
root = ET.parse(r["output"]).getroot()
|
||||
self.assertEqual(root.find("symbol_texts").get("upright"), "true")
|
||||
|
||||
def test_new_project_with_zero_folios(self):
|
||||
r = m.tool_project_new(BINARY, self.sb.p("z.qet"), title="Z", folios=0)
|
||||
self.assertTrue(r["ok"])
|
||||
|
||||
+27
-18
@@ -50,19 +50,26 @@ namespace {
|
||||
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, with @a elmt's
|
||||
mirrors when @a mirrored. Used for every symbol drawn in full, and
|
||||
once per block for a block's content.
|
||||
mirrors and its texts kept readable (Element::symbolTextsTransform())
|
||||
when @a as_placed. 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,
|
||||
bool mirrored = false)
|
||||
bool as_placed = 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 bool mirror_h = as_placed && elmt -> hasHorizontalMirror();
|
||||
const bool mirror_v = as_placed && elmt -> hasVerticalMirror();
|
||||
const QTransform texts_transform = as_placed
|
||||
? elmt -> symbolTextsTransform()
|
||||
: QTransform();
|
||||
const qreal text_turn = as_placed && elmt -> hasUprightSymbolTexts()
|
||||
? 0
|
||||
: rotation_angle;
|
||||
const qreal mirror_x = mirror_h ? -1 : 1;
|
||||
const qreal mirror_y = mirror_v ? -1 : 1;
|
||||
ElementPictureFactory::primitives primitives = ElementPictureFactory::instance()->getPrimitives(elmt->location());
|
||||
@@ -75,11 +82,13 @@ namespace {
|
||||
fontSize = text->font().pixelSize();
|
||||
|
||||
QPointF text_pos = text->pos();
|
||||
if (mirror_h || mirror_v)
|
||||
//A text the element mirrors, or turns in a project that keeps
|
||||
//symbol texts horizontal, keeps reading as in the symbol:
|
||||
//only its box moves, as ElementPictureFactory draws it on the
|
||||
//folio. It is then drawn as in a symbol neither mirrored nor
|
||||
//turned, moved to where the element puts the box's centre.
|
||||
if (!texts_transform.isIdentity())
|
||||
{
|
||||
//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());
|
||||
@@ -88,19 +97,18 @@ namespace {
|
||||
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));
|
||||
text_pos += texts_transform.map(centre) - centre;
|
||||
}
|
||||
|
||||
qreal x = elem_pos_x + text_pos.x();
|
||||
qreal y = elem_pos_y + text_pos.y();
|
||||
|
||||
qreal angle = text -> rotation() + rotation_angle;
|
||||
qreal angle = text -> rotation() + text_turn;
|
||||
qreal angler = angle * M_PI/180;
|
||||
int xdir = -sin(angler);
|
||||
int ydir = -cos(angler);
|
||||
|
||||
QPointF transformed_point = DxfExport::rotation_transformed(x, y, elem_pos_x, elem_pos_y, -rotation_angle);
|
||||
QPointF transformed_point = DxfExport::rotation_transformed(x, y, elem_pos_x, elem_pos_y, -text_turn);
|
||||
x = transformed_point.x() - ydir * fontSize * 0.5;
|
||||
y = transformed_point.y() - xdir * fontSize * 0.5;
|
||||
QStringList lines = text->text().split('\n');
|
||||
@@ -384,7 +392,8 @@ 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 (elmt -> isMirrored() || block_names.contains(key)
|
||||
if (!elmt -> symbolTextsTransform().isIdentity()
|
||||
|| block_names.contains(key)
|
||||
|| drawsNothing(elmt, properties.draw_terminals))
|
||||
continue;
|
||||
QString file_name = elmt -> location().fileName();
|
||||
@@ -402,11 +411,11 @@ 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
|
||||
//A mirrored symbol, or a turned one whose texts stay horizontal,
|
||||
//is drawn in full: an INSERT would mirror or turn its texts too,
|
||||
//which QElectroTech keeps readable
|
||||
const auto blockOf = [&block_names](Element *elmt) {
|
||||
return elmt -> isMirrored()
|
||||
return !elmt -> symbolTextsTransform().isIdentity()
|
||||
? QString()
|
||||
: block_names.value(elmt -> location().toString());
|
||||
};
|
||||
|
||||
@@ -95,37 +95,46 @@ 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.
|
||||
@brief ElementPictureFactory::getReadablePictures
|
||||
Same as getPictures(), for an element that mirrors or turns its symbol
|
||||
on a folio (Element::symbolTextsTransform()). The drawing is the same;
|
||||
only the texts differ: each one is drawn so that, once the element has
|
||||
applied @p texts_transform, its box lands where the element puts it but
|
||||
its letters read as they do in the symbol itself.
|
||||
@param location
|
||||
@param horizontal : the element's left and right swap
|
||||
@param vertical : the element's top and bottom swap
|
||||
@param texts_transform : the mirrors, then the turn, of the element
|
||||
@param picture
|
||||
@param low_picture
|
||||
*/
|
||||
void ElementPictureFactory::getMirroredPictures(const ElementsLocation &location,
|
||||
bool horizontal,
|
||||
bool vertical,
|
||||
void ElementPictureFactory::getReadablePictures(const ElementsLocation &location,
|
||||
const QTransform &texts_transform,
|
||||
QPicture &picture,
|
||||
QPicture &low_picture)
|
||||
{
|
||||
if(!location.exist() || !(horizontal || vertical)) {
|
||||
if(!location.exist() || texts_transform.isIdentity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPair<QUuid, int> key(cacheKey(location),
|
||||
(horizontal ? 1 : 0) | (vertical ? 2 : 0));
|
||||
const QPair<QUuid, int> key(cacheKey(location), readableKey(texts_transform));
|
||||
|
||||
if(!m_mirrored_pictures_H.contains(key)
|
||||
&& !build(location, nullptr, nullptr,
|
||||
QTransform::fromScale(horizontal ? -1 : 1, vertical ? -1 : 1))) {
|
||||
if(!m_readable_pictures_H.contains(key)
|
||||
&& !build(location, nullptr, nullptr, texts_transform)) {
|
||||
return;
|
||||
}
|
||||
picture = m_mirrored_pictures_H.value(key);
|
||||
low_picture = m_mirrored_low_pictures_H.value(key);
|
||||
picture = m_readable_pictures_H.value(key);
|
||||
low_picture = m_readable_low_pictures_H.value(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementPictureFactory::readableKey
|
||||
@param texts_transform : mirrors and quarter turns only
|
||||
@return a number for @p texts_transform, the same for the same one
|
||||
*/
|
||||
int ElementPictureFactory::readableKey(const QTransform &texts_transform)
|
||||
{
|
||||
auto digit = [](qreal value) {return qRound(value) + 1;}; // -1, 0 or 1
|
||||
return ((digit(texts_transform.m11()) * 3 + digit(texts_transform.m12())) * 3
|
||||
+ digit(texts_transform.m21())) * 3 + digit(texts_transform.m22());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,9 +156,12 @@ 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));
|
||||
const auto keys = m_readable_pictures_H.keys();
|
||||
for (const auto &key : keys) {
|
||||
if (key.first == uuid) {
|
||||
m_readable_pictures_H.remove(key);
|
||||
m_readable_low_pictures_H.remove(key);
|
||||
}
|
||||
}
|
||||
m_pixmap_H.remove(uuid);
|
||||
//The text items belong to the cache; nothing else holds them.
|
||||
@@ -255,16 +267,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()
|
||||
(m_readable_pictures_H and m_readable_low_pictures_H if
|
||||
@p texts_transform is not the identity)
|
||||
@param texts_transform : what an element does to the texts of its
|
||||
symbol, which the drawing undoes, see getReadablePictures()
|
||||
@return
|
||||
*/
|
||||
bool ElementPictureFactory::build(const ElementsLocation &location,
|
||||
QPicture *picture,
|
||||
QPicture *low_picture,
|
||||
const QTransform &mirror)
|
||||
const QTransform &texts_transform)
|
||||
{
|
||||
QDomElement dom = location.xml();
|
||||
|
||||
@@ -324,7 +336,7 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
|
||||
tmp.setCosmetic(true);
|
||||
low_painter.setPen(tmp);
|
||||
|
||||
m_build_mirror = mirror;
|
||||
m_build_texts_undo = texts_transform.inverted();
|
||||
|
||||
//scroll of the Children of the Definition: Parts of the Drawing
|
||||
// Extract PLC master data for rendering plc_table parts
|
||||
@@ -378,21 +390,20 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
|
||||
//End of the drawing
|
||||
painter.end();
|
||||
low_painter.end();
|
||||
m_build_mirror = QTransform();
|
||||
m_build_texts_undo = 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
|
||||
if (!texts_transform.isIdentity()) {
|
||||
//The primitives are those of the plain drawing, 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));
|
||||
const QPair<QUuid, int> key(uuid_, readableKey(texts_transform));
|
||||
if (!picture) {
|
||||
m_mirrored_pictures_H.insert(key, pic);
|
||||
m_readable_pictures_H.insert(key, pic);
|
||||
}
|
||||
if (!low_picture) {
|
||||
m_mirrored_low_pictures_H.insert(key, low_pic);
|
||||
m_readable_low_pictures_H.insert(key, low_pic);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -714,17 +725,16 @@ void ElementPictureFactory::parseText(const QDomElement &dom, QPainter &painter,
|
||||
text_transform.rotate(dom.attribute("rotation", "0").toDouble());
|
||||
text_transform.translate(qpainter_offset.x(), qpainter_offset.y());
|
||||
|
||||
if (!m_build_mirror.isIdentity())
|
||||
if (!m_build_texts_undo.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.
|
||||
//The element mirrors or turns its symbol when it is drawn.
|
||||
//Undo that on the text first, about the centre of its box: the
|
||||
//letters read as in the symbol itself, and the box still ends
|
||||
//up where the element 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
|
||||
* m_build_texts_undo
|
||||
* QTransform::fromTranslate(centre.x(), centre.y()));
|
||||
}
|
||||
painter.setTransform(text_transform, true);
|
||||
|
||||
@@ -85,7 +85,7 @@ class ElementPictureFactory
|
||||
}
|
||||
|
||||
void getPictures(const ElementsLocation &location, QPicture &picture, QPicture &low_picture);
|
||||
void getMirroredPictures(const ElementsLocation &location, bool horizontal, bool vertical,
|
||||
void getReadablePictures(const ElementsLocation &location, const QTransform &texts_transform,
|
||||
QPicture &picture, QPicture &low_picture);
|
||||
QPixmap pixmap(const ElementsLocation &location);
|
||||
ElementPictureFactory::primitives getPrimitives(const ElementsLocation &location);
|
||||
@@ -98,8 +98,9 @@ class ElementPictureFactory
|
||||
~ElementPictureFactory();
|
||||
|
||||
static QUuid cacheKey(const ElementsLocation &location);
|
||||
static int readableKey(const QTransform &texts_transform);
|
||||
bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr,
|
||||
const QTransform &mirror=QTransform());
|
||||
const QTransform &texts_transform=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;
|
||||
@@ -113,12 +114,13 @@ 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;
|
||||
/// Drawings whose texts read normally once an element has
|
||||
/// mirrored or turned them, by element and readableKey(), see
|
||||
/// getReadablePictures()
|
||||
QHash<QPair<QUuid, int>, QPicture> m_readable_pictures_H;
|
||||
QHash<QPair<QUuid, int>, QPicture> m_readable_low_pictures_H;
|
||||
/// What build() undoes on each text, read by parseText()
|
||||
QTransform m_build_texts_undo;
|
||||
QHash<QUuid, QPixmap> m_pixmap_H;
|
||||
QHash<QUuid, primitives> m_primitives_H;
|
||||
static ElementPictureFactory* m_factory;
|
||||
|
||||
@@ -167,6 +167,8 @@ Element::Element(
|
||||
//The mirror is about the element's own axis, so it follows the
|
||||
//rotation, see applyMirrorTransform()
|
||||
connect(this, &Element::rotationChanged, this, &Element::applyMirrorTransform);
|
||||
//Texts kept horizontal in a turned symbol depend on the turn
|
||||
connect(this, &Element::rotationChanged, this, [this]() {updateSymbolPictures();});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,16 +301,7 @@ void Element::setMirror(bool horizontal, bool vertical)
|
||||
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);
|
||||
|
||||
updateSymbolPictures();
|
||||
applyMirrorTransform();
|
||||
keepChildrenReadable();
|
||||
update();
|
||||
@@ -325,6 +318,69 @@ QTransform Element::mirrorTransform() const
|
||||
m_vertical_mirror ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Element::hasUprightSymbolTexts
|
||||
@return true if the project of this element keeps the texts of its
|
||||
turned symbols horizontal (QETProject::uprightSymbolTexts())
|
||||
*/
|
||||
bool Element::hasUprightSymbolTexts() const
|
||||
{
|
||||
const Diagram *d = diagram();
|
||||
return d && d->project() && d->project()->uprightSymbolTexts();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Element::symbolTextsTransform
|
||||
@return what this element does to the texts drawn in its symbol, which
|
||||
the drawing undoes to keep them readable (updateSymbolPictures()):
|
||||
its mirrors, and its turn when the project keeps them horizontal.
|
||||
The identity when the symbol is drawn as it is.
|
||||
*/
|
||||
QTransform Element::symbolTextsTransform() const
|
||||
{
|
||||
//QTransform's product applies its left operand first
|
||||
QTransform transform = mirrorTransform();
|
||||
if (hasUprightSymbolTexts())
|
||||
transform *= QTransform().rotate(orientation() * 90);
|
||||
return transform;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Element::updateSymbolPictures
|
||||
Take the drawing of the symbol whose texts read normally once this
|
||||
element has mirrored and turned it (symbolTextsTransform()), or drop it
|
||||
when the plain drawing does.
|
||||
@param force : take it again even if the transform did not change, as
|
||||
when the symbol's definition was reloaded
|
||||
*/
|
||||
void Element::updateSymbolPictures(bool force)
|
||||
{
|
||||
const QTransform transform = symbolTextsTransform();
|
||||
if (transform.isIdentity())
|
||||
{
|
||||
if (!m_readable_transform.isIdentity())
|
||||
{
|
||||
m_readable_transform = QTransform();
|
||||
m_readable_picture = QPicture();
|
||||
m_readable_low_zoom_picture = QPicture();
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (transform == m_readable_transform && !force)
|
||||
return;
|
||||
|
||||
m_readable_transform = transform;
|
||||
m_readable_picture = QPicture();
|
||||
m_readable_low_zoom_picture = QPicture();
|
||||
ElementPictureFactory::instance()->getReadablePictures(
|
||||
m_location,
|
||||
transform,
|
||||
m_readable_picture,
|
||||
m_readable_low_zoom_picture);
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Element::applyMirrorTransform
|
||||
QGraphicsItem applies transform() after the rotation, so the mirrors of
|
||||
@@ -427,9 +483,10 @@ 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;
|
||||
const bool readable = !m_readable_transform.isIdentity();
|
||||
const QPicture &picture = readable ? m_readable_picture : m_picture;
|
||||
const QPicture &low_zoom_picture = readable ? m_readable_low_zoom_picture
|
||||
: m_low_zoom_picture;
|
||||
if (options && options->levelOfDetailFromTransform(painter->worldTransform()) < 0.5)
|
||||
{
|
||||
if (!low_zoom_picture.isNull())
|
||||
@@ -1965,6 +2022,10 @@ QVariant Element::itemChange(GraphicsItemChange change, const QVariant &value)
|
||||
deti->refreshResizeHandlesVisibility();
|
||||
}
|
||||
}
|
||||
//Whether the texts of a turned symbol stay horizontal is the
|
||||
//project's setting, known once the element is on one of its folios
|
||||
else if (change == QGraphicsItem::ItemSceneHasChanged)
|
||||
updateSymbolPictures();
|
||||
return QetGraphicsItem::itemChange(change, value);
|
||||
}
|
||||
|
||||
@@ -2186,13 +2247,7 @@ 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);
|
||||
updateSymbolPictures(true);
|
||||
update();
|
||||
return ReloadPictureResult::Reloaded;
|
||||
}
|
||||
|
||||
@@ -195,6 +195,9 @@ class Element : public QetGraphicsItem
|
||||
void setHorizontalMirror(bool mirror);
|
||||
void setVerticalMirror(bool mirror);
|
||||
QTransform mirrorTransform() const;
|
||||
bool hasUprightSymbolTexts() const;
|
||||
QTransform symbolTextsTransform() const;
|
||||
void updateSymbolPictures(bool force = false);
|
||||
void keepReadable(QGraphicsItem *child) const;
|
||||
|
||||
//METHODS related to texts
|
||||
@@ -307,8 +310,11 @@ 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;
|
||||
/// The drawing with its texts kept readable, when the symbol's
|
||||
/// mirror or turn would not leave them so (m_readable_transform)
|
||||
QPicture m_readable_picture;
|
||||
QPicture m_readable_low_zoom_picture;
|
||||
QTransform m_readable_transform;
|
||||
ElementData m_data;
|
||||
QList<QPointF> m_plc_table_positions; // Positions of plc_table parts in the element definition
|
||||
|
||||
|
||||
@@ -306,20 +306,22 @@ 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);
|
||||
// When the element mirrors its symbol, or turns it in a project
|
||||
// that keeps symbol texts horizontal, undo that on the name
|
||||
// about the centre of its box, in the frame it is drawn in
|
||||
// (turned by label_rotation): its box stays where the element
|
||||
// puts it, but it reads as in the symbol (Element::keepReadable())
|
||||
const Element *element = qgraphicsitem_cast<Element *>(parentItem());
|
||||
const QTransform texts_transform = element ? element->symbolTextsTransform()
|
||||
: QTransform();
|
||||
auto keep_readable = [painter, &texts_transform](const QRectF &rect, qreal label_rotation) {
|
||||
if (texts_transform.isIdentity()) return;
|
||||
const QTransform undo = QTransform().rotate(label_rotation)
|
||||
* texts_transform.inverted()
|
||||
* QTransform().rotate(-label_rotation);
|
||||
const QPointF c = rect.center();
|
||||
painter->setTransform(QTransform::fromTranslate(-c.x(), -c.y())
|
||||
* mirror
|
||||
* undo
|
||||
* QTransform::fromTranslate(c.x(), c.y()), true);
|
||||
};
|
||||
|
||||
|
||||
@@ -74,6 +74,11 @@ m_project_properties_handler{this}
|
||||
m_elements_collection = new XmlElementCollection(this);
|
||||
init();
|
||||
|
||||
//A new project keeps the texts of its turned symbols horizontal; a
|
||||
//project saved without the setting is read with it off, so it looks
|
||||
//as it always did (readSymbolTextsXml())
|
||||
m_upright_symbol_texts = true;
|
||||
|
||||
QSettings settings;
|
||||
|
||||
//Read auto break conductor default from global settings
|
||||
@@ -1380,6 +1385,37 @@ void QETProject::setWireHops(WireHops::Mode mode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::uprightSymbolTexts
|
||||
@return true if the texts drawn in a symbol stay horizontal, and read
|
||||
normally, when the symbol is turned or mirrored on a folio, instead of
|
||||
turning with it. A mirrored symbol always keeps its texts readable.
|
||||
*/
|
||||
bool QETProject::uprightSymbolTexts() const {
|
||||
return m_upright_symbol_texts;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::setUprightSymbolTexts
|
||||
Set whether the texts drawn in a turned symbol stay horizontal, and
|
||||
redraw every symbol of the project.
|
||||
@param upright
|
||||
*/
|
||||
void QETProject::setUprightSymbolTexts(bool upright)
|
||||
{
|
||||
if (upright == m_upright_symbol_texts) {
|
||||
return;
|
||||
}
|
||||
m_upright_symbol_texts = upright;
|
||||
for (Diagram *diagram : diagrams()) {
|
||||
for (QGraphicsItem *item : diagram->items()) {
|
||||
if (Element *element = qgraphicsitem_cast<Element *>(item)) {
|
||||
element->updateSymbolPictures();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::wiringRules
|
||||
@return how many wires a terminal of this project may take
|
||||
@@ -1521,6 +1557,7 @@ QDomDocument QETProject::toXml()
|
||||
// local, non-transmitted usage tracking (time spent on this project)
|
||||
writeUsageXml(project_root);
|
||||
writeWireHopsXml(project_root);
|
||||
writeSymbolTextsXml(project_root);
|
||||
writeWiringRulesXml(project_root);
|
||||
|
||||
// Properties for news diagrams
|
||||
@@ -2032,6 +2069,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
||||
//Load the local, non-transmitted usage tracking
|
||||
readUsageXml(xml_project);
|
||||
readWireHopsXml(xml_project);
|
||||
readSymbolTextsXml(xml_project);
|
||||
readWiringRulesXml(xml_project);
|
||||
|
||||
//Load the default properties for the new diagrams
|
||||
@@ -2229,6 +2267,21 @@ void QETProject::readWireHopsXml(QDomDocument &xml_project)
|
||||
: WireHops::fromString(crossings.attribute(QStringLiteral("hop")));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::readSymbolTextsXml
|
||||
Read the <symbol_texts> element of the project, if any. A project
|
||||
without it was saved before the setting existed, or with it off, and
|
||||
keeps the texts of its turned symbols turned.
|
||||
@param xml_project : the xml description of the project
|
||||
*/
|
||||
void QETProject::readSymbolTextsXml(QDomDocument &xml_project)
|
||||
{
|
||||
const QDomElement texts = xml_project.documentElement()
|
||||
.firstChildElement(QStringLiteral("symbol_texts"));
|
||||
m_upright_symbol_texts = !texts.isNull()
|
||||
&& texts.attribute(QStringLiteral("upright")) == QLatin1String("true");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::readWiringRulesXml
|
||||
Read the <wiring_rules> element of the project, if any.
|
||||
@@ -2420,6 +2473,23 @@ void QETProject::writeWireHopsXml(QDomElement &xml_element)
|
||||
xml_element.appendChild(crossings);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::writeSymbolTextsXml
|
||||
Export whether the texts of turned symbols stay horizontal, as a
|
||||
<symbol_texts> child of \a xml_element. Written only when on, so a
|
||||
project with it off saves exactly as before.
|
||||
*/
|
||||
void QETProject::writeSymbolTextsXml(QDomElement &xml_element)
|
||||
{
|
||||
if (!m_upright_symbol_texts) {
|
||||
return;
|
||||
}
|
||||
QDomElement texts = xml_element.ownerDocument()
|
||||
.createElement(QStringLiteral("symbol_texts"));
|
||||
texts.setAttribute(QStringLiteral("upright"), QStringLiteral("true"));
|
||||
xml_element.appendChild(texts);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::writeWiringRulesXml
|
||||
Export the project's wiring rules as a <wiring_rules> child of
|
||||
|
||||
@@ -229,6 +229,8 @@ class QETProject : public QObject
|
||||
void setAutoConductor (bool ac);
|
||||
WireHops::Mode wireHops() const;
|
||||
void setWireHops(WireHops::Mode mode);
|
||||
bool uprightSymbolTexts() const;
|
||||
void setUprightSymbolTexts(bool upright);
|
||||
WiringRules::Settings wiringRules() const;
|
||||
WiringRules::Settings projectWiringRules() const;
|
||||
void setWiringRules(const WiringRules::Settings &rules);
|
||||
@@ -329,12 +331,14 @@ class QETProject : public QObject
|
||||
void readTerminalStripXml(const QDomDocument &xml_project);
|
||||
void readUsageXml(QDomDocument &xml_project);
|
||||
void readWireHopsXml(QDomDocument &xml_project);
|
||||
void readSymbolTextsXml(QDomDocument &xml_project);
|
||||
void readWiringRulesXml(QDomDocument &xml_project);
|
||||
|
||||
void writeProjectPropertiesXml(QDomElement &);
|
||||
void writeDefaultPropertiesXml(QDomElement &);
|
||||
void writeUsageXml(QDomElement &);
|
||||
void writeWireHopsXml(QDomElement &);
|
||||
void writeSymbolTextsXml(QDomElement &);
|
||||
void writeWiringRulesXml(QDomElement &);
|
||||
void addDiagram(Diagram *diagram, int pos = -1);
|
||||
void detachDiagram(Diagram *diagram);
|
||||
@@ -400,6 +404,9 @@ class QETProject : public QObject
|
||||
void linkElementsToElementAutoNums();
|
||||
bool m_auto_conductor = true;
|
||||
WireHops::Mode m_wire_hops = WireHops::Mode::None;
|
||||
/// Texts drawn in a turned symbol stay horizontal (on for a new
|
||||
/// project, off for one saved without it), see uprightSymbolTexts()
|
||||
bool m_upright_symbol_texts = false;
|
||||
WiringRules::Settings m_wiring_rules;
|
||||
bool m_auto_break_conductor = false;
|
||||
XmlElementCollection *m_elements_collection = nullptr;
|
||||
|
||||
@@ -185,6 +185,11 @@ void ProjectMainConfigPage::applyProjectConf()
|
||||
modified_project = true;
|
||||
}
|
||||
|
||||
if (m_project -> uprightSymbolTexts() != upright_symbol_texts_cb_ -> isChecked()) {
|
||||
m_project -> setUprightSymbolTexts(upright_symbol_texts_cb_ -> isChecked());
|
||||
modified_project = true;
|
||||
}
|
||||
|
||||
//Following the application's rules stores nothing in the project
|
||||
WiringRules::Settings wiring_rules;
|
||||
if (!use_application_rules_cb_ -> isChecked()) {
|
||||
@@ -252,6 +257,14 @@ void ProjectMainConfigPage::initWidgets()
|
||||
"Seul le dessin change : aucun élément n'est ajouté et aucun conducteur n'est coupé.",
|
||||
"tooltip"));
|
||||
|
||||
//Texts of turned symbols kept horizontal
|
||||
upright_symbol_texts_cb_ = new QCheckBox(tr("Garder horizontaux les textes des symboles pivotés",
|
||||
"checkbox label"));
|
||||
upright_symbol_texts_cb_ -> setToolTip(tr("Les textes dessinés dans un symbole et les noms de ses bornes restent "
|
||||
"lisibles quand le symbole est pivoté : leur cadre tourne avec le symbole, "
|
||||
"pas le texte. Décochez pour qu'ils tournent avec le symbole, comme avant.",
|
||||
"tooltip"));
|
||||
|
||||
//How many wires a terminal may take (discussion #1158)
|
||||
wiring_rules_gb_ = new QGroupBox(tr("Conducteurs par borne", "group box title"));
|
||||
use_application_rules_cb_ = new QCheckBox(tr("Utiliser les réglages de l'application", "checkbox label"));
|
||||
@@ -318,6 +331,9 @@ void ProjectMainConfigPage::initLayout()
|
||||
main_layout0 -> addLayout(wire_hops_layout0);
|
||||
main_layout0 -> addSpacing(10);
|
||||
|
||||
main_layout0 -> addWidget(upright_symbol_texts_cb_);
|
||||
main_layout0 -> addSpacing(10);
|
||||
|
||||
QVBoxLayout *wiring_rules_layout = new QVBoxLayout(wiring_rules_gb_);
|
||||
wiring_rules_layout -> addWidget(use_application_rules_cb_);
|
||||
QHBoxLayout *max_wires_layout = new QHBoxLayout();
|
||||
@@ -351,6 +367,7 @@ void ProjectMainConfigPage::readValuesFromProject()
|
||||
|
||||
const int wire_hops_index = wire_hops_cb_ -> findData(WireHops::toString(m_project -> wireHops()));
|
||||
wire_hops_cb_ -> setCurrentIndex(qMax(0, wire_hops_index));
|
||||
upright_symbol_texts_cb_ -> setChecked(m_project -> uprightSymbolTexts());
|
||||
|
||||
//The rules that apply: the project's own or the application's
|
||||
const WiringRules::Settings wiring_rules = m_project -> wiringRules();
|
||||
@@ -406,6 +423,7 @@ void ProjectMainConfigPage::adjustReadOnly()
|
||||
usage_enabled_cb_ -> setDisabled(is_read_only);
|
||||
usage_reset_pb_ -> setDisabled(is_read_only);
|
||||
wire_hops_cb_ -> setDisabled(is_read_only);
|
||||
upright_symbol_texts_cb_ -> setDisabled(is_read_only);
|
||||
wiring_rules_gb_ -> setDisabled(is_read_only);
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ class ProjectMainConfigPage : public ProjectConfigPage {
|
||||
QPushButton *usage_reset_pb_;
|
||||
QLabel *wire_hops_label_;
|
||||
QComboBox *wire_hops_cb_;
|
||||
QCheckBox *upright_symbol_texts_cb_;
|
||||
QGroupBox *wiring_rules_gb_;
|
||||
QCheckBox *use_application_rules_cb_;
|
||||
QLabel *max_wires_label_;
|
||||
|
||||
@@ -788,6 +788,18 @@ if(QET_HAS_SCRIPTING)
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# A project can keep the texts of its turned symbols horizontal: the
|
||||
# DXF of a turned folio of tremie_vibrante.qet with the setting off and on.
|
||||
add_executable(
|
||||
tst_uprightsymboltexts
|
||||
tst_uprightsymboltexts.cpp)
|
||||
add_test(NAME tst_uprightsymboltexts COMMAND tst_uprightsymboltexts)
|
||||
add_dependencies(tst_uprightsymboltexts qelectrotech)
|
||||
target_link_libraries(tst_uprightsymboltexts PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_uprightsymboltexts 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,138 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// A project can keep the texts drawn in its turned symbols horizontal
|
||||
// (<symbol_texts upright="true"/>, Project properties). Through the real
|
||||
// binary on tremie_vibrante.qet: the symbols of the first folio are turned
|
||||
// by 90 degrees, then the folio is exported to DXF with the setting off and
|
||||
// on. The "M" of each motor turns with its symbol when it is off, and stays
|
||||
// horizontal when it is on.
|
||||
class tst_uprightsymboltexts : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
QProcessEnvironment environment()
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
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());
|
||||
return env;
|
||||
}
|
||||
|
||||
bool runQet(const QStringList &arguments)
|
||||
{
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(environment());
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
|
||||
return proc.waitForFinished(120000) && proc.exitCode() == 0;
|
||||
}
|
||||
|
||||
// The rotation (DXF group code 50) of every TEXT entity of @p dxf whose
|
||||
// text (group code 1) is @p text
|
||||
static QList<double> textRotations(const QString &dxf, const QString &text)
|
||||
{
|
||||
QFile file(dxf);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
|
||||
const QStringList lines = QString::fromUtf8(file.readAll())
|
||||
.split(QLatin1Char('\n'));
|
||||
QList<double> rotations;
|
||||
bool in_text = false, match = false;
|
||||
double rotation = 0;
|
||||
for (int i = 0; i + 1 < lines.size(); i += 2) {
|
||||
const QString code = lines.at(i).trimmed();
|
||||
const QString value = lines.at(i + 1).trimmed();
|
||||
if (code == QLatin1String("0")) {
|
||||
if (in_text && match) rotations << rotation;
|
||||
in_text = value == QLatin1String("TEXT");
|
||||
match = false;
|
||||
rotation = 0;
|
||||
} else if (in_text && code == QLatin1String("1")) {
|
||||
match = value == text;
|
||||
} else if (in_text && code == QLatin1String("50")) {
|
||||
rotation = value.toDouble();
|
||||
}
|
||||
}
|
||||
return rotations;
|
||||
}
|
||||
|
||||
// The DXF of the first folio of @p project, exported into @p name
|
||||
QString exportDxf(const QString &project, const QString &name)
|
||||
{
|
||||
const QString out = m_dir.filePath(name);
|
||||
QDir().mkpath(out);
|
||||
if (!runQet({QStringLiteral("--export-dxf"), project, out})) return {};
|
||||
const QStringList files = QDir(out).entryList({QStringLiteral("01*.dxf")});
|
||||
return files.isEmpty() ? QString() : QDir(out).filePath(files.first());
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void motorTextStaysHorizontal()
|
||||
{
|
||||
//The symbols of the first folio, turned by 90 degrees
|
||||
const QString off = m_dir.filePath(QStringLiteral("off.qet"));
|
||||
const QString script = m_dir.filePath(QStringLiteral("turn.js"));
|
||||
QFile js(script);
|
||||
QVERIFY(js.open(QIODevice::WriteOnly));
|
||||
js.write(QStringLiteral(
|
||||
"var els = qet.elementUuids(0);\n"
|
||||
"for (var e = 0; e < els.length; e++) qet.rotateElement(0, els[e], 90);\n"
|
||||
"qet.save(%1);\n").arg(QLatin1Char('"') + off + QLatin1Char('"')).toUtf8());
|
||||
js.close();
|
||||
QVERIFY(runQet({QStringLiteral("--run"), script,
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet")}));
|
||||
|
||||
//An existing project is read with the setting off, and saved so
|
||||
QFile off_file(off);
|
||||
QVERIFY(off_file.open(QIODevice::ReadOnly));
|
||||
QString xml = QString::fromUtf8(off_file.readAll());
|
||||
off_file.close();
|
||||
QVERIFY(!xml.contains(QStringLiteral("symbol_texts")));
|
||||
|
||||
//The same project with the setting on
|
||||
const QString on = m_dir.filePath(QStringLiteral("on.qet"));
|
||||
const int root_end = xml.indexOf(QLatin1Char('>'), xml.indexOf(QStringLiteral("<project"))) + 1;
|
||||
QVERIFY(root_end > 0);
|
||||
xml.insert(root_end, QStringLiteral("\n<symbol_texts upright=\"true\"/>"));
|
||||
QFile on_file(on);
|
||||
QVERIFY(on_file.open(QIODevice::WriteOnly));
|
||||
on_file.write(xml.toUtf8());
|
||||
on_file.close();
|
||||
|
||||
const QList<double> turned = textRotations(exportDxf(off, QStringLiteral("dxf-off")),
|
||||
QStringLiteral("M"));
|
||||
const QList<double> upright = textRotations(exportDxf(on, QStringLiteral("dxf-on")),
|
||||
QStringLiteral("M"));
|
||||
//The two motors of the folio
|
||||
QCOMPARE(turned.size(), 2);
|
||||
QCOMPARE(upright.size(), 2);
|
||||
for (double rotation : turned)
|
||||
QCOMPARE(std::fmod(rotation, 360.0), 270.0);
|
||||
for (double rotation : upright)
|
||||
QCOMPARE(std::fmod(rotation, 360.0), 0.0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_uprightsymboltexts)
|
||||
|
||||
#include "tst_uprightsymboltexts.moc"
|
||||
Reference in New Issue
Block a user