mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 05:44:14 +02:00
qet-mcp: qet_edit names a terminal by its uuid
"terminal", "from_terminal" and "to_terminal" take the terminal's uuid (as qet_element_info lists it) in place of its index. It names a terminal of the op's own element -- for add_conductor, of that end's element, "$name" references included -- and is turned at run time into the index the call takes by qet.terminalIndex(); if the element has no such terminal the op fails with a note. Unlike the index, a sort by position, it is defined between two terminals at the same point. The lookup is required only when a uuid is used, so index-only edits still run on older builds. README: the terminal uuids; wires of older projects now get a lasting uuid (#1107) instead of staying unnamed. Tests: script generation for every terminal-taking op, "$name" and folio uuid resolution, the index still passed through; through the binary, bobine_ka_a_remanence (file order A2, A1; index order A1, A2) wired A2 to A1 by uuid lands on exactly those ends, and an unknown uuid stops the run with its note. 255/255 against a build with qet.terminalIndex(); a build without it is reported as missing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+13
-3
@@ -305,9 +305,19 @@ Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
`qet.conductorEnds()` in the build. A uuid names one wire, but
|
||||
`set_conductor` still changes the whole potential, the same as by
|
||||
terminal. A project saved before conductors carried a uuid has none in
|
||||
the file: QElectroTech makes a new one on every load and does not save
|
||||
it, so `qet_conductors` reports `uuid` as empty and those conductors are
|
||||
named by `element` + `terminal` until wire uuids last (#1103).
|
||||
the file until it is saved once: since #1107 QElectroTech works one out
|
||||
from the wire's two ends on load and writes it on the next save, so it
|
||||
appears after a first `qet_edit`.
|
||||
- **A terminal can be named by its uuid**: `terminal`, `from_terminal` and
|
||||
`to_terminal` take the terminal's uuid (as `qet_element_info` lists it)
|
||||
in place of its index, on the op's own element (for `add_conductor`, on
|
||||
that end's element). It is turned at run time into the index the call
|
||||
takes; if the element has no terminal with it the op fails and its
|
||||
`note` says so. Unlike the index, which is a sort by position, it is
|
||||
defined between two terminals at the same point. Needs
|
||||
`qet.terminalIndex()` in the build. A symbol file saved without terminal
|
||||
uuids lists them empty; QElectroTech gives the terminals of every
|
||||
project's copy of it a uuid on opening (#1118), written on the next save.
|
||||
- **`qet_export` isolates its launch.** SingleApplication keys its socket
|
||||
on `applicationFilePath()`, so a second launch of the same binary path
|
||||
forwards its request to an already-running instance and returns *that*
|
||||
|
||||
+38
-7
@@ -1292,14 +1292,14 @@ OPS = {
|
||||
"set_info": ("setElementInfo", [("folio", "folio"), ("element", "elmt"),
|
||||
("key", "str"), ("value", "str")]),
|
||||
"add_conductor": ("addConductor", [("folio", "folio"),
|
||||
("from", "elmt"), ("from_terminal", "num"),
|
||||
("to", "elmt"), ("to_terminal", "num")]),
|
||||
("from", "elmt"), ("from_terminal", "term"),
|
||||
("to", "elmt"), ("to_terminal", "term")]),
|
||||
"delete_element": ("deleteElement", [("folio", "folio"), ("element", "elmt")]),
|
||||
"set_conductor": ("setConductorProperty", [("folio", "folio"), ("element", "elmt"),
|
||||
("terminal", "num"), ("property", "str"),
|
||||
("terminal", "term"), ("property", "str"),
|
||||
("value", "str")]),
|
||||
"move_conductor_segment": ("moveConductorSegment", [("folio", "folio"), ("element", "elmt"),
|
||||
("terminal", "num"), ("segment", "num"),
|
||||
("terminal", "term"), ("segment", "num"),
|
||||
("dx", "num"), ("dy", "num")]),
|
||||
"link_elements": ("linkElements", [("folio", "folio"), ("element", "elmt"),
|
||||
("to_folio", "folio"), ("to", "elmt")]),
|
||||
@@ -1320,7 +1320,7 @@ OPS = {
|
||||
# uuid, which does not shift when another one is added. So
|
||||
# it can be named as "$id". Indexes shift when one is added or deleted.
|
||||
"delete_conductor": ("deleteConductor", [("folio", "folio"), ("element", "elmt"),
|
||||
("terminal", "num")]),
|
||||
("terminal", "term")]),
|
||||
"remove_folio": ("removeFolio", [("folio", "folio")]),
|
||||
"set_folio": ("setFolioProperty", [("folio", "folio"), ("property", "str"),
|
||||
("value", "str")]),
|
||||
@@ -1469,6 +1469,7 @@ def _build_script(operations: list, output: str) -> str:
|
||||
uuid_methods: set[str] = set()
|
||||
folio_js = "0"
|
||||
element_js = None # the op's element, for lookups scoped to it
|
||||
owner_js = None # the element a terminal argument belongs to
|
||||
lines = [
|
||||
"// generated by qet-mcp; do not edit",
|
||||
"var R = {};", # $name -> value from an earlier op
|
||||
@@ -1509,6 +1510,16 @@ def _build_script(operations: list, output: str) -> str:
|
||||
"{kind: 'op_note', index: index, note: why}));",
|
||||
" return null;",
|
||||
"}",
|
||||
# A terminal named by uuid is turned into the index the calls take,
|
||||
# on its own element; -1, with the reason logged, if that element
|
||||
# has no terminal with it.
|
||||
"function qetMcpTerminal(index, folio, element, uuid) {",
|
||||
" var t = qet.terminalIndex(folio, element, uuid);",
|
||||
" if (t < 0) qet.log(" + _js(_MARKER) + " + JSON.stringify({kind: 'op_note', "
|
||||
"index: index, note: 'no terminal ' + uuid + ' on element ' + element + "
|
||||
"' (or two of its terminals carry it)'}));",
|
||||
" return t;",
|
||||
"}",
|
||||
"if (missing.length === 0) {",
|
||||
]
|
||||
|
||||
@@ -1561,6 +1572,20 @@ def _build_script(operations: list, output: str) -> str:
|
||||
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 == "term":
|
||||
# A terminal's uuid comes from its symbol's definition, so it
|
||||
# names a terminal only on its element: the op's element, or for
|
||||
# add_conductor the end's own. Turned into the index the call
|
||||
# takes at run time; unlike the index, it is defined between two
|
||||
# terminals at the same point.
|
||||
if isinstance(value, str) and _UUID_RE.fullmatch(value):
|
||||
uuid_methods.add("terminalIndex")
|
||||
return (f"qetMcpTerminal({op_index}, {folio_js}, {owner_js}, "
|
||||
f"{_js(value)})")
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise ValueError(f"operation {op_index}: {key!r} must be a terminal 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.
|
||||
@@ -1700,6 +1725,7 @@ def _build_script(operations: list, output: str) -> str:
|
||||
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
|
||||
owner_js = args[-1] if args else None # a terminal's element precedes it
|
||||
args.append(ref_or(op[key], kind, i, key))
|
||||
if conductor_js is not None:
|
||||
args[1], args[2] = f"e{i}.element", f"e{i}.terminal"
|
||||
@@ -2635,8 +2661,13 @@ TOOLS = [
|
||||
"qet_project_info call 1 is \"folio\": 0 here. A folio can "
|
||||
"be given as its uuid instead (qet_project_info lists them), "
|
||||
"which still names the same folio after an earlier op adds or "
|
||||
"removes one. Terminals are numbered by their "
|
||||
"index in the element definition; qet_element_info lists them. "
|
||||
"removes one. A terminal (\"terminal\", \"from_terminal\", "
|
||||
"\"to_terminal\") is given by its index -- qet_element_info "
|
||||
"lists terminals in index order, top to bottom then left to "
|
||||
"right -- or by its uuid, which qet_element_info also lists and "
|
||||
"which, unlike the index, tells apart two terminals at the same "
|
||||
"point; a terminal uuid names a terminal of the op's own "
|
||||
"element (for add_conductor, of that end's element). "
|
||||
"set_conductor addresses a conductor as the one on a given "
|
||||
"terminal and applies the change to its whole electrical "
|
||||
"potential, so name a terminal carrying exactly one conductor, "
|
||||
|
||||
@@ -242,6 +242,43 @@ class EditValidation(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "saved before conductors carried a uuid"):
|
||||
self.build([{"op": "delete_conductor", "folio": 0, "conductor": ""}])
|
||||
|
||||
def test_terminal_by_uuid(self):
|
||||
"""A terminal named by uuid is turned into its index at run time, on
|
||||
the element it belongs to: the op's element, or each add_conductor
|
||||
end's own. The lookup is required only then; a plain index is
|
||||
passed through as before."""
|
||||
E, F = "{11111111-2222-4333-8444-555555555555}", "{21111111-2222-4333-8444-555555555555}"
|
||||
T, V = "{31111111-2222-4333-8444-555555555555}", "{41111111-2222-4333-8444-555555555555}"
|
||||
s = self.build([{"op": "add_conductor", "folio": 1, "from": E, "from_terminal": T,
|
||||
"to": F, "to_terminal": V}])
|
||||
self.assertIn(f'qet.addConductor(1, "{E}", qetMcpTerminal(0, 1, "{E}", "{T}"), '
|
||||
f'"{F}", qetMcpTerminal(0, 1, "{F}", "{V}"))', s)
|
||||
self.assertIn('"terminalIndex"', s)
|
||||
s = self.build([{"op": "set_conductor", "folio": 0, "element": E, "terminal": T,
|
||||
"property": "num", "value": "W1"}])
|
||||
self.assertIn(f'qet.setConductorProperty(0, "{E}", qetMcpTerminal(0, 0, "{E}", "{T}"), '
|
||||
'"num", "W1")', s)
|
||||
s = self.build([{"op": "delete_conductor", "folio": 0, "element": E, "terminal": T}])
|
||||
self.assertIn(f'qet.deleteConductor(0, "{E}", qetMcpTerminal(0, 0, "{E}", "{T}"))', s)
|
||||
s = self.build([{"op": "move_conductor_segment", "folio": 0, "element": E,
|
||||
"terminal": T, "segment": 1, "dx": 5, "dy": 0}])
|
||||
self.assertIn(f'qet.moveConductorSegment(0, "{E}", qetMcpTerminal(0, 0, "{E}", "{T}"), 1, 5, 0)', s)
|
||||
# a $name element and a folio uuid reach the lookup resolved
|
||||
s = self.build([{"op": "add_folio", "id": "f"},
|
||||
{"op": "add_element", "id": "a", "folio": "$f", "path": "x.elmt", "x": 0, "y": 0},
|
||||
{"op": "delete_conductor", "folio": "$f", "element": "$a", "terminal": T}])
|
||||
self.assertIn(f'qet.deleteConductor(R["f"], R["a"], qetMcpTerminal(2, R["f"], R["a"], "{T}"))', s)
|
||||
s = self.build([{"op": "delete_conductor", "folio": V, "element": E, "terminal": T}])
|
||||
self.assertIn(f'qetMcpTerminal(0, qet.folioIndex("{V}"), "{E}", "{T}")', s)
|
||||
# an index is unchanged and needs no lookup
|
||||
s = self.build([{"op": "delete_conductor", "folio": 0, "element": E, "terminal": 2}])
|
||||
self.assertIn(f'qet.deleteConductor(0, "{E}", 2)', s)
|
||||
self.assertNotIn('"terminalIndex"', s)
|
||||
with self.assertRaisesRegex(ValueError, "terminal index or its uuid"):
|
||||
self.build([{"op": "delete_conductor", "folio": 0, "element": E, "terminal": "A1"}])
|
||||
with self.assertRaisesRegex(ValueError, "terminal index or its uuid"):
|
||||
self.build([{"op": "delete_conductor", "folio": 0, "element": E, "terminal": True}])
|
||||
|
||||
def test_every_op_generates_a_script(self):
|
||||
# one minimal valid instance of every op
|
||||
f = {"op": "add_folio", "id": "f"}
|
||||
@@ -2235,6 +2272,39 @@ class Integration(unittest.TestCase):
|
||||
nums = [c["num"] for c in m.tool_conductors(r["output"])["conductors"]]
|
||||
self.assertEqual(nums, ["W1"])
|
||||
|
||||
def test_add_conductor_by_terminal_uuid(self):
|
||||
"""The coil's file lists A2 before A1, and its index order puts A1
|
||||
first: wiring by uuid reaches the terminals named, whatever their
|
||||
index, and an unknown uuid stops the run with a note."""
|
||||
info = m.tool_element_info(str(Path(ELEMENTS) / COIL.removeprefix("common://")))
|
||||
by_name = {t["name"]: t for t in info["terminals"]}
|
||||
a1, a2 = by_name["A1"], by_name["A2"]
|
||||
self.assertTrue(a1["uuid"] and a2["uuid"])
|
||||
self.assertEqual((a1["index"], a2["index"]), (0, 1))
|
||||
base = self.sb.new()
|
||||
r = self.ok(self.sb.edit(base, [
|
||||
{"op": "add_element", "id": "a", "folio": 0, "path": COIL, "x": 100, "y": 100},
|
||||
{"op": "add_element", "id": "b", "folio": 0, "path": COIL, "x": 300, "y": 100},
|
||||
{"op": "add_conductor", "folio": 0, "from": "$a", "from_terminal": a2["uuid"],
|
||||
"to": "$b", "to_terminal": a1["uuid"]}]))
|
||||
self.assertTrue(all(o["succeeded"] for o in r["operations"]))
|
||||
wires = [c for c in ET.parse(r["output"]).getroot().iter("conductor")
|
||||
if c.get("terminal1") and c.get("element1")]
|
||||
self.assertEqual(len(wires), 1)
|
||||
ends = {(wires[0].get("element1"), wires[0].get("terminal1")),
|
||||
(wires[0].get("element2"), wires[0].get("terminal2"))}
|
||||
a_uuid, b_uuid = (o["result"] for o in r["operations"][:2])
|
||||
self.assertEqual(ends, {(a_uuid, a2["uuid"]), (b_uuid, a1["uuid"])})
|
||||
|
||||
r = self.sb.edit(base, [
|
||||
{"op": "add_element", "id": "a", "folio": 0, "path": COIL, "x": 100, "y": 100},
|
||||
{"op": "add_conductor", "folio": 0, "from": "$a",
|
||||
"from_terminal": "{00000000-0000-4000-8000-000000000001}",
|
||||
"to": "$a", "to_terminal": a1["uuid"]}])
|
||||
self.assertFalse(r["ok"])
|
||||
self.assertTrue(r["stopped_early"])
|
||||
self.assertIn("no terminal", json.dumps(r["operations"][1]))
|
||||
|
||||
def test_noop_edit_has_no_conductor_churn(self):
|
||||
"""Re-saving renumbers the file's terminal ids; the diff must not
|
||||
report the untouched conductors as removed and re-added."""
|
||||
|
||||
Reference in New Issue
Block a user