@huaqiu/dsh-kicad 0.4.13 → 0.5.2
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/lib/index.mjs +5 -0
- package/package.json +2 -2
- package/skills/hardware-design-brief/SKILL.md +65 -31
- package/skills/hardware-design-brief/agents/openai.yaml +3 -3
- package/skills/hardware-design-brief/references/design-brief-guide.md +84 -11
- package/skills/hardware-design-brief/references/examples/hardware-solution-example.md +442 -0
- package/skills/hardware-design-brief/references/hardware-prd-guide.md +53 -10
- package/skills/hardware-design-brief/references/mrd-guide.md +45 -10
- package/skills/hardware-design-brief/references/multi-board-guide.md +51 -0
- package/skills/hardware-design-brief/references/project-controls.md +45 -0
- package/skills/hardware-design-brief/references/solution-deliverable.md +55 -0
- package/skills/hardware-design-brief/references/stage-boundaries.md +32 -0
- package/skills/pcb-initial-placement/SKILL.md +343 -0
- package/skills/pcb-initial-placement/agents/openai.yaml +4 -0
- package/skills/pcb-initial-placement/references/algorithm.md +317 -0
- package/skills/pcb-initial-placement/scripts/apply_via_ipc.py +211 -0
- package/skills/pcb-initial-placement/scripts/board_geometry.py +368 -0
- package/skills/pcb-initial-placement/scripts/kipy_common.py +80 -0
- package/skills/pcb-initial-placement/scripts/pack.py +576 -0
- package/skills/pcb-initial-placement/scripts/placement_config.example.json +116 -0
- package/skills/pcb-initial-placement/scripts/placement_view.py +340 -0
- package/skills/pcb-initial-placement/scripts/pose_table.py +86 -0
- package/skills/pcb-initial-placement/scripts/projpaths.py +160 -0
- package/skills/pcb-initial-placement/scripts/sexpr.py +180 -0
- package/skills/pcb-initial-placement/scripts/suggest_modules.py +420 -0
- package/skills/pcb-initial-placement/scripts/verify_placement.py +226 -0
- package/skills/pcb-initial-placement/scripts/write_board_file.py +109 -0
- package/src/skills.ts +11 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": [
|
|
3
|
+
"Phase-1 placement config for the KiCad 'video' demo board (312x106.7 mm PCI card).",
|
|
4
|
+
"This is the worked example behind the pcb-initial-placement skill: every number here",
|
|
5
|
+
"was derived from the board's own geometry (see references/algorithm.md).",
|
|
6
|
+
"Copy it, replace the modules/anchors/fixed entries with your own board's facts,",
|
|
7
|
+
"and run: pack.py -> verify_placement.py -> apply_via_ipc.py"
|
|
8
|
+
],
|
|
9
|
+
|
|
10
|
+
"board": "video.kicad_pcb",
|
|
11
|
+
"grid_mm": 0.635,
|
|
12
|
+
"gap_mm": 0.3,
|
|
13
|
+
"big_part_area_mm2": 40.0,
|
|
14
|
+
"radius_schedule_mm": [30, 45, 65, 90, 130, 200],
|
|
15
|
+
"module_order": ["M1", "M4", "M5", "M6", "M3", "M2"],
|
|
16
|
+
|
|
17
|
+
"fixed": {
|
|
18
|
+
"BUS1": {"pad": "A1", "at": [95.25, 160.195], "rot": 0,
|
|
19
|
+
"_comment": "PCI card-edge fingers: pin A1 sits on the board edge, so the whole connector is derived from the Edge.Cuts geometry"},
|
|
20
|
+
|
|
21
|
+
"P1": {"x": 58.0, "y": 63.0, "rot": 180, "_comment": "BNC bracket connector, body overhangs the left edge x=53.594"},
|
|
22
|
+
"P2": {"x": 58.0, "y": 74.4, "rot": 180},
|
|
23
|
+
"P3": {"x": 58.0, "y": 85.8, "rot": 180},
|
|
24
|
+
"P8": {"x": 58.0, "y": 97.2, "rot": 180},
|
|
25
|
+
"J4": {"x": 68.154, "y": 137.0, "rot": 90,
|
|
26
|
+
"_comment": "DB9; footprint is EdgePinOffset14.56mm so origin x = 53.594 + 14.56"},
|
|
27
|
+
|
|
28
|
+
"U10": {"x": 80.25, "y": 73.0, "rot": 0, "_comment": "video decoder, next to the BNC inputs"},
|
|
29
|
+
"U8": {"x": 113.9, "y": 74.3, "rot": 0, "_comment": "RAMDAC"},
|
|
30
|
+
"U9": {"x": 111.25, "y": 107.0, "rot": 0, "_comment": "encoder"},
|
|
31
|
+
"U20": {"x": 69.9, "y": 100.0, "rot": 0, "_comment": "video encoder"},
|
|
32
|
+
|
|
33
|
+
"U11": {"x": 116.25, "y": 138.5, "rot": 0, "_comment": "PCI controller, hugging the card edge"},
|
|
34
|
+
"U24": {"x": 152.25, "y": 138.5, "rot": 0, "_comment": "main FPGA, 47 nets to U11"},
|
|
35
|
+
"U23": {"x": 184.25, "y": 127.25, "rot": 90},
|
|
36
|
+
"U22": {"x": 205.8, "y": 124.65, "rot": 0, "_comment": "SIMM controller, 32 nets to every socket"},
|
|
37
|
+
"U2": {"x": 179.0, "y": 143.9, "rot": 90},
|
|
38
|
+
|
|
39
|
+
"U3": {"x": 221.5, "y": 123.0, "rot": 0, "_comment": "SIMM data-bus buffers, lined up beside the bank"},
|
|
40
|
+
"U4": {"x": 234.5, "y": 123.0, "rot": 0},
|
|
41
|
+
"U5": {"x": 247.5, "y": 123.0, "rot": 0},
|
|
42
|
+
"U6": {"x": 260.5, "y": 123.0, "rot": 0},
|
|
43
|
+
|
|
44
|
+
"U12": {"x": 189.494, "y": 61.2, "rot": 180, "_comment": "SIMM bank: 2 columns x 4 rows, 14.5 mm row pitch (0.571 in)"},
|
|
45
|
+
"U13": {"x": 189.494, "y": 74.7, "rot": 180},
|
|
46
|
+
"U14": {"x": 189.494, "y": 88.2, "rot": 180},
|
|
47
|
+
"U15": {"x": 189.494, "y": 101.7, "rot": 180},
|
|
48
|
+
"U16": {"x": 307.634, "y": 61.2, "rot": 180},
|
|
49
|
+
"U17": {"x": 307.634, "y": 74.7, "rot": 180},
|
|
50
|
+
"U18": {"x": 307.634, "y": 88.2, "rot": 180},
|
|
51
|
+
"U19": {"x": 307.634, "y": 101.7, "rot": 180}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
"keepouts": [
|
|
55
|
+
{"rect": [131.594, 105.1, 247.494, 115.8],
|
|
56
|
+
"why": "SIMM modules lean towards +y once inserted and cover the space below row 4; keep the tilt volume clear of copper-side parts"},
|
|
57
|
+
{"rect": [249.734, 105.1, 365.634, 115.8],
|
|
58
|
+
"why": "same for the second SIMM column"}
|
|
59
|
+
],
|
|
60
|
+
|
|
61
|
+
"priority": ["RR1", "RR2", "RR3", "RR4", "RR5", "RR6", "RR7", "RR8",
|
|
62
|
+
"X1", "X2", "U1", "U7", "U21", "CV1", "POT1", "U20"],
|
|
63
|
+
|
|
64
|
+
"modules": {
|
|
65
|
+
"M1": {
|
|
66
|
+
"title": "板边连接器 bracket connectors",
|
|
67
|
+
"anchors": [[79.0, 137.0], [58.0, 62.0], [58.0, 80.0], [58.0, 99.0]],
|
|
68
|
+
"members": ["J4", "P1", "P2", "P3", "P8",
|
|
69
|
+
"R22", "R23", "R24", "R39", "R40", "R41", "R42", "R43", "R44",
|
|
70
|
+
"R45", "R46", "R47", "C3", "C4", "C59"]
|
|
71
|
+
},
|
|
72
|
+
"M2": {
|
|
73
|
+
"title": "PCI 总线接口 PCI bus interface",
|
|
74
|
+
"anchors": [[116.25, 138.5], [158.1, 158.4], [240.0, 123.0]],
|
|
75
|
+
"members": ["BUS1", "U11", "U1", "U3", "U4", "U5", "U6",
|
|
76
|
+
"RR1", "RR2", "RR3", "RR4", "RR5", "RR6",
|
|
77
|
+
"R5", "R6", "R7", "R28", "R29", "C68", "C69",
|
|
78
|
+
"W1", "W2", "W3", "W4", "W5", "C9", "C11", "C17", "C27", "C31"]
|
|
79
|
+
},
|
|
80
|
+
"M3": {
|
|
81
|
+
"title": "图形逻辑 FPGA",
|
|
82
|
+
"anchors": [[152.25, 138.5], [179.0, 143.9], [184.25, 127.25],
|
|
83
|
+
[205.8, 124.65], [177.0, 120.0]],
|
|
84
|
+
"members": ["U24", "U23", "U22", "U21", "U2", "U7",
|
|
85
|
+
"RR7", "RR8", "R1", "R13", "R21", "R26", "R30", "R31", "R32",
|
|
86
|
+
"R33", "R34", "C23", "C49", "C50", "C63", "D6", "P4", "P5",
|
|
87
|
+
"P9", "P10", "P11", "P12", "X1", "C10", "C15", "C18", "C19",
|
|
88
|
+
"C20", "C21", "R48"]
|
|
89
|
+
},
|
|
90
|
+
"M4": {
|
|
91
|
+
"title": "视频输入解码 video capture",
|
|
92
|
+
"anchors": [[80.25, 73.0], [68.0, 100.0]],
|
|
93
|
+
"members": ["U10", "X2", "C1", "C2", "C5", "C6", "C7", "C8", "C12", "C13",
|
|
94
|
+
"C14", "C16", "C62", "C65", "C66", "D1", "D2", "D3", "D4",
|
|
95
|
+
"L1", "L2", "L3", "L4", "L6", "R2", "R3", "R12", "R14", "R19",
|
|
96
|
+
"R20", "R25", "R27", "R35", "Q1", "Q2", "Q3", "CV1", "POT1",
|
|
97
|
+
"C47", "C55"]
|
|
98
|
+
},
|
|
99
|
+
"M5": {
|
|
100
|
+
"title": "视频输出 RAMDAC+encoder",
|
|
101
|
+
"anchors": [[113.9, 74.3], [111.25, 107.0], [69.9, 100.0]],
|
|
102
|
+
"members": ["U8", "U9", "U20", "X3", "C32", "C33", "C34", "C35", "C36",
|
|
103
|
+
"C39", "C40", "C41", "C42", "C43", "C44", "C45", "C48", "C51",
|
|
104
|
+
"C52", "C54", "C58", "C60", "C61", "C64", "L5", "R4", "R8",
|
|
105
|
+
"R9", "R10", "R11", "R15", "R16", "R17", "R18", "R36", "R37",
|
|
106
|
+
"R38", "C22", "C24", "C25", "C26", "C28", "C29", "C30", "C37",
|
|
107
|
+
"C46", "C53"]
|
|
108
|
+
},
|
|
109
|
+
"M6": {
|
|
110
|
+
"title": "帧缓存 frame buffer SIMM",
|
|
111
|
+
"anchors": [[189.5, 83.0], [307.6, 83.0], [255.0, 125.0]],
|
|
112
|
+
"members": ["U12", "U13", "U14", "U15", "U16", "U17", "U18", "U19",
|
|
113
|
+
"C38", "C56", "C57", "C67", "C70", "C71", "C72", "C73"]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render a placement plan as a module-coloured SVG plan view + evaluation panel.
|
|
3
|
+
|
|
4
|
+
python placement_view.py --geometry geometry.json --plan placement_plan.json
|
|
5
|
+
# -> <placement dir>/placement_view.svg
|
|
6
|
+
|
|
7
|
+
This is the picture a reviewer opens first, and it is deliberately self-contained:
|
|
8
|
+
standard library only, no KiCad, no browser, no rasteriser, so it can be produced on
|
|
9
|
+
any machine that got as far as running the packer.
|
|
10
|
+
|
|
11
|
+
What it draws
|
|
12
|
+
-------------
|
|
13
|
+
* the real board outline from Edge.Cuts, filled, so the board shape (notches, tabs,
|
|
14
|
+
rounded corners) is visible rather than a bounding box;
|
|
15
|
+
* every keep-out registered in the plan, hatched, with its reason in the panel - the
|
|
16
|
+
tilt volume of a socket or the antenna clear zone is a decision, not noise;
|
|
17
|
+
* every courtyard rectangle tinted by the module that owns it, labelled once it is
|
|
18
|
+
big enough on screen to carry text, with fixed/anchor parts drawn at full opacity
|
|
19
|
+
so a reviewer can separate *decisions* from *consequences*;
|
|
20
|
+
* each module's anchor points, drawn as crosses in the module colour.
|
|
21
|
+
|
|
22
|
+
The panel underneath carries the numbers that decide whether the plan is acceptable -
|
|
23
|
+
occupancy, per-module area, and the hard gates. The canvas is sized from the content
|
|
24
|
+
that actually exists, so a board with 14 modules or 5 keep-outs does not run its text
|
|
25
|
+
off the bottom edge.
|
|
26
|
+
|
|
27
|
+
The gates shown here are for DISPLAY: the authoritative check is
|
|
28
|
+
``verify_placement.py``, which computes the same quantities from the same inputs
|
|
29
|
+
without sharing the packer's code. If the two ever disagree, believe
|
|
30
|
+
verify_placement.py.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import datetime
|
|
36
|
+
import json
|
|
37
|
+
import math
|
|
38
|
+
import os
|
|
39
|
+
import sys
|
|
40
|
+
|
|
41
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
42
|
+
import projpaths # noqa: E402
|
|
43
|
+
|
|
44
|
+
#: Palette for up to 12 modules; assigned in sorted module order so the same board
|
|
45
|
+
#: always produces the same colours run to run.
|
|
46
|
+
PALETTE = ["#2e8b57", "#c0392b", "#8e44ad", "#e67e22", "#2471a3", "#16a085",
|
|
47
|
+
"#7f8c8d", "#34495e", "#b7950b", "#a04000", "#1a5276", "#7d3c98"]
|
|
48
|
+
UNASSIGNED = "#95a5a6"
|
|
49
|
+
FONT = "DejaVu Sans, Segoe UI, Microsoft YaHei, Arial, sans-serif"
|
|
50
|
+
ROW_H = 18
|
|
51
|
+
LEGEND_COL_PX = 330
|
|
52
|
+
PANEL_HEAD_PX = 72
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def esc(s):
|
|
56
|
+
return (str(s).replace("&", "&").replace("<", "<").replace(">", ">"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def rect_of(geo_ref, pose):
|
|
60
|
+
"""Courtyard rectangle in board coordinates (KiCad Y down => clockwise turn)."""
|
|
61
|
+
x0, y0, x1, y1 = geo_ref["bbox_local"]
|
|
62
|
+
t = math.radians(pose["rot"])
|
|
63
|
+
c, s = math.cos(t), math.sin(t)
|
|
64
|
+
pts = [(lx * c + ly * s, -lx * s + ly * c) for lx in (x0, x1) for ly in (y0, y1)]
|
|
65
|
+
return (pose["x"] + min(p[0] for p in pts), pose["y"] + min(p[1] for p in pts),
|
|
66
|
+
pose["x"] + max(p[0] for p in pts), pose["y"] + max(p[1] for p in pts))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def poly_area(poly):
|
|
70
|
+
a = 0.0
|
|
71
|
+
n = len(poly)
|
|
72
|
+
for i in range(n):
|
|
73
|
+
x0, y0 = poly[i]
|
|
74
|
+
x1, y1 = poly[(i + 1) % n]
|
|
75
|
+
a += x0 * y1 - x1 * y0
|
|
76
|
+
return abs(a) / 2.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def inside_poly(poly, x, y):
|
|
80
|
+
r, n = False, len(poly)
|
|
81
|
+
for i in range(n):
|
|
82
|
+
x0, y0 = poly[i]
|
|
83
|
+
x1, y1 = poly[(i + 1) % n]
|
|
84
|
+
if (y0 > y) != (y1 > y) and x < x0 + (y - y0) * (x1 - x0) / (y1 - y0):
|
|
85
|
+
r = not r
|
|
86
|
+
return r
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main() -> None:
|
|
90
|
+
ap = argparse.ArgumentParser(description=__doc__,
|
|
91
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
92
|
+
ap.add_argument("--geometry", required=True)
|
|
93
|
+
ap.add_argument("--plan", required=True)
|
|
94
|
+
ap.add_argument("--config", help="optional placement_config.json; only used to "
|
|
95
|
+
"recover module titles/keep-outs if the plan lacks them")
|
|
96
|
+
ap.add_argument("--out", help="SVG to write (default <placement dir>/placement_view.svg)")
|
|
97
|
+
ap.add_argument("--scale", type=float,
|
|
98
|
+
help="pixels per mm; default auto-fits the board to --target-width-px")
|
|
99
|
+
ap.add_argument("--title", help="override the drawing title")
|
|
100
|
+
ap.add_argument("--target-width-px", type=float, default=1500.0)
|
|
101
|
+
projpaths.add_outdir(ap)
|
|
102
|
+
args = ap.parse_args()
|
|
103
|
+
|
|
104
|
+
geom = json.load(open(args.geometry, encoding="utf-8-sig"))
|
|
105
|
+
plan = json.load(open(args.plan, encoding="utf-8-sig"))
|
|
106
|
+
cfg = json.load(open(args.config, encoding="utf-8-sig")) if args.config else {}
|
|
107
|
+
|
|
108
|
+
geo = {g["ref"]: g for g in geom["footprints"]}
|
|
109
|
+
pos = plan["positions"]
|
|
110
|
+
poly = [tuple(p) for p in geom["outline"]]
|
|
111
|
+
fixed = set(plan.get("fixed", []))
|
|
112
|
+
keepouts = plan.get("keepouts") or cfg.get("keepouts") or []
|
|
113
|
+
titles = dict(plan.get("module_titles") or {})
|
|
114
|
+
for m, spec in (cfg.get("modules") or {}).items():
|
|
115
|
+
titles.setdefault(m, spec.get("title", m))
|
|
116
|
+
|
|
117
|
+
modules = plan.get("modules") or {m: s.get("members", [])
|
|
118
|
+
for m, s in (cfg.get("modules") or {}).items()}
|
|
119
|
+
mod_of = {}
|
|
120
|
+
for m, members in modules.items():
|
|
121
|
+
for r in members:
|
|
122
|
+
mod_of[r] = m
|
|
123
|
+
order = sorted(modules)
|
|
124
|
+
color = {m: PALETTE[i % len(PALETTE)] for i, m in enumerate(order)}
|
|
125
|
+
|
|
126
|
+
bx0 = min(p[0] for p in poly)
|
|
127
|
+
bx1 = max(p[0] for p in poly)
|
|
128
|
+
by0 = min(p[1] for p in poly)
|
|
129
|
+
by1 = max(p[1] for p in poly)
|
|
130
|
+
bw, bh = bx1 - bx0, by1 - by0
|
|
131
|
+
|
|
132
|
+
S = args.scale or max(1.5, min(14.0, args.target_width_px / max(bw, 1e-6)))
|
|
133
|
+
PAD = 34
|
|
134
|
+
plan_w = bw * S + 2 * PAD
|
|
135
|
+
plan_h = bh * S + 2 * PAD
|
|
136
|
+
|
|
137
|
+
# ---- geometry of every part, and the display metrics -------------------
|
|
138
|
+
R = {r: rect_of(geo[r], p) for r, p in pos.items() if r in geo}
|
|
139
|
+
refs = sorted(R)
|
|
140
|
+
|
|
141
|
+
overlaps = []
|
|
142
|
+
for i in range(len(refs)):
|
|
143
|
+
a = refs[i]
|
|
144
|
+
ax0, ay0, ax1, ay1 = R[a]
|
|
145
|
+
for j in range(i + 1, len(refs)):
|
|
146
|
+
b = refs[j]
|
|
147
|
+
bxx0, byy0, bxx1, byy1 = R[b]
|
|
148
|
+
if ax0 < bxx1 and bxx0 < ax1 and ay0 < byy1 and byy0 < ay1:
|
|
149
|
+
overlaps.append((a, b))
|
|
150
|
+
|
|
151
|
+
off_board = []
|
|
152
|
+
for r in refs:
|
|
153
|
+
x0, y0, x1, y1 = R[r]
|
|
154
|
+
out = tot = 0
|
|
155
|
+
for k in range(12):
|
|
156
|
+
px = x0 + (x1 - x0) * ((k % 4) / 3.0)
|
|
157
|
+
py = y0 + (y1 - y0) * ((k // 4) / 2.0)
|
|
158
|
+
tot += 1
|
|
159
|
+
if not inside_poly(poly, px, py):
|
|
160
|
+
out += 1
|
|
161
|
+
if out:
|
|
162
|
+
off_board.append((r, out / tot))
|
|
163
|
+
|
|
164
|
+
board_area = poly_area(poly)
|
|
165
|
+
court_area = sum((R[r][2] - R[r][0]) * (R[r][3] - R[r][1]) for r in refs)
|
|
166
|
+
density = 100.0 * court_area / board_area if board_area else 0.0
|
|
167
|
+
|
|
168
|
+
mod_area, mod_count = {}, {}
|
|
169
|
+
for r in refs:
|
|
170
|
+
m = mod_of.get(r, "?")
|
|
171
|
+
mod_area[m] = mod_area.get(m, 0.0) + (R[r][2] - R[r][0]) * (R[r][3] - R[r][1])
|
|
172
|
+
mod_count[m] = mod_count.get(m, 0) + 1
|
|
173
|
+
|
|
174
|
+
weak = sorted({(g["ref"], g["bbox_src"]) for g in geom["footprints"]
|
|
175
|
+
if g.get("bbox_src") not in (None, "courtyard")})
|
|
176
|
+
|
|
177
|
+
# ---- panel content, counted BEFORE the canvas is sized -----------------
|
|
178
|
+
legend = [(m, mod_count.get(m, 0),
|
|
179
|
+
100.0 * mod_area.get(m, 0.0) / board_area if board_area else 0.0)
|
|
180
|
+
for m in order]
|
|
181
|
+
unassigned = sorted(set(refs) - set(mod_of))
|
|
182
|
+
if unassigned:
|
|
183
|
+
legend.append(("(unassigned)", len(unassigned), 0.0))
|
|
184
|
+
|
|
185
|
+
rows = [
|
|
186
|
+
("footprints on board / placed", "%d / %d" % (len(geo), len(pos)), "#222"),
|
|
187
|
+
("fixed anchors / algorithm-placed",
|
|
188
|
+
"%d / %d" % (len(fixed), len(pos) - len(fixed)), "#222"),
|
|
189
|
+
("board size / area", "%.1f x %.1f mm / %.0f mm2" % (bw, bh, board_area), "#222"),
|
|
190
|
+
("courtyard area / density", "%.0f mm2 / %.1f%%" % (court_area, density),
|
|
191
|
+
"#c0392b" if density > 75 else "#1e8449"),
|
|
192
|
+
("courtyard overlap pairs",
|
|
193
|
+
"%d%s" % (len(overlaps), " PASS" if not overlaps else " FAIL"),
|
|
194
|
+
"#1e8449" if not overlaps else "#c0392b"),
|
|
195
|
+
("parts off the board",
|
|
196
|
+
"%d%s" % (len(off_board), " PASS" if not off_board else " REVIEW"),
|
|
197
|
+
"#1e8449" if not off_board else "#b7950b"),
|
|
198
|
+
("unplaced by the packer", "%d" % len(plan.get("unplaced") or []),
|
|
199
|
+
"#222" if not plan.get("unplaced") else "#c0392b"),
|
|
200
|
+
("keep-outs registered", "%d" % len(keepouts), "#222"),
|
|
201
|
+
]
|
|
202
|
+
if weak:
|
|
203
|
+
rows.append(("courtyard stand-ins / no geometry",
|
|
204
|
+
"%d / %d" % (len(weak), sum(1 for w in weak if w[1] == "none")),
|
|
205
|
+
"#b7950b"))
|
|
206
|
+
|
|
207
|
+
notes = []
|
|
208
|
+
if overlaps:
|
|
209
|
+
notes.append(("overlapping: %s" % ", ".join("%s/%s" % p for p in overlaps[:6]),
|
|
210
|
+
"#c0392b"))
|
|
211
|
+
if off_board:
|
|
212
|
+
notes.append(("off-board: %s" % ", ".join("%s %.0f%%" % (r, f * 100)
|
|
213
|
+
for r, f in off_board[:6]), "#b7950b"))
|
|
214
|
+
for i, ko in enumerate(keepouts):
|
|
215
|
+
notes.append(("keep-out %d: %s" % (i + 1, (ko.get("why") or "")[:110]), "#c0392b"))
|
|
216
|
+
|
|
217
|
+
# legend columns, then the canvas that fits them
|
|
218
|
+
per_col = max(1, min(len(legend), 12))
|
|
219
|
+
ncol = max(1, -(-len(legend) // per_col))
|
|
220
|
+
legend_w = ncol * LEGEND_COL_PX
|
|
221
|
+
mx = PAD + legend_w + 16
|
|
222
|
+
body_rows = max(per_col, len(rows) + len(notes) + 1)
|
|
223
|
+
panel_h = PANEL_HEAD_PX + body_rows * ROW_H + 28
|
|
224
|
+
W = max(plan_w, mx + 520.0)
|
|
225
|
+
H = plan_h + panel_h
|
|
226
|
+
|
|
227
|
+
def tx(x):
|
|
228
|
+
return PAD + (x - bx0) * S
|
|
229
|
+
|
|
230
|
+
def ty(y):
|
|
231
|
+
return PAD + (y - by0) * S
|
|
232
|
+
|
|
233
|
+
title = args.title or ("%s - Phase-1 initial placement: courtyard occupancy by module"
|
|
234
|
+
% geom.get("board", "board"))
|
|
235
|
+
|
|
236
|
+
o = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
237
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="%.0f" height="%.0f" '
|
|
238
|
+
'viewBox="0 0 %.0f %.0f" font-family="%s">' % (W, H, W, H, FONT),
|
|
239
|
+
'<defs><pattern id="hatch" width="8" height="8" patternUnits="userSpaceOnUse" '
|
|
240
|
+
'patternTransform="rotate(45)">'
|
|
241
|
+
'<rect width="8" height="8" fill="#fdecea"/>'
|
|
242
|
+
'<line x1="0" y1="0" x2="0" y2="8" stroke="#c0392b" stroke-width="2.4"/>'
|
|
243
|
+
'</pattern></defs>',
|
|
244
|
+
'<rect width="100%" height="100%" fill="#fbfbfb"/>']
|
|
245
|
+
|
|
246
|
+
o.append('<polygon points="%s" fill="#eef3f7" stroke="#1a1a1a" stroke-width="2.5"/>'
|
|
247
|
+
% " ".join("%.1f,%.1f" % (tx(px), ty(py)) for px, py in poly))
|
|
248
|
+
|
|
249
|
+
for ko in keepouts:
|
|
250
|
+
x0, y0, x1, y1 = ko["rect"]
|
|
251
|
+
o.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="url(#hatch)" '
|
|
252
|
+
'stroke="#c0392b" stroke-width="1.4" stroke-dasharray="5,3"/>'
|
|
253
|
+
% (tx(x0), ty(y0), (x1 - x0) * S, (y1 - y0) * S))
|
|
254
|
+
|
|
255
|
+
# big parts first, so a small part's label is never buried under a big rectangle
|
|
256
|
+
for ref in sorted(refs, key=lambda r: -((R[r][2] - R[r][0]) * (R[r][3] - R[r][1]))):
|
|
257
|
+
x0, y0, x1, y1 = R[ref]
|
|
258
|
+
fx, fy = tx(x0), ty(y0)
|
|
259
|
+
w, h = (x1 - x0) * S, (y1 - y0) * S
|
|
260
|
+
col = color.get(mod_of.get(ref), UNASSIGNED)
|
|
261
|
+
is_fixed = ref in fixed
|
|
262
|
+
o.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s" '
|
|
263
|
+
'fill-opacity="%.2f" stroke="%s" stroke-width="%.1f" rx="1.5"/>'
|
|
264
|
+
% (fx, fy, w, h, col, 0.85 if is_fixed else 0.5,
|
|
265
|
+
"#111" if is_fixed else col, 1.2 if is_fixed else 0.6))
|
|
266
|
+
if w >= 34 and h >= 15:
|
|
267
|
+
o.append('<text x="%.1f" y="%.1f" font-size="10" fill="#111" '
|
|
268
|
+
'text-anchor="middle">%s</text>'
|
|
269
|
+
% (fx + w / 2, fy + h / 2 + 3.5, esc(ref)))
|
|
270
|
+
|
|
271
|
+
anchors = plan.get("anchors") or {}
|
|
272
|
+
for m in order:
|
|
273
|
+
for ax, ay in anchors.get(m, []):
|
|
274
|
+
o.append('<path d="M %.1f %.1f l 7 7 m 0 -7 l -7 7" stroke="%s" '
|
|
275
|
+
'stroke-width="2.2" fill="none"/>'
|
|
276
|
+
% (tx(ax) - 3.5, ty(ay) - 3.5, color.get(m, "#111")))
|
|
277
|
+
|
|
278
|
+
bar_mm = 10.0 if bw > 60 else 20.0
|
|
279
|
+
while bar_mm * S < 60:
|
|
280
|
+
bar_mm *= 2
|
|
281
|
+
bxp, byp = PAD + 6, plan_h - PAD - 8
|
|
282
|
+
o.append('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="#111" stroke-width="3"/>'
|
|
283
|
+
% (bxp, byp, bxp + bar_mm * S, byp))
|
|
284
|
+
o.append('<text x="%.1f" y="%.1f" font-size="12" fill="#111">%g mm</text>'
|
|
285
|
+
% (bxp + bar_mm * S + 8, byp + 4, bar_mm))
|
|
286
|
+
|
|
287
|
+
# ---- panel -------------------------------------------------------------
|
|
288
|
+
def text(x, y, s, size=12, fill="#222", weight="normal", anchor="start"):
|
|
289
|
+
o.append('<text x="%.1f" y="%.1f" font-size="%d" font-weight="%s" fill="%s" '
|
|
290
|
+
'text-anchor="%s">%s</text>'
|
|
291
|
+
% (x, y, size, weight, fill, anchor, esc(s)))
|
|
292
|
+
|
|
293
|
+
head_y = plan_h + 26
|
|
294
|
+
o.append('<line x1="0" y1="%.1f" x2="%.0f" y2="%.1f" stroke="#ccc" stroke-width="1"/>'
|
|
295
|
+
% (plan_h, W, plan_h))
|
|
296
|
+
text(PAD, head_y, title, 17, "#111", "bold")
|
|
297
|
+
text(PAD, head_y + 20,
|
|
298
|
+
"%s | generated %s | display metrics only - the gate is verify_placement.py"
|
|
299
|
+
% (geom.get("board", ""), datetime.datetime.now().strftime("%Y-%m-%d %H:%M")),
|
|
300
|
+
11, "#555")
|
|
301
|
+
|
|
302
|
+
top = plan_h + PANEL_HEAD_PX
|
|
303
|
+
text(PAD, top, "modules", 13, "#111", "bold")
|
|
304
|
+
for i, (m, n, share) in enumerate(legend):
|
|
305
|
+
col_i, row_i = divmod(i, per_col)
|
|
306
|
+
lx = PAD + col_i * LEGEND_COL_PX
|
|
307
|
+
ly = top + ROW_H * (row_i + 1)
|
|
308
|
+
col = color.get(m, UNASSIGNED)
|
|
309
|
+
o.append('<rect x="%.1f" y="%.1f" width="12" height="12" fill="%s" '
|
|
310
|
+
'fill-opacity="0.6" stroke="%s"/>' % (lx, ly - 10, col, col))
|
|
311
|
+
text(lx + 17, ly, "%s %s (%d, %.1f%%)" % (m, titles.get(m, ""), n, share), 11)
|
|
312
|
+
|
|
313
|
+
text(mx, top, "evaluation panel", 13, "#111", "bold")
|
|
314
|
+
my = top + ROW_H
|
|
315
|
+
for label, value, col in rows:
|
|
316
|
+
text(mx, my, "%s:" % label, 11, "#555")
|
|
317
|
+
text(mx + 250, my, value, 11, col, "bold")
|
|
318
|
+
my += ROW_H
|
|
319
|
+
my += 4
|
|
320
|
+
for note, col in notes:
|
|
321
|
+
text(mx, my, note, 10, col)
|
|
322
|
+
my += 15
|
|
323
|
+
|
|
324
|
+
o.append("</svg>")
|
|
325
|
+
|
|
326
|
+
out_path = projpaths.resolve_out(args.out, "placement_view.svg",
|
|
327
|
+
refs=(args.geometry, args.plan), outdir=args.outdir)
|
|
328
|
+
with open(out_path, "w", encoding="utf-8") as fh:
|
|
329
|
+
fh.write("\n".join(o) + "\n")
|
|
330
|
+
|
|
331
|
+
print("wrote %s (%.0f x %.0f px, %.2f px/mm)" % (out_path, W, H, S))
|
|
332
|
+
print("modules %d | parts %d | fixed %d | density %.1f%% | overlaps %d | off-board %d"
|
|
333
|
+
% (len(order), len(pos), len(fixed), density, len(overlaps), len(off_board)))
|
|
334
|
+
if weak:
|
|
335
|
+
print("stand-in courtyards (name these in the report): %s"
|
|
336
|
+
% ", ".join("%s=%s" % w for w in weak[:12]))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
if __name__ == "__main__":
|
|
340
|
+
main()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Turn a placement plan into the hand-off pose table (CSV).
|
|
3
|
+
|
|
4
|
+
python pose_table.py --geometry geometry.json --plan placement_plan.json
|
|
5
|
+
# -> <placement dir>/placement_pose_table.csv (override with --out / --outdir)
|
|
6
|
+
|
|
7
|
+
Columns: module, ref, value, footprint, origin_x_mm, origin_y_mm, rotation_deg,
|
|
8
|
+
courtyard_w_mm, courtyard_h_mm, center_x_mm, center_y_mm, source
|
|
9
|
+
``source`` is ``fixed`` for anchors the config pinned and ``placed`` otherwise, so a
|
|
10
|
+
reviewer can tell decisions from consequences. Written UTF-8 with BOM so Excel
|
|
11
|
+
opens the Chinese module names correctly.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import csv
|
|
17
|
+
import json
|
|
18
|
+
import math
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
23
|
+
import projpaths # noqa: E402
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
ap = argparse.ArgumentParser(description=__doc__,
|
|
28
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
29
|
+
ap.add_argument("--geometry", required=True)
|
|
30
|
+
ap.add_argument("--plan", required=True)
|
|
31
|
+
ap.add_argument("--out", help="CSV to write "
|
|
32
|
+
"(default <placement dir>/placement_pose_table.csv)")
|
|
33
|
+
projpaths.add_outdir(ap)
|
|
34
|
+
args = ap.parse_args()
|
|
35
|
+
|
|
36
|
+
out_path = projpaths.resolve_out(args.out, "placement_pose_table.csv",
|
|
37
|
+
refs=(args.geometry, args.plan),
|
|
38
|
+
outdir=args.outdir)
|
|
39
|
+
|
|
40
|
+
geo = {g["ref"]: g
|
|
41
|
+
for g in json.load(open(args.geometry, encoding="utf-8-sig"))["footprints"]}
|
|
42
|
+
plan = json.load(open(args.plan, encoding="utf-8-sig"))
|
|
43
|
+
pos = plan["positions"]
|
|
44
|
+
fixed = set(plan.get("fixed", []))
|
|
45
|
+
order = list(plan["modules"].keys())
|
|
46
|
+
|
|
47
|
+
rows = []
|
|
48
|
+
for mod in order:
|
|
49
|
+
for ref in plan["modules"][mod]:
|
|
50
|
+
if ref not in pos:
|
|
51
|
+
continue
|
|
52
|
+
p = pos[ref]
|
|
53
|
+
x0, y0, x1, y1 = geo[ref]["bbox_local"]
|
|
54
|
+
t = math.radians(p["rot"])
|
|
55
|
+
c, s = math.cos(t), math.sin(t)
|
|
56
|
+
pts = [(lx * c + ly * s, -lx * s + ly * c) for lx in (x0, x1) for ly in (y0, y1)]
|
|
57
|
+
xs = [q[0] for q in pts]
|
|
58
|
+
ys = [q[1] for q in pts]
|
|
59
|
+
rows.append({
|
|
60
|
+
"module": mod, "ref": ref, "value": geo[ref]["value"],
|
|
61
|
+
"footprint": geo[ref]["libid"],
|
|
62
|
+
"origin_x_mm": "%.3f" % p["x"], "origin_y_mm": "%.3f" % p["y"],
|
|
63
|
+
"rotation_deg": "%.4g" % (p["rot"] % 360),
|
|
64
|
+
"courtyard_w_mm": "%.2f" % (max(xs) - min(xs)),
|
|
65
|
+
"courtyard_h_mm": "%.2f" % (max(ys) - min(ys)),
|
|
66
|
+
"center_x_mm": "%.2f" % (p["x"] + (min(xs) + max(xs)) / 2),
|
|
67
|
+
"center_y_mm": "%.2f" % (p["y"] + (min(ys) + max(ys)) / 2),
|
|
68
|
+
"source": "fixed" if ref in fixed else "placed",
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
cols = ["module", "ref", "value", "footprint", "origin_x_mm", "origin_y_mm",
|
|
72
|
+
"rotation_deg", "courtyard_w_mm", "courtyard_h_mm",
|
|
73
|
+
"center_x_mm", "center_y_mm", "source"]
|
|
74
|
+
with open(out_path, "w", encoding="utf-8-sig", newline="") as fh:
|
|
75
|
+
w = csv.DictWriter(fh, fieldnames=cols)
|
|
76
|
+
w.writeheader()
|
|
77
|
+
w.writerows(rows)
|
|
78
|
+
print("wrote %s (%d rows)" % (out_path, len(rows)))
|
|
79
|
+
by_module = {}
|
|
80
|
+
for r in rows:
|
|
81
|
+
by_module[r["module"]] = by_module.get(r["module"], 0) + 1
|
|
82
|
+
print("per module:", by_module)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
main()
|