From d952fb9a0dfb1778402d8a5da037bfb0d32fb48b Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 3 Oct 2026 13:41:25 +1300 Subject: [PATCH] qet-mcp: place_element takes an angle, for symbols turned along a rung Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD --- misc/qet-mcp/qet_mcp.py | 16 ++++++++++------ misc/qet-mcp/test_qet_mcp.py | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index f3c672f98..5af3a5c34 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -1535,7 +1535,8 @@ OPS = { "place_element": ("qetMcpPlace", [("folio", "folio"), ("path", "str"), ("terminal", "anyterm"), ("next_to", "elmt"), ("next_to_terminal", "anyterm"), - ("side", "str"), ("gap", "num")]), + ("side", "str"), ("gap", "num"), + ("angle", "num")]), "align_terminal": ("qetMcpAlignTerminal", [("folio", "folio"), ("element", "elmt"), ("terminal", "anyterm"), ("to", "elmt"), ("to_terminal", "anyterm")]), @@ -1545,7 +1546,7 @@ OPS = { OP_DEFAULTS = { "align_elements": {"to": ""}, "distribute_elements": {"pitch": 0}, - "place_element": {"side": "", "gap": 40}, + "place_element": {"side": "", "gap": 40, "angle": 0}, } ALIGN_EDGES = ["left", "center", "right", "top", "middle", "bottom"] DISTRIBUTE_AXES = ["horizontal", "vertical"] @@ -1555,7 +1556,7 @@ _HELPER_NEEDS = { "qetMcpAlign": {"elementGeometry", "moveElement"}, "qetMcpDistribute": {"elementGeometry", "moveElement"}, "qetMcpPlace": {"elementGeometry", "addElement", "deleteElement", "moveElement", - "terminalPosition", "terminalIndex"}, + "rotateElement", "terminalPosition", "terminalIndex"}, "qetMcpAlignTerminal": {"moveElement", "terminalPosition", "terminalIndex"}, } @@ -1752,13 +1753,15 @@ def _build_script(operations: list, output: str) -> str: "var qetMcpStep = {below: [0, 1], above: [0, -1], right: [1, 0], left: [-1, 0]};", "var qetMcpFacingSide = {s: 'below', n: 'above', e: 'right', w: 'left'};", "var qetMcpBack = {below: 'n', above: 's', right: 'w', left: 'e'};", - "function qetMcpPlace(folio, path, term, near, nearTerm, side, gap) {", + "function qetMcpPlace(folio, path, term, near, nearTerm, side, gap, angle) {", " var g = qet.elementGeometry(folio, near);", " if (g.left === undefined) { qetMcpNote('no element ' + near); return ''; }", " var to = qet.terminalPosition(folio, near, qetMcpTerm(folio, near, nearTerm));", " if (to.x === undefined) { qetMcpNote('next_to_terminal ' + nearTerm + ' is not a terminal of ' + near); return ''; }", " var el = qet.addElement(folio, path, g.x, g.y);", " if (!el) return '';", + # turned first, so the terminal is measured where it ends up + " if (angle && !qet.rotateElement(folio, el, angle)) { qet.deleteElement(folio, el); return ''; }", " var mine = qet.terminalPosition(folio, el, qetMcpTerm(folio, el, term));", " if (mine.x === undefined) {", " qet.deleteElement(folio, el);", @@ -3774,8 +3777,9 @@ TOOLS = [ "straight only when its two terminals are exactly in line. " "place_element (folio, path, terminal, next_to, " "next_to_terminal, optional side below|above|right|left -- " - "default the way next_to_terminal faces -- and gap, 40 px " - "terminal to terminal) adds a symbol with its terminal in line " + "default the way next_to_terminal faces -- gap, 40 px " + "terminal to terminal, and angle, a rotation applied first, as " + "for a ladder rung) adds a symbol with its terminal in line " "with next_to's, and notes when that terminal faces the wrong " "way; align_terminal (folio, element, terminal, to, " "to_terminal) moves a placed symbol across so the two are in " diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index 8019d85dd..bb298383b 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -1353,7 +1353,7 @@ class LayoutOpsValidation(unittest.TestCase): s = m._build_script([{**base, "terminal": "{11111111-2222-3333-4444-555555555555}"}], "/tmp/x.qet") self.assertIn('qetMcpPlace(0, "common://x.elmt", "{11111111-2222-3333-4444-555555555555}", ' - f'"{self.A}", 1, "", 40)', s) + f'"{self.A}", 1, "", 40, 0)', s) def test_helpers_are_called_bare_and_needs_follow_use(self): place = m._build_script([{"op": "align_terminal", "folio": 0, "element": self.A, @@ -5398,6 +5398,20 @@ class LayoutOpsIntegration(unittest.TestCase): self.assertEqual(self.xy(r["output"])[placed], (100.0, 100 + 10 + 40 + 20)) self.assertNotIn("note", r["operations"][1]) + def test_place_element_turned_for_a_rung(self): + # NFPA: turned 270, the block's terminal 2 (its bottom one) faces + # east; the contact placed to its right, turned the same way, then + # meets it with terminal 0 (its top one), facing west + r = self.placed([self.add(0, 100, 100, TERMINAL), + {"op": "rotate_element", "folio": 0, "element": "$e0", "angle": 270}, + {"op": "place_element", "folio": 0, "path": SLAVE, "terminal": 0, + "next_to": "$e0", "next_to_terminal": 2, "angle": 270, "id": "k"}]) + self.need_positions(r) + self.assertTrue(r["ok"], r.get("hint")) + self.assertNotIn("note", r["operations"][2], r["operations"][2]) + x, y = self.xy(r["output"])[r["operations"][2]["result"]] + self.assertEqual(y, 100.0) + def test_place_element_warns_about_a_terminal_facing_away(self): r = self.placed([self.add(0, 100, 100, TERMINAL), {"op": "place_element", "folio": 0, "path": SLAVE, "terminal": 0,