app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
main
John Lauer
Publish 0.2.19
81ffca7
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
"""Issue #89 (reply), offline (synthetic step_meta, a mocked service; no network): `adom-aiflow-etch outline
--mode etched|composed|auto`. The etched mode draws the name from the etched model's own mark edges (read from
the STEP text, posed with the etch's quarter turns, projected with iso() and the body drawing's calibrate()),
and the outline gate (mark_check.check_outline) passes it; auto falls back to composed when the etched STEP
has no mark or its mark fails the gate; etched refuses without a readable mark.
Run: python3 -m unittest -v test_outline_modes (from tools/laser-etch)"""
import os, re, sys, tempfile, unittest
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import mark_geom as MG
import mark_outline as MO
import step_text as ST
import test_mark_plan_rows as T
STROKE = ' fill="none" stroke="#8b949e" stroke-width="1"'
class FakeService:
"""/outline draws the posed body as one polyline through its projected vertices (the extents are what
calibrate() reads); a named request (the composed name) answers `named`; /step-meta answers `meta`."""
def __init__(self, meta, named=None):
self.meta, self.named, self.calls = meta, named or {}, []
def outline(self, text, name=None, rot=None, suffix=None, line2=None, job=""):
self.calls.append(("outline", name))
if name:
return {"outlines": self.named}
P = [MG.iso(v) for v in MG.vertices(ST.Step(text))]
pts = " ".join("%.3f,%.3f" % (160 + 40 * x, 160 + 40 * y) for x, y in P)
return {"outlines": {"dark": f'<svg viewBox="0 0 320 320"><polyline points="{pts}"{STROKE}/></svg>'}}
def step_meta(self, text, job=""):
self.calls.append(("meta", job))
return self.meta
def __enter__(self):
self._o = (MO.SVC.outline, MO.SVC.step_meta)
MO.SVC.outline, MO.SVC.step_meta = self.outline, self.step_meta
return self
def __exit__(self, *a):
MO.SVC.outline, MO.SVC.step_meta = self._o
def letters(plan, cap):
"""Two lines of glyph boxes on the planned band in the etch frame (reading +X), centred on the band."""
b = plan["band"]["regionMm"]; z = plan["band"]["zMm"]; qz = plan["etch"]["poseQuarterTurnsZ"]
pts = [MO.MP._rot([x, y, 0], "z", qz) for x, y in ((b[0], b[1]), (b[2], b[3]))]
cx = (min(p[0] for p in pts) + max(p[0] for p in pts)) / 2; cy = (min(p[1] for p in pts) + max(p[1] for p in pts)) / 2
out = []
for row, n in ((1, 4), (-1, 5)):
y0 = cy + row * 0.6 * cap - cap / 2
for i in range(n):
x0 = cx + (i - n / 2) * 0.7 * cap
w = 0.55 * cap
out.append([[x0, y0, z], [x0 + w, y0, z], [x0 + w, y0 + cap, z], [x0, y0 + cap, z], [x0, y0, z]])
return out
class OutlineModes(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.plan = T.run_plan(T.tssop16_meta(), T.tssop16_pads())
# the body: the proxy box sized to the TSSOP-16 body the plan was made for
self.step = os.path.join(self.tmp.name, "part.step")
ST.Step(os.path.join(HERE, "proxy_box.step")).scale_axes(-2.2, -2.5, 0.0, 4.4, 5.0, 1.1).write(self.step)
self.etched = os.path.join(self.tmp.name, "part-etched.step")
ST.Step(self.step).write(self.etched)
self._edges = MG.mark_edges
def tearDown(self):
MG.mark_edges = self._edges
self.tmp.cleanup()
def run_mode(self, mode, edges, named=None, etched=True):
MG.mark_edges = lambda step, name=None, tol=1e-3: edges
logs = []
with FakeService(T.tssop16_meta(), named) as svc:
r = MO.outlines(self.step, self.plan, self.etched if etched else None, self.tmp.name, ("dark",), "P", log=logs.append, mode=mode)
return r, logs, svc
def test_etched_mode_draws_the_mark_edges_and_passes_the_gate(self):
r, logs, svc = self.run_mode("etched", letters(self.plan, 0.73))
o = r["outlines"][0]
self.assertEqual(o["gate"]["status"], "PASS", (o["gate"], logs))
self.assertTrue(r["mode"].startswith("etched"))
svg = open(o["path"]).read()
self.assertIn('data-source="etched-step-mark-edges"', svg)
self.assertIn('data-lines="74HC|595PW"', svg)
self.assertEqual(len(re.findall(r"<polyline", re.search(r'<g id="adom-mark-name".*?</g>', svg, re.S).group(0))), 9)
self.assertIn('stroke="#8b949e"', svg.split('id="adom-mark-name"')[1]) # the body's own stroke style
self.assertNotIn(("outline", "74HC"), svc.calls) # no composed name was drawn
self.assertGreater(o["gate"]["capHeightPx320"], 6.0)
def test_auto_uses_the_etched_mark(self):
r, _, _ = self.run_mode("auto", letters(self.plan, 0.73))
self.assertEqual(r["outlines"][0]["mode"], MO.ETCHED_MODE)
def test_auto_without_a_mark_composes(self):
r, logs, svc = self.run_mode("auto", [])
self.assertIn(("outline", "74HC"), svc.calls) # the composed name was asked for
self.assertTrue(any("no readable mark edges" in m for m in logs), logs)
def test_auto_falls_back_when_the_mark_fails_the_gate(self):
r, logs, svc = self.run_mode("auto", letters(self.plan, 0.05)) # a mark too small to read in the widget
self.assertTrue(any("fail the outline gate" in m for m in logs), logs)
self.assertIn(("outline", "74HC"), svc.calls)
def test_composed_mode_never_reads_the_mark(self):
r, logs, svc = self.run_mode("composed", letters(self.plan, 0.73))
self.assertIn(("outline", "74HC"), svc.calls)
self.assertFalse(any(o["mode"] == MO.ETCHED_MODE for o in r["outlines"]))
def test_etched_mode_needs_a_readable_etched_step(self):
with self.assertRaises(SystemExit) as e:
self.run_mode("etched", letters(self.plan, 0.73), etched=False)
self.assertIn("--etched", str(e.exception))
with self.assertRaises(SystemExit) as e:
self.run_mode("etched", [])
self.assertIn("no readable mark edges", str(e.exception))
class StepEdges(unittest.TestCase):
"""Edge points read from STEP text: a line's two vertices, a B-spline evaluated (plain and rational)."""
TEXT = """ISO-10303-21;
HEADER;
ENDSEC;
DATA;
#1=CARTESIAN_POINT('',(0.,0.,1.));
#2=VERTEX_POINT('',#1);
#3=CARTESIAN_POINT('',(1.,0.,1.));
#4=VERTEX_POINT('',#3);
#5=LINE('',#1,#6);
#6=VECTOR('',#7,1.);
#7=DIRECTION('',(1.,0.,0.));
#8=EDGE_CURVE('',#2,#4,#5,.T.);
#9=CARTESIAN_POINT('',(1.,1.,1.));
#10=CARTESIAN_POINT('',(0.,1.,1.));
#11=VERTEX_POINT('',#10);
#12=B_SPLINE_CURVE_WITH_KNOTS('',2,(#3,#9,#10),
.UNSPECIFIED.,.F.,.F.,(3,3),(0.,1.),.PIECEWISE_BEZIER_KNOTS.);
#13=SURFACE_CURVE('',#12,(#99),.PCURVE_S1.);
#14=EDGE_CURVE('',#4,#11,#13,.T.);
#15=( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#3,#9,#10),.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((3,3),(0.,1.),.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() RATIONAL_B_SPLINE_CURVE((1.,0.7071067811865476,1.)) REPRESENTATION_ITEM('') );
#16=EDGE_CURVE('',#4,#11,#15,.T.);
#99=CARTESIAN_POINT('',(0.,0.,0.));
ENDSEC;
END-ISO-10303-21;
"""
def test_line_and_splines(self):
s = ST.Step(self.TEXT)
self.assertEqual(MG.edge_points(s, 8), [[0.0, 0.0, 1.0], [1.0, 0.0, 1.0]])
q = MG.edge_points(s, 14)
self.assertEqual(q[0], [1.0, 0.0, 1.0]); self.assertEqual(q[-1], [0.0, 1.0, 1.0])
mid = q[len(q) // 2]
self.assertAlmostEqual(mid[0], 0.75, 2); self.assertAlmostEqual(mid[1], 0.75, 2) # the parabola, not the control point
r = MG.edge_points(s, 16) # a quarter circle about (0,0)
for p in r:
self.assertAlmostEqual((p[0] ** 2 + p[1] ** 2) ** 0.5, 1.0, 6)
if __name__ == "__main__":
unittest.main()