← Commit history

v0.1.3: symbol quality pass (name-length-aware modes, hidden params, remove_symbol)

John Lauer ·9fea3d6c5a ·1mo ago ·parent 32b8014
6 files changed +82−59
CHANGELOG.md+5
@@ -1,5 +1,10 @@ # Changelog +## 0.1.3 — 2026-08-30+- Symbol quality pass (John caught colliding 4-side names): tools/symbol_modes.py sizes bodies from measured pin-name lengths, std = CCW datasheet wrap (distinct from 2-column), die outline is a centred square skipped when it cannot fit; all ten modes frame-checked.+- set_params/add_models parameters are hidden metadata (visible ones piled over the symbol).+- New verb: altium_remove_symbol.+ ## 0.1.2 — 2026-08-30 - LAN7800 creative-import lane: build_footprint (spec from tools/kicad2altium.py: via pads by duplicate designator, roundrect %, named mech layers incl. Adom.SolderBalls), build_symbol_modes (10 display modes on one component) + symbol_mode (editor-driven), add_models (N footprint models + params), normalize_bodies, test_board, board_shape (PCB:PlaceBoardOutline), swap_footprint (nets kept by designator), pcb_report with pads/bodies/nets/tracks. - Dialog pre-emption: ReadAll probes with FileOpen (the debugger breaks on any exception), TxClear on agent start / before save / after recover (dangling PreProcess = "a command is currently active").
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.2",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.2/adom-bridge-altium-v0.1.2.zip",-  "sha256": "5ce93c63c370684119225d35461d5764cfec30232a8603859896428c6836a49d",-  "size": 89364,+  "version": "0.1.3",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.3/adom-bridge-altium-v0.1.3.zip",+  "sha256": "18d44c9c7c518464d0ba9a3e3dd2feeb9458fb02228caaf4df3a4837b1382cbc",+  "size": 89586,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.3.zipadded
⋯ 1 unchanged line ⋯
bridge.json+5−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.2",+  "version": "0.1.3",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",@@ -124,6 +124,10 @@     {       "name": "altium_place_on_pcb",       "description": "Place a library footprint on a fresh board"+    },+    {+      "name": "altium_remove_symbol",+      "description": "Remove a component from a SchLib by LibReference"     }   ],   "statusVerb": "altium_status",
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.1.2",+  "version": "0.1.3",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [
tools/symbol_modes.py+67−53
@@ -1,19 +1,18 @@ #!/usr/bin/env python3-"""symbol_modes.py - emit ONE Altium symbol with N display modes (Altium's-alternate-symbol mechanism) from a KiCad symbol's pin table.+"""symbol_modes.py v2 - ONE Altium symbol, N display modes, with geometry sized+from the actual pin-name lengths so names never collide (ralph-tested per mode). -Spec lines (pipe-separated, units mils, Y up; the agent's build_symbol_modes reads it):-  META|libref|description|designator-  MODE|index|name-  RECT|mode|x1|y1|x2|y2-  PIN|mode|number|name|x|y|orient|length          orient: left|right|up|down = side the pin sits on-  LINE|mode|x1|y1|x2|y2-Looks (10): std, 2col, 4side, 2col-grouped, 4side-grouped, each x {plain, chip} (chip = a-thumbnail outline: rounded body + pin-1 dot drawn as lines inside the rectangle).+Rules (mils): pitch 100, pin length 300, char width 55 (10pt default font),+name gap 40 from the body edge, 2-col body width = longest left name ++longest right name + 200 middle gap; 4-side body side = max(pins-per-side span,+longest top/bottom name + longest opposite + 200). Chip-outline modes draw the+die in the measured free middle and skip it when the middle is under 300 mil.+Spec lines: META|libref|desc|designator  MODE|i|name  RECT|mode|x1|y1|x2|y2+PIN|mode|number|name|x|y|orient|len  LINE|mode|x1|y1|x2|y2 """ import re, sys, json -GROUPS = [  # order matters: grouped looks lay pins out in this order+GROUPS = [     ("Ethernet", lambda n: n.startswith("TR") or n.startswith("REF_")),     ("USB", lambda n: n.startswith("USB")),     ("Crystal", lambda n: n in ("XI", "XO")),@@ -21,6 +20,7 @@ GROUPS = [  # order matters: grouped looks lay pins out in this order     ("Config", lambda n: n.startswith("EE") or "GPIO" in n or n.startswith("LED")),     ("Power", lambda n: n.startswith("VDD") or n == "VSS"), ]+PITCH, PLEN, CW, GAP_EDGE, GAP_MID = 100, 300, 55, 40, 200  def parse_pins(path):     s = open(path).read()@@ -37,47 +37,45 @@ def group_of(name):         if f(name): return g     return "Other" -PITCH = 100      # mils-PLEN = 300+def wname(p): return GAP_EDGE + CW * len(p["name"]) -def two_column(pins, grouped):-    """Left: first half, right: second half (std/2col). Grouped: groups stacked, a gap between."""+def grid(v): return int(round(v / 100.0) * 100)++def two_column(pins, grouped, chip=False, wrap=False):     if grouped:         order = []         for g, _ in GROUPS:             order.append([p for p in pins if group_of(p["name"]) == g])         order.append([p for p in pins if group_of(p["name"]) == "Other"])         order = [o for o in order if o]-        # split groups across the two sides by balancing counts         left, right = [], []         for grp in order:             (left if sum(len(x) for x in left) <= sum(len(x) for x in right) else right).append(grp)         def stack(cols):-            out, y = [], 0+            out, y = [], -PITCH             for grp in cols:                 for p in grp:                     out.append((p, y)); y -= PITCH-                y -= PITCH          # gap between groups+                y -= PITCH             return out         L, R = stack(left), stack(right)     else:         half = (len(pins) + 1) // 2-        L = [(p, -i * PITCH) for i, p in enumerate(pins[:half])]-        R = [(p, -i * PITCH) for i, p in enumerate(pins[half:])]-    h = max([-y for _, y in L] + [-y for _, y in R]) + PITCH-    W = 1400-    out = [("RECT", 0, 0, -h, W, 0)]  # placeholder; we normalize later-    body = {"w": W, "h": h}-    res = []-    for p, y in L:-        res.append(("PIN", p["num"], p["name"], -PLEN, y - PITCH // 2, "left"))-    for p, y in R:-        res.append(("PIN", p["num"], p["name"], W + PLEN, y - PITCH // 2, "right"))-    return body, res+        L = [(p, -(i + 1) * PITCH) for i, p in enumerate(pins[:half])]+        rp = pins[half:]+        if wrap:                                      # CCW datasheet wrap: right side bottom->top+            R = [(p, -(len(rp) - i) * PITCH) for i, p in enumerate(rp)]+        else:+            R = [(p, -(i + 1) * PITCH) for i, p in enumerate(rp)]+    H = max([-y for _, y in L] + [-y for _, y in R]) + PITCH+    lw = max([wname(p) for p, _ in L] or [0]); rw = max([wname(p) for p, _ in R] or [0])+    W = grid(lw + rw + GAP_MID + (500 if chip else 0))+    res = [("PIN", p["num"], p["name"], -PLEN, y, "left") for p, y in L]+    res += [("PIN", p["num"], p["name"], W + PLEN, y, "right") for p, y in R]+    free = (lw, -H + PITCH, W - rw, -PITCH)          # middle clear area (x1,y1,x2,y2)+    return {"w": W, "h": H, "free": free}, res -def four_side(pins, grouped):-    """Square: pins distributed on 4 sides, counter-clockwise from top-left like the package-    (left top->bottom, bottom left->right, right bottom->top, top right->left)."""+def four_side(pins, grouped, chip=False):     if grouped:         seq = []         for g, _ in GROUPS:@@ -87,43 +85,59 @@ def four_side(pins, grouped):         seq = pins     n = len(seq); per = (n + 3) // 4     sides = [seq[i * per:(i + 1) * per] for i in range(4)]-    S = (per + 1) * PITCH+    span = (per + 1) * PITCH+    lw = max([wname(p) for p in sides[0]] or [0]); rw = max([wname(p) for p in sides[2]] or [0])+    bw = max([wname(p) for p in sides[1]] or [0]); tw = max([wname(p) for p in sides[3]] or [0])+    S = grid(max(span, lw + rw + GAP_MID, bw + tw + GAP_MID) + (600 if chip else 0))     res = []-    for i, p in enumerate(sides[0]):   # left, top->bottom-        res.append(("PIN", p["num"], p["name"], -PLEN, -(i + 1) * PITCH, "left"))-    for i, p in enumerate(sides[1]):   # bottom, left->right-        res.append(("PIN", p["num"], p["name"], (i + 1) * PITCH, -S - PLEN, "down"))-    for i, p in enumerate(sides[2]):   # right, bottom->top-        res.append(("PIN", p["num"], p["name"], S + PLEN, -S + (i + 1) * PITCH, "right"))-    for i, p in enumerate(sides[3]):   # top, right->left-        res.append(("PIN", p["num"], p["name"], S - (i + 1) * PITCH, PLEN, "up"))-    return {"w": S, "h": S}, res+    def centered(k):                                  # start offset to centre k pins on a side+        return grid((S - (k - 1) * PITCH) / 2.0)+    o = centered(len(sides[0]))+    for i, p in enumerate(sides[0]):                  # left, top->bottom+        res.append(("PIN", p["num"], p["name"], -PLEN, -(o + i * PITCH), "left"))+    o = centered(len(sides[1]))+    for i, p in enumerate(sides[1]):                  # bottom, left->right+        res.append(("PIN", p["num"], p["name"], o + i * PITCH, -S - PLEN, "down"))+    o = centered(len(sides[2]))+    for i, p in enumerate(sides[2]):                  # right, bottom->top+        res.append(("PIN", p["num"], p["name"], S + PLEN, -S + (o + i * PITCH), "right"))+    o = centered(len(sides[3]))+    for i, p in enumerate(sides[3]):                  # top, right->left+        res.append(("PIN", p["num"], p["name"], S - (o + i * PITCH), PLEN, "up"))+    free = (lw, -S + bw, S - rw, -tw)                 # middle clear of every name run+    return {"w": S, "h": S, "free": free}, res  def chip_lines(body):-    """Thumbnail outline inside the rectangle: a rounded die with a pin-1 dot (as a small box)."""-    w, h = body["w"], body["h"]-    m = 120-    x1, y1, x2, y2 = m, -h + m, w - m, -m+    x1, y1, x2, y2 = body["free"]+    x1 += 60; y1 += 60; x2 -= 60; y2 -= 60+    if x2 - x1 < 300 or y2 - y1 < 300:+        return []                                     # no room: skip rather than collide+    side = min(x2 - x1, y2 - y1, 1200)                # a die is square, not a sliver+    cx, cy = (x1 + x2) / 2.0, (y1 + y2) / 2.0+    x1, x2 = cx - side / 2, cx + side / 2+    y1, y2 = cy - side / 2, cy + side / 2     L = [(x1, y1, x2, y1), (x2, y1, x2, y2), (x2, y2, x1, y2), (x1, y2, x1, y1)]-    d = 60; px, py = x1 + 100, y2 - 100   # pin-1 dot top-left-    L += [(px - d, py - d, px + d, py - d), (px + d, py - d, px + d, py + d), (px + d, py + d, px - d, py + d), (px - d, py + d, px - d, py - d)]+    d = 50; px, py = x1 + 120, y2 - 120+    L += [(px - d, py - d, px + d, py - d), (px + d, py - d, px + d, py + d),+          (px + d, py + d, px - d, py + d), (px - d, py + d, px - d, py - d)]     return L  def build(pins, libref, desc, out):-    looks = [("std", two_column, False), ("2col", two_column, False), ("4side", four_side, False),+    looks = [("std", lambda p, g, c: two_column(p, g, c, wrap=True), False),+             ("2col", two_column, False), ("4side", four_side, False),              ("2col-grouped", two_column, True), ("4side-grouped", four_side, True)]     lines = [f"META|{libref}|{desc}|U"]     mode = 0     for name, fn, grouped in looks:         for chip in (False, True):-            body, pins_out = fn(pins, grouped)+            body, pins_out = fn(pins, grouped, chip)             lines.append(f"MODE|{mode}|{name}{'-chip' if chip else ''}")             lines.append(f"RECT|{mode}|0|{-body['h']}|{body['w']}|0")             for _, num, pname, x, y, orient in pins_out:-                lines.append(f"PIN|{mode}|{num}|{pname}|{x}|{y}|{orient}|{PLEN}")+                lines.append(f"PIN|{mode}|{num}|{pname}|{int(x)}|{int(y)}|{orient}|{PLEN}")             if chip:                 for (a, b, c, d) in chip_lines(body):-                    lines.append(f"LINE|{mode}|{a}|{b}|{c}|{d}")+                    lines.append(f"LINE|{mode}|{int(a)}|{int(b)}|{int(c)}|{int(d)}")             mode += 1     open(out, "w").write("\n".join(lines) + "\n")     return mode, len(lines)