mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 22:24:13 +02:00
Merge pull request #1101 from ispyisail/feat/qet-mcp-edit-table-text-uuid
Let qet_edit address tables and symbol text fields by uuid
This commit is contained in:
+31
-4
@@ -1159,10 +1159,10 @@ OPS = {
|
||||
("source", "str"), ("value", "str"),
|
||||
("x", "num"), ("y", "num")]),
|
||||
"set_element_text": ("setElementTextProperty", [("folio", "folio"), ("element", "elmt"),
|
||||
("index", "folio"), ("property", "str"),
|
||||
("index", "element_text"), ("property", "str"),
|
||||
("value", "str")]),
|
||||
"delete_element_text": ("deleteElementText", [("folio", "folio"), ("element", "elmt"),
|
||||
("index", "folio")]),
|
||||
("index", "element_text")]),
|
||||
# Returns the uuids of the copies IN THE ORDER the elements were named,
|
||||
# so "$copies[0]" is the copy of the first one. Conductors between the
|
||||
# copied elements are copied with them; copies arrive without labels or
|
||||
@@ -1222,9 +1222,9 @@ OPS = {
|
||||
("closed", "bool")]),
|
||||
"add_table": ("addTable", [("folio", "folio"), ("kind", "str"),
|
||||
("name", "str"), ("query", "str")]),
|
||||
"set_table_position": ("setTablePosition", [("folio", "folio"), ("table", "folio"),
|
||||
"set_table_position": ("setTablePosition", [("folio", "folio"), ("table", "table"),
|
||||
("x", "num"), ("y", "num")]),
|
||||
"delete_table": ("deleteTable", [("folio", "folio"), ("table", "folio")]),
|
||||
"delete_table": ("deleteTable", [("folio", "folio"), ("table", "table")]),
|
||||
}
|
||||
|
||||
SHAPES = ["line", "rectangle", "ellipse", "polygon"]
|
||||
@@ -1278,6 +1278,7 @@ def _build_script(operations: list, output: str) -> str:
|
||||
# an index-only edit still runs on a build that predates them.
|
||||
uuid_methods: set[str] = set()
|
||||
folio_js = "0"
|
||||
element_js = None # the op's element, for lookups scoped to it
|
||||
lines = [
|
||||
"// generated by qet-mcp; do not edit",
|
||||
"var R = {};", # $name -> value from an earlier op
|
||||
@@ -1331,6 +1332,26 @@ def _build_script(operations: list, output: str) -> str:
|
||||
raise ValueError(f"operation {op_index}: {key!r} must be a non-empty list of "
|
||||
f"integer indices, got {value!r}")
|
||||
return _js(value)
|
||||
if kind == "table":
|
||||
# As for texts below: a uuid is resolved to the current index at
|
||||
# run time, since deleting an earlier table shifts every index.
|
||||
if isinstance(value, str) and _UUID_RE.fullmatch(value):
|
||||
uuid_methods.add("tableIndex")
|
||||
return f"qet.tableIndex({folio_js}, {_js(value)})"
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise ValueError(f"operation {op_index}: {key!r} must be a table index "
|
||||
f"or its uuid, got {value!r}")
|
||||
return _js(value)
|
||||
if kind == "element_text":
|
||||
# A field's uuid is unique only within its element (copies keep
|
||||
# them), so the lookup takes the op's element too.
|
||||
if isinstance(value, str) and _UUID_RE.fullmatch(value):
|
||||
uuid_methods.add("elementTextIndex")
|
||||
return f"qet.elementTextIndex({folio_js}, {element_js}, {_js(value)})"
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise ValueError(f"operation {op_index}: {key!r} must be a text field index "
|
||||
f"or its uuid, got {value!r}")
|
||||
return _js(value)
|
||||
if kind in ("text", "shape", "image"):
|
||||
# A uuid names the item for good; it is turned into the index
|
||||
# the call takes at run time, by the item's own folio.
|
||||
@@ -1430,6 +1451,7 @@ def _build_script(operations: list, output: str) -> str:
|
||||
if key not in op:
|
||||
raise ValueError(f"operation {i} ({name}) is missing {key!r}")
|
||||
folio_js = args[0] if args else "0"
|
||||
element_js = args[1] if len(args) > 1 else None
|
||||
args.append(ref_or(op[key], kind, i, key))
|
||||
|
||||
ident = op.get("id")
|
||||
@@ -2465,6 +2487,11 @@ TOOLS = [
|
||||
"shape's points can reorder it relative to the others -- re-list "
|
||||
"before addressing one by index again if more than one is being "
|
||||
"edited in the same run, or address it by uuid. "
|
||||
"set_table_position/delete_table take a table's index or its uuid, "
|
||||
"and set_element_text/delete_element_text a text field's index or "
|
||||
"its uuid (the field's own, looked up within the op's element) "
|
||||
"-- a uuid still names the right item after an "
|
||||
"earlier one is deleted. "
|
||||
"Tables: add_table places a BOM/nomenclature or summary table "
|
||||
"(kind is \"nomenclature\" or \"summary\") built from a query "
|
||||
"against a project database view -- run qet.query() (the "
|
||||
|
||||
@@ -167,6 +167,33 @@ class EditValidation(unittest.TestCase):
|
||||
def build(self, ops):
|
||||
return m._build_script(ops, "/tmp/out.qet")
|
||||
|
||||
def test_tables_and_text_fields_by_uuid(self):
|
||||
"""A table or a symbol text field named by uuid is looked up at run
|
||||
time; a field's lookup is scoped to the op's element."""
|
||||
U = "{11111111-2222-4333-8444-555555555555}"
|
||||
E = "{aaaaaaaa-0000-4000-8000-000000000001}"
|
||||
s = self.build([{"op": "set_table_position", "folio": 2, "table": U, "x": 1, "y": 2}])
|
||||
self.assertIn(f'qet.setTablePosition(2, qet.tableIndex(2, "{U}"), 1, 2)', s)
|
||||
s = self.build([{"op": "delete_table", "folio": 0, "table": 3}])
|
||||
self.assertIn("qet.deleteTable(0, 3)", s)
|
||||
s = self.build([{"op": "set_element_text", "folio": 1, "element": E, "index": U,
|
||||
"property": "x", "value": "5"}])
|
||||
self.assertIn(f'qet.setElementTextProperty(1, "{E}", '
|
||||
f'qet.elementTextIndex(1, "{E}", "{U}"), "x", "5")', s)
|
||||
# the element may be one placed earlier in the same run
|
||||
s = self.build([{"op": "add_folio", "id": "f"},
|
||||
{"op": "add_element", "id": "k", "folio": "$f", "path": "p", "x": 0, "y": 0},
|
||||
{"op": "delete_element_text", "folio": "$f", "element": "$k", "index": U}])
|
||||
self.assertIn(f'qet.deleteElementText(R["f"], R["k"], qet.elementTextIndex(R["f"], R["k"], "{U}"))', s)
|
||||
# the lookups are required only when a uuid is used
|
||||
self.assertIn('"tableIndex"', self.build([{"op": "delete_table", "folio": 0, "table": U}]))
|
||||
self.assertNotIn('"tableIndex"', self.build([{"op": "delete_table", "folio": 0, "table": 0}]))
|
||||
for op in ({"op": "delete_table", "folio": 0, "table": "second"},
|
||||
{"op": "delete_element_text", "folio": 0, "element": E, "index": "label"}):
|
||||
with self.subTest(op=op["op"]):
|
||||
with self.assertRaisesRegex(ValueError, "index or its uuid"):
|
||||
self.build([op])
|
||||
|
||||
def test_every_op_generates_a_script(self):
|
||||
# one minimal valid instance of every op
|
||||
f = {"op": "add_folio", "id": "f"}
|
||||
@@ -3284,6 +3311,58 @@ class UuidIndexLookups(unittest.TestCase):
|
||||
self.assertEqual((out["bogus"], out["not_uuid"]), (-1, -1))
|
||||
self.assertEqual((out["second_after"], out["first_after"]), (0, -1))
|
||||
|
||||
def test_qet_edit_deletes_then_moves_tables_by_uuid(self):
|
||||
"""Delete one table, then move the other, both by uuid. By index the
|
||||
second op would name the wrong table: deleting table 0 shifts table 1."""
|
||||
text = (Path(EXAMPLES) / "industrial.qet").read_text(encoding="utf-8")
|
||||
root = ET.fromstring(text)
|
||||
folio, table = next((i, d.find("tables/graphics_table")) for i, d in enumerate(root.iter("diagram"))
|
||||
if d.find("tables/graphics_table") is not None)
|
||||
twin = ET.fromstring(ET.tostring(table))
|
||||
twin.set("uuid", "{11111111-2222-4333-8444-555555555555}")
|
||||
twin.set("x", str(float(table.get("x")) + 900))
|
||||
# Insert into the raw text: re-serialising the whole file with
|
||||
# ElementTree rewrites the embedded SVG logo's namespace, which
|
||||
# QElectroTech then saves without its declaration.
|
||||
start = text.index(f'uuid="{table.get("uuid")}"')
|
||||
end = text.index("</graphics_table>", start) + len("</graphics_table>")
|
||||
text = text[:end] + ET.tostring(twin, encoding="unicode") + text[end:]
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src, out = Path(tmp) / "two.qet", Path(tmp) / "out.qet"
|
||||
src.write_text(text, encoding="utf-8")
|
||||
r = m.tool_edit(BINARY, str(src), [
|
||||
{"op": "delete_table", "folio": folio, "table": table.get("uuid")},
|
||||
{"op": "set_table_position", "folio": folio, "table": twin.get("uuid"), "x": 120, "y": 340}],
|
||||
str(out), elements_dir=ELEMENTS or None)
|
||||
self.assertTrue(r["ok"], r.get("hint"))
|
||||
left = list(ET.parse(out).getroot().iter("diagram"))[folio].findall("tables/graphics_table")
|
||||
self.assertEqual([(t.get("uuid"), float(t.get("x")), float(t.get("y"))) for t in left],
|
||||
[(twin.get("uuid"), 120.0, 340.0)])
|
||||
|
||||
def test_qet_edit_edits_one_copys_field_by_uuid(self):
|
||||
"""Two copies of a symbol share a field uuid; addressing it with the
|
||||
symbol changes that copy's field only."""
|
||||
root = ET.parse(Path(EXAMPLES) / "2612_ats_singlephase.qet").getroot()
|
||||
owners = {}
|
||||
for i, d in enumerate(root.iter("diagram")):
|
||||
for el in d.iter("element"):
|
||||
for t in el.findall("dynamic_texts/dynamic_elmt_text"):
|
||||
if t.get("uuid"):
|
||||
owners.setdefault((i, t.get("uuid")), []).append(el.get("uuid"))
|
||||
(folio, field), (a, b) = next((k, v[:2]) for k, v in owners.items() if len(v) >= 2)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src, out = Path(tmp) / "in.qet", Path(tmp) / "out.qet"
|
||||
shutil.copy(Path(EXAMPLES) / "2612_ats_singlephase.qet", src)
|
||||
r = m.tool_edit(BINARY, str(src), [
|
||||
{"op": "set_element_text", "folio": folio, "element": b, "index": field,
|
||||
"property": "x", "value": "77"}], str(out), elements_dir=ELEMENTS or None)
|
||||
self.assertTrue(r["ok"], r.get("hint"))
|
||||
x_of = lambda path, el_uuid: next(
|
||||
t.get("x") for el in ET.parse(path).getroot().iter("element") if el.get("uuid") == el_uuid
|
||||
for t in el.findall("dynamic_texts/dynamic_elmt_text") if t.get("uuid") == field)
|
||||
self.assertEqual(float(x_of(out, b)), 77.0)
|
||||
self.assertEqual(x_of(out, a), x_of(src, a))
|
||||
|
||||
def test_element_text_index_needs_the_element_as_well(self):
|
||||
"""Copies of a symbol share their text fields' uuids (2612_ats_singlephase.qet):
|
||||
the same field uuid resolves on each copy to that copy's own field."""
|
||||
|
||||
Reference in New Issue
Block a user