@huaqiu/dsh-hqsch 0.4.8
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 +21 -0
- package/README.md +43 -0
- package/cordis.patch.yml +13 -0
- package/lib/index.d.mts +98 -0
- package/lib/index.mjs +159 -0
- package/package.json +40 -0
- package/skills/hqsch-scenario-b/AGENTS.md +209 -0
- package/skills/hqsch-scenario-b/CLAUDE.md +209 -0
- package/skills/hqsch-scenario-b/INSTALL.md +140 -0
- package/skills/hqsch-scenario-b/SKILL.md +107 -0
- package/skills/hqsch-scenario-b/SYSTEM-PROMPT.md +209 -0
- package/skills/hqsch-scenario-b/docs/circuit-pattern-layout.md +304 -0
- package/skills/hqsch-scenario-b/docs/editing-a-circuit.md +167 -0
- package/skills/hqsch-scenario-b/docs/part-search.md +244 -0
- package/skills/hqsch-scenario-b/docs/placement-conventions.md +714 -0
- package/skills/hqsch-scenario-b/docs/property-conventions.md +225 -0
- package/skills/hqsch-scenario-b/docs/quickstart.md +99 -0
- package/skills/hqsch-scenario-b/docs/reading-a-circuit.md +317 -0
- package/skills/hqsch-scenario-b/docs/rpc/canvas-auto-connect-objects-by-id.md +98 -0
- package/skills/hqsch-scenario-b/docs/rpc/canvas-delete-objects-by-ids.md +94 -0
- package/skills/hqsch-scenario-b/docs/rpc/canvas-find-object-by-property.md +101 -0
- package/skills/hqsch-scenario-b/docs/rpc/canvas-list-wire-segments.md +94 -0
- package/skills/hqsch-scenario-b/docs/rpc/canvas-mirror-objects-by-ids.md +96 -0
- package/skills/hqsch-scenario-b/docs/rpc/canvas-rotate-objects-by-ids.md +94 -0
- package/skills/hqsch-scenario-b/docs/rpc/canvas-set-object-property.md +99 -0
- package/skills/hqsch-scenario-b/docs/rpc/canvas-zoom-all.md +92 -0
- package/skills/hqsch-scenario-b/docs/rpc/find-find-object.md +96 -0
- package/skills/hqsch-scenario-b/docs/rpc/kernel-get-snapshot.md +104 -0
- package/skills/hqsch-scenario-b/docs/rpc/netlist-get-active-page-net-list.md +87 -0
- package/skills/hqsch-scenario-b/docs/rpc/netlist-get-port-pairing.md +34 -0
- package/skills/hqsch-scenario-b/docs/rpc/obj-place-place-pin-stub-wire-and-net-alias.md +114 -0
- package/skills/hqsch-scenario-b/docs/rpc/pattern-layout-apply-circuit-pattern.md +125 -0
- package/skills/hqsch-scenario-b/docs/rpc/pattern-layout-get-page-occupancy.md +100 -0
- package/skills/hqsch-scenario-b/docs/rpc/pattern-layout-list-circuit-patterns.md +97 -0
- package/skills/hqsch-scenario-b/docs/rpc/placement-list-part-libraries.md +94 -0
- package/skills/hqsch-scenario-b/docs/rpc/placement-list-symbol-libraries.md +94 -0
- package/skills/hqsch-scenario-b/docs/rpc/placement-place-kicad-symbol.md +105 -0
- package/skills/hqsch-scenario-b/docs/rpc/placement-place-part-from-library.md +106 -0
- package/skills/hqsch-scenario-b/docs/rpc/placement-place-symbol-from-library.md +108 -0
- package/skills/hqsch-scenario-b/docs/rpc/project-get-active-project.md +96 -0
- package/skills/hqsch-scenario-b/docs/rpc/project-list-open-projects.md +99 -0
- package/skills/hqsch-scenario-b/docs/rpc-availability.md +97 -0
- package/skills/hqsch-scenario-b/docs/rpc-index/canvas.md +81 -0
- package/skills/hqsch-scenario-b/docs/rpc-index/find.md +36 -0
- package/skills/hqsch-scenario-b/docs/rpc-index/kernel.md +36 -0
- package/skills/hqsch-scenario-b/docs/rpc-index/netlist.md +38 -0
- package/skills/hqsch-scenario-b/docs/rpc-index/obj-place.md +76 -0
- package/skills/hqsch-scenario-b/docs/rpc-index/pattern-layout.md +38 -0
- package/skills/hqsch-scenario-b/docs/rpc-index/placement.md +48 -0
- package/skills/hqsch-scenario-b/docs/rpc-index/project.md +47 -0
- package/skills/hqsch-scenario-b/docs/serialization.md +60 -0
- package/skills/hqsch-scenario-b/docs/skill-dependencies.md +93 -0
- package/skills/hqsch-scenario-b/skill.json +13 -0
- package/skills/hqsch-scenario-b/template/package.json +16 -0
- package/skills/hqsch-scenario-b/template/scripts/edit-property-by-ref.ts +44 -0
- package/skills/hqsch-scenario-b/template/scripts/fix-designators.ts +52 -0
- package/skills/hqsch-scenario-b/template/scripts/hello.ts +46 -0
- package/skills/hqsch-scenario-b/template/scripts/read-circuit.ts +357 -0
- package/skills/hqsch-scenario-b/template/scripts/replace-part-by-ref.ts +44 -0
- package/skills/hqsch-scenario-b/template/scripts/rewire-pin-by-ref.ts +42 -0
- package/skills/hqsch-scenario-b/template/tsconfig.json +11 -0
- package/src/config.ts +25 -0
- package/src/index.ts +186 -0
- package/src/paths.ts +48 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
# Placement Conventions (HQSCH)
|
|
2
|
+
|
|
3
|
+
## Coordinate Convention
|
|
4
|
+
|
|
5
|
+
All three library-placement RPCs share the same **external display coordinates**
|
|
6
|
+
for `local_pos`:
|
|
7
|
+
|
|
8
|
+
| Space | Origin | Y direction | Range |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| **User / Agent** (`local_pos`) | Page top-left | Downward ↑ Y | **0 ~ page display height** (varies by A4/Letter/custom size) |
|
|
11
|
+
| **Internal canvas** | Bottom-left | Upward | Page top at `PAGE_SIZE_START_Y` (typically **1400**) |
|
|
12
|
+
|
|
13
|
+
**Important:** `1400` is the **canvas Y anchor of the page top edge**, not the
|
|
14
|
+
maximum user `extY`. User `extY` counts down from the page top and is bounded
|
|
15
|
+
by the active page's display height (e.g. A4 ≠ Letter ≠ custom).
|
|
16
|
+
|
|
17
|
+
Conversion (inside `SDK_PlacePartByExtCoord` / `SDK_PlaceSymbolByExtCoord`):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
canvasY = PAGE_SIZE_START_Y - extY // PAGE_SIZE_START_Y ≈ 1400 (page top)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Example (default page): user places GND at `(50, 100)` → canvas Y = `1400 - 100 = 1300`.
|
|
24
|
+
|
|
25
|
+
Reverse (from `kernel-get-snapshot` pin positions):
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
extY = PAGE_SIZE_START_Y - canvasY
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Applies to:
|
|
32
|
+
|
|
33
|
+
- `PlaceKicadSymbol` → `SCH_PlaceKicadSymbolFromResource`
|
|
34
|
+
- `PlacePartFromLibrary` → `SCH_PlacePartFromLibrary`
|
|
35
|
+
- `PlaceSymbolFromLibrary` → `SCH_PlaceSymbolFromLibrary`
|
|
36
|
+
- `PlaceWire` → wire segment endpoints (same external convention)
|
|
37
|
+
|
|
38
|
+
See also [`property-conventions.md`](property-conventions.md) for pin/wiring coords.
|
|
39
|
+
|
|
40
|
+
## Orientation: Mirror vs Rotate
|
|
41
|
+
|
|
42
|
+
| Goal | API | Notes |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| **Fixed facing** (e.g. LDO VIN pin left) | `RotateObjectsByIds` | Each call = 90° clockwise; predictable, call 1–3 times as needed |
|
|
45
|
+
| **Flip symbol** | `MirrorObjectsByIds` | **`mode` is required** — see table below |
|
|
46
|
+
|
|
47
|
+
### MirrorObjectsByIds — `mode` is mandatory
|
|
48
|
+
|
|
49
|
+
Protobuf `int32 mode` defaults to **0** (`MIRROR_NONE`), which performs **no transform**.
|
|
50
|
+
The RPC now returns `success: false` when `mode` is 0 or missing.
|
|
51
|
+
|
|
52
|
+
| `mode` | Effect |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| **1** | Horizontal mirror (left ↔ right) |
|
|
55
|
+
| **2** | Vertical mirror (top ↔ bottom) |
|
|
56
|
+
| **3** | Both axes |
|
|
57
|
+
| **0** | **Invalid** — no-op, `success: false` |
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Correct — flip U1 horizontally
|
|
61
|
+
await canvasOps.mirrorObjectsByIds({
|
|
62
|
+
context: projectCtx,
|
|
63
|
+
objectIds: [u1ObjectId],
|
|
64
|
+
mode: 1,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Wrong — mode omitted or 0 looks like "success" in old builds but changes nothing
|
|
68
|
+
await canvasOps.mirrorObjectsByIds({ context: projectCtx, objectIds: [u1ObjectId] }); // BAD
|
|
69
|
+
|
|
70
|
+
// Prefer rotate when layout goal is "pin N faces direction X"
|
|
71
|
+
await canvasOps.rotateObjectsByIds({ context: projectCtx, objectIds: [u1ObjectId] });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Objective
|
|
75
|
+
|
|
76
|
+
Guide agents to place schematic objects through the **correct library surface**.
|
|
77
|
+
Do **not** hand-craft engine-internal JSON for `PlacePart` / `PlaceSymbol`.
|
|
78
|
+
|
|
79
|
+
## Part Placement (R, C, Q, U, …) — Online First
|
|
80
|
+
|
|
81
|
+
| Priority | Step | RPC / API | Notes |
|
|
82
|
+
| --- | --- | --- | --- |
|
|
83
|
+
| **1 (preferred)** | Search | `client.parts.searchParts` / DSH `search_hqsch_parts` | Pick candidate with `hasSymbol` + `hasFootprint`. |
|
|
84
|
+
| **2** | Models | `client.parts.getEdaModels` / DSH `get_hqsch_part_models` | Get symbol / footprint download URLs. |
|
|
85
|
+
| **3** | Place | `PlaceKicadSymbol` | Maps to SDK `SCH_PlaceKicadSymbolFromResource` — engine downloads `.kicad_sym`, converts, places. |
|
|
86
|
+
| Fallback | List | `ListPartLibraries` | Only when online search has no suitable part. |
|
|
87
|
+
| Fallback | Place | `PlacePartFromLibrary` | Local/builtin `.HQSchLib` part. |
|
|
88
|
+
|
|
89
|
+
**Do not use** `PlacePart` with hand-crafted `json_part` / `json_part_inst`.
|
|
90
|
+
|
|
91
|
+
See [`../eda/part-search/SKILL.md`](../eda/part-search/SKILL.md) for search strategy.
|
|
92
|
+
|
|
93
|
+
### TypeScript Example (0805 capacitor)
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { connect } from "@huaqiu/huaqiu-client";
|
|
97
|
+
|
|
98
|
+
const client = await connect({ instanceId: "..." });
|
|
99
|
+
const projectCtx = client.createProjectContext(projectId);
|
|
100
|
+
|
|
101
|
+
// 1. Online search
|
|
102
|
+
const page = await client.parts.searchParts({
|
|
103
|
+
query: "0805 0.1uF capacitor",
|
|
104
|
+
requirements: { symbol: true, footprint: true },
|
|
105
|
+
pageSize: 10,
|
|
106
|
+
});
|
|
107
|
+
const hit = page.items.find((i) => i.hasSymbol && i.hasFootprint && i.manufacturer.id);
|
|
108
|
+
if (!hit) throw new Error("no online part candidate");
|
|
109
|
+
|
|
110
|
+
// 2. EDA model URLs
|
|
111
|
+
const models = await client.parts.getEdaModels({
|
|
112
|
+
manufacturerId: hit.manufacturer.id!,
|
|
113
|
+
mpn: hit.mpn,
|
|
114
|
+
});
|
|
115
|
+
if (!models.symbol?.url) throw new Error("no symbol URL");
|
|
116
|
+
|
|
117
|
+
// 3. Place via SCH_PlaceKicadSymbolFromResource (gRPC: PlaceKicadSymbol)
|
|
118
|
+
// localPos Y: 0 = page top, increases downward (NOT capped at 1400)
|
|
119
|
+
const res = await client.componentPlace.placeKicadSymbol({
|
|
120
|
+
context: projectCtx,
|
|
121
|
+
localPos: { x: 50n, y: 75n },
|
|
122
|
+
component: {
|
|
123
|
+
componentName: "C1_cap",
|
|
124
|
+
symbolResource: { uri: models.symbol.url },
|
|
125
|
+
footprintResources: models.footprint?.url
|
|
126
|
+
? [{ uri: models.footprint.url, models: models.model3d?.url ? [{ uri: models.model3d.url }] : [] }]
|
|
127
|
+
: [],
|
|
128
|
+
attributes: [
|
|
129
|
+
{ name: "Reference", value: "C1" },
|
|
130
|
+
{ name: "Value", value: "0.1 μF" },
|
|
131
|
+
{ name: "Footprint", value: models.footprint?.fileName ?? hit.package ?? "0805" },
|
|
132
|
+
{ name: "MPN", value: hit.mpn },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
if (!res.success || !res.objectId) throw new Error("PlaceKicadSymbol failed");
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
After placement, confirm designator/value with `SetObjectProperty` (`propKey: "Part Reference"`, `"Value"`, `"Footprint"`) — see [`property-conventions.md`](property-conventions.md).
|
|
140
|
+
|
|
141
|
+
## Symbol Placement (GND, VCC, port, off-page)
|
|
142
|
+
|
|
143
|
+
Global/power symbols, hierarchical ports, and off-page connectors use the **symbol library** surface (not part-search):
|
|
144
|
+
|
|
145
|
+
| Step | RPC | Notes |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| 1 | `ListSymbolLibraries` | Read `library_name`, `symbol_name`, `kind`. |
|
|
148
|
+
| 2 | `PlaceSymbolFromLibrary` | Maps to SDK library load + place; do not hand-craft JSON. |
|
|
149
|
+
|
|
150
|
+
**Do not use** `PlaceSymbol` with `json_symbol` / `json_symbol_inst`.
|
|
151
|
+
|
|
152
|
+
### Approximate `symbol_name` matching
|
|
153
|
+
|
|
154
|
+
`symbol_name` does not have to be an exact library entry. The engine scores every
|
|
155
|
+
symbol of the **resolved library only** and takes the best hit — it never searches
|
|
156
|
+
other libraries, so step 1 (`ListSymbolLibraries`) is still required to pick the
|
|
157
|
+
right `library_name` / `library_file_path`.
|
|
158
|
+
|
|
159
|
+
Ranking, from strongest to weakest: exact → case-insensitive → normalized
|
|
160
|
+
(`_ - . space` stripped) → separator-boundary prefix → separator-boundary token →
|
|
161
|
+
normalized prefix → plain substring. Ties prefer the shorter candidate name.
|
|
162
|
+
|
|
163
|
+
Because separator-boundary hits outrank plain substrings, `GND` never resolves to
|
|
164
|
+
`VCC_5V`. If nothing scores, the call fails with `SymbolNotFound`.
|
|
165
|
+
|
|
166
|
+
`net_name` is written to the `"Name"` user property of that one placed instance;
|
|
167
|
+
the shared library / design-cache definition keeps its original name. Leaving
|
|
168
|
+
`net_name` empty means "same as `symbol_name`", which is usually what you want:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { HqServicesV1ComponentPlaceService } from "@hqedge/connect";
|
|
172
|
+
|
|
173
|
+
const { LibrarySymbolKind } = HqServicesV1ComponentPlaceService;
|
|
174
|
+
|
|
175
|
+
// Step 1 — list libraries (required: symbol_name is matched within one library only)
|
|
176
|
+
const symLibs = await client.componentPlace.listSymbolLibraries({ context: projectCtx });
|
|
177
|
+
|
|
178
|
+
// Step 2 — find the library entry whose symbol_name matches (e.g. "VCC_5V" or "GND")
|
|
179
|
+
let placedId: bigint | undefined;
|
|
180
|
+
for (const lib of symLibs.libraries) {
|
|
181
|
+
const sym = lib.symbols.find((s) => s.symbolName === "VCC_5V");
|
|
182
|
+
if (!sym) continue;
|
|
183
|
+
|
|
184
|
+
// Step 3 — place at external display coordinates (Y down from page top)
|
|
185
|
+
const res = await client.componentPlace.placeSymbolFromLibrary({
|
|
186
|
+
context: projectCtx,
|
|
187
|
+
localPos: { x: 50n, y: 50n }, // user view: (50, 50) from top-left
|
|
188
|
+
libraryName: lib.libraryName, // from ListSymbolLibraries
|
|
189
|
+
symbolName: sym.symbolName, // e.g. "VCC_5V" (approx match → library "VCC")
|
|
190
|
+
libraryFilePath: lib.libraryFilePath, // from ListSymbolLibraries
|
|
191
|
+
kind: sym.kind ?? LibrarySymbolKind.LIBRARY_SYMBOL_KIND_GLOBAL,
|
|
192
|
+
netName: "VCC_5V", // → "Name" user property on this instance
|
|
193
|
+
});
|
|
194
|
+
if (res.success && res.objectId) {
|
|
195
|
+
placedId = res.objectId;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!placedId) throw new Error("PlaceSymbolFromLibrary failed for VCC_5V");
|
|
200
|
+
|
|
201
|
+
// Save object_id for pin lookup — use kernel-get-snapshot canvas_object_id, not designator.
|
|
202
|
+
// See property-conventions.md "Pin Coordinates and Wiring".
|
|
203
|
+
|
|
204
|
+
// Step 4 — set designator (find uses "Reference"; set uses "Part Reference")
|
|
205
|
+
await client.canvasOps.setObjectProperty({
|
|
206
|
+
context: projectCtx,
|
|
207
|
+
objectId: placedId,
|
|
208
|
+
propKey: "Part Reference",
|
|
209
|
+
propValue: "VCC_5V",
|
|
210
|
+
op: 1, // OBJ_PROP_SET
|
|
211
|
+
commitUndo: true,
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `PlaceSymbolFromLibrary` request fields (VCC_5V example)
|
|
216
|
+
|
|
217
|
+
| Field | Example | Source |
|
|
218
|
+
| --- | --- | --- |
|
|
219
|
+
| `localPos` | `{ x: 50, y: 50 }` | Agent/user coordinates — **external**, Y down |
|
|
220
|
+
| `libraryName` | `"JupSym"` | `ListSymbolLibraries` → `libraries[].library_name` |
|
|
221
|
+
| `symbolName` | `"VCC_5V"` | Desired net label; engine fuzzy-matches within the resolved library |
|
|
222
|
+
| `libraryFilePath` | `"…/JupSym.HQSchLib"` | `ListSymbolLibraries` → `libraries[].library_file_path` |
|
|
223
|
+
| `kind` | `LIBRARY_SYMBOL_KIND_GLOBAL` | `ListSymbolLibraries` → `symbols[].kind` |
|
|
224
|
+
| `netName` | `"VCC_5V"` | Written to instance `"Name"` property; empty = same as `symbolName` |
|
|
225
|
+
|
|
226
|
+
The response does not report which symbol was actually matched. Read the placed
|
|
227
|
+
object's `"Name"` back with `GetObjectProperty` when you need to confirm.
|
|
228
|
+
|
|
229
|
+
**Do not** use `PlaceSymbol` with `json_symbol: { lib_id: "power:VCC_5V" }` on
|
|
230
|
+
the agent path — that is an internal fallback only when `ListSymbolLibraries`
|
|
231
|
+
returns no match.
|
|
232
|
+
|
|
233
|
+
## Wiring After Placement
|
|
234
|
+
|
|
235
|
+
After any placement RPC, **save `response.object_id`** for every component.
|
|
236
|
+
Wire using the saved ids — do not look up power symbols by designator string.
|
|
237
|
+
|
|
238
|
+
### Routing mode gate (mandatory before every net)
|
|
239
|
+
|
|
240
|
+
**Agents forget to measure** — they either `autoConnect` everything (long ugly
|
|
241
|
+
wires) or stub+alias everything (missing real wires when pins are adjacent).
|
|
242
|
+
Run this gate **per net**, after patterns, **before** picking a mode.
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
For each net to wire (pinA on partA ↔ pinB on partB):
|
|
246
|
+
│
|
|
247
|
+
├─ Already routed inside a pattern (connections[] routed=true)?
|
|
248
|
+
│ → STOP — do not hand-wire
|
|
249
|
+
│
|
|
250
|
+
├─ Measure: Manhattan ext, L-route length, wire crossing count
|
|
251
|
+
│ (cross-part is OK if pins are close — same gate as same-part)
|
|
252
|
+
│
|
|
253
|
+
├─ Short hop? aligned (same X or Y), Manhattan ≤ 300 ext,
|
|
254
|
+
│ L-route ≤ 500 ext, crossings < 3, not a ≥4-node hub net
|
|
255
|
+
│ → autoConnect (preferred — real wire, clean sheet)
|
|
256
|
+
│ e.g. cap→GND beside host, EN→C_EN, nearby LDO.VOUT→MCU.VDD
|
|
257
|
+
│
|
|
258
|
+
└─ Long / crowded / off-page / ≥4 nodes on net
|
|
259
|
+
→ dual-end placePinStubWireAndNetAlias (same netName)
|
|
260
|
+
NO autoConnect between these two pins afterward
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
| Situation | Mode | Example |
|
|
264
|
+
| --- | --- | --- |
|
|
265
|
+
| Two pins close (gate passes short-hop checks) | **autoConnect** | Decap→GND, EN→reset cap, LDO→MCU when placed adjacent |
|
|
266
|
+
| Two pins far apart or L-route > 500 / ≥3 crossings | **stub+alias** | Type-C ↔ LDO ↔ ESP when blocks are spaced out |
|
|
267
|
+
| Pattern already wired one end | **stub+alias on the other end only** | GPIO9 after `pull_resistor` → SW |
|
|
268
|
+
| Replacing a whole pull/reset/decoupling cluster | **pattern** — not stub instead of pattern | CC→R→GND, NRST reset |
|
|
269
|
+
|
|
270
|
+
**Forbidden (common script bugs):**
|
|
271
|
+
|
|
272
|
+
- `autoConnect` **without measuring** — the usual mistake on cross-block power nets
|
|
273
|
+
- `autoConnect` between far pins, then alias on **one** side only — long wire + duplicate labels
|
|
274
|
+
- Skipping `pull_resistor` / `reset_circuit` and using stub+alias for the **whole** R→rail chain
|
|
275
|
+
- `autoConnect` as fallback when stub fails — fix pin_name / pinNumber instead
|
|
276
|
+
- Hub: one pin `autoConnect` to many targets on the same net
|
|
277
|
+
|
|
278
|
+
**Script helper:** implement `routeNet(a, pinA, b, pinB, netName)` that **measures
|
|
279
|
+
first**, then calls `autoConnectObjectsById` or dual-end stub+alias (`tieFarNet`).
|
|
280
|
+
Reserve `tieFarNet()` for nets the gate classifies as long — do not call it blindly
|
|
281
|
+
for every cross-part net when placement already put the pins next to each other.
|
|
282
|
+
|
|
283
|
+
### Circuit pattern first — decision checklist
|
|
284
|
+
|
|
285
|
+
Before **any** hand placement coordinates, `autoConnect`, or `PlaceWire`, run
|
|
286
|
+
this checklist. It applies to the whole sheet and to **each local subnet**
|
|
287
|
+
independently (see mixed-circuit decomposition in
|
|
288
|
+
[`circuit-pattern-layout.md`](circuit-pattern-layout.md)).
|
|
289
|
+
|
|
290
|
+
#### Step 0 — Refresh catalog
|
|
291
|
+
|
|
292
|
+
- [ ] Call `pattern-layout-list-circuit-patterns` (or read the latest catalog).
|
|
293
|
+
- [ ] New pattern ids may have been added since the last session — do not rely on
|
|
294
|
+
memory or old scripts alone.
|
|
295
|
+
|
|
296
|
+
#### Step 1 — Decompose the design
|
|
297
|
+
|
|
298
|
+
- [ ] List every local cluster (power pin decoupling, single-pin pull-up/down,
|
|
299
|
+
bus interface, crystal, pin shorts, termination R, net aliases, …).
|
|
300
|
+
- [ ] Mark each cluster **pattern** or **hand** — independently. A sheet may be
|
|
301
|
+
both.
|
|
302
|
+
|
|
303
|
+
#### Step 2 — Match clusters to catalog (current shortcuts)
|
|
304
|
+
|
|
305
|
+
| Local circuit | Pattern id | Skill |
|
|
306
|
+
| --- | --- | --- |
|
|
307
|
+
| Bypass / decoupling caps on a power pin | `CIRCUIT_PATTERN_DECOUPLING_CAP` | `pattern-layout-apply-circuit-pattern` |
|
|
308
|
+
| Pull-up / pull-down resistors (signal → R → rail) | `CIRCUIT_PATTERN_PULL_RESISTOR` | `pattern-layout-apply-circuit-pattern` |
|
|
309
|
+
| MCU reset (pull-up + cap + optional button) | `CIRCUIT_PATTERN_RESET_CIRCUIT` | `pattern-layout-apply-circuit-pattern` |
|
|
310
|
+
| I2C master + slaves + SDA/SCL pull-ups | `CIRCUIT_PATTERN_I2C_BUS` | `pattern-layout-apply-circuit-pattern` |
|
|
311
|
+
| SPI master + slaves | `CIRCUIT_PATTERN_SPI_BUS` | `pattern-layout-apply-circuit-pattern` |
|
|
312
|
+
| Crystal + load caps (+ feedback R) | `CIRCUIT_PATTERN_CRYSTAL` | `pattern-layout-apply-circuit-pattern` |
|
|
313
|
+
|
|
314
|
+
*When new patterns ship, extend this table from `list-circuit-patterns` — the RPC
|
|
315
|
+
is the source of truth.*
|
|
316
|
+
|
|
317
|
+
| Usually **hand** (no catalog entry yet) | Approach |
|
|
318
|
+
| --- | --- |
|
|
319
|
+
| Cross-module / different parts on one net | Run [Routing mode gate](#routing-mode-gate-mandatory-before-every-net): **autoConnect if short**, stub+NetAlias if long |
|
|
320
|
+
| Termination / series R between two signal pins | Same gate — short → autoConnect; far → stub+NetAlias |
|
|
321
|
+
| Two pins shorted on one IC | `autoConnect` (same part, short) |
|
|
322
|
+
| Off-page / port labels | stub + NetAlias after patterns |
|
|
323
|
+
|
|
324
|
+
#### Step 3 — Apply patterns (per matching cluster)
|
|
325
|
+
|
|
326
|
+
- [ ] `pattern-layout-get-page-occupancy` — pick anchors outside reserved boxes.
|
|
327
|
+
- [ ] Place parts (rough position is OK for movable roles).
|
|
328
|
+
- [ ] `apply-circuit-pattern(plan_only=true)` until anchor is free.
|
|
329
|
+
- [ ] `apply-circuit-pattern(plan_only=false)` — book-keep `occupied_box`.
|
|
330
|
+
- [ ] Repeat for **each** matching subnet (multiple applies per sheet is normal).
|
|
331
|
+
|
|
332
|
+
**Same IC, multiple patterns:** allowed when the IC is `host` / `signal_host`
|
|
333
|
+
/ `master` (`movable=false`). For **same-polarity pulls to one rail**, prefer
|
|
334
|
+
**one** `pull_resistor` apply with multiple `slotIndex` pairs + bound `rail`.
|
|
335
|
+
**Not allowed:**
|
|
336
|
+
the same **movable** part (one resistor, one cap) bound in two applies. Details:
|
|
337
|
+
[`circuit-pattern-layout.md` — Pull resistor: multiple pins](circuit-pattern-layout.md#pull-resistor-multiple-pins).
|
|
338
|
+
|
|
339
|
+
#### Step 4 — Hand-layout leftovers only
|
|
340
|
+
|
|
341
|
+
- [ ] Run [Routing mode gate](#routing-mode-gate-mandatory-before-every-net) **for each net**.
|
|
342
|
+
- [ ] Do **not** re-wire nets already listed in `response.connections` with `routed=true`.
|
|
343
|
+
- [ ] **Short hop (gate pass):** `autoConnectObjectsById` — even between different parts if pins are close.
|
|
344
|
+
- [ ] **Long hop (gate fail):** dual-end `placePinStubWireAndNetAlias` (same `netName`).
|
|
345
|
+
- [ ] Power symbols: `autoConnect` pin **0** when the rail tap is adjacent; stub+alias when modules are far apart.
|
|
346
|
+
|
|
347
|
+
If **no** subnet matches any catalog entry, skip Step 3 and hand-layout the
|
|
348
|
+
whole local circuit.
|
|
349
|
+
|
|
350
|
+
Full role / terminal / option catalog and plan-then-apply rules:
|
|
351
|
+
[`circuit-pattern-layout.md`](circuit-pattern-layout.md).
|
|
352
|
+
|
|
353
|
+
**Symbol pin numbers:** for **power / GND / hierarchical port / off-page**
|
|
354
|
+
symbols placed via `PlaceSymbolFromLibrary`, the connection pin number in
|
|
355
|
+
`AutoConnectObjectsById` is **always `0`** — not `1`. Regular parts (R, C, L,
|
|
356
|
+
U, …) use the symbol's printed pin numbers (`1`, `2`, …).
|
|
357
|
+
|
|
358
|
+
### Decoupling cap example (VCC_5V — C1 — GND)
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// 1. Place symbols/parts — save object_id from each response
|
|
362
|
+
const vccRes = await client.componentPlace.placeSymbolFromLibrary({ /* … */ localPos: { x: 50n, y: 50n }, netName: "VCC_5V" });
|
|
363
|
+
const gndRes = await client.componentPlace.placeSymbolFromLibrary({ /* … */ localPos: { x: 50n, y: 100n }, netName: "GND" });
|
|
364
|
+
const c1Res = await client.componentPlace.placePartFromLibrary({ /* … */ localPos: { x: 50n, y: 75n } });
|
|
365
|
+
|
|
366
|
+
const vccId = vccRes.objectId!;
|
|
367
|
+
const gndId = gndRes.objectId!;
|
|
368
|
+
const c1Id = c1Res.objectId!;
|
|
369
|
+
|
|
370
|
+
// 2. Set properties (optional but recommended)
|
|
371
|
+
await client.canvasOps.setObjectProperty({ objectId: c1Id, propKey: "Part Reference", propValue: "C1", op: 1, commitUndo: true });
|
|
372
|
+
await client.canvasOps.setObjectProperty({ objectId: c1Id, propKey: "Value", propValue: "0.1 μF", op: 1, commitUndo: true });
|
|
373
|
+
|
|
374
|
+
// 3. Wire by object_id (preferred — no snapshot needed)
|
|
375
|
+
// Symbol pins (VCC, GND, port, off-page): pin number is ALWAYS 0.
|
|
376
|
+
await client.canvasOps.autoConnectObjectsById({
|
|
377
|
+
context: projectCtx,
|
|
378
|
+
objectId1: c1Id, pinNum1: [1],
|
|
379
|
+
objectId2: vccId, pinNum2: [0],
|
|
380
|
+
});
|
|
381
|
+
await client.canvasOps.autoConnectObjectsById({
|
|
382
|
+
context: projectCtx,
|
|
383
|
+
objectId1: c1Id, pinNum1: [2],
|
|
384
|
+
objectId2: gndId, pinNum2: [0],
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Schematic wire routing (avoid overlap)
|
|
389
|
+
|
|
390
|
+
`AutoConnectObjectsById` routes **each call independently**. If many endpoints
|
|
391
|
+
on the same net all connect to one anchor (hub / star topology), the router
|
|
392
|
+
reuses the same corridor and wire segments stack visually.
|
|
393
|
+
|
|
394
|
+
Apply these rules for **any multi-node net** — power, GND, buses, control
|
|
395
|
+
signals, or groups of parallel passives — not only decoupling caps.
|
|
396
|
+
|
|
397
|
+
| Rule | Requirement |
|
|
398
|
+
| --- | --- |
|
|
399
|
+
| **Separate branch lanes at placement time** | Before wiring, offset each branch along the axis **perpendicular** to the intended bus so taps do not share one column (or row). On a horizontal bus, give each branch a **unique X**; on a vertical bus, a **unique Y**. Spacing can be small but must not be identical. |
|
|
400
|
+
| **Chain nodes on the same net** | Connect **two adjacent nodes per call** in sequence: `anchor → n₁ → n₂ → … → nₖ`. This builds a bus with independent vertical (or horizontal) drops instead of re-routing from the anchor every time. |
|
|
401
|
+
| **Follow a logical order** | Order the chain by signal or power flow (source → load, input → output, left → right). Keep bus direction consistent across related nets on the same sheet when practical. |
|
|
402
|
+
| **Layout is part of routing** | If segments still overlap, **move components** and re-wire — do not stack multiple hub connections from one symbol. |
|
|
403
|
+
|
|
404
|
+
**Avoid (hub — segments overlap):**
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
NET ──┬── node₁
|
|
408
|
+
├── node₂
|
|
409
|
+
└── node₃
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Prefer (chain — each tap uses its own lane):**
|
|
413
|
+
|
|
414
|
+
```
|
|
415
|
+
NET ── node₁ ── node₂ ── node₃
|
|
416
|
+
│ │ │
|
|
417
|
+
(unique X or Y per branch when dropping off the bus)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Minimal pattern:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// Same net, sequential links — NOT anchor → every node
|
|
424
|
+
for (const [id1, pin1, id2, pin2] of [
|
|
425
|
+
[anchorId, 1, n1Id, 1],
|
|
426
|
+
[n1Id, 1, n2Id, 1],
|
|
427
|
+
[n2Id, 1, n3Id, 1],
|
|
428
|
+
] as const) {
|
|
429
|
+
await getSkill("canvas-auto-connect-objects-by-id")!.execute(ctx, {
|
|
430
|
+
context: projectCtx,
|
|
431
|
+
objectId1: id1, pinNum1: [pin1],
|
|
432
|
+
objectId2: id2, pinNum2: [pin2],
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Place the anchor (power symbol, port, or upstream driver) at one end of the
|
|
438
|
+
chain; distribute loads/passives along the bus axis with **non-colliding**
|
|
439
|
+
coordinates before calling `AutoConnectObjectsById`.
|
|
440
|
+
|
|
441
|
+
### Stub + Net Alias (long distance / many crossings)
|
|
442
|
+
|
|
443
|
+
> Use when the [routing mode gate](#routing-mode-gate-mandatory-before-every-net)
|
|
444
|
+
> **fails** the short-hop checks. **Short nets still use `autoConnect`** — stub+alias
|
|
445
|
+
> is not the default for every cross-part net, only for nets that would draw a long
|
|
446
|
+
> or crowded wire.
|
|
447
|
+
|
|
448
|
+
When two pins are far apart or a direct orthogonal route would cross many
|
|
449
|
+
existing wires, **do not** call `AutoConnectObjectsById` between them.
|
|
450
|
+
Instead, place **matching `NetAlias` labels** on the same net name at each pin
|
|
451
|
+
end — either on a **new short stub** (**30** ext units) or **directly on an
|
|
452
|
+
existing wire** already connected to that pin.
|
|
453
|
+
|
|
454
|
+
```
|
|
455
|
+
Pin A ── stub ── NetAlias NET_X NetAlias NET_X ── stub ── Pin B
|
|
456
|
+
(30) (30)
|
|
457
|
+
|
|
458
|
+
Pin A ── existing wire ── NetAlias NET_X (no new stub if wire already on pin)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Electrical connectivity comes from two aliases sharing the same `netName` on
|
|
462
|
+
the active page — not from a long wire between the pins.
|
|
463
|
+
|
|
464
|
+
#### When to use (agent heuristics)
|
|
465
|
+
|
|
466
|
+
**Do not use pin-to-pin Manhattan alone** — routed wire length often exceeds straight
|
|
467
|
+
distance when the path wraps around symbol bounding boxes (typical multi-pin IC layouts).
|
|
468
|
+
|
|
469
|
+
| Condition | Action |
|
|
470
|
+
| --- | --- |
|
|
471
|
+
| Manhattan distance between pins (ext coords) **> 400** (cross-part / off-page) | Use stub + alias (both pin ends, same `netName`) |
|
|
472
|
+
| Estimated **L-shaped** route length (H→V or V→H, shorter variant, including bbox detour) **> 500** | Use stub + alias |
|
|
473
|
+
| Proposed L-route crosses **≥ 3** existing `wireSegments` on the active page | Use stub + alias |
|
|
474
|
+
| Same net has **≥ 4 nodes** to interconnect | Stub + alias at each pin; **no** hub/star `AutoConnectObjectsById` |
|
|
475
|
+
| **Different parts**, pins **aligned**, Manhattan **≤ 300**, L-route **≤ 500**, crossings **< 3** | **`AutoConnectObjectsById`** — cross-part is fine when close |
|
|
476
|
+
| **Different parts**, Manhattan **> 400** or L-route **> 500** or crossings **≥ 3** | Dual-end stub + alias |
|
|
477
|
+
| Pins **aligned** (same X or same Y), Manhattan **≤ 300**, crossings **< 3** (same or different parts) | `AutoConnectObjectsById` or a single `PlaceWire` |
|
|
478
|
+
| **Several signal pins on one IC edge** (see [Dense same-edge pins](#dense-same-edge-pins)) | Prefer `AutoConnectObjectsById` per net to the remote pin; reserve stub + alias for long hops or single-pin parts |
|
|
479
|
+
|
|
480
|
+
**Stub + alias is symmetric:** call `placePinStubWireAndNetAlias` at **each** pin end
|
|
481
|
+
with the **same** `netName`. Do **not** `autoConnect` between the pins and add an alias
|
|
482
|
+
on only one side — that leaves long physical wires on the page.
|
|
483
|
+
|
|
484
|
+
**One routing mode per net:** for a given `(netName, pinA, pinB)` pair, choose **either**
|
|
485
|
+
`AutoConnectObjectsById` + alias on the resulting wires **or** dual-end stub + alias —
|
|
486
|
+
**not both**. Mixing modes leaves orphan stubs, duplicate NetAlias labels, and ERC noise.
|
|
487
|
+
|
|
488
|
+
**Never** call `PlaceWire` manually before `placePinStubWireAndNetAlias` to “ensure a
|
|
489
|
+
stub exists” — stub creation is the RPC’s job. Manual stubs often duplicate what
|
|
490
|
+
`autoConnect` already placed and are not removed automatically.
|
|
491
|
+
|
|
492
|
+
**List existing wires** before routing: `canvas-list-wire-segments` (active page,
|
|
493
|
+
real-time) or `kernel-get-snapshot` → `wireSegments[]`. Endpoints are in
|
|
494
|
+
**canvas coordinates (Y up)** — convert to ext before comparing with pin ext
|
|
495
|
+
coords, or convert pins to canvas for crossing checks.
|
|
496
|
+
|
|
497
|
+
**Crossing check (agent reasoning):**
|
|
498
|
+
|
|
499
|
+
1. Each `wireSegment` is a straight segment `(x1,y1)–(x2,y2)`.
|
|
500
|
+
2. Candidate direct route = orthogonal L-path: pinA → corner → pinB (two segments).
|
|
501
|
+
3. Segments that share an endpoint do **not** count as crossings; collinear overlap does not add extra crossings.
|
|
502
|
+
4. Evaluate both H→V and V→H corners; use the variant with **fewer** crossings. If the minimum is still **≥ 3**, use stub + alias.
|
|
503
|
+
|
|
504
|
+
#### Pin already has a wire
|
|
505
|
+
|
|
506
|
+
Before placing a stub at a pin, check whether that pin **already has a wire
|
|
507
|
+
segment connected** (from prior `AutoConnectObjectsById`, `PlaceWire`, or manual
|
|
508
|
+
editing):
|
|
509
|
+
|
|
510
|
+
| Pin state | Action |
|
|
511
|
+
| --- | --- |
|
|
512
|
+
| **No wire** at pin | `obj-place-place-pin-stub-wire-and-net-alias` creates stub + alias (default **30** ext; C++ extends **+10** when stub end hits a **perpendicular** wire; parallel stubs on the same edge are ignored) |
|
|
513
|
+
| **Wire already connected** at pin | Same RPC — **no** new stub; alias on existing wire (**30** ext from pin) |
|
|
514
|
+
|
|
515
|
+
**Detecting an existing connection (intended rule):** `placePinStubWireAndNetAlias`
|
|
516
|
+
should set `usedExistingWire=true` **only** when a wire segment **endpoint** matches
|
|
517
|
+
**this pin's** connection point (`objectId` + `pinNum`/`pinName` → exact `ex/ey`,
|
|
518
|
+
tolerance ≈ `PIN_CONNECT_OFFSET`, **not** one full grid step). A wire merely
|
|
519
|
+
passing near an adjacent pin, or attached to a **neighbor pin on the same symbol**,
|
|
520
|
+
should **not** count — the engine should create a **new stub** (`wireObjectId > 0`).
|
|
521
|
+
|
|
522
|
+
Agents mirroring this check: from `canvas-list-wire-segments` or `wireSegments[]`,
|
|
523
|
+
require **start or end** coords equal to **that pin's** position (within ~3 ext),
|
|
524
|
+
not “any wire on the same row / same escape direction”.
|
|
525
|
+
|
|
526
|
+
**Observed limitation — pin-level wire detection:** On symbols with **multiple
|
|
527
|
+
adjacent signal pins on one edge** (same escape direction, typical **10 ext** grid
|
|
528
|
+
spacing), sequential `placePinStubWireAndNetAlias` calls can still attach a NetAlias
|
|
529
|
+
to the **wrong physical wire** even when RPC returns `success=true`:
|
|
530
|
+
|
|
531
|
+
| What you see | Meaning |
|
|
532
|
+
| --- | --- |
|
|
533
|
+
| `stub=30`, `wireObjectId > 0` on every call | RPC succeeded; stubs were created |
|
|
534
|
+
| `stub=0`, `usedExistingWire=true` on later pins | Alias placed on an **existing** segment — verify it belongs to **this** `pinNum` |
|
|
535
|
+
| `listWireSegments` near pin *N* lists **other** net names | Physical segment is shared or mis-attached — logical NetAlias may still differ |
|
|
536
|
+
| All adjacent pins show the **same** wire `netName` | Nets merged on one stub column — **do not** trust wire names alone |
|
|
537
|
+
|
|
538
|
+
Reproduction pattern: place distinct `netName` values on **consecutive pin numbers**
|
|
539
|
+
on one part (e.g. pins 10–17 → `net1`…`net8` on the active page). Compare NetAlias
|
|
540
|
+
labels and `netlist-get-active-page-net-list` to wire `netName` at each pin.
|
|
541
|
+
|
|
542
|
+
**Root cause (agent diagnosis):** “Detect existing wire” inside
|
|
543
|
+
`PlacePinStubWireAndNetAlias` is not always scoped to **(objectId + pinNum + pin
|
|
544
|
+
connection point)**; stubs escaping the same way on one edge can overlap or be treated
|
|
545
|
+
as shared, so labels land on the wrong segment.
|
|
546
|
+
|
|
547
|
+
**Engine fix request:** Restrict “wire at pin” to segments whose start **or** end
|
|
548
|
+
equals **this pin’s** connection geometry — never any wire on the symbol body or escape row.
|
|
549
|
+
|
|
550
|
+
**Alias placement on existing wire:**
|
|
551
|
+
|
|
552
|
+
- Set `localPos` to a point **on the wire**, typically **30 ext units along
|
|
553
|
+
the wire away from the pin** (same spacing as a stub would use), not on the
|
|
554
|
+
symbol body.
|
|
555
|
+
- Prefer `attachObjectId = <wire segment canvasObjectId>` so the alias snaps to
|
|
556
|
+
the wire; `attachObjectId = 0` (auto attach) is acceptable when the position
|
|
557
|
+
lies on the segment.
|
|
558
|
+
- If the existing wire is very short (< 30 ext units), place the alias at the
|
|
559
|
+
far end of that segment or slightly beyond along the same axis — still **no
|
|
560
|
+
duplicate stub**.
|
|
561
|
+
|
|
562
|
+
Apply this check **independently per pin** — pin A may need a new stub while pin
|
|
563
|
+
B already has a wire and only needs an alias.
|
|
564
|
+
|
|
565
|
+
#### Stub length and direction
|
|
566
|
+
|
|
567
|
+
| Item | Rule |
|
|
568
|
+
| --- | --- |
|
|
569
|
+
| **Length** | **30** ext units initial; C++ extends **+10** ext when stub end hits a **perpendicular** wire (horizontal stub ↔ vertical wire); parallel wires ignored (up to `maxStubLength`, default 300) |
|
|
570
|
+
| **Direction** | Extend **away from the symbol body** along the pin escape axis: horizontal pin → ±X; vertical pin → ±Y (ext Y **down**, +Y = downward) |
|
|
571
|
+
| **Grid** | Always `snapToGrid: true`; 30 is a multiple of the schematic grid (`GRIDSPACE = 10`) |
|
|
572
|
+
| **Avoid** | Do not route stubs through the symbol bounding box — flip direction if blocked |
|
|
573
|
+
|
|
574
|
+
Pin positions: snapshot `pinInstances[].position` (canvas, Y up) →
|
|
575
|
+
`extY = PAGE_SIZE_START_Y - canvasY` (see [`property-conventions.md`](property-conventions.md)).
|
|
576
|
+
|
|
577
|
+
#### Procedure (one net name, two pins)
|
|
578
|
+
|
|
579
|
+
For net `NET_X` between pinA and pinB:
|
|
580
|
+
|
|
581
|
+
1. Resolve pin positions in **external display coordinates**.
|
|
582
|
+
2. **Per pin**, list connected wire segments (see [Pin already has a wire](#pin-already-has-a-wire)).
|
|
583
|
+
3. **Pin A side** — `obj-place-place-pin-stub-wire-and-net-alias` with `objectId`, `pinNum`/`pinName`, `netName = "NET_X"`, `snapToGrid: true`. The RPC detects existing wire, places stub + alias when needed, and merges wire + alias into **one undo step**.
|
|
584
|
+
4. **Pin B side** — same RPC; `netName` **must match** pin A.
|
|
585
|
+
5. **Do not** call `AutoConnectObjectsById` between A and B afterward (that defeats the pattern).
|
|
586
|
+
6. **Verify** — `kernel-get-snapshot` or `netlist-get-active-page-net-list`: both pins on the same net; run ERC if needed.
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
function manhattanExt(a: { x: number; y: number }, b: { x: number; y: number }) {
|
|
590
|
+
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Measure first — short → wire, long → alias both ends. */
|
|
594
|
+
async function routeNet(
|
|
595
|
+
partA: bigint, pinA: { pinNum?: number; pinName?: string; x: number; y: number },
|
|
596
|
+
partB: bigint, pinB: { pinNum?: number; pinName?: string; x: number; y: number },
|
|
597
|
+
netName: string,
|
|
598
|
+
) {
|
|
599
|
+
if (manhattanExt(pinA, pinB) <= 300) {
|
|
600
|
+
await client.canvasOps.autoConnectObjectsById({
|
|
601
|
+
context: projectCtx,
|
|
602
|
+
objectId1: partA, pinNum1: pinA.pinNum != null ? [pinA.pinNum] : undefined,
|
|
603
|
+
pinName1: pinA.pinName ? [pinA.pinName] : undefined,
|
|
604
|
+
objectId2: partB, pinNum2: pinB.pinNum != null ? [pinB.pinNum] : undefined,
|
|
605
|
+
pinName2: pinB.pinName ? [pinB.pinName] : undefined,
|
|
606
|
+
});
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
for (const [objectId, pin] of [[partA, pinA], [partB, pinB]] as const) {
|
|
610
|
+
await client.objPlace.placePinStubWireAndNetAlias({
|
|
611
|
+
context: projectCtx, objectId, pinNum: pin.pinNum, pinName: pin.pinName,
|
|
612
|
+
netName, snapToGrid: true,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
#### Relation to other wiring rules
|
|
619
|
+
|
|
620
|
+
- **Bus / chain nets**: still connect **adjacent** pairs with `AutoConnectObjectsById` when distance and crossings are low; apply stub + alias only on **edges** that fail the heuristics above (signal name = `netName`, e.g. `SCL`, `KEY_IN`).
|
|
621
|
+
- **Power / GND**: short taps keep `AutoConnectObjectsById` with symbol pin **0**; long power trees may use stub + alias with `VCC_3V3`, `GND`, etc.
|
|
622
|
+
- **Single-point labels**: placing one alias at a junction (e.g. `KEY_IN` at one node) is for **annotation**; stub + alias is for **connecting two distant pins** — one alias per pin end (stub or existing wire), same `netName`.
|
|
623
|
+
|
|
624
|
+
#### Dense same-edge pins
|
|
625
|
+
|
|
626
|
+
When **several signal pins on one symbol edge** each need connectivity (same escape
|
|
627
|
+
direction, **~10 ext** pin spacing — common on QFP/QFN left/right columns):
|
|
628
|
+
|
|
629
|
+
**Prefer**
|
|
630
|
+
|
|
631
|
+
- **`AutoConnectObjectsById`** from each pin to its **remote** counterpart (one net
|
|
632
|
+
at a time), then **one** `placePinStubWireAndNetAlias` per pin to name the wire
|
|
633
|
+
already on **that** pin (`usedExistingWire=true` is OK when the wire came from
|
|
634
|
+
`autoConnect` on the same `pinNum`).
|
|
635
|
+
- **Dual-end stub + alias** only when the **other end** is far away **and** this part
|
|
636
|
+
exposes **one pin** for that net on this edge (e.g. a sensor’s single SDA pin).
|
|
637
|
+
|
|
638
|
+
**Avoid**
|
|
639
|
+
|
|
640
|
+
- Sequential stub + alias on **every** pin of a dense column without physical routes —
|
|
641
|
+
see [Observed limitation — pin-level wire detection](#pin-already-has-a-wire).
|
|
642
|
+
- Pre-drawing a shared bus with `PlaceWire` before labeling — creates orphan segments.
|
|
643
|
+
- `autoConnect` between two pins, then alias on **one** side only — long wires + duplicate labels.
|
|
644
|
+
- Re-running stub/alias or `autoConnect` on a pin already labeled for that `netName`
|
|
645
|
+
(track `(objectId, pinNum, netName)` and skip duplicates).
|
|
646
|
+
|
|
647
|
+
**Multi-tap / shared nets (one signal, ≥ 3 parts):**
|
|
648
|
+
|
|
649
|
+
- Wire **one new segment per tap** (A↔B, then B↔C) or stub + alias at **each** endpoint once.
|
|
650
|
+
- After A↔B is done, adding C: connect **only the new segment** (B↔C or A↔C); **do not**
|
|
651
|
+
re-connect A↔B or re-alias pins already done for that `netName`.
|
|
652
|
+
- **Wrong pattern:** hub/star `autoConnect` from one pin to many slaves **and** stub +
|
|
653
|
+
alias on the same net — duplicates wires and NetAlias on shared pins.
|
|
654
|
+
|
|
655
|
+
#### Verifying stub + alias
|
|
656
|
+
|
|
657
|
+
- **Do not** treat `wireSegments[].netName` as proof of success — physical wire
|
|
658
|
+
net names may differ from the **NetAlias label text** until net refresh completes.
|
|
659
|
+
- **Do** confirm `netAliasObjectId > 0` in the RPC response and, after a brief
|
|
660
|
+
retry, that `netlist-get-active-page-net-list` shows both pins on the same logical net.
|
|
661
|
+
- **Do** inspect NetAlias label objects (not wire `netName` alone) when debugging
|
|
662
|
+
missing or wrong labels.
|
|
663
|
+
|
|
664
|
+
#### Known limitations
|
|
665
|
+
|
|
666
|
+
- **`PlacePinStubWireAndNetAlias` on dense same-edge pins** — see
|
|
667
|
+
[Observed limitation — pin-level wire detection](#pin-already-has-a-wire) and
|
|
668
|
+
[Dense same-edge pins](#dense-same-edge-pins).
|
|
669
|
+
- **`listWireSegments` / `wireSegments[].netName` ≠ NetAlias text** — stub + alias
|
|
670
|
+
logical nets may show **zero** wire segments with the alias name; verify with
|
|
671
|
+
NetAlias objects and `netlist-get-active-page-net-list` / ERC.
|
|
672
|
+
- `PlaceNetAliasAt` has returned **REFUSED** in some API audits — if alias placement fails, fall back to direct `PlaceWire` between pins and report the failure. See [hqsch-api-slow-and-broken-rpcs.md](../../../docs/issues/hqsch-api-slow-and-broken-rpcs.md).
|
|
673
|
+
- Snapshot may lag immediately after placement — retry `kernel-get-snapshot` briefly before verifying net membership.
|
|
674
|
+
|
|
675
|
+
### Fallback — PlaceWire via snapshot pin lookup
|
|
676
|
+
|
|
677
|
+
If `AutoConnectObjectsById` fails, call `kernel-get-snapshot`, locate pins via
|
|
678
|
+
`pinInstances[].canvas_object_id` (equal to placement `object_id`), convert
|
|
679
|
+
canvas Y-up positions to external Y-down coords, then `PlaceWire`.
|
|
680
|
+
See [`property-conventions.md`](property-conventions.md) for conversion and retry.
|
|
681
|
+
|
|
682
|
+
### Enumerating existing wires
|
|
683
|
+
|
|
684
|
+
| Use case | Preferred skill | Notes |
|
|
685
|
+
| --- | --- | --- |
|
|
686
|
+
| List wires on the **active page** (real-time) | `canvas-list-wire-segments` | O(page objects); returns `objectId`, canvas-local endpoints, optional `netName`. Use for **pin endpoint** matching and crossing checks — **not** as sole proof that a NetAlias label exists (`netName` on wire ≠ alias text). |
|
|
687
|
+
| Batch topology / ERC / BOM pull model | `kernel-get-snapshot` → `wireSegments[]` | Each segment includes `canvasObjectId` + geometry; same id as `canvas-list-wire-segments` when the page is unchanged. |
|
|
688
|
+
|
|
689
|
+
Do **not** brute-force `canvas-get-objects-json-by-ids` over id ranges to find wires.
|
|
690
|
+
|
|
691
|
+
## Capability Skill IDs
|
|
692
|
+
|
|
693
|
+
| Task | Preferred skill |
|
|
694
|
+
| --- | --- |
|
|
695
|
+
| Search online parts | `eda-part-search` / `search_hqsch_parts` |
|
|
696
|
+
| Place R/C/Q/U (online) | `placement-place-kicad-symbol` |
|
|
697
|
+
| Place GND/VCC/port/off-page | `placement-place-symbol-from-library` |
|
|
698
|
+
| List symbol libraries | `placement-list-symbol-libraries` |
|
|
699
|
+
| Known local circuit (I2C / SPI / crystal / decoupling / pull) | `pattern-layout-list-circuit-patterns` → `pattern-layout-apply-circuit-pattern` — see [circuit-pattern-layout.md](circuit-pattern-layout.md) |
|
|
700
|
+
| Leftover nets — short hop (gate pass, ≤300 ext, low crossings) | `canvas-auto-connect-objects-by-id` |
|
|
701
|
+
| Leftover nets — long hop (gate fail) | `obj-place-place-pin-stub-wire-and-net-alias` at **both** pin ends — see [Routing mode gate](#routing-mode-gate-mandatory-before-every-net) |
|
|
702
|
+
| List existing wires (active page) | `canvas-list-wire-segments` |
|
|
703
|
+
| Wire via pin coords (fallback) | `kernel-get-snapshot` → `obj-place-place-wire` |
|
|
704
|
+
| Wire topology from snapshot | `kernel-get-snapshot` → `wireSegments[].canvasObjectId` |
|
|
705
|
+
| Fallback: local part lib | `placement-list-part-libraries` → `placement-place-part-from-library` |
|
|
706
|
+
| **Avoid for agents** | `placement-place-part`, `placement-place-symbol` |
|
|
707
|
+
|
|
708
|
+
## Backend Mapping
|
|
709
|
+
|
|
710
|
+
| gRPC | SDK |
|
|
711
|
+
| --- | --- |
|
|
712
|
+
| `PlaceKicadSymbol` | `SCH_PlaceKicadSymbolFromResource` |
|
|
713
|
+
| `PlaceSymbolFromLibrary` | `SCH_PlaceSymbolFromLibrary` |
|
|
714
|
+
| `PlacePartFromLibrary` | `SCH_PlacePartFromLibrary` |
|