qet-mcp: place_element takes an angle, for symbols turned along a rung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
This commit is contained in:
ispyisail
2026-10-03 13:41:25 +13:00
parent 372e541a00
commit d952fb9a0d
2 changed files with 25 additions and 7 deletions
+10 -6
View File
@@ -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 "
+15 -1
View File
@@ -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,