mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-08 04:44:13 +02:00
Add horizontal and vertical mirror for symbols on a folio (#1335)
Edit > "Miroir horizontal" (M) and "Miroir vertical" (F), also in the folio's right-click menu, mirror the selected symbols in place. The keys are the ones the element editor uses for the same two actions. An element keeps two mirrors about its own axes, applied before its rotation, and saves them as mirror="horizontal|vertical|both" on its <element> (written only when set, so other projects save byte for byte as before). On a symbol turned by 90 or 270 degrees, a mirror of the folio is the other mirror of the symbol itself, so the rotation never changes: a label kept upright does not swing round, and "Pivoter" still turns a mirrored symbol clockwise. - Terminals face the mirrored way (Terminal::orientation()), so wires follow. - The symbol stays where it was: its centre is kept, on the grid, since the hotspot is often a corner. - Texts read normally. The element's texts, text groups and cross reference are mirrored a second time about the centre of their own box (Element::keepReadable()), and ElementPictureFactory draws the texts of the symbol itself the same way, in a cached picture per mirror. Groups and cross references held at the bottom of the folio stay centred under their element. - DXF export mirrors the symbol's lines, arcs and texts. - Scripting: qet.mirrorElement(folio, uuid, vertical) and qet.elementMirror(folio, uuid); live mode may run both menu commands; the MCP server gets a mirror_element op and qet_diff reports mirrors. Not done: the parts of a PLC table drawn at run time (Element::drawPlcTable()) are not kept readable on a mirrored PLC; the project database has no column for it, as it has none for the rotation. Test: tst_scriptmirror mirrors a symbol through --run, checks every terminal's side and facing, the round trip, undo, a save and reload, and a symbol turned by 90 degrees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+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)
|
||||
|
||||
Reference in New Issue
Block a user