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.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
import importlib.util
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('sch', ROOT / 'rust/bridge/src/sch_script.py')
s = importlib.util.module_from_spec(spec)
spec.loader.exec_module(s)
FIXTURE = ROOT / 'demo/routing/fixture/fusion-routing.sch'
class SchReadTests(unittest.TestCase):
def setUp(self):
self.root = s.sch_root_from_file(str(FIXTURE))
self.n, self.sheet = s.pick_sheet(self.root, {})
def test_every_pin_sits_on_a_wire_end_of_its_own_segment(self):
# The proof the transform is right: Fusion's ULP pin coordinates count the part's offset
# twice (live 2026-10-27), but its wires do not, and a connected pin ends a wire.
lib = s.Lib(self.root.find('drawing/schematic'))
pins = {(p['ref'], p['gate'], p['pin']): p['at'] for p in s.sheet_pins(lib, self.sheet)}
checked = 0
for _, seg, _ in s.segments(self.sheet):
ends = [(s._sf(w, 'x1'), s._sf(w, 'y1')) for w in seg.findall('wire')] + [(s._sf(w, 'x2'), s._sf(w, 'y2')) for w in seg.findall('wire')]
for pr in seg.findall('pinref'):
at = pins[(pr.get('part'), pr.get('gate'), pr.get('pin'))]
self.assertTrue(any(abs(at[0] - x) < 1e-3 and abs(at[1] - y) < 1e-3 for x, y in ends), (pr.attrib, at))
checked += 1
self.assertEqual(13, checked)
def test_items_and_ids(self):
it = s.items(self.root, self.sheet)
self.assertEqual(7, it['counts']['wires'])
self.assertEqual(1, it['counts']['junctions'])
w = it['wires'][0]
self.assertEqual(s.wire_id(*w['from'], *w['to']), w['id'])
self.assertEqual(s.wire_id(1, 2, 3, 4), s.wire_id(3, 4, 1, 2))
def test_trace_point_and_nearest(self):
r = s.trace_point(self.root, self.sheet, [7, 7], 0.01)
self.assertEqual(['NET_1'], [n['name'] for n in r['nets']])
self.assertIn('J1.1', r['nets'][0]['pins'])
r = s.trace_point(self.root, self.sheet, [-50, -50], 0.01)
self.assertEqual([], r['items'])
self.assertIsNotNone(r['nearest'])
def test_pin_side_follows_turn_and_mirror(self):
self.assertEqual('left', s.pin_side('R0', 'R0'))
self.assertEqual('right', s.pin_side('R0', 'MR0'))
self.assertEqual('bottom', s.pin_side('R0', 'R90'))
self.assertEqual('top', s.pin_side('R90', 'R180'))
def test_sheet_choice(self):
self.assertEqual(1, s.pick_sheet(self.root, {'sheet': '1'})[0])
with self.assertRaises(s.SchError):
s.pick_sheet(self.root, {'sheet': 2})
class SchEditPlanTests(unittest.TestCase):
def setUp(self):
self.root = s.sch_root_from_file(str(FIXTURE))
self.n, self.sheet = s.pick_sheet(self.root, {})
def test_route_turns_once_along_the_first_leg(self):
self.assertEqual([(0, 0), (10, 0), (10, 5)], s.route_points((0, 0), (10, 5), 'h', None))
self.assertEqual([(0, 0), (0, 5), (10, 5)], s.route_points((0, 0), (10, 5), 'v', None))
self.assertEqual([(0, 0), (10, 0)], s.route_points((0, 0), (10, 0), 'v', None))
self.assertEqual([(0, 0), (3, 3), (10, 3), (10, 5)], s.route_points((0, 0), (10, 5), 'h', [[3, 3]]))
def test_pins_by_number_then_name(self):
p = s.find_pin(self.root, self.sheet, 'J1.1')
self.assertEqual([7.0, 7.0], p['at'])
for bad in ('J1', 'ZZ9.1', 'J1.99'):
with self.assertRaises(s.SchError):
s.find_pin(self.root, self.sheet, bad)
def test_cover_ignores_how_a_wire_is_split(self):
import copy, xml.etree.ElementTree as ET
sh = copy.deepcopy(self.sheet)
seg = sh.find('nets/net/segment')
w = seg.find('wire')
x1, y1, x2, y2 = (float(w.get(k)) for k in ('x1', 'y1', 'x2', 'y2'))
mx, my = (x1 + x2) / 2, (y1 + y2) / 2
w.set('x2', str(mx)); w.set('y2', str(my))
ET.SubElement(seg, 'wire', x1=str(mx), y1=str(my), x2=str(x2), y2=str(y2), width='0.1524', layer='91')
self.assertEqual(s.wire_cover(self.sheet), s.wire_cover(sh))
def test_ends_at_counts_a_t(self):
w = self.sheet.find('nets/net/segment/wire')
a = (float(w.get('x1')), float(w.get('y1')))
self.assertGreaterEqual(s.ends_at(self.sheet, a), 1)
self.assertEqual(0, s.ends_at(self.sheet, (-500, -500)))
def test_every_drawn_junction_counts_as_needed(self):
# the rule sch_delete_wire cleans up with: a junction joining a pin and two wires is needed
# (counting wires only once removed 25 good junctions on BQ25792, live 2026-10-27)
pp = s.pin_points(self.root, self.sheet)
for q in s.junction_points(self.sheet):
self.assertGreaterEqual(s.ends_at(self.sheet, q, pins=pp), 3, q)
def test_bus_members(self):
self.assertEqual(['D0', 'D1', 'D2'], s.bus_members('D[0..2]'))
self.assertEqual(['A3', 'A2', 'CLK'], s.bus_members('A[3..2],CLK'))
self.assertEqual(['SDA', 'SCL'], s.bus_members('{SDA SCL}'))
def test_field_home_matches_the_fields_left_in_place(self):
bq = Path('/home/adom/project/fusion-lane/bq25792-charger/bq25792-charger.sch')
if not bq.exists():
self.skipTest('BQ25792 fixture not on this machine')
root = s.sch_root_from_file(str(bq))
_, sh = s.pick_sheet(root, {})
lib = s.Lib(root.find('drawing/schematic'))
n = 0
for inst in sh.findall('instances/instance'):
for a in inst.findall('attribute'):
h = s.field_home(lib, inst, a.get('name'))
if h and abs(h['at'][0] - float(a.get('x'))) < 1e-3 and abs(h['at'][1] - float(a.get('y'))) < 1e-3:
self.assertEqual(h['rot'], a.get('rot', 'R0'))
self.assertEqual(h['align'], a.get('align', 'bottom-left'))
n += 1
self.assertGreater(n, 100)
def test_quotes_are_doubled(self):
self.assertEqual("'it''s'", s.quote("it's"))
class CrossProbeTests(unittest.TestCase):
def test_pairs_by_designator(self):
import xml.etree.ElementTree as ET
sr = ET.parse(str(FIXTURE)).getroot()
br = ET.parse(str(ROOT / 'demo/routing/fixture/fusion-routing.brd')).getroot()
m, without = s.cross_probe_pairs(sr, br, {})
self.assertTrue(m and all(x['linked'] for x in m))
self.assertEqual([], without)
j1 = s.cross_probe_pairs(sr, br, {'ref': 'J1'})[0]
self.assertEqual(['J1'], [x['ref'] for x in j1])
self.assertEqual('J1:G$1', j1[0]['schematic']['id'])
self.assertEqual('top', j1[0]['board']['side'])
miss = s.cross_probe_pairs(sr, br, {'ref': 'ZZ9'})[0]
self.assertIs(False, miss[0]['found'])
def test_no_board_means_unlinked(self):
import xml.etree.ElementTree as ET
sr = ET.parse(str(FIXTURE)).getroot()
m, without = s.cross_probe_pairs(sr, None, {})
self.assertTrue(m and not any(x['linked'] for x in m))
self.assertEqual(sorted(x['ref'] for x in m), sorted(without))
class SchematicDataTests(unittest.TestCase):
def test_parts_pins_and_params(self):
root = s.sch_root_from_file(str(FIXTURE))
d = s.schematic_data(root, {})
self.assertGreater(d['count'], 0)
j1 = [c for c in d['components'] if c['designator'] == 'J1'][0]
self.assertTrue(j1['pins'] and all(p['number'] for p in j1['pins']))
self.assertIn(j1['pins'][0]['electrical'], (0, 1, 2, 3, 4, 5, 7))
names = s.part_parameters(root, {})['names']
self.assertIsInstance(names, list)
class SwapPartCommand(unittest.TestCase):
"""REPLACE takes the part first, then the device. The other order answers "Device not found" from
Fusion and changes nothing: the verb did nothing from 1.13.8 to 1.13.53, and only its dry run was tested."""
def test_replace_names_the_part_first(self):
import os, re
src = open(os.path.join(os.path.dirname(__file__), '..', 'rust', 'bridge', 'src', 'sch_script.py'), encoding='utf-8').read()
body = src[src.index('def sch_swap_part'):src.index('def sch_split_wire')]
self.assertIn("REPLACE %s '%s@%s'; SET CONFIRM OFF;\" % (quote(ref), full, libname)", body)
self.assertNotRegex(body, r"REPLACE '%s@%s' %s")
# Fusion ignores NAME right after REPLACE until its events are pumped
self.assertRegex(body, r"doEvents[\s\S]{0,120}e\.again\(\[\"SET CONFIRM YES; NAME")
if __name__ == '__main__':
unittest.main()