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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
"""Hot paths of the revision digest, offline: what each native read costs is the point, so these count reads."""
import importlib
import time
import unittest.mock
from types import SimpleNamespace as NS
import unittest
from test_mechanical import adsk
core = importlib.import_module('mechanical_test_commands.mechanical.core')
readback = importlib.import_module('mechanical_test_commands.mechanical.readback')
regular_joints = importlib.import_module('mechanical_test_commands.mechanical.regular_joints')
bom = importlib.import_module('mechanical_test_commands.mechanical.bill_of_materials')
class Counted:
"""An occurrence whose entityToken read is counted (it costs about 2 ms in Fusion)."""
objectType = "adsk::fusion::Occurrence"
def __init__(self, path):
self.fullPathName, self.reads, self.isValid = path, 0, True
@property
def entityToken(self):
self.reads += 1
return "token-" + self.fullPathName
class Identity(unittest.TestCase):
def test_a_known_entity_is_found_without_reading_its_token(self):
occ = Counted("Frame:1+Rail:3")
first = core.identity(occ)
reads = occ.reads
for _ in range(5):
self.assertEqual(core.identity(occ), first)
self.assertEqual(occ.reads, reads)
def test_a_renamed_entity_is_still_found_by_its_token(self):
occ = Counted("Frame:1+Rail:4")
key = core.identity(occ)
occ.fullPathName = "Frame:1+Rail renamed:4" # new bucket; same token string? no: token follows the object
occ.__class__ = type("Renamed", (Counted,), {"entityToken": property(lambda self: "token-Frame:1+Rail:4")})
self.assertEqual(core.identity(occ), key)
def test_a_reference_reuses_the_token_read_at_registration(self):
occ = Counted("Frame:1+Rail:5")
core.identity(occ)
reads = occ.reads
for _ in range(3):
self.assertEqual(core.entity_token(occ), "token-Frame:1+Rail:5")
self.assertEqual(occ.reads, reads)
def test_an_unregistered_occurrence_reads_its_token_once(self):
occ = Counted("Frame:1+Rail:6")
self.assertEqual(core.entity_token(occ), "token-Frame:1+Rail:6")
self.assertEqual(core.entity_token(occ), "token-Frame:1+Rail:6")
self.assertEqual(occ.reads, 1)
def test_other_entities_read_their_own_token(self):
body = NS(objectType="adsk::fusion::BRepBody", entityToken="body-token")
self.assertEqual(core.entity_token(body), "body-token")
class Body:
"""A body whose witness reads are counted (physical properties cost milliseconds in Fusion)."""
objectType = "adsk::fusion::BRepBody"
def __init__(self, name, component):
self.name, self.parentComponent, self.isValid, self.revisionId = name, component, True, "b1"
self.material, self.reads = NS(id="steel"), 0
empty = NS(count=0, item=lambda i: None)
self.faces, self.edges, self.isSolid = empty, empty, True
self.boundingBox = NS(minPoint=NS(x=0, y=0, z=0), maxPoint=NS(x=1, y=1, z=1))
@property
def physicalProperties(self):
self.reads += 1
return NS(volume=1, area=6, mass=7.8, centerOfMass=NS(x=0.5, y=0.5, z=0.5))
class Witnesses(unittest.TestCase):
def test_an_unchanged_body_is_not_read_again(self):
body = Body("Plate", NS(name="Bracket"))
first = readback.topology_witness(body)
self.assertEqual(readback.topology_witness(body), first)
self.assertEqual(body.reads, 1)
def test_a_changed_body_material_or_name_is_read_again(self):
body = Body("Plate", NS(name="Bracket 2"))
readback.topology_witness(body)
body.revisionId = "b2"
readback.topology_witness(body)
body.material = NS(id="aluminum")
readback.topology_witness(body)
self.assertEqual(body.reads, 3)
def test_a_cached_witness_is_a_copy(self):
body = Body("Plate", NS(name="Bracket 3"))
readback.topology_witness(body)["name"] = "changed by a caller"
self.assertEqual(readback.topology_witness(body)["name"], "Plate")
class Feature:
objectType = "adsk::fusion::BaseFeature"
def __init__(self, name, component):
self.name, self.parentComponent, self.isValid, self.index_reads = name, component, True, 0
feature = self
class Timeline:
isSuppressed, isValid = False, True
@property
def index(self):
feature.index_reads += 1
raise RuntimeError("2 : InternalValidationError : res >= 0")
self.timelineObject = Timeline()
class UnindexedFeatures(unittest.TestCase):
def test_a_refused_timeline_index_is_asked_once_per_component_revision(self):
comp = NS(name="Vendor model", revisionId="r1")
feature = Feature("Base Feature1", comp)
for _ in range(4):
data = readback.feature_data(feature, order=0)
self.assertEqual((data["index"], data["indexUnavailable"]), (None, "native_timeline_position_unavailable"))
self.assertEqual(feature.index_reads, 1)
comp.revisionId = "r2" # the component changed: ask again
readback.feature_data(feature, order=0)
self.assertEqual(feature.index_reads, 2)
class Components(unittest.TestCase):
def comp(self, name):
return NS(objectType="adsk::fusion::Component", name=name, isValid=True)
def design(self, comps, occurrences):
reads = []
collection = NS(count=len(comps), item=lambda i: reads.append(i) or comps[i])
return NS(rootComponent=comps[0], allComponents=collection), reads
def test_components_come_from_occurrences_without_reading_the_slow_collection(self):
root, a, b = self.comp("root"), self.comp("A"), self.comp("B")
occurrences = [NS(component=a), NS(component=b), NS(component=a)]
design, reads = self.design([root, a, b], occurrences)
found = core.components_by_occurrence(design, occurrences, 3)
self.assertEqual([c.name for c in found], ["root", "A", "B"])
self.assertEqual(reads, [])
def test_a_count_mismatch_falls_back_to_the_full_list(self):
root, a, orphan = self.comp("root"), self.comp("A"), self.comp("Orphan")
design, reads = self.design([root, a, orphan], [NS(component=a)])
found = core.components_by_occurrence(design, [NS(component=a)], 3)
self.assertEqual([c.name for c in found], ["root", "A", "Orphan"])
self.assertEqual(reads, [0, 1, 2])
class Level:
"""Occurrences under one parent, found by name the way Fusion's itemByName does."""
def __init__(self, occurrences=()):
self.values = list(occurrences)
@property
def count(self):
return len(self.values)
def item(self, index):
return self.values[index]
def itemByName(self, name):
return next((o for o in self.values if o.name == name), None)
class Occ:
objectType = "adsk::fusion::Occurrence"
compared = 0
def __init__(self, name, children=(), parent=None):
self.name, self.isValid, self.parent = name, True, parent
self.childOccurrences = Level(children)
for child in children:
child.parent = self
@property
def fullPathName(self):
return (self.parent.fullPathName + "+" if self.parent else "") + self.name
def __eq__(self, other):
Occ.compared += 1
return self is other
__hash__ = object.__hash__
class Paths(unittest.TestCase):
def tree(self):
c = Occ("Nut:1")
b = Occ("Bolt:1", [c])
a = Occ("Frame:1", [b])
plus = Occ("M3+washer:1", [Occ("Ring:2")])
every = [a, b, c, plus, plus.childOccurrences.item(0)]
class Everything(Level):
reads = 0
def item(self, index):
Everything.reads += 1
return self.values[index]
all_occurrences = Everything(every)
design = NS(rootComponent=NS(occurrences=Level([a, plus]), allOccurrences=all_occurrences))
return design, a, b, c, plus, all_occurrences
def test_an_occurrence_is_found_by_walking_its_path(self):
design, a, b, c, plus, _ = self.tree()
self.assertIs(core.occurrence_at(design, "Frame:1+Bolt:1+Nut:1"), c)
self.assertIs(core.occurrence_at(design, "Frame:1"), a)
self.assertIsNone(core.occurrence_at(design, "Frame:1+Washer:1"))
self.assertIsNone(core.occurrence_at(design, ""))
def test_a_name_holding_a_plus_is_still_found(self):
design, a, b, c, plus, _ = self.tree()
self.assertIs(core.occurrence_at(design, "M3+washer:1+Ring:2"), plus.childOccurrences.item(0))
def test_resolving_a_context_does_not_read_every_occurrence(self):
design, a, b, c, plus, every = self.tree()
every.__class__.reads = 0
self.assertIs(core.occurrence_context(design, c, "Frame:1+Bolt:1+Nut:1"), c)
self.assertTrue(core.in_root_context(design, b))
self.assertEqual(every.__class__.reads, 0)
def test_an_occurrence_outside_the_root_context_still_reads_them_all(self):
design, a, b, c, plus, every = self.tree()
local = Occ("Nut:1") # a definition-local occurrence: its path is relative to its parent
every.__class__.reads = 0
self.assertFalse(core.in_root_context(design, local))
self.assertEqual(every.__class__.reads, every.count)
class JointPoses(unittest.TestCase):
def test_pose_check_compares_each_occurrence_once(self):
occurrences = [Occ("Part:%d" % i) for i in range(200)]
for o in occurrences:
o.transform2 = NS(asArray=lambda: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])
o.isGrounded, o.isGroundToParent = False, False
design = NS(rootComponent=NS(allOccurrences=Level(occurrences)))
before = regular_joints.poses(design)
Occ.compared = 0
result = regular_joints.pose_result(design, before)
self.assertEqual(result["changedOccurrences"], [])
self.assertLessEqual(Occ.compared, len(occurrences))
class Part:
"""A component whose part number is a counted cloud read (about 0.5 s each in Fusion)."""
objectType = "adsk::fusion::Component"
def __init__(self, name):
self.name, self.isValid, self.revisionId, self.reads = name, True, "r1", 0
self.bRepBodies, self.occurrences, self.attributes, self.material = Level(), Level(), Level(), None
@property
def partNumber(self):
self.reads += 1
return "PN-" + self.name
description = "a part"
class BomBudget(unittest.TestCase):
def design(self, tag):
parts = [Part("%s %d" % (tag, i)) for i in range(3)]
occurrences = [NS(objectType="adsk::fusion::Occurrence", component=p, fullPathName=p.name + ":1", isLightBulbOn=True,
timelineObject=NS(isSuppressed=False), childOccurrences=Level(), bRepBodies=Level(), isConfiguration=False)
for p in parts]
root = Part(tag + " root")
root.occurrences, root.allOccurrences = Level(occurrences), Level(occurrences)
design = NS(isConfiguredDesign=False, isConfiguration=False, configurationRowId="", rootComponent=root)
return design, [root] + parts
def generate(self, design, started):
with unittest.mock.patch.object(bom, "ref", return_value={}):
return bom.generate(design, {"documentId": "D", "revision": "R"}, {}, started, 40)
def test_past_the_budget_part_numbers_are_left_for_the_next_run(self):
design, comps = self.design("Late")
result = self.generate(design, time.perf_counter() - 100)
self.assertEqual(result["nativePending"], 4)
self.assertEqual(sum(c.reads for c in comps), 0)
self.assertIn("run bom again", result["nativeHint"])
def test_inside_the_budget_every_part_number_is_read(self):
design, comps = self.design("Early")
result = self.generate(design, time.perf_counter())
self.assertNotIn("nativePending", result)
self.assertEqual([r["metadata"]["partNumber"] for r in result["instances"]], ["PN-" + c.name for c in comps])
def test_what_an_earlier_run_read_is_used_past_the_budget(self):
design, comps = self.design("Again")
self.generate(design, time.perf_counter())
result = self.generate(design, time.perf_counter() - 100)
self.assertNotIn("nativePending", result)
self.assertEqual(sum(c.reads for c in comps), 4)
if __name__ == "__main__":
unittest.main()