mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 05:44:14 +02:00
qet-mcp: qet_diff reports rotated symbols; equal angles compare equal
Rotating a symbol changes only its orientation attribute (quarter turns), which qet_diff did not read, so a pure rotation reported no change in any section. The elements section now has "rotated": each symbol whose orientation changed, with the value before and after. Rotating and undoing leaves QElectroTech writing text-field rotations as "-270" where they were "90" (or "-90" for "270"); compared as strings that read as a change. Rotations of texts, shapes, pictures and element text fields are now compared reduced to [0, 360). Found by a seeded-edit invariant run over the shipped examples: 51 rotations unreported across 20 projects, and 7 undo sequences reported as changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+31
-6
@@ -312,6 +312,21 @@ def _plain_text(html: str) -> str:
|
||||
return " ".join(inner.split())
|
||||
|
||||
|
||||
def _angle(value: str) -> str:
|
||||
"""A rotation in degrees, reduced to [0, 360) so equal angles compare equal.
|
||||
|
||||
QElectroTech writes the same angle in more than one way: rotating a symbol
|
||||
and undoing it leaves its text fields at "-270" where they were "90", or
|
||||
"-90" where they were "270". Compared as written, that read as a change.
|
||||
Anything that is not a number is returned unchanged.
|
||||
"""
|
||||
try:
|
||||
deg = float(value) % 360
|
||||
except (TypeError, ValueError):
|
||||
return value
|
||||
return f"{deg:g}"
|
||||
|
||||
|
||||
def _extras(root: ET.Element) -> dict:
|
||||
"""Everything a folio holds besides elements and conductors.
|
||||
|
||||
@@ -333,7 +348,7 @@ def _extras(root: ET.Element) -> dict:
|
||||
texts.append(record(
|
||||
t, {"folio": n, "x": t.get("x", ""), "y": t.get("y", ""),
|
||||
"text": _plain_text(t.get("text", ""))},
|
||||
{"rotation": t.get("rotation", "0"),
|
||||
{"rotation": _angle(t.get("rotation", "0")),
|
||||
"font": t.get("font", ""), "color": t.get("color", "")}))
|
||||
for sh in d.iter("shape"):
|
||||
pen, brush = sh.find("pen"), sh.find("brush")
|
||||
@@ -346,11 +361,11 @@ def _extras(root: ET.Element) -> dict:
|
||||
"line_width": pen.get("widthF", "") if pen is not None else "",
|
||||
"fill": (brush.get("color", "") if brush is not None and
|
||||
brush.get("style", "") != "NoBrush" else "none"),
|
||||
"rotation": sh.get("rotation", "0")}))
|
||||
"rotation": _angle(sh.get("rotation", "0"))}))
|
||||
for im in d.iter("image"):
|
||||
images.append(record(
|
||||
im, {"folio": n, "x": im.get("x", ""), "y": im.get("y", "")},
|
||||
{"scale": im.get("size", ""), "rotation": im.get("rotation", "")}))
|
||||
{"scale": im.get("size", ""), "rotation": _angle(im.get("rotation", ""))}))
|
||||
|
||||
element_texts = {}
|
||||
for n, d in _folios(root):
|
||||
@@ -367,7 +382,7 @@ def _extras(root: ET.Element) -> dict:
|
||||
fs = (t.get("font", "").split(",") + ["", ""])[1]
|
||||
element_texts[base + (seen[base],)] = {
|
||||
"x": t.get("x", ""), "y": t.get("y", ""), "size": fs,
|
||||
"frame": t.get("frame", ""), "rotation": t.get("rotation", ""),
|
||||
"frame": t.get("frame", ""), "rotation": _angle(t.get("rotation", "")),
|
||||
"width": t.get("text_width", ""),
|
||||
"shows": t.findtext("text") or ""}
|
||||
|
||||
@@ -472,16 +487,24 @@ def tool_diff(before: str, after: str) -> dict:
|
||||
a_el, b_el = {}, {}
|
||||
for i, e in _elements(_root(before)):
|
||||
r = _element_row(i, e)
|
||||
# Rotation is saved as "orientation", in quarter turns (0-3); it is
|
||||
# the only thing a rotation changes, so without it a rotated symbol
|
||||
# reads as untouched.
|
||||
r["orientation"] = e.get("orientation", "0")
|
||||
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")
|
||||
b_el[r["uuid"] or f"{i}:{r['x']},{r['y']}:{r['name']}"] = r
|
||||
|
||||
moved, relabelled, changed_info = [], [], []
|
||||
moved, rotated, relabelled, changed_info = [], [], [], []
|
||||
for k, a in a_el.items():
|
||||
b = b_el.get(k)
|
||||
if b is None:
|
||||
continue
|
||||
if a["orientation"] != b["orientation"]:
|
||||
rotated.append({"uuid": k, "name": a["name"], "folio": a["folio"],
|
||||
"orientation": [a["orientation"], b["orientation"]]})
|
||||
if (a["x"], a["y"]) != (b["x"], b["y"]):
|
||||
moved.append({
|
||||
"uuid": k, "name": a["name"], "folio": a["folio"],
|
||||
@@ -540,6 +563,7 @@ def tool_diff(before: str, after: str) -> dict:
|
||||
"distinct_move_deltas": [list(d) for d in deltas],
|
||||
"relabelled": relabelled[:50],
|
||||
"info_changed": changed_info[:50],
|
||||
"rotated": rotated[:50],
|
||||
},
|
||||
"conductors": {
|
||||
"before": len(a_co), "after": len(b_co),
|
||||
@@ -2178,7 +2202,8 @@ TOOLS = [
|
||||
{
|
||||
"name": "qet_diff",
|
||||
"description": "Structurally diff two .qet files: which elements moved and "
|
||||
"by what delta, which were added, removed or relabelled, and "
|
||||
"by what delta, which were rotated (orientation in quarter "
|
||||
"turns, 0-3), which were added, removed or relabelled, and "
|
||||
"which conductor fields changed. Use this to verify what an "
|
||||
"edit actually did, rather than reading a screenshot.",
|
||||
"inputSchema": {
|
||||
|
||||
@@ -899,13 +899,39 @@ class DiffContracts(unittest.TestCase):
|
||||
"moved_count": 1, "distinct_move_deltas": [[5.0, 0.0]],
|
||||
"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"}}]})
|
||||
"to": {"label": "K2", "comment": "d"}}],
|
||||
"rotated": []})
|
||||
|
||||
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"]),
|
||||
([], [], [], [], []))
|
||||
self.assertEqual((e["moved"], e["relabelled"], e["info_changed"], e["added"], e["removed"],
|
||||
e["rotated"]), ([], [], [], [], [], []))
|
||||
|
||||
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")],
|
||||
["90", "90", "270", "270", "0", "0", "90", "12.5", "", "x"])
|
||||
def field(rot):
|
||||
return self.el(self.A, 0, 0, texts=(
|
||||
f'<dynamic_elmt_text x="0" y="0" rotation="{rot}" text_from="UserText"><text>t</text>'
|
||||
'</dynamic_elmt_text>'))
|
||||
d = m.tool_diff(self.qet(self.folio(field("90"))), self.qet(self.folio(field("-270"))))
|
||||
self.assertEqual(d["element_texts"]["changed"], [])
|
||||
d = m.tool_diff(self.qet(self.folio(field("90"))), self.qet(self.folio(field("180"))))
|
||||
self.assertEqual(d["element_texts"]["changed"][0]["changed"], {"rotation": ["90", "180"]})
|
||||
|
||||
def test_a_rotation_is_reported(self):
|
||||
"""Rotating a symbol changes only its orientation (quarter turns); a diff
|
||||
that ignored it reported a rotation as no change at all."""
|
||||
rot = lambda o: self.qet(self.folio(
|
||||
self.el(self.A, 1, 2).replace('x="1"', f'orientation="{o}" x="1"')))
|
||||
d = m.tool_diff(rot(0), rot(1))
|
||||
self.assertEqual(d["elements"]["rotated"],
|
||||
[{"uuid": self.A, "name": "coil", "folio": 1, "orientation": ["0", "1"]}])
|
||||
self.assertEqual((d["elements"]["moved"], d["elements"]["relabelled"]), ([], []))
|
||||
# no attribute is orientation 0, as QElectroTech reads it
|
||||
plain = self.qet(self.folio(self.el(self.A, 1, 2)))
|
||||
self.assertEqual(m.tool_diff(plain, rot(0))["elements"]["rotated"], [])
|
||||
|
||||
def test_conductors_section_exact(self):
|
||||
A, B, C = self.A, self.B, self.C
|
||||
@@ -1064,6 +1090,10 @@ class DiffContracts(unittest.TestCase):
|
||||
e = m.tool_diff(self.qet(self.folio(many(51, "a"))),
|
||||
self.qet(self.folio(many(51, "b"))))["elements"]
|
||||
self.assertEqual((len(e["relabelled"]), len(e["info_changed"])), (50, 50))
|
||||
turned = lambda o: "".join(self.el(U(i), 0, 0).replace('x="0"', f'orientation="{o}" x="0"')
|
||||
for i in range(51))
|
||||
e = m.tool_diff(self.qet(self.folio(turned(0))), self.qet(self.folio(turned(2))))["elements"]
|
||||
self.assertEqual(len(e["rotated"]), 50)
|
||||
els = many(102, None)
|
||||
wires = lambda n, num: "".join(self.wire(U(i), U(i + 1), num=num) for i in range(n))
|
||||
c = m.tool_diff(self.qet(self.folio(els, wires(101, "a"))),
|
||||
|
||||
Reference in New Issue
Block a user