@protoboardai/ext-frame-cad 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/checks/frames.ts +25 -0
- package/checks/hardware.ts +26 -0
- package/evals/regenerate-after-vtx-move/case.json +175 -0
- package/lib/alias.ts +57 -0
- package/lib/artifacts.ts +185 -0
- package/lib/hardware-cad.ts +127 -0
- package/lib/hardware.ts +100 -0
- package/lib/params.ts +255 -0
- package/lib/plate.ts +233 -0
- package/lib/run.ts +24 -0
- package/ops/cad.ts +78 -0
- package/ops/frame-cad.ts +46 -0
- package/ops/hardware-cad.ts +54 -0
- package/ops/package.ts +96 -0
- package/ops/rebind.ts +94 -0
- package/package.json +42 -4
- package/protoboard-extension.json +187 -0
- package/py/frame_cad.py +343 -0
- package/py/hardware_cad.py +111 -0
- package/skills/frame-cad/SKILL.md +279 -0
- package/skills/frame-cad/references/generator.md +110 -0
- package/skills/frame-cad/references/manufacturing.md +42 -0
- package/skills/frame-cad-verify/SKILL.md +117 -0
- package/skills/frame-cad-verify/scripts/dxf_report.py +124 -0
- package/README.md +0 -3
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Manufacturability report for a frame-cad DXF (CNC-routed plates).
|
|
2
|
+
|
|
3
|
+
.protoboard/tools/cad/bin/python dxf_report.py <name>.dxf [--bit 3.0] [--json] (or $PROTOBOARD_PYTHON)
|
|
4
|
+
|
|
5
|
+
Per layer: extent, holes by diameter, the smallest hole-to-edge distance
|
|
6
|
+
(edge = outline or any cut-out), the smallest web between two holes, and
|
|
7
|
+
holes or slots a router bit of the given diameter cannot cut. Uses ezdxf,
|
|
8
|
+
which build123d already depends on. Exit 1 when a rule is violated:
|
|
9
|
+
- hole edge distance < hole diameter (breakout risk in carbon)
|
|
10
|
+
- web between holes < 1 mm
|
|
11
|
+
- a hole smaller than the bit (drill it, and say so) is reported, not failed
|
|
12
|
+
- a straight cut-out narrower than the bit
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import math
|
|
18
|
+
import sys
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
|
|
21
|
+
import ezdxf
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def seg_points(e, step=1.0):
|
|
25
|
+
t = e.dxftype()
|
|
26
|
+
if t == "LINE":
|
|
27
|
+
return [(e.dxf.start.x, e.dxf.start.y), (e.dxf.end.x, e.dxf.end.y)]
|
|
28
|
+
if t == "ARC":
|
|
29
|
+
a0, a1 = e.dxf.start_angle, e.dxf.end_angle
|
|
30
|
+
if a1 < a0:
|
|
31
|
+
a1 += 360
|
|
32
|
+
n = max(2, int(math.radians(a1 - a0) * e.dxf.radius / step) + 1)
|
|
33
|
+
c, r = e.dxf.center, e.dxf.radius
|
|
34
|
+
return [(c.x + r * math.cos(math.radians(a0 + (a1 - a0) * k / (n - 1))), c.y + r * math.sin(math.radians(a0 + (a1 - a0) * k / (n - 1)))) for k in range(n)]
|
|
35
|
+
try:
|
|
36
|
+
return [(p.x, p.y) for p in e.flattening(0.05)]
|
|
37
|
+
except Exception: # noqa: BLE001
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def point_seg(p, a, b):
|
|
42
|
+
ax, ay, bx, by = *a, *b
|
|
43
|
+
dx, dy = bx - ax, by - ay
|
|
44
|
+
L = dx * dx + dy * dy
|
|
45
|
+
t = 0.0 if L == 0 else max(0.0, min(1.0, ((p[0] - ax) * dx + (p[1] - ay) * dy) / L))
|
|
46
|
+
return math.hypot(p[0] - (ax + t * dx), p[1] - (ay + t * dy))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> None:
|
|
50
|
+
args = sys.argv[1:]
|
|
51
|
+
path = args[0]
|
|
52
|
+
bit = float(args[args.index("--bit") + 1]) if "--bit" in args else 3.0
|
|
53
|
+
doc = ezdxf.readfile(path)
|
|
54
|
+
layers: dict[str, dict] = defaultdict(lambda: {"holes": [], "polys": []})
|
|
55
|
+
for e in doc.modelspace():
|
|
56
|
+
L = layers[e.dxf.layer]
|
|
57
|
+
if e.dxftype() == "CIRCLE":
|
|
58
|
+
L["holes"].append(((e.dxf.center.x, e.dxf.center.y), e.dxf.radius))
|
|
59
|
+
else:
|
|
60
|
+
pts = seg_points(e)
|
|
61
|
+
if len(pts) >= 2:
|
|
62
|
+
L["polys"].append(pts)
|
|
63
|
+
report, violations = {}, []
|
|
64
|
+
for name, L in sorted(layers.items()):
|
|
65
|
+
segs = [(a, b) for pts in L["polys"] for a, b in zip(pts, pts[1:])]
|
|
66
|
+
xs = [p[0] for pts in L["polys"] for p in pts] + [c[0] for c, _ in L["holes"]]
|
|
67
|
+
ys = [p[1] for pts in L["polys"] for p in pts] + [c[1] for c, _ in L["holes"]]
|
|
68
|
+
by_d = defaultdict(int)
|
|
69
|
+
for _, r in L["holes"]:
|
|
70
|
+
by_d[round(2 * r, 3)] += 1
|
|
71
|
+
edge = []
|
|
72
|
+
for c, r in L["holes"]:
|
|
73
|
+
d = min((point_seg(c, a, b) for a, b in segs), default=math.inf) - r
|
|
74
|
+
edge.append((d, c, r))
|
|
75
|
+
if d < 2 * r:
|
|
76
|
+
violations.append(f"{name}: hole Ø{2 * r:.2f} at ({c[0]:.2f}, {c[1]:.2f}) is {d:.2f} mm from an edge (< its diameter)")
|
|
77
|
+
drill = f"{name}: Ø{2 * r:.2f} holes are smaller than the {bit} mm bit: drill them (note it on the drawing)"
|
|
78
|
+
if 2 * r < bit and drill not in violations:
|
|
79
|
+
violations.append(drill)
|
|
80
|
+
web = []
|
|
81
|
+
hs = L["holes"]
|
|
82
|
+
for i in range(len(hs)):
|
|
83
|
+
for j in range(i + 1, len(hs)):
|
|
84
|
+
w = math.hypot(hs[i][0][0] - hs[j][0][0], hs[i][0][1] - hs[j][0][1]) - hs[i][1] - hs[j][1]
|
|
85
|
+
web.append(w)
|
|
86
|
+
if w < 1.0:
|
|
87
|
+
violations.append(f"{name}: {w:.2f} mm web between holes at ({hs[i][0][0]:.1f}, {hs[i][0][1]:.1f}) and ({hs[j][0][0]:.1f}, {hs[j][0][1]:.1f})")
|
|
88
|
+
# narrow straight cut-outs: two parallel, overlapping edges closer than the bit
|
|
89
|
+
narrow = []
|
|
90
|
+
lines = [(a, b) for a, b in segs if math.hypot(b[0] - a[0], b[1] - a[1]) > 0]
|
|
91
|
+
for (a, b) in lines:
|
|
92
|
+
for (c, d) in lines:
|
|
93
|
+
if (a, b) >= (c, d):
|
|
94
|
+
continue
|
|
95
|
+
v1 = (b[0] - a[0], b[1] - a[1])
|
|
96
|
+
v2 = (d[0] - c[0], d[1] - c[1])
|
|
97
|
+
cross = v1[0] * v2[1] - v1[1] * v2[0]
|
|
98
|
+
if abs(cross) > 1e-6 * math.hypot(*v1) * math.hypot(*v2):
|
|
99
|
+
continue # not parallel
|
|
100
|
+
gap = point_seg(c, a, b)
|
|
101
|
+
if 0 < gap < bit and min(math.hypot(*v1), math.hypot(*v2)) > 2 * bit and point_seg(d, a, b) - gap < 1e-6:
|
|
102
|
+
narrow.append(round(gap, 3))
|
|
103
|
+
for g in sorted(set(narrow)):
|
|
104
|
+
violations.append(f"{name}: a straight cut-out {g} mm wide is narrower than the {bit} mm bit")
|
|
105
|
+
report[name] = {
|
|
106
|
+
"extent_mm": [round(min(xs), 3), round(max(xs), 3), round(min(ys), 3), round(max(ys), 3)] if xs else None,
|
|
107
|
+
"holes_by_diameter": dict(sorted(by_d.items())),
|
|
108
|
+
"min_edge_distance_mm": round(min(edge)[0], 3) if edge else None,
|
|
109
|
+
"min_web_mm": round(min(web), 3) if web else None,
|
|
110
|
+
}
|
|
111
|
+
out = {"file": path, "bit_mm": bit, "layers": report, "violations": violations}
|
|
112
|
+
if "--json" in args:
|
|
113
|
+
print(json.dumps(out, indent=2))
|
|
114
|
+
else:
|
|
115
|
+
for name, r in report.items():
|
|
116
|
+
print(f"{name}: extent {r['extent_mm']}, holes {r['holes_by_diameter']}, min edge distance {r['min_edge_distance_mm']} mm, min web {r['min_web_mm']} mm")
|
|
117
|
+
for v in violations:
|
|
118
|
+
print(f" ! {v}")
|
|
119
|
+
hard = [v for v in violations if "drill them" not in v]
|
|
120
|
+
sys.exit(1 if hard else 0)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
main()
|
package/README.md
DELETED