--- fusion-project.py.orig 2026-10-18 12:14:00.093896005 +0100 +++ /home/adom/project/adom_modules/adom/adom-aiflow/tools/fusion-project.py 2026-10-18 16:00:59.942836476 +0100 @@ -795,6 +795,55 @@ # ---------------------------------------------------------------- schematic +def wired_groups(layout, lib, by_ref, values, libname, fixed_refs): + """A wired sheet from a hand layout: every part where the layout puts it and every net drawn as wires + from pin to pin, so the sheet reads as a circuit rather than a list of parts with net names. + layout: {"groups": [{"name", "text": [x, y]?, "parts": {ref: [x, y, rot]}, "nets": {net: [[point, ...], ...]}, + "labels": [[net, x, y]]}]}; a point is "REF.PIN" (pad or pin name) or [x, y] in mm. The fixed parts the + seed drew are reached by coordinates (their stub ends). Points are checked: every "REF.PIN" must exist.""" + syms = {s.get('name'): s for s in lib.iter('symbol')} + pins_of = {} # deviceset -> {pad or pin name: (x, y)} + for d in lib.iter('deviceset'): + g = next(d.iter('gate')); s = syms[g.get('symbol')] + pin_xy = {p.get('name'): (float(p.get('x')), float(p.get('y'))) for p in s.iter('pin')} + m = dict(pin_xy) + for dv in d.iter('device'): + for c in dv.iter('connect'): + for pad in c.get('pad', '').split(): m.setdefault(pad, pin_xy[c.get('pin')]) + pins_of[d.get('name').upper()] = m + placed = {} + for g in layout['groups']: + for r, (x, y, rot) in g.get('parts', {}).items(): placed[r] = (float(x), float(y), rot or 'R0') + def pt(p): + if isinstance(p, (list, tuple)): return (float(p[0]), float(p[1])) + r, pin = p.split('.', 1) + if r not in placed: err('layout: %s is not placed (only the parts in "parts" can be reached by pin)' % p) + x, y, rot = placed[r] + px, py = pins_of[by_ref[r].upper()].get(pin) or err('layout: %s has no pin %s' % (by_ref[r], pin)) + k = int(rot.lstrip('MR') or 0) // 90 % 4 + for _ in range(k): px, py = -py, px + if rot.startswith('M'): px = -px + return (round(x + px, 3), round(y + py, 3)) + f = lambda q: '(%.3fmm %.3fmm)' % q + missing = [r for r in by_ref if r not in placed and r not in fixed_refs] + if missing: err('layout: no place for %s' % ', '.join(missing), ['Every part that is not fixed needs an entry in a group\'s "parts".']) + groups = [] + for g in layout['groups']: + cmds = [] + if g.get('text'): + cmds += ['LAYER 97;', 'CHANGE SIZE 2.54mm;', "TEXT '%s' R0 %s;" % (g['name'], f(tuple(g['text']))), 'CHANGE SIZE 1.778mm;'] + cmds.append('LAYER 91;') + for r, (x, y, rot) in g.get('parts', {}).items(): + cmds += ["ADD '%s@%s' '%s' %s %s;" % (by_ref[r], libname, r, rot or 'R0', f((float(x), float(y)))), "VALUE '%s' '%s';" % (r, values.get(r) or by_ref[r])] + for n, paths in g.get('nets', {}).items(): + for path in paths: + cmds.append("NET '%s' %s;" % (n, ' '.join(f(pt(p)) for p in path))) + for n, x, y in g.get('labels', []): + cmds.append('LABEL %s %s;' % (f((x, y)), f((x, y)))) + groups.append({'group': g['name'], 'parts': list(g.get('parts', {})), 'commands': cmds}) + return groups + + def cmd_schematic(a): st = load_state() if not st.get('libraryPath'): err('no project library yet', ['Run `adom-aiflow fusion library --parts ` first.']) @@ -843,9 +892,12 @@ ab('fusion_schematic', {'action': 'show'}); time.sleep(3) fixed_refs = set(fx['fixed']) parts = [{'ref': c['ref'], 'deviceset': by_ref[c['ref']], 'value': c.get('value'), 'block': c.get('block')} for c in nl['components'] if c['ref'] not in fixed_refs] - plan = ab('fusion_schematic_plan', {'libraryPath': st['libraryPath'], 'parts': parts, 'nets': {n: [m for m in ms if m.split('.')[0] not in fixed_refs] for n, ms in nets.items()}}) + if a.layout: + plan = {'groups': wired_groups(json.loads(Path(a.layout).read_text()), lib, by_ref, {c['ref']: c.get('value') for c in nl['components']}, Path(st['libraryPath'].replace('\\', '/')).stem, fixed_refs)} + else: + plan = ab('fusion_schematic_plan', {'libraryPath': st['libraryPath'], 'parts': parts, 'nets': {n: [m for m in ms if m.split('.')[0] not in fixed_refs] for n, ms in nets.items()}}) if not plan.get('groups'): err('schematic plan: %s' % plan.get('error')) - if fixed_refs: + if fixed_refs and not a.layout: # the seed draws the fixed parts (a molecule's pins and contacts) as a column at the sheet's left edge; # the planner starts its groups at that same x, so move every group clear of the column shift = 45.72 @@ -1109,6 +1161,7 @@ src = ab('fusion_board_source', {'expectDocument': name}); text = (src.get('data') or src).get('source') or src.get('source') or '' if ']*smashed="yes"[^>]*>(.*?)', text, re.S) if 'name="NAME"' not in m.group(2)} contacts = [e for e in els if is_contact(e)] fresh_cmds = [] if a.fresh: @@ -1284,7 +1337,12 @@ if L['kind'] == 'name': ax, ay = anchor(L['box'], aligns.get(L['ref'], 'bottom-left'), L.get('rot', 'R0')) L['anchor'] = [round(ax, 3), round(ay, 3)] - if abs(L['size'] - 0.8) < 1e-6: + if L['ref'] in nameless: + # a smashed part whose NAME text is gone (an earlier, interrupted pass deleted it): there is no + # name to move or remove, so the reference is printed as plain text at the planned spot + tx, ty = (x0, y0) if L.get('rot', 'R0') == 'R0' else (L['box'][2], L['box'][1]) + cmds += ['CHANGE LAYER 21;', 'CHANGE SIZE %s;' % L['size'], 'CHANGE RATIO 15;', 'CHANGE ALIGN bottom-left;', "TEXT '%s' %s (%.3f %.3f);" % (L['text'], L.get('rot', 'R0'), tx, ty)] + elif abs(L['size'] - 0.8) < 1e-6: cmds += ["SMASH %s;" % L['ref'], "ROTATE =%s '%s>NAME';" % (L.get('rot', 'R0'), L['ref']), "MOVE '%s>NAME' (%.3f %.3f);" % (L['ref'], ax, ay)] else: # CHANGE SIZE does not land in Fusion (by point or by name): the smashed name comes off at its @@ -1550,6 +1608,7 @@ p = sub.add_parser('schematic'); p.add_argument('--netlist', required=True); p.add_argument('--spec'); p.add_argument('--fixed'); p.add_argument('--outline', nargs=2, type=float) p.add_argument('--mpns'); p.add_argument('--name', required=True); p.add_argument('--project', default='Main'); p.add_argument('--folder', default='Adom AI Flow') p.add_argument('--board-out', default='board.brd'); p.add_argument('--pause-ms', type=int, default=60) + p.add_argument('--layout', help='wired.json: parts placed by hand and every net drawn as wires pin to pin (a sheet an engineer can read), instead of the planner\'s labelled stubs') p = sub.add_parser('reset'); p.add_argument('--design') p = sub.add_parser('export'); p.add_argument('--design'); p.add_argument('--out-dir', default=str(RUN / 'manufacturing')) p = sub.add_parser('view3d', help='the board in 3D: a still to check and a live orbit'); p.add_argument('--design'); p.add_argument('--steps', type=int, default=24); p.add_argument('--pause', type=float, default=0.6); p.add_argument('--settle', type=float, default=8)