app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
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.
← Commit history
v1.9.48: stepShot works without PIL (stdlib BMP->PNG downscaler); inline gallery capped at 4MB with downscaled fallback
5 files changed
+82−14
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.47+1.9.48
CHANGELOG.md+10@@ -3,6 +3,16 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.9.48 - 2026-08-16++- **stepShot frames actually deliver on PIL-less bridges.** AD's provisioned python has no PIL,+ so captures fall back to raw ~24MB BMPs; 1.9.47's encoder silently skipped every one (live+ tour: 9 shots, 0 stepShot frames). New pure-stdlib BMP->PNG nearest-neighbor downscaler+ (zlib+struct, no deps) feeds the ~640px stepShot contract regardless of PIL.+- **Inline gallery hard cap.** imagesAs:base64 now refuses to inline files over 4MB and ships+ the downscaled (<=1280px) frame + imageNote instead - a BMP-era done frame would have been a+ ~280MB response.+ ## 1.9.47 - 2026-08-16 - **Live per-step tour evidence (ab's #30 contract, John's call).** fusion_demo_progress frames
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.47",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.47/adom-bridge-fusion-v1.9.47.zip",- "sha256": "dd86103860fcbb45e3345f8fd9171bcf96184a6a49bf1a57685f85f9ffef94de",- "size": 395774,+ "version": "1.9.48",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.48/adom-bridge-fusion-v1.9.48.zip",+ "sha256": "94fc6edeb64a8366928d736bdc574282a3d2e3448ff4d2ca5e42b6fdbb0e7fc4",+ "size": 396880, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.47",+ "version": "1.9.48", "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.",
server.py+66−8@@ -6216,14 +6216,62 @@ def _demo_tour_load(demo_id: str): return None +def _bmp_downscale_png(path: str, max_w: int = 640):+ """Pure-STDLIB BMP -> small PNG (nearest-neighbor). Exists because AD's provisioned+ python has NO PIL, so screenshot captures fall back to raw 24MB BMPs - and a 24MB BMP+ is 31MB as base64, which no relay frame should ever carry (caught live 2026-08-16:+ every tour stepShot silently skipped, and the done gallery would have been ~280MB).+ BMP is uncompressed BGR rows and PNG needs only zlib+struct, so no dependency."""+ import struct+ import zlib+ raw = open(path, "rb").read()+ if raw[:2] != b"BM":+ raise ValueError("not a BMP")+ pix_off = struct.unpack_from("<I", raw, 10)[0]+ w = struct.unpack_from("<i", raw, 18)[0]+ h = struct.unpack_from("<i", raw, 22)[0]+ bpp = struct.unpack_from("<H", raw, 28)[0]+ comp = struct.unpack_from("<I", raw, 30)[0]+ if comp != 0 or bpp not in (24, 32) or w <= 0 or h == 0:+ raise ValueError("unsupported BMP (bpp=%s comp=%s)" % (bpp, comp))+ bottom_up = h > 0+ h = abs(h)+ bypp = bpp // 8+ stride = ((w * bypp) + 3) & ~3+ ow = min(max_w, w)+ oh = max(1, int(h * ow / w))+ xs = [int(x * w / ow) * bypp for x in range(ow)]+ rows = []+ for y in range(oh):+ sy = int(y * h / oh)+ row_off = pix_off + ((h - 1 - sy) if bottom_up else sy) * stride+ line = bytearray(1 + ow * 3) # PNG filter byte 0 + RGB+ i = 1+ for xo in xs:+ o = row_off + xo+ line[i] = raw[o + 2] # R (BMP stores BGR)+ line[i + 1] = raw[o + 1] # G+ line[i + 2] = raw[o] # B+ i += 3+ rows.append(bytes(line))+ def chunk(tag, data):+ return (struct.pack(">I", len(data)) + tag + data+ + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))+ ihdr = struct.pack(">IIBBBBB", ow, oh, 8, 2, 0, 0, 0)+ idat = zlib.compress(b"".join(rows), 6)+ return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", idat)+ + chunk(b"IEND", b""))++ def _shot_b64_small(path: str, max_w: int = 640): """Base64 evidence frame for ab's stepShot contract (~640px wide). PIL downscale when- available; else the raw file if it is small enough for the relay envelope."""+ available; the stdlib BMP downscaler when not (AD's python ships without PIL); else+ the raw file if it is small enough for the relay envelope."""+ import base64 try: try: from PIL import Image import io- import base64 im = Image.open(path) if im.width > max_w: im = im.resize((max_w, max(1, int(im.height * max_w / im.width))))@@ -6231,10 +6279,12 @@ def _shot_b64_small(path: str, max_w: int = 640): im.convert("RGB").save(buf, format="PNG") return base64.b64encode(buf.getvalue()).decode() except ImportError:- import base64- raw = open(path, "rb").read()- if len(raw) <= 1_500_000:- return base64.b64encode(raw).decode()+ pass+ if path.lower().endswith(".bmp"):+ return base64.b64encode(_bmp_downscale_png(path, max_w)).decode()+ raw = open(path, "rb").read()+ if len(raw) <= 1_500_000:+ return base64.b64encode(raw).decode() except Exception: pass return None@@ -6574,8 +6624,16 @@ def _handle_demo_progress(args: dict) -> dict: for s in shots: s = dict(s) try:- with open(s["path"], "rb") as f:- s["image"] = base64.b64encode(f.read()).decode()+ raw = open(s["path"], "rb").read()+ if len(raw) <= 4 * 1024 * 1024:+ s["image"] = base64.b64encode(raw).decode()+ else:+ # PIL-less bridge python saves raw 24MB BMPs - NEVER ship those inline+ # (9 x 31MB base64 = a ~280MB frame). Downscaled evidence instead.+ img = _shot_b64_small(s["path"], max_w=1280)+ if img:+ s["image"] = img+ s["imageNote"] = "downscaled to <=1280px (original is a %dMB capture; pull_file the path for full size)" % (len(raw) // (1024 * 1024)) except Exception: pass encoded.append(s)