mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 22:24:13 +02:00
Merge pull request #1108 from ispyisail/feat/qet-mcp-conductor-uuid
Let qet_edit name a wire by its uuid
This commit is contained in:
@@ -290,6 +290,18 @@ Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
`index` also takes that uuid, which does not shift the way an index
|
||||
does. `qet_element_build` gives every part of a symbol a uuid as well,
|
||||
returned in `part_uuids`; `qet_element_info` lists them in `part_list`.
|
||||
- **A conductor can be named by its uuid** (`qet_conductors` reports it):
|
||||
`set_conductor`, `move_conductor_segment` and `delete_conductor` take
|
||||
`"conductor": "{uuid}"` in place of `element` + `terminal`, which works
|
||||
where two conductors meet at a terminal. It is turned at run time into an
|
||||
end whose terminal carries only that conductor; where both of its ends
|
||||
are shared the op fails and its `note` says why. Needs
|
||||
`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).
|
||||
- **`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*
|
||||
|
||||
+72
-3
@@ -1354,6 +1354,10 @@ SEARCH_REPLACE_KINDS = ["element_info", "conductor", "text"]
|
||||
FOLIO_PROPERTIES = ["title", "author", "filename", "plant", "locmach",
|
||||
"indexrev", "folio", "template"]
|
||||
|
||||
# The ops that address one conductor by element + terminal, and so also
|
||||
# take "conductor": "{uuid}" (qet_conductors reports each one's uuid).
|
||||
CONDUCTOR_UUID_OPS = ("set_conductor", "move_conductor_segment", "delete_conductor")
|
||||
|
||||
# Accepted by set_conductor. The names are the project file's own, so what
|
||||
# a script sets is what qet_conductors reports back.
|
||||
CONDUCTOR_PROPERTIES = ["num", "formula", "function", "bus", "cable",
|
||||
@@ -1405,6 +1409,35 @@ def _build_script(operations: list, output: str) -> str:
|
||||
f"qet.log({_js(_MARKER)} + JSON.stringify("
|
||||
"{kind: 'capabilities', missing: missing}));",
|
||||
"var stop = false;",
|
||||
# A conductor named by uuid is turned into one of its ends, as the
|
||||
# conductor calls take it: an end whose terminal carries no other
|
||||
# conductor, so the call cannot pick the wrong one. null, with the
|
||||
# reason logged, if it is not on the folio or both ends are shared.
|
||||
"function qetMcpConductorEnd(index, folio, uuid) {",
|
||||
" var ends = qet.conductorEnds(folio, uuid);",
|
||||
" var why = 'no conductor ' + uuid + ' on folio ' + folio;",
|
||||
" if (ends && ends.length === 2) {",
|
||||
" var lines = qet.conductors(folio);",
|
||||
" for (var k = 0; k < 2; k++) {",
|
||||
" if (ends[k] === '?') continue;",
|
||||
" var n = 0;",
|
||||
" for (var j = 0; j < lines.length; j++) {",
|
||||
" var p = lines[j].split(' : ')[0].split(' -- ');",
|
||||
" if (p[0] === ends[k] || p[1] === ends[k]) n++;",
|
||||
" }",
|
||||
" if (n === 1) {",
|
||||
" var m = ends[k].split(' terminal ');",
|
||||
" return {element: m[0], terminal: parseInt(m[1], 10)};",
|
||||
" }",
|
||||
" }",
|
||||
" why = 'conductor ' + uuid + ' shares both of its terminals with other '",
|
||||
" + 'conductors; the conductor calls address one by a terminal carrying '",
|
||||
" + 'only it';",
|
||||
" }",
|
||||
f" qet.log({_js(_MARKER)} + JSON.stringify("
|
||||
"{kind: 'op_note', index: index, note: why}));",
|
||||
" return null;",
|
||||
"}",
|
||||
"if (missing.length === 0) {",
|
||||
]
|
||||
|
||||
@@ -1561,6 +1594,25 @@ def _build_script(operations: list, output: str) -> str:
|
||||
if name == "add_shape" and op.get("shape") not in SHAPES:
|
||||
raise ValueError(f"operation {i}: unknown shape {op.get('shape')!r}; "
|
||||
f"expected one of {', '.join(SHAPES)}")
|
||||
# The conductor ops take "conductor": "{uuid}" in place of element +
|
||||
# terminal: a uuid names one conductor for good, where a terminal
|
||||
# can carry several.
|
||||
conductor_js = None
|
||||
if name in CONDUCTOR_UUID_OPS and "conductor" in op:
|
||||
if "element" in op or "terminal" in op:
|
||||
raise ValueError(f"operation {i} ({name}): give either \"conductor\" "
|
||||
f"(its uuid) or \"element\" + \"terminal\", not both")
|
||||
if op["conductor"] == "":
|
||||
raise ValueError(f"operation {i}: \"conductor\" is empty -- a conductor "
|
||||
f"from a project saved before conductors carried a uuid "
|
||||
f"has none in the file; name it by \"element\" + "
|
||||
f"\"terminal\" instead")
|
||||
if not isinstance(op["conductor"], str) or not _UUID_RE.fullmatch(op["conductor"]):
|
||||
raise ValueError(f"operation {i}: \"conductor\" must be a conductor uuid, "
|
||||
f"got {op['conductor']!r}")
|
||||
conductor_js = _js(op["conductor"])
|
||||
uuid_methods.add("conductorEnds")
|
||||
op = {**op, "element": "{00000000-0000-0000-0000-000000000000}", "terminal": 0}
|
||||
args = []
|
||||
for key, kind in spec:
|
||||
if key not in op:
|
||||
@@ -1568,6 +1620,8 @@ def _build_script(operations: list, output: str) -> str:
|
||||
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))
|
||||
if conductor_js is not None:
|
||||
args[1], args[2] = f"e{i}.element", f"e{i}.terminal"
|
||||
|
||||
ident = op.get("id")
|
||||
if ident is not None:
|
||||
@@ -1579,6 +1633,9 @@ def _build_script(operations: list, output: str) -> str:
|
||||
|
||||
call = "qet.addFolio()" if method is None else f"qet.{method}({', '.join(args)})"
|
||||
lines.append(" if (!stop) {")
|
||||
if conductor_js is not None:
|
||||
lines.append(f" var e{i} = qetMcpConductorEnd({i}, {args[0]}, {conductor_js});")
|
||||
call = f"(e{i} ? {call} : false)"
|
||||
lines.append(f" var v{i} = {call};")
|
||||
if ident is not None:
|
||||
lines.append(f" R[{_js(ident)}] = v{i};")
|
||||
@@ -1624,7 +1681,7 @@ def _parse_script_output(text: str) -> dict:
|
||||
the cost is nothing and the failure it prevents is silent (an edit that
|
||||
worked, reported as having run no operations at all, which is what the
|
||||
first version of this tool did)."""
|
||||
caps, ops, saved, stopped = None, [], None, False
|
||||
caps, ops, saved, stopped, notes = None, [], None, False, {}
|
||||
for line in text.splitlines():
|
||||
idx = line.find(_MARKER)
|
||||
if idx < 0:
|
||||
@@ -1643,9 +1700,14 @@ def _parse_script_output(text: str) -> dict:
|
||||
rec["succeeded"] = not (r is None or r is False or r == "" or r == [] or r == {} or
|
||||
(isinstance(r, int) and not isinstance(r, bool) and r == -1))
|
||||
ops.append(rec)
|
||||
elif rec.get("kind") == "op_note":
|
||||
notes[rec.get("index")] = rec.get("note")
|
||||
elif rec.get("kind") == "save":
|
||||
saved = bool(rec.get("result"))
|
||||
stopped = bool(rec.get("stopped_early"))
|
||||
for rec in ops:
|
||||
if rec.get("index") in notes:
|
||||
rec["note"] = notes[rec["index"]]
|
||||
return {"missing_methods": caps, "operations": ops, "saved": saved,
|
||||
"stopped_early": stopped}
|
||||
|
||||
@@ -2491,8 +2553,15 @@ TOOLS = [
|
||||
"index in the element definition; qet_element_info lists them. "
|
||||
"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; "
|
||||
"its \"property\" is one of " + ", ".join(CONDUCTOR_PROPERTIES) +
|
||||
"potential, so name a terminal carrying exactly one conductor, "
|
||||
"or give \"conductor\": its uuid from qet_conductors in place "
|
||||
"of \"element\" + \"terminal\" (set_conductor, "
|
||||
"move_conductor_segment and delete_conductor all accept it; it "
|
||||
"needs one of the conductor's two terminals to carry only it, "
|
||||
"and a conductor qet_conductors lists with an empty uuid has "
|
||||
"none to give). A uuid names one wire, but set_conductor still "
|
||||
"changes its whole potential, as it does by terminal. "
|
||||
"set_conductor's \"property\" is one of " + ", ".join(CONDUCTOR_PROPERTIES) +
|
||||
". move_conductor_segment reroutes the drawn path itself rather "
|
||||
"than a property of the potential -- addressed the same way (a "
|
||||
"terminal carrying exactly one conductor), plus a segment index "
|
||||
|
||||
@@ -194,6 +194,34 @@ class EditValidation(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "index or its uuid"):
|
||||
self.build([op])
|
||||
|
||||
def test_conductor_by_uuid(self):
|
||||
"""A conductor named by uuid is turned into one of its ends at run
|
||||
time; the lookup is required only then, and "conductor" cannot be
|
||||
given alongside element + terminal."""
|
||||
U = "{11111111-2222-4333-8444-555555555555}"
|
||||
s = self.build([{"op": "delete_conductor", "folio": 1, "conductor": U}])
|
||||
self.assertIn(f'var e0 = qetMcpConductorEnd(0, 1, "{U}");', s)
|
||||
self.assertIn("(e0 ? qet.deleteConductor(1, e0.element, e0.terminal) : false)", s)
|
||||
self.assertIn('"conductorEnds"', s)
|
||||
s = self.build([{"op": "set_conductor", "folio": 0, "conductor": U,
|
||||
"property": "num", "value": "W1"}])
|
||||
self.assertIn('qet.setConductorProperty(0, e0.element, e0.terminal, "num", "W1")', s)
|
||||
s = self.build([{"op": "move_conductor_segment", "folio": 2, "conductor": U,
|
||||
"segment": 1, "dx": 10, "dy": 0}])
|
||||
self.assertIn(f'var e0 = qetMcpConductorEnd(0, 2, "{U}");', s)
|
||||
self.assertIn("(e0 ? qet.moveConductorSegment(2, e0.element, e0.terminal, 1, 10, 0) : false)", s)
|
||||
# an end whose terminal or element is missing is never picked
|
||||
self.assertIn("if (ends[k] === '?') continue;", s)
|
||||
self.assertNotIn('"conductorEnds"', self.build(
|
||||
[{"op": "delete_conductor", "folio": 0, "element": U, "terminal": 0}]))
|
||||
with self.assertRaisesRegex(ValueError, "not both"):
|
||||
self.build([{"op": "delete_conductor", "folio": 0, "conductor": U,
|
||||
"element": U, "terminal": 0}])
|
||||
with self.assertRaisesRegex(ValueError, "must be a conductor uuid"):
|
||||
self.build([{"op": "delete_conductor", "folio": 0, "conductor": "W1"}])
|
||||
with self.assertRaisesRegex(ValueError, "saved before conductors carried a uuid"):
|
||||
self.build([{"op": "delete_conductor", "folio": 0, "conductor": ""}])
|
||||
|
||||
def test_every_op_generates_a_script(self):
|
||||
# one minimal valid instance of every op
|
||||
f = {"op": "add_folio", "id": "f"}
|
||||
@@ -2379,6 +2407,40 @@ class Integration(unittest.TestCase):
|
||||
r = self.ok(self.sb.edit(base, ops))
|
||||
self.assertEqual([c["num"] for c in m.tool_conductors(r["output"])["conductors"]], ["W7"])
|
||||
|
||||
def _hub(self):
|
||||
"""e0's terminal 0 carries two conductors, to e1 and to e2. Returns
|
||||
the saved project and the two conductors' uuids."""
|
||||
base = self.sb.new()
|
||||
r = self.ok(self.sb.edit(base, [
|
||||
{"op": "add_folio", "id": "f"},
|
||||
*[{"op": "add_element", "id": f"e{i}", "folio": "$f", "path": COIL, "x": 100 + i * 200, "y": 100}
|
||||
for i in range(3)],
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e0", "from_terminal": 0, "to": "$e1", "to_terminal": 0},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e0", "from_terminal": 0, "to": "$e2", "to_terminal": 0}]))
|
||||
uuids = [c["uuid"] for c in m.tool_conductors(r["output"])["conductors"]]
|
||||
self.assertEqual(len(uuids), 2)
|
||||
self.assertTrue(all(uuids), "new conductors carry a saved uuid")
|
||||
return r["output"], uuids
|
||||
|
||||
def test_conductor_by_uuid_where_two_meet_at_a_terminal(self):
|
||||
"""The terminal e0/0 carries both conductors, which element + terminal
|
||||
cannot name; each uuid names one, whichever it is."""
|
||||
project, uuids = self._hub()
|
||||
for gone, kept in ((uuids[0], uuids[1]), (uuids[1], uuids[0])):
|
||||
with self.subTest(deleted=gone):
|
||||
r = self.ok(self.sb.edit(project, [
|
||||
{"op": "delete_conductor", "folio": 1, "conductor": gone}], out="out2.qet"))
|
||||
left = [c["uuid"] for c in m.tool_conductors(r["output"])["conductors"]]
|
||||
self.assertEqual(left, [kept])
|
||||
|
||||
def test_unknown_conductor_uuid_is_reported(self):
|
||||
project, _ = self._hub()
|
||||
r = self.sb.edit(project, [{"op": "delete_conductor", "folio": 1,
|
||||
"conductor": "{11111111-2222-4333-8444-555555555555}"}],
|
||||
out="out2.qet")
|
||||
self.assertFalse(r["ok"])
|
||||
self.assertIn("no conductor", r["operations"][0].get("note", ""))
|
||||
|
||||
# ---- cross references, strips, folios ----
|
||||
|
||||
def test_cross_reference_across_folios(self):
|
||||
|
||||
Reference in New Issue
Block a user