← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.105+1.9.106
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.105",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.120/adom-bridge-fusion-v1.9.105.zip",-    "sha256": "766da0cbb4887b49a9388d2f7b3ff8a1038d769bb05fb7e972530e0d2a5bf4f6",-    "size": 441042,+    "version": "1.9.106",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.121/adom-bridge-fusion-v1.9.106.zip",+    "sha256": "3e0fe6e2fc817a76a2c2e62f02c0201fd98f1bba0ea9e3496b043e1865c90c64",+    "size": 448670,     "verbPrefixes": [         "fusion_"     ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.105",+  "version": "1.9.106",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.120"+version = "1.9.121" edition = "2021"  [dependencies]
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.120"+DASH_VERSION="1.9.121" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
kicad_lbr.pyadded+438
@@ -0,0 +1,438 @@+"""KiCad -> EAGLE .lbr conversion (page issue #43).++At Adom, KiCad format is the FUNDAMENTAL representation for symbols and footprints - every+part page carries .kicad_sym / .kicad_mod as first-class artifacts, and the .lbr is DERIVED.+This bridge already ingests one foreign format (STEP -> f3d via bind_library_3d), so refusing+the ecosystem's native symbol/footprint format was an arbitrary line. ab argued it, John+confirmed it, and the concrete blocker was real: adom/cr0603-fx-5601elf shipped a .lbr with an+EMPTY <packages> element while a perfectly good .kicad_mod sat beside it.++Two entry points:++    convert(kicad_sym_path, kicad_mod_path) -> {"symbol": xml, "package": xml, ...}+    patch_lbr(lbr_path, kicad_mod_path=..., kicad_sym_path=...) -> patched .lbr text++`patch_lbr` is the important one: given a half-generated library (empty <packages>, empty+<connects/>), it fills in the missing pieces from the KiCad sources instead of refusing.++Geometry notes that matter (learned from the KiCad/EAGLE docs, verified on the CR0603):+  - KiCad footprint Y axis points DOWN; EAGLE Y points UP. Every Y is negated.+  - KiCad (at x y [rot]) rotation is degrees CCW in symbol space; EAGLE spells it R<deg>.+  - Layers: F.Cu->1  B.Cu->16  F.SilkS->21  B.SilkS->22  F.Fab->51  B.Fab->52+            F.CrtYd->39  B.CrtYd->40  F.Mask->29  B.Mask->30  F.Paste->31  B.Paste->32+            Edge.Cuts->20  (unknown layers land on 51 rather than being dropped)+  - roundrect_rratio 0.25 -> EAGLE smd roundness="25" (percent).+Pure stdlib, no Fusion required: the conversion is text -> text, so it is unit-testable+offline and the browser client can rely on the bridge doing it.+"""++import re++# ── s-expression parser (tiny, sufficient for kicad_sym / kicad_mod) ────────────────────+++def _tokenize(text):+    i, n = 0, len(text)+    while i < n:+        c = text[i]+        if c.isspace():+            i += 1+        elif c in "()":+            yield c+            i += 1+        elif c == '"':+            j = i + 1+            buf = []+            while j < n:+                if text[j] == "\\" and j + 1 < n:+                    buf.append(text[j + 1]); j += 2; continue+                if text[j] == '"':+                    break+                buf.append(text[j]); j += 1+            yield ("STR", "".join(buf))+            i = j + 1+        else:+            j = i+            while j < n and not text[j].isspace() and text[j] not in "()":+                j += 1+            yield ("ATOM", text[i:j])+            i = j+++def parse_sexpr(text):+    """Parse into nested lists; atoms are str, quoted strings are str too."""+    stack = [[]]+    for tok in _tokenize(text):+        if tok == "(":+            stack.append([])+        elif tok == ")":+            done = stack.pop()+            stack[-1].append(done)+        else:+            stack[-1].append(tok[1])+    root = stack[0]+    return root[0] if len(root) == 1 else root+++def _find_all(node, head):+    """Direct children of `node` whose first element equals `head`."""+    return [c for c in node if isinstance(c, list) and c and c[0] == head]+++def _find(node, head):+    hits = _find_all(node, head)+    return hits[0] if hits else None+++def _num(v, default=0.0):+    try:+        return float(v)+    except Exception:+        return default+++def _fmt(x):+    """EAGLE-friendly number: no trailing zeros, no negative zero."""+    v = round(float(x) + 0.0, 4)+    if v == 0:+        v = 0.0+    s = ("%f" % v).rstrip("0").rstrip(".")+    return s if s else "0"+++_LAYER = {+    "F.Cu": 1, "B.Cu": 16, "F.SilkS": 21, "B.SilkS": 22, "F.Fab": 51, "B.Fab": 52,+    "F.CrtYd": 39, "B.CrtYd": 40, "F.Mask": 29, "B.Mask": 30, "F.Paste": 31, "B.Paste": 32,+    "Edge.Cuts": 20, "Dwgs.User": 47, "Cmts.User": 48,+}+++def _layer(name):+    return _LAYER.get(str(name), 51)+++def _esc(s):+    return (str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")+            .replace('"', "&quot;"))+++# ── footprint (.kicad_mod) -> <package> ─────────────────────────────────────────────────+++def footprint_to_package(text, name=None):+    """Convert a .kicad_mod to an EAGLE <package> XML string + pad name list."""+    root = parse_sexpr(text)+    if not (isinstance(root, list) and root and root[0] == "footprint"):+        raise ValueError("not a kicad footprint (expected (footprint ...))")+    fp_name = name or (root[1] if len(root) > 1 else "FOOTPRINT")+    out = []+    pads = []+    descr = _find(root, "descr")+    if descr and len(descr) > 1:+        out.append("<description>%s</description>" % _esc(descr[1]))++    for el in root:+        if not isinstance(el, list) or not el:+            continue+        head = el[0]+        if head == "fp_line":+            s, e = _find(el, "start"), _find(el, "end")+            st = _find(el, "stroke")+            w = _find(st, "width") if st else _find(el, "width")+            ly = _find(el, "layer")+            if s and e:+                out.append('<wire x1="%s" y1="%s" x2="%s" y2="%s" width="%s" layer="%d"/>' % (+                    _fmt(_num(s[1])), _fmt(-_num(s[2])), _fmt(_num(e[1])), _fmt(-_num(e[2])),+                    _fmt(_num(w[1], 0.1) if w else 0.1), _layer(ly[1] if ly else "F.SilkS")))+        elif head == "fp_rect":+            s, e = _find(el, "start"), _find(el, "end")+            st = _find(el, "stroke")+            w = _find(st, "width") if st else None+            ly = _find(el, "layer")+            if s and e:+                x1, y1 = _num(s[1]), -_num(s[2])+                x2, y2 = _num(e[1]), -_num(e[2])+                wd = _fmt(_num(w[1], 0.1) if w else 0.1)+                l = _layer(ly[1] if ly else "F.SilkS")+                # stroked rectangle -> 4 wires (EAGLE <rectangle> is filled copper)+                for (ax, ay, bx, by) in ((x1, y1, x2, y1), (x2, y1, x2, y2),+                                         (x2, y2, x1, y2), (x1, y2, x1, y1)):+                    out.append('<wire x1="%s" y1="%s" x2="%s" y2="%s" width="%s" layer="%d"/>'+                               % (_fmt(ax), _fmt(ay), _fmt(bx), _fmt(by), wd, l))+        elif head == "fp_circle":+            c, e = _find(el, "center"), _find(el, "end")+            st = _find(el, "stroke")+            w = _find(st, "width") if st else None+            ly = _find(el, "layer")+            if c and e:+                r = ((_num(e[1]) - _num(c[1])) ** 2 + (_num(e[2]) - _num(c[2])) ** 2) ** 0.5+                out.append('<circle x="%s" y="%s" radius="%s" width="%s" layer="%d"/>' % (+                    _fmt(_num(c[1])), _fmt(-_num(c[2])), _fmt(r),+                    _fmt(_num(w[1], 0.1) if w else 0.1), _layer(ly[1] if ly else "F.SilkS")))+        elif head == "fp_text":+            kind = el[1] if len(el) > 1 else ""+            at = _find(el, "at")+            ly = _find(el, "layer")+            x, y = (_num(at[1]), -_num(at[2])) if at else (0, 0)+            if kind == "reference":+                out.append('<text x="%s" y="%s" size="1" layer="25">&gt;NAME</text>' % (_fmt(x), _fmt(y)))+            elif kind == "value":+                out.append('<text x="%s" y="%s" size="1" layer="27">&gt;VALUE</text>' % (_fmt(x), _fmt(y)))+            else:+                txt = el[2] if len(el) > 2 and isinstance(el[2], str) else ""+                if txt:+                    out.append('<text x="%s" y="%s" size="1" layer="%d">%s</text>' % (+                        _fmt(x), _fmt(y), _layer(ly[1] if ly else "F.SilkS"), _esc(txt)))+        elif head == "pad":+            pname = el[1] if len(el) > 1 else ""+            ptype = el[2] if len(el) > 2 else ""+            shape = el[3] if len(el) > 3 else ""+            at = _find(el, "at")+            size = _find(el, "size")+            drill = _find(el, "drill")+            x, y = (_num(at[1]), -_num(at[2])) if at else (0, 0)+            rot = _num(at[3]) if (at and len(at) > 3 and not isinstance(at[3], list)) else 0+            rot_attr = (' rot="R%d"' % int(rot % 360)) if rot else ""+            if ptype == "smd":+                dx = _num(size[1], 1) if size else 1+                dy = _num(size[2], 1) if size else 1+                extra = ""+                rr = _find(el, "roundrect_rratio")+                if shape == "roundrect" and rr:+                    extra = ' roundness="%d"' % int(round(_num(rr[1], 0.25) * 100))+                out.append('<smd name="%s" x="%s" y="%s" dx="%s" dy="%s" layer="1"%s%s/>' % (+                    _esc(pname), _fmt(x), _fmt(y), _fmt(dx), _fmt(dy), extra, rot_attr))+                pads.append(str(pname))+            elif ptype in ("thru_hole", "np_thru_hole"):+                d = _num(drill[1], 0.8) if drill else 0.8+                if ptype == "np_thru_hole":+                    out.append('<hole x="%s" y="%s" drill="%s"/>' % (_fmt(x), _fmt(y), _fmt(d)))+                else:+                    dia = _num(size[1], d * 1.6) if size else d * 1.6+                    sh = {"circle": "round", "rect": "square", "oval": "long",+                          "roundrect": "round"}.get(shape, "round")+                    out.append('<pad name="%s" x="%s" y="%s" drill="%s" diameter="%s" shape="%s"%s/>' % (+                        _esc(pname), _fmt(x), _fmt(y), _fmt(d), _fmt(dia), sh, rot_attr))+                    pads.append(str(pname))+    xml = '<package name="%s">\n%s\n</package>' % (_esc(fp_name), "\n".join(out))+    return xml, pads, fp_name+++# ── symbol (.kicad_sym) -> <symbol> + pin list ──────────────────────────────────────────++_PIN_ROT = {0: "R0", 90: "R90", 180: "R180", 270: "R270"}+++def symbol_to_eagle(text, name=None):+    """Convert the FIRST symbol in a .kicad_sym to an EAGLE <symbol> XML + pin list.++    KiCad nests units as (symbol "NAME_0_1" ...) inside (symbol "NAME" ...); geometry and+    pins live in the units, so all units are walked.+    """+    root = parse_sexpr(text)+    lib = root if (isinstance(root, list) and root and root[0] == "kicad_symbol_lib") else None+    syms = _find_all(lib, "symbol") if lib else ([root] if root and root[0] == "symbol" else [])+    if not syms:+        raise ValueError("no (symbol ...) found in kicad_sym")+    top = syms[0]+    sym_name = name or top[1]+    units = [top] + _find_all(top, "symbol")+    out, pins = [], []+    for u in units:+        for el in u:+            if not isinstance(el, list) or not el:+                continue+            head = el[0]+            if head == "rectangle":+                s, e = _find(el, "start"), _find(el, "end")+                if s and e:+                    x1, y1, x2, y2 = _num(s[1]), _num(s[2]), _num(e[1]), _num(e[2])+                    for (ax, ay, bx, by) in ((x1, y1, x2, y1), (x2, y1, x2, y2),+                                             (x2, y2, x1, y2), (x1, y2, x1, y1)):+                        out.append('<wire x1="%s" y1="%s" x2="%s" y2="%s" width="0.254" layer="94"/>'+                                   % (_fmt(ax), _fmt(ay), _fmt(bx), _fmt(by)))+            elif head == "polyline":+                pts = _find(el, "pts")+                xy = _find_all(pts, "xy") if pts else []+                for a, b in zip(xy, xy[1:]):+                    out.append('<wire x1="%s" y1="%s" x2="%s" y2="%s" width="0.254" layer="94"/>'+                               % (_fmt(_num(a[1])), _fmt(_num(a[2])), _fmt(_num(b[1])), _fmt(_num(b[2]))))+            elif head == "circle":+                c = _find(el, "center")+                r = _find(el, "radius")+                if c and r:+                    out.append('<circle x="%s" y="%s" radius="%s" width="0.254" layer="94"/>'+                               % (_fmt(_num(c[1])), _fmt(_num(c[2])), _fmt(_num(r[1]))))+            elif head == "pin":+                at = _find(el, "at")+                ln = _find(el, "length")+                pn = _find(el, "name")+                pnum = _find(el, "number")+                x, y = (_num(at[1]), _num(at[2])) if at else (0, 0)+                rot = int(_num(at[3])) % 360 if (at and len(at) > 3) else 0+                nm = pn[1] if pn and len(pn) > 1 else (pnum[1] if pnum else "P")+                num = pnum[1] if pnum and len(pnum) > 1 else nm+                length = _num(ln[1], 2.54) if ln else 2.54+                lattr = "short" if length <= 2.6 else "middle"+                # KiCad pins point INTO the body from (at); EAGLE pins point out from their+                # origin - same anchor, same rotation vocabulary.+                out.append('<pin name="%s" x="%s" y="%s" length="%s" rot="%s"/>' % (+                    _esc(nm if nm != "~" else num), _fmt(x), _fmt(y), lattr,+                    _PIN_ROT.get(rot, "R0")))+                pins.append(str(num))+    out.append('<text x="0" y="1" size="1.778" layer="95">&gt;NAME</text>')+    out.append('<text x="0" y="-2" size="1.778" layer="96">&gt;VALUE</text>')+    xml = '<symbol name="%s">\n%s\n</symbol>' % (_esc(sym_name), "\n".join(out))+    return xml, pins, sym_name+++# ── the patch path: fill a half-generated .lbr from KiCad sources ───────────────────────+++def patch_lbr(lbr_text, kicad_mod_text=None, kicad_sym_text=None, name=None):+    """Fill missing pieces of an .lbr from KiCad sources. Returns (new_text, report).++    THE case from issue #43: <packages></packages> is empty while the .kicad_mod is fine.+    Also generates <connects> (pin->pad by matching names, falling back to order) when a+    deviceset exists with an empty <connects/>, because the ganged Library Manager view -+    John's evidence image #4 - shows pin connections, and an empty connects renders none.+    Existing content is NEVER overwritten: this adds what is absent, only.+    """+    report = {"addedPackage": False, "addedSymbol": False, "addedConnects": False,+              "packageName": None, "pads": [], "pins": []}+    text = lbr_text+    pads, pkg_name = [], None++    if kicad_mod_text and re.search(r"<packages>\s*</packages>", text):+        pkg_xml, pads, pkg_name = footprint_to_package(kicad_mod_text, name)+        text = re.sub(r"<packages>\s*</packages>",+                      "<packages>\n%s\n</packages>" % pkg_xml, text, count=1)+        report.update(addedPackage=True, packageName=pkg_name, pads=pads)++    if kicad_sym_text and re.search(r"<symbols>\s*</symbols>", text):+        sym_xml, pins, _ = symbol_to_eagle(kicad_sym_text, name)+        text = re.sub(r"<symbols>\s*</symbols>",+                      "<symbols>\n%s\n</symbols>" % sym_xml, text, count=1)+        report.update(addedSymbol=True, pins=pins)++    # connects: needs pins (from the lbr symbol or the kicad_sym) and pads (just added)+    if pads and ("<connects/>" in text or re.search(r"<connects>\s*</connects>", text)):+        pins = report["pins"] or re.findall(r'<pin name="([^"]+)"', text)+        gate = (re.findall(r'<gate name="([^"]+)"', text) or ["G$1"])[0]+        by_name = {p.lower(): p for p in pins}+        conns = []+        for i, pad in enumerate(pads):+            pin = by_name.get(str(pad).lower()) or (pins[i] if i < len(pins) else None)+            if pin:+                conns.append('<connect gate="%s" pin="%s" pad="%s"/>' % (+                    _esc(gate), _esc(pin), _esc(pad)))+        if conns:+            joined = "<connects>\n%s\n</connects>" % "\n".join(conns)+            text = text.replace("<connects/>", joined, 1) if "<connects/>" in text else \+                re.sub(r"<connects>\s*</connects>", joined, text, count=1)+            report["addedConnects"] = True+            report["pins"] = pins+    # a deviceset whose <devices> has a device with no package attr: point it at the new pkg+    if pkg_name:+        text = re.sub(r'(<device name="[^"]*")(?![^>]*package=)([^>]*>)',+                      r'\1 package="%s"\2' % pkg_name, text, count=1)+    return text, report+++_LBR_SHELL = """<?xml version="1.0" encoding="utf-8"?>+<!DOCTYPE eagle SYSTEM "eagle.dtd">+<eagle version="9.6.2">+<drawing>+<settings><setting alwaysvectorfont="no"/><setting verticaltext="up"/></settings>+<grid distance="0.1" unitdist="inch" unit="inch" style="lines" multiple="1" display="no" altdistance="0.01" altunitdist="inch" altunit="inch"/>+<layers>+<layer number="1" name="Top" color="4" fill="1" visible="yes" active="yes"/>+<layer number="16" name="Bottom" color="1" fill="1" visible="yes" active="yes"/>+<layer number="20" name="Dimension" color="15" fill="1" visible="yes" active="yes"/>+<layer number="21" name="tPlace" color="7" fill="1" visible="yes" active="yes"/>+<layer number="22" name="bPlace" color="7" fill="1" visible="yes" active="yes"/>+<layer number="25" name="tNames" color="7" fill="1" visible="yes" active="yes"/>+<layer number="27" name="tValues" color="7" fill="1" visible="yes" active="yes"/>+<layer number="29" name="tStop" color="7" fill="3" visible="no" active="yes"/>+<layer number="30" name="bStop" color="7" fill="6" visible="no" active="yes"/>+<layer number="31" name="tCream" color="7" fill="4" visible="no" active="yes"/>+<layer number="32" name="bCream" color="7" fill="5" visible="no" active="yes"/>+<layer number="39" name="tKeepout" color="4" fill="11" visible="no" active="yes"/>+<layer number="40" name="bKeepout" color="1" fill="11" visible="no" active="yes"/>+<layer number="47" name="Measures" color="7" fill="1" visible="no" active="yes"/>+<layer number="48" name="Document" color="7" fill="1" visible="yes" active="yes"/>+<layer number="51" name="tDocu" color="7" fill="1" visible="yes" active="yes"/>+<layer number="52" name="bDocu" color="7" fill="1" visible="yes" active="yes"/>+<layer number="94" name="Symbols" color="4" fill="1" visible="yes" active="yes"/>+<layer number="95" name="Names" color="7" fill="1" visible="yes" active="yes"/>+<layer number="96" name="Values" color="7" fill="1" visible="yes" active="yes"/>+</layers>+<library>+<packages>+%(package)s+</packages>+<symbols>+%(symbol)s+</symbols>+<devicesets>+<deviceset name="%(name)s" prefix="%(prefix)s">+<description>%(descr)s</description>+<gates>+<gate name="G$1" symbol="%(symname)s" x="0" y="0"/>+</gates>+<devices>+<device name="" package="%(pkgname)s">+%(connects)s+<technologies><technology name=""/></technologies>+</device>+</devices>+</deviceset>+</devicesets>+</library>+</drawing>+</eagle>+"""+++def build_lbr(kicad_sym_text=None, kicad_mod_text=None, name=None, prefix="U", descr=""):+    """Create a COMPLETE .lbr from KiCad sources: package + symbol + deviceset + connects.++    The create half of issue #43 (patch_lbr is the patch half). Either source may be absent:+    a missing symbol gets a minimal 2-pin box (so the footprint still has a home), a missing+    footprint yields a symbol-only library the show verbs can already handle.+    """+    pkg_xml, pads, pkg_name = ("", [], "")+    if kicad_mod_text:+        pkg_xml, pads, pkg_name = footprint_to_package(kicad_mod_text, name)+    sym_xml, pins, sym_name = ("", [], "")+    if kicad_sym_text:+        sym_xml, pins, sym_name = symbol_to_eagle(kicad_sym_text, name)+    the_name = name or pkg_name or sym_name or "PART"+    if not sym_xml:+        sym_name = the_name+        pins = [str(p) for p in pads] or ["1", "2"]+        body = ['<wire x1="-5.08" y1="2.54" x2="5.08" y2="2.54" width="0.254" layer="94"/>',+                '<wire x1="5.08" y1="2.54" x2="5.08" y2="-2.54" width="0.254" layer="94"/>',+                '<wire x1="5.08" y1="-2.54" x2="-5.08" y2="-2.54" width="0.254" layer="94"/>',+                '<wire x1="-5.08" y1="-2.54" x2="-5.08" y2="2.54" width="0.254" layer="94"/>']+        for i, p in enumerate(pins[:2]):+            side = -1 if i == 0 else 1+            body.append('<pin name="%s" x="%s" y="0" length="short" rot="%s"/>'+                        % (_esc(p), _fmt(side * 7.62), "R0" if side < 0 else "R180"))+        body.append('<text x="0" y="3.3" size="1.778" layer="95">&gt;NAME</text>')+        body.append('<text x="0" y="-4.3" size="1.778" layer="96">&gt;VALUE</text>')+        sym_xml = '<symbol name="%s">\n%s\n</symbol>' % (_esc(sym_name), "\n".join(body))+    by_name = {p.lower(): p for p in pins}+    conns = []+    for i, pad in enumerate(pads):+        pin = by_name.get(str(pad).lower()) or (pins[i] if i < len(pins) else None)+        if pin:+            conns.append('<connect gate="G$1" pin="%s" pad="%s"/>' % (_esc(pin), _esc(pad)))+    connects = ("<connects>\n%s\n</connects>" % "\n".join(conns)) if conns else "<connects/>"+    text = _LBR_SHELL % {+        "package": pkg_xml, "symbol": sym_xml, "name": _esc(the_name),+        "symname": _esc(sym_name), "pkgname": _esc(pkg_name or the_name),+        "prefix": _esc(prefix), "descr": _esc(descr), "connects": connects,+    }+    return text, {"name": the_name, "pads": pads, "pins": pins,+                  "packageName": pkg_name, "symbolName": sym_name}
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.120",+  "version": "1.9.121",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+10−7
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.120",+  "version": "1.9.121",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -70,7 +74,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -99,9 +106,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }+  ] }\ No newline at end of file
server.py+86
@@ -9028,6 +9028,92 @@ def _handle_prove_library(fusion_info: dict, args: dict) -> dict: COMMAND_HANDLERS["prove_library"] = _handle_prove_library  ++def _handle_import_kicad(fusion_info: dict, args: dict) -> dict:+    """Ingest KiCad symbol/footprint files and BUILD or PATCH a valid .lbr (issue #43).++    KiCad is the FUNDAMENTAL symbol/footprint representation at Adom; the .lbr is derived,+    and a derived file can be missing, stale, or half-generated. This verb makes the .lbr an+    optimization instead of a prerequisite - the same posture bind_library_3d already takes+    for STEP.++    args:+      kicadFootprintPath - Windows path to a .kicad_mod   (optional)+      kicadSymbolPath    - Windows path to a .kicad_sym   (optional)+      libraryPath        - .lbr to PATCH if it exists, else where to WRITE the new library+                           (optional; defaults next to the footprint/symbol file)+      name, prefix       - optional overrides for the created deviceset++    PATCH fills only what is EMPTY (<packages></packages>, <connects/>) and never overwrites+    existing content. The concrete case: cr0603-fx-5601elf's .lbr had an empty <packages>+    while a correct .kicad_mod sat beside it, so every downstream verb refused honestly and+    uselessly. After this verb, that part just works.+    """+    import os as _os+    import kicad_lbr as _kl+    mod_p = (args.get("kicadFootprintPath") or "").replace("\\", "/")+    sym_p = (args.get("kicadSymbolPath") or "").replace("\\", "/")+    lbr_p = (args.get("libraryPath") or "").replace("\\", "/")+    if not mod_p and not sym_p:+        return {"success": False, "errorCode": "kicad_source_required",+                "error": "Pass kicadFootprintPath and/or kicadSymbolPath.",+                "_hint": "Both are wiki first-class artifacts; stage them with send_files, "+                         "then call this with their Windows paths."}+    def _read(p, what):+        if not p:+            return None, None+        try:+            return open(p, "r", encoding="utf-8", errors="replace").read(), None+        except Exception as e:+            return None, {"success": False, "errorCode": "kicad_source_unreadable",+                          "error": "Could not read %s: %s (%s)" % (what, p, str(e)[:80])}+    mod_text, err = _read(mod_p, ".kicad_mod")+    if err:+        return err+    sym_text, err = _read(sym_p, ".kicad_sym")+    if err:+        return err+    if not lbr_p:+        base = (mod_p or sym_p).rsplit("/", 1)+        stem = base[-1].rsplit(".", 1)[0]+        lbr_p = (base[0] + "/" if len(base) > 1 else "") + stem + ".lbr"+    mode = "patched" if _os.path.exists(lbr_p) else "created"+    try:+        if mode == "patched":+            original = open(lbr_p, "r", encoding="utf-8", errors="replace").read()+            new_text, report = _kl.patch_lbr(original, kicad_mod_text=mod_text,+                                             kicad_sym_text=sym_text, name=args.get("name"))+            if new_text == original:+                report["unchanged"] = True+            else:+                try:+                    open(lbr_p + ".bak", "w", encoding="utf-8").write(original)+                except Exception:+                    pass+                open(lbr_p, "w", encoding="utf-8").write(new_text)+        else:+            new_text, report = _kl.build_lbr(kicad_sym_text=sym_text, kicad_mod_text=mod_text,+                                             name=args.get("name"),+                                             prefix=args.get("prefix") or "U")+            open(lbr_p, "w", encoding="utf-8").write(new_text)+    except Exception as e:+        return {"success": False, "errorCode": "kicad_conversion_failed",+                "error": "Conversion failed: %s" % str(e)[:200]}+    names = _library_artifact_names(lbr_p)+    return {"success": True, "libraryPath": lbr_p, "mode": mode, "report": report,+            "symbols": names.get("sym", []), "packages": names.get("pac", []),+            "devicesets": names.get("dev", []), "has3d": bool(names.get("p3d")),+            "sources": {"kicadFootprint": mod_p or None, "kicadSymbol": sym_p or None},+            "_hint": ("%s %s from KiCad sources. fusion_show_symbol / show_footprint consume "+                      "it directly; bind the 3D with fusion_bind_library_3d {libraryPath, "+                      "stepPath} (has3d is %s), then fusion_prove_library captures the four "+                      "evidence frames. A .bak sits beside a patched file."+                      % (mode.capitalize(), lbr_p, bool(names.get("p3d"))))}+++COMMAND_HANDLERS["import_kicad"] = _handle_import_kicad++ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> dict:     """Thin wrapper: run the verb, then guarantee the windowEtiquette annotation lands on     EVERY backgrounded window-raising result - the inner dispatcher's closure only covered
tools/release-files.txt+1
@@ -56,3 +56,4 @@ start.bat uninstall.sh handlers/taskbar_overlay.py resources/adom-overlay-32.png+kicad_lbr.py