← Commit history

v1.6.19: long-axis markings + sidecar manifest + multi-line

John Lauer ·191917bd6b ·3mo ago ·parent 6346ca6
8 files changed +116−19
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.18\ No newline at end of file+1.6.19\ No newline at end of file
CHANGELOG.md+11
@@ -3,6 +3,17 @@ All notable changes to the Adom Desktop Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/adom-desktop-fusion-bridge ยท GitHub backup: `adom-inc/adom-desktop-fusion-bridge` +## 1.6.19 - 2026-07-04++- **Markings run along the LONGEST axis of the top face** (auto-rotated 90 deg when the long axis is+  sketch-y) - maximum room for the text (John's rule). **Multi-line markings** work: newline-joined+  text (MPN + variant) with per-line auto-fit (h = min(boxH/(1.35*lines), boxW/(0.75*maxChars))).+- **Sidecar manifest for marking refreshes:** with `outputStep`, `<step>.manifest.json` records every+  setting picked + WHY + the calculated bboxes (chip bbox, top-5% band, face extents, text box,+  height, rotation). To refresh a marking (e.g. add a variant on line 2), re-call the verb with the+  manifest's `type` + `paramsCm` (`unitsCm:true`) + the new etch text - deterministic generation+  reproduces placement exactly. Manifest also returned inline; `_hint` teaches the whole workflow.+ ## 1.6.18 - 2026-07-04  - **Chip markings are now READABLE: raised WHITE text by default (John's spec).** Humans need
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.18",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.18/adom-bridge-fusion-v1.6.18.zip",-  "sha256": "a3df7c12b837287cf5f9702d770aa447751f631dc501f7a51f23b6a2e5d55b65",-  "size": 310510,+  "version": "1.6.19",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.19/adom-bridge-fusion-v1.6.19.zip",+  "sha256": "6bb2970739a8a5733b7c6e557b9da4e0c111dc1f2cc6271b2a1e1ffd2fbf1692",+  "size": 312585,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-04T17:20:00Z"+  "released_at": "2026-07-04T18:00:00Z" }\ No newline at end of file
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.18",+  "version": "1.6.19",   "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/adom-desktop-fusion-bridge",   "author": "Adom Inc.",
describe.py+2−2
@@ -80,8 +80,8 @@ _T = [      {"filePath": "required str (.lbr WINDOWS path C:/...)", "modelPath": "required str (.step WINDOWS path C:/...)", "packageName": "optional str"},      {"success": "bool", "savePending": "bool", "data": "{steps}", "_hint": "the Save-dialog click + verify steps"}, 240, _STATUS, True, {"filePath": "C:/Users/<user>/adom-lib/ADS8588SIPM.fusion.lbr", "modelPath": "C:/Users/<user>/adom-lib/ADS8588SIPM.step", "packageName": "ADS8588SIPM"}),     ("fusion_generate_package", "Generate an IPC-7351-compliant parametric 3D package via Fusion's BUILT-IN ElectronicsPackageGenerator (EPG) - ~50 package families (chip/soic/qfn/qfp/bga/sot23/dfn/melf/ecap/crystal/headers/...), each a few seconds, ZERO GUI. Optionally LASER-ETCH a marking (MPN / '103') into the body top as REAL cut geometry (survives STEP), and export STEP in the same call - the royalty-free, license-clean path to a whole component library (our own generated output, not vendor models). Proven live: 0603 + '103' etch + STEP in ~2s.",-     {"type": "required str (EPG family, e.g. chip|soic|qfn|qfp|bga|sot23|dfn2|melf|ecap|crystal|header_straight - see supportedTypes in the error for the full list)", "params": "optional dict (generator dims in MM by default, e.g. chip: {D,E,A,L,L1}; soic: {A,A1,b,D,E,E1,e,L,DPins}; omitted keys use EPG defaults)", "unitsCm": "optional bool (params already EPG-native cm)", "etch": "optional str (marking text on the chip top: MPN / '103')", "etchStyle": "optional str raised|engraved (default raised = thin WHITE positive extrude, silkscreen-style contrast; engraved = sunken cut)", "etchDepthMm": "optional float (marking height/depth, default 0.03)", "etchHeightMm": "optional float (text height; default auto-fit: top-5%-band face, 10% margin, width-aware)", "outputStep": "optional str (WINDOWS path; exports STEP incl. the marking)"},-     {"success": "bool", "bodies": "[{name,vol}]", "etched": "str|null", "stepPath": "str|null", "epgDir": "str"}, 120, _STATUS, True, {"type": "chip", "params": {"D": 1.6, "E": 0.8, "A": 0.45, "L": 0.3, "L1": 0.3}, "etch": "103", "outputStep": "C:/tmp/ADOM-R0603-103.step"}),+     {"type": "required str (EPG family, e.g. chip|soic|qfn|qfp|bga|sot23|dfn2|melf|ecap|crystal|header_straight - see supportedTypes in the error for the full list)", "params": "optional dict (generator dims in MM by default, e.g. chip: {D,E,A,L,L1}; soic: {A,A1,b,D,E,E1,e,L,DPins}; omitted keys use EPG defaults)", "unitsCm": "optional bool (params already EPG-native cm)", "etch": "optional str (marking text on the chip top: MPN / '103'; multi-line with newline, e.g. MPN+variant - laid along the LONGEST face axis)", "etchStyle": "optional str raised|engraved (default raised = thin WHITE positive extrude, silkscreen-style contrast; engraved = sunken cut)", "etchDepthMm": "optional float (marking height/depth, default 0.03)", "etchHeightMm": "optional float (text height; default auto-fit: top-5%-band face, 10% margin, width-aware)", "outputStep": "optional str (WINDOWS path; exports STEP incl. the marking)"},+     {"success": "bool", "bodies": "[{name,vol}]", "etched": "str|null", "stepPath": "str|null", "manifest": "obj (settings+why+bboxes; also written as <step>.manifest.json sidecar for marking refreshes)", "manifestPath": "str|null", "epgDir": "str"}, 120, _STATUS, True, {"type": "chip", "params": {"D": 1.6, "E": 0.8, "A": 0.45, "L": 0.3, "L1": 0.3}, "etch": "103", "outputStep": "C:/tmp/ADOM-R0603-103.step"}),     ("fusion_make_3d_package", "Create a RENDERING component 3D package (FOOTPRINT + chip, aligned) for ONE part, fully programmatically with ZERO GUI dialogs. Opens the .lbr, runs Electron.Create3DPackage to load the package's footprint into a generator doc, imports the STEP onto it, orients it flat, saveAs an .f3d (skips the FINISH Save dialog + the two unbeatable 'Fusion360' CEF modals), uploads the f3d via APS, and returns an fs.file:vf wip_urn to hand-write into the library's <packages3d>. projectId/folderId default to the MAIN-project upload folder. Pass captureLabel to also get a BEFORE (footprint) + AFTER (chip placed) screenshot. PITFALL: a RAW STEP upload binds but renders NOTHING ('Thumbnail download failed') - this verb makes an f3d, which renders. Prefer fusion_build_library_3d for many parts.",      {"lbrPath": "required str (.lbr WINDOWS path; its FIRST package's footprint is loaded)", "modelPath": "required str (.step WINDOWS path)", "projectId": "optional str (default MAIN-project)", "folderId": "optional str (default upload folder)", "fileName": "optional str", "captureLabel": "optional str (capture before/after)", "orient": "optional bool (default true)"},      {"success": "bool", "wip_urn": "str", "dims_mm": "[x,y,z]", "f3d": "str", "before": "str|null", "after": "str|null"}, 200, _STATUS, True, {"lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step", "captureLabel": "R_4k7"}),
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-desktop-fusion-bridge",-  "version": "1.6.18",+  "version": "1.6.19",   "type": "app",   "description": "Drive Autodesk Fusion 360 from the cloud via Adom Desktop: 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 with browse/recent/versions, cloud download/upload and folder creation, and in-app parametric modeling. Never-charge: APS calls are capped to the free tier.",   "dependencies": {},
server.py+88−9
@@ -2016,6 +2016,7 @@ def _orchestrate_generate_package(args: dict) -> dict:     etch_depth_cm = float(args.get("etchDepthMm") or 0.03) / 10.0     etch_height_mm = args.get("etchHeightMm")  # None = auto-fit     output_step = args.get("outputStep") or ""+    bridge_version = BRIDGE_VERSION      script = f""" import sys, glob, os, importlib@@ -2073,20 +2074,58 @@ if {etch!r}:     bb = top_face.boundingBox     p_min = sk.modelToSketchSpace(bb.minPoint)     p_max = sk.modelToSketchSpace(bb.maxPoint)-    face_w = abs(p_max.x - p_min.x)   # sketch-x = the text baseline axis-    face_h = abs(p_max.y - p_min.y)-    w = face_w * 0.80                 # 10% margin each side-    box_h_cap = face_h * 0.80-    # width-aware auto-fit: a glyph is ~0.75*h wide in Fusion's default text font-    n_chars = max(1, len({etch!r}))-    h = ({etch_height_mm!r} / 10.0) if {etch_height_mm!r} else max(0.015, min(box_h_cap * 0.55, w / (0.75 * n_chars)))-    c1 = adsk.core.Point3D.create(center.x - w/2, center.y - h*0.75, 0)-    c2 = adsk.core.Point3D.create(center.x + w/2, center.y + h*0.75, 0)+    face_w = abs(p_max.x - p_min.x)   # extent along sketch-x (the default text baseline)+    face_h = abs(p_max.y - p_min.y)   # extent along sketch-y+    # ALWAYS run the text along the LONGEST axis of the top face (most room for+    # the marking - John's rule, 2026-07-04). If the long axis is sketch-y,+    # rotate the text 90deg rather than squeezing it onto the short axis.+    import math as _math+    rot_deg = 90 if face_h > face_w else 0+    long_len = max(face_w, face_h)+    short_len = min(face_w, face_h)+    w = long_len * 0.80               # 10% margin each side along the long axis+    box_h_cap = short_len * 0.80      # 10% margin each side across it+    # multi-line support (e.g. MPN + variant on line 2): fit per-line+    lines = {etch!r}.split(chr(10))+    n_lines = max(1, len(lines))+    max_chars = max(1, max(len(l) for l in lines))+    # width-aware auto-fit: glyph ~0.75*h wide; line block ~1.35*h per line+    h = ({etch_height_mm!r} / 10.0) if {etch_height_mm!r} else \+        max(0.015, min(box_h_cap / (1.35 * n_lines), w / (0.75 * max_chars)))+    if rot_deg:+        c1 = adsk.core.Point3D.create(center.x - box_h_cap/2, center.y - w/2, 0)+        c2 = adsk.core.Point3D.create(center.x + box_h_cap/2, center.y + w/2, 0)+    else:+        c1 = adsk.core.Point3D.create(center.x - w/2, center.y - box_h_cap/2, 0)+        c2 = adsk.core.Point3D.create(center.x + w/2, center.y + box_h_cap/2, 0)     ti = sk.sketchTexts.createInput2({etch!r}, h)     ti.setAsMultiLine(c1, c2,                       adsk.core.HorizontalAlignments.CenterHorizontalAlignment,                       adsk.core.VerticalAlignments.MiddleVerticalAlignment, 0)+    if rot_deg:+        try:+            ti.angle = _math.pi / 2.0+        except Exception:+            rot_deg = 0  # older API without angle - fall back to unrotated     txt = sk.sketchTexts.add(ti)+    marking_geom = {{+        'chipBBox': {{'min': [round(gmin,5), 0, 0], 'maxZ': round(gmax,5)}},+        'chipBBoxZ': {{'min': round(gmin,5), 'max': round(gmax,5)}},+        'topBandZ': round(band_z,5),+        'topFace': {{'area': round(top_area,6), 'centroidZ': round(top_face.centroid.z,5),+                    'sketchExtents': {{'x': round(face_w,5), 'y': round(face_h,5)}},+                    'body': body.name}},+        'longAxis': 'sketch-y' if face_h > face_w else 'sketch-x',+        'rotatedDeg': rot_deg,+        'textBox': {{'c1': [round(c1.x,5), round(c1.y,5)], 'c2': [round(c2.x,5), round(c2.y,5)],+                    'marginPct': 10}},+        'textHeightMm': round(h*10,4), 'lines': n_lines, 'maxLineChars': max_chars,+        'heightAuto': not bool({etch_height_mm!r}),+        'why': {{'band': 'top 5% of whole-chip bbox excludes terminal tops that tie the body',+                'largestArea': 'molded body top beats small terminal faces in the band',+                'longAxis': 'text runs along the longest face axis for maximum room',+                'fit': 'h = min(boxH/(1.35*lines), boxW/(0.75*maxChars)) - glyphs ~0.75h wide'}},+    }}     if {etch_style!r} == 'engraved':         # engraved (sunken) cut - the pre-2026-07-04 default         ext_in = root.features.extrudeFeatures.createInput(txt, adsk.fusion.FeatureOperations.CutFeatureOperation)@@ -2115,6 +2154,36 @@ if {output_step!r}:     em.execute(opts)     step_path = {output_step!r} +# SIDECAR MANIFEST: every setting we picked + WHY + the bboxes we calculated,+# so a marking refresh (e.g. adding a variant name as a 2nd line) can rework the+# text without re-deriving anything. Written next to the STEP as <step>.manifest.json.+manifest = {{+    'schema': 'adom-fusion-generate-package/1',+    'bridgeVersion': {bridge_version!r},+    'type': {pkg_type!r},+    'paramsCm': {params!r},+    'paramsNote': 'paramsCm are EPG-native cm (mm inputs were /10)',+    'marking': ({{'text': {etch!r}, 'style': {etch_style!r},+                'depthMm': {etch_depth_cm!r} * 10, **marking_geom}} if etched else None),+    'bodies': inv,+    'outputs': {{'step': step_path}},+    'refresh': {{+        'howTo': 'To re-mark (e.g. add a variant on line 2): call fusion_generate_package again '+                 'with the SAME type+paramsCm (unitsCm:true) and the new multi-line etch text '+                 '(join lines with a newline). The generator is deterministic, so geometry and '+                 'placement reproduce exactly; this manifest carries the boxes/height the last '+                 'run chose for comparison.',+        'example': {{'type': {pkg_type!r}, 'unitsCm': True, 'params': 'paramsCm from this file',+                    'etch': ({etch!r} + chr(10) + 'VARIANT') if etched else 'MPN' + chr(10) + 'VARIANT'}},+    }},+}}+manifest_path = None+if step_path:+    manifest_path = step_path + '.manifest.json'+    import json as _json+    with open(manifest_path, 'w', encoding='utf-8') as _f:+        _json.dump(manifest, _f, indent=2)+ vp = app.activeViewport cam = vp.camera cam.isSmoothTransition = False@@ -2126,6 +2195,7 @@ vp.camera = cam vp.refresh()  result = {{'type': {pkg_type!r}, 'bodies': inv, 'etched': etched, 'stepPath': step_path,+           'manifest': manifest, 'manifestPath': manifest_path,            'epgDir': epg_dir, 'doc': app.activeDocument.name}} """ @@ -2155,6 +2225,15 @@ result = {{'type': {pkg_type!r}, 'bodies': inv, 'etched': etched, 'stepPath': st                     "the text is laid out with a 10% margin inside that face, auto-fit width-aware "                     "(override with etchHeightMm). Default style is raised+white for CONTRAST (humans "                     "can't read a dark-on-dark emboss); pass etchStyle:'engraved' for a sunken laser cut. "+                    "The text ALWAYS runs along the LONGEST axis of the top face (rotated 90deg when "+                    "needed) for maximum room, and multi-line markings work - join lines with a newline "+                    "(e.g. 'LM358' + newline + 'ADOM-A' for MPN + variant; per-line auto-fit). "+                    "SIDECAR MANIFEST: when outputStep is set, <step>.manifest.json records every "+                    "setting picked + WHY + the calculated bboxes (chip bbox, top band, face extents, "+                    "text box, height, rotation) - to refresh a marking (add a variant line), re-call "+                    "this verb with the manifest's type + paramsCm (unitsCm:true) + the new etch text; "+                    "generation is deterministic so placement reproduces exactly. The manifest is also "+                    "returned inline as 'manifest'. "                     "PITFALLS: params are mm by default (unitsCm:true for EPG-native cm); dimension names "                     "follow each EPG generator (chip: D/E/A/L/L1; soic: A/A1/b/D/E/E1/e/L/DPins); raised "                     "markings are separate 'Marking' bodies (white in Fusion + colored STEP)."),
skills/fusion-build-parts-library/SKILL.md+7
@@ -96,3 +96,10 @@ adom-desktop fusion_generate_package '{"type":"soic","params":{"DPins":8,"D":4.9  Read the verb's `_hint` on every call - it carries the band/margin/contrast rules and the per-family param names (chip: D/E/A/L/L1; soic: A/A1/b/D/E/E1/e/L/DPins).++**Long-axis + manifest (v1.6.19).** The marking always runs along the LONGEST axis of the top face+(auto-rotated 90 deg when the long axis is sketch-y) - maximum room for text. Multi-line works: join+with a newline ("LM358\nADOM-A" = MPN + variant, per-line auto-fit). With `outputStep`, a sidecar+`<step>.manifest.json` records every setting + why + the calculated bboxes; to REFRESH a marking+(e.g. add a variant line) re-call the verb with the manifest's `type` + `paramsCm` (`unitsCm:true`)++ the new etch text - generation is deterministic, so placement reproduces exactly.