From 219208448332516acc1d1c69e4d4d060ff5ac9d6 Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Wed, 7 Oct 2026 23:10:50 +0200 Subject: [PATCH] Mirror a symbol twice and it comes back where it was Mirroring a symbol on a folio keeps its centre in place and then snapped the new position to the grid. A symbol that was not on the grid was pulled onto it, so a second mirror the same way left it a few pixels away from where it started. In perceuse.qet that is 171 symbols, each way. tst_scriptmirror::mirrorBackAndSave() picks its symbol in an order that changes from run to run, so it failed only some of the time (three runs in eight on macOS, and on the Windows CI). The move is now snapped to the grid instead of the position. A symbol on the grid stays on it, one off the grid keeps its offset, and because qRound() rounds halves away from zero both ways, a second mirror moves it exactly back. The new tst_scriptmirror::mirrorTwiceComesBack mirrors every symbol of perceuse.qet twice, horizontally and vertically, and checks that every terminal is back in place. It fails on master (171 symbols moved). No change to the file format. Co-Authored-By: Claude Opus 5.5 --- .../undocommand/mirrorselectioncommand.cpp | 8 ++-- tests/qttest/tst_scriptmirror.cpp | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/sources/undocommand/mirrorselectioncommand.cpp b/sources/undocommand/mirrorselectioncommand.cpp index 46dd64078..0f32b0f44 100644 --- a/sources/undocommand/mirrorselectioncommand.cpp +++ b/sources/undocommand/mirrorselectioncommand.cpp @@ -83,15 +83,17 @@ MirrorSelectionCommand::MirrorSelectionCommand(const QList &elements, //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 + //centre on centre. The move is snapped to the grid, not the new + //position: an element on the grid stays on it, one off the grid + //keeps its offset, and a second mirror moves it exactly back. 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); + const QPointF new_pos = old_pos + Diagram::snapToGrid( + element->mapToScene(element->boundingRect().center()) - centre - old_pos); if (new_pos != old_pos) new QPropertyUndoCommand(element, "pos", old_pos, new_pos, this); } diff --git a/tests/qttest/tst_scriptmirror.cpp b/tests/qttest/tst_scriptmirror.cpp index 338b5e19d..a75d10aa2 100644 --- a/tests/qttest/tst_scriptmirror.cpp +++ b/tests/qttest/tst_scriptmirror.cpp @@ -191,6 +191,48 @@ private slots: r.value(QStringLiteral("v")).toArray()); } + /** + Mirroring a symbol twice the same way puts it back where it was, + for every symbol of the project, including those that do not sit + on the grid. Before, the second mirror snapped such a symbol onto + the grid, a few pixels away. Which symbol mirrorBackAndSave() + picks changes from run to run, so this checks them all. + */ + void mirrorTwiceComesBack_data() + { + QTest::addColumn("vertical"); + QTest::newRow("horizontal") << false; + QTest::newRow("vertical") << true; + } + + void mirrorTwiceComesBack() + { + QFETCH(bool, vertical); + const QJsonObject r = run(script(QStringLiteral( + "var out = {count: 0, moved: []};\n" + "for (var f = 0; f < qet.folioCount(); f++) {\n" + " var els = qet.elementUuids(f);\n" + " for (var e = 0; e < els.length; e++) {\n" + " var before = JSON.stringify(terms(f, els[e]));\n" + " qet.mirrorElement(f, els[e], %1);\n" + " qet.mirrorElement(f, els[e], %1);\n" + " var after = JSON.stringify(terms(f, els[e]));\n" + " out.count++;\n" + " if (after !== before) out.moved.push(els[e] + ' ' + before + ' -> ' + after);\n" + " }\n" + "}\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"); + QVERIFY(r.value(QStringLiteral("count")).toInt() > 0); + const QJsonArray moved = r.value(QStringLiteral("moved")).toArray(); + for (const QJsonValue &m : moved) + qWarning("moved: %s", qPrintable(m.toString())); + QCOMPARE(moved.size(), 0); + } + /** 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