@tscircuit/jumper-topology-generator 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 tscircuit Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # @tscircuit/jumper-topology-generator
2
+
3
+ Generate 0603 jumper placement/topology as a HyperGraph, including:
4
+
5
+ - top-layer conductive regions
6
+ - jumper pad regions
7
+ - connectivity ports between touching regions
8
+ - via locations and overall bounds
9
+
10
+ ## Preview
11
+
12
+ ![Jumper topology preview](./tests/__snapshots__/0603-horizontal-3x2.snap.svg)
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ bun add @tscircuit/jumper-topology-generator
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```ts
23
+ import { generate0603JumperHyperGraph } from "@tscircuit/jumper-topology-generator"
24
+
25
+ const graph = generate0603JumperHyperGraph({
26
+ cols: 3,
27
+ rows: 2,
28
+ orientation: "horizontal",
29
+ })
30
+
31
+ console.log(graph.regions.length)
32
+ console.log(graph.ports.length)
33
+ console.log(graph.bounds)
34
+ ```
35
+
36
+ ## API
37
+
38
+ ### `generate0603JumperHyperGraph(options)`
39
+
40
+ Creates a `JumperHyperGraph` from a 0603 jumper matrix.
41
+
42
+ Required options:
43
+
44
+ - `cols: number` - number of jumper columns (`> 0`)
45
+ - `rows: number` - number of jumper rows (`> 0`)
46
+
47
+ Optional options:
48
+
49
+ - `pattern: "grid" | "staggered"` (default: `"grid"`)
50
+ - `orientation: "horizontal" | "vertical"` (default: `"horizontal"`)
51
+ - `pitchX: number` or `colSpacing: number` (default: `2.2`)
52
+ - `pitchY: number` or `rowSpacing: number` (default: `1.8`)
53
+ - `staggerAxis: "x" | "y"` (default: `"x"`)
54
+ - `staggerOffset: number` (or alias `staggerOffsetX`)
55
+ - `padWidth: number` (default: `0.9`)
56
+ - `padHeight: number` (default: `1.0`)
57
+ - `padGap: number` (default: `0.35`)
58
+ - `viaDiameter: number` (default: `0.3`)
59
+ - `clearance: number` (default: `0.2`)
60
+ - `concavityTolerance: number` (default: `0.3`)
61
+ - `boundsPadding: number` (default: `1.2`)
62
+ - `portSpacing: number` (default: `0.25`, must be `> 0`)
63
+
64
+ Behavior notes:
65
+
66
+ - `colSpacing` overrides `pitchX`; `rowSpacing` overrides `pitchY`.
67
+ - Pitch values are clamped to avoid overlap:
68
+ - `pitchX >= jumperBodyWidth + clearance`
69
+ - `pitchY >= jumperBodyHeight + clearance`
70
+ - For `pattern: "staggered"`, if `staggerOffset` is omitted, it defaults to half the jumper size along the stagger axis.
71
+ - Smaller `portSpacing` creates denser top-layer edge ports.
72
+
73
+ ## Return Shape
74
+
75
+ `generate0603JumperHyperGraph` returns a `JumperHyperGraph` with:
76
+
77
+ - `regions`: all regions (`topLayerRegions + jumperRegions`)
78
+ - `ports`: all region-to-region connectivity ports
79
+ - `jumperLocations`: jumper-centric mapping used by `@tscircuit/hypergraph`
80
+ - `topLayerRegions`: convex top copper regions
81
+ - `jumperRegions`: one region per jumper body
82
+ - `jumpers`: raw jumper placements and pad centers
83
+ - `vias`: one via per pad center
84
+ - `bounds`: overall bounding box
85
+
86
+ ## Visualization Helper
87
+
88
+ Use `visualizeJumperHyperGraph` to generate a `graphics-debug` object:
89
+
90
+ ```ts
91
+ import {
92
+ generate0603JumperHyperGraph,
93
+ visualizeJumperHyperGraph,
94
+ } from "@tscircuit/jumper-topology-generator"
95
+
96
+ const graph = generate0603JumperHyperGraph({ cols: 2, rows: 2 })
97
+ const debugGraphics = visualizeJumperHyperGraph(graph)
98
+ ```
99
+
100
+ ## Examples
101
+
102
+ Staggered pattern along X:
103
+
104
+ ```ts
105
+ import { generate0603JumperHyperGraph } from "@tscircuit/jumper-topology-generator"
106
+
107
+ const graph = generate0603JumperHyperGraph({
108
+ cols: 4,
109
+ rows: 3,
110
+ pattern: "staggered",
111
+ staggerAxis: "x",
112
+ orientation: "horizontal",
113
+ })
114
+ ```
115
+
116
+ Vertical jumpers with coarser edge ports:
117
+
118
+ ```ts
119
+ import { generate0603JumperHyperGraph } from "@tscircuit/jumper-topology-generator"
120
+
121
+ const graph = generate0603JumperHyperGraph({
122
+ cols: 2,
123
+ rows: 2,
124
+ orientation: "vertical",
125
+ portSpacing: 0.75,
126
+ })
127
+ ```
128
+
129
+ ## Output Previews
130
+
131
+ Horizontal grid (`cols: 3, rows: 2, orientation: "horizontal"`):
132
+
133
+ ![Horizontal 3x2 output](./tests/__snapshots__/0603-horizontal-3x2.snap.svg)
134
+
135
+ Vertical grid (`cols: 2, rows: 2, orientation: "vertical"`):
136
+
137
+ ![Vertical 2x2 output](./tests/__snapshots__/0603-vertical-2x2.snap.svg)
138
+
139
+ Staggered X (`cols: 4, rows: 3, pattern: "staggered", staggerAxis: "x"`):
140
+
141
+ ![Staggered X 4x3 output](./tests/__snapshots__/0603-staggered-4x3.snap.svg)
142
+
143
+ Staggered Y (`cols: 3, rows: 3, pattern: "staggered", staggerAxis: "y"`):
144
+
145
+ ![Staggered Y 3x3 output](./tests/__snapshots__/0603-staggered-y-3x3.snap.svg)
146
+
147
+ SVG export fixture (`cols: 2, rows: 1`):
148
+
149
+ ![SVG export 2x1 output](./tests/__snapshots__/0603-svg-export-2x1.snap.svg)
150
+
151
+ ## Local Development
152
+
153
+ Install dependencies:
154
+
155
+ ```bash
156
+ bun install
157
+ ```
158
+
159
+ Build:
160
+
161
+ ```bash
162
+ bun run build
163
+ ```
164
+
165
+ Explore fixtures in React Cosmos:
166
+
167
+ ```bash
168
+ bun run start
169
+ ```
@@ -0,0 +1,78 @@
1
+ import { Point, Via, Bounds } from '@tscircuit/find-convex-regions';
2
+ import { JumperGraph, JRegion } from '@tscircuit/hypergraph';
3
+ import { GraphicsObject } from 'graphics-debug';
4
+
5
+ type JumperOrientation = "horizontal" | "vertical";
6
+ type JumperPatternName = "grid" | "staggered";
7
+ type StaggerAxis = "x" | "y";
8
+ interface Generate0603JumperHyperGraphOptions {
9
+ cols: number;
10
+ rows: number;
11
+ pattern?: JumperPatternName;
12
+ colSpacing?: number;
13
+ rowSpacing?: number;
14
+ pitchX?: number;
15
+ pitchY?: number;
16
+ staggerAxis?: StaggerAxis;
17
+ staggerOffset?: number;
18
+ staggerOffsetX?: number;
19
+ padWidth?: number;
20
+ padHeight?: number;
21
+ padGap?: number;
22
+ viaDiameter?: number;
23
+ clearance?: number;
24
+ concavityTolerance?: number;
25
+ boundsPadding?: number;
26
+ orientation?: JumperOrientation;
27
+ portSpacing?: number;
28
+ }
29
+ interface Resolved0603GridOptions {
30
+ cols: number;
31
+ rows: number;
32
+ pattern: JumperPatternName;
33
+ pitchX: number;
34
+ pitchY: number;
35
+ staggerAxis: StaggerAxis;
36
+ staggerOffset: number;
37
+ staggerOffsetX: number;
38
+ padWidth: number;
39
+ padHeight: number;
40
+ padGap: number;
41
+ viaDiameter: number;
42
+ clearance: number;
43
+ concavityTolerance: number;
44
+ boundsPadding: number;
45
+ orientation: JumperOrientation;
46
+ portSpacing: number;
47
+ }
48
+ interface JumperPlacement {
49
+ jumperId: string;
50
+ center: Point;
51
+ orientation: JumperOrientation;
52
+ padCenters: [Point, Point];
53
+ }
54
+ interface JumperPatternResult {
55
+ jumpers: JumperPlacement[];
56
+ vias: Via[];
57
+ bounds: Bounds;
58
+ }
59
+ interface JumperHyperGraph extends JumperGraph {
60
+ topLayerRegions: JRegion[];
61
+ jumperRegions: JRegion[];
62
+ jumpers: JumperPlacement[];
63
+ vias: Via[];
64
+ bounds: Bounds;
65
+ }
66
+
67
+ declare const generate0603JumperHyperGraph: (input: Generate0603JumperHyperGraphOptions) => JumperHyperGraph;
68
+
69
+ declare const generate0603Pattern: (options: Resolved0603GridOptions) => JumperPatternResult;
70
+
71
+ declare const resolve0603GridOptions: (input: Generate0603JumperHyperGraphOptions) => Resolved0603GridOptions;
72
+ declare const generate0603GridPattern: (options: Resolved0603GridOptions) => JumperPatternResult;
73
+
74
+ declare const generate0603StaggeredPattern: (options: Resolved0603GridOptions) => JumperPatternResult;
75
+
76
+ declare const visualizeJumperHyperGraph: (graph: JumperHyperGraph) => GraphicsObject;
77
+
78
+ export { type Generate0603JumperHyperGraphOptions, type JumperHyperGraph, type JumperOrientation, type JumperPatternName, type JumperPatternResult, type JumperPlacement, type Resolved0603GridOptions, type StaggerAxis, generate0603GridPattern, generate0603JumperHyperGraph, generate0603Pattern, generate0603StaggeredPattern, resolve0603GridOptions, visualizeJumperHyperGraph };
package/dist/index.js ADDED
@@ -0,0 +1,511 @@
1
+ // lib/generate0603JumperHyperGraph.ts
2
+ import { ConvexRegionsSolver } from "@tscircuit/find-convex-regions";
3
+
4
+ // lib/patterns/grid0603.ts
5
+ var getJumperSizeAlongAxis = (orientation, axis, padWidth, padHeight, padGap) => {
6
+ const horizontalSizeX = padGap + padWidth * 2;
7
+ const horizontalSizeY = padHeight;
8
+ const verticalSizeX = padHeight;
9
+ const verticalSizeY = padGap + padWidth * 2;
10
+ if (orientation === "horizontal") {
11
+ return axis === "x" ? horizontalSizeX : horizontalSizeY;
12
+ }
13
+ return axis === "x" ? verticalSizeX : verticalSizeY;
14
+ };
15
+ var getJumperBodySize = (orientation, padWidth, padHeight, padGap) => {
16
+ if (orientation === "horizontal") {
17
+ return {
18
+ x: padGap + padWidth * 2,
19
+ y: padHeight
20
+ };
21
+ }
22
+ return {
23
+ x: padHeight,
24
+ y: padGap + padWidth * 2
25
+ };
26
+ };
27
+ var DEFAULT_0603_GRID_OPTIONS = {
28
+ pattern: "grid",
29
+ pitchX: 2.2,
30
+ pitchY: 1.8,
31
+ staggerAxis: "x",
32
+ staggerOffset: 1.1,
33
+ staggerOffsetX: 1.1,
34
+ padWidth: 0.9,
35
+ padHeight: 1,
36
+ padGap: 0.35,
37
+ viaDiameter: 0.3,
38
+ clearance: 0.2,
39
+ concavityTolerance: 0.3,
40
+ boundsPadding: 1.2,
41
+ orientation: "horizontal",
42
+ portSpacing: 0.25
43
+ };
44
+ var resolve0603GridOptions = (input) => {
45
+ const resolvedOrientation = input.orientation ?? DEFAULT_0603_GRID_OPTIONS.orientation;
46
+ const resolvedStaggerAxis = input.staggerAxis ?? DEFAULT_0603_GRID_OPTIONS.staggerAxis;
47
+ const resolvedPadWidth = input.padWidth ?? DEFAULT_0603_GRID_OPTIONS.padWidth;
48
+ const resolvedPadHeight = input.padHeight ?? DEFAULT_0603_GRID_OPTIONS.padHeight;
49
+ const resolvedPadGap = input.padGap ?? DEFAULT_0603_GRID_OPTIONS.padGap;
50
+ const resolvedClearance = input.clearance ?? DEFAULT_0603_GRID_OPTIONS.clearance;
51
+ const resolvedPitchX = input.colSpacing ?? input.pitchX ?? DEFAULT_0603_GRID_OPTIONS.pitchX;
52
+ const resolvedPitchY = input.rowSpacing ?? input.pitchY ?? DEFAULT_0603_GRID_OPTIONS.pitchY;
53
+ const minBodySize = getJumperBodySize(
54
+ resolvedOrientation,
55
+ resolvedPadWidth,
56
+ resolvedPadHeight,
57
+ resolvedPadGap
58
+ );
59
+ const minInterJumperGap = resolvedClearance;
60
+ const resolvedDefaultStaggerOffset = getJumperSizeAlongAxis(
61
+ resolvedOrientation,
62
+ resolvedStaggerAxis,
63
+ resolvedPadWidth,
64
+ resolvedPadHeight,
65
+ resolvedPadGap
66
+ ) / 2;
67
+ const resolvedStaggerOffset = input.staggerOffset ?? input.staggerOffsetX ?? resolvedDefaultStaggerOffset;
68
+ const options = {
69
+ ...DEFAULT_0603_GRID_OPTIONS,
70
+ ...input,
71
+ orientation: resolvedOrientation,
72
+ staggerAxis: resolvedStaggerAxis,
73
+ pitchX: Math.max(resolvedPitchX, minBodySize.x + minInterJumperGap),
74
+ pitchY: Math.max(resolvedPitchY, minBodySize.y + minInterJumperGap),
75
+ padWidth: resolvedPadWidth,
76
+ padHeight: resolvedPadHeight,
77
+ padGap: resolvedPadGap,
78
+ staggerOffset: resolvedStaggerOffset,
79
+ staggerOffsetX: resolvedStaggerOffset,
80
+ concavityTolerance: input.concavityTolerance ?? DEFAULT_0603_GRID_OPTIONS.concavityTolerance,
81
+ clearance: resolvedClearance,
82
+ portSpacing: input.portSpacing ?? DEFAULT_0603_GRID_OPTIONS.portSpacing
83
+ };
84
+ if (options.cols <= 0 || options.rows <= 0) {
85
+ throw new Error("rows and cols must be > 0");
86
+ }
87
+ if (!Number.isFinite(options.portSpacing) || options.portSpacing <= 0) {
88
+ throw new Error("portSpacing must be > 0");
89
+ }
90
+ return options;
91
+ };
92
+ var generate0603GridPattern = (options) => {
93
+ const jumpers = [];
94
+ const vias = [];
95
+ const padOffset = options.padGap / 2 + options.padWidth / 2;
96
+ const xStart = -((options.cols - 1) * options.pitchX) / 2;
97
+ const yStart = -((options.rows - 1) * options.pitchY) / 2;
98
+ for (let row = 0; row < options.rows; row++) {
99
+ for (let col = 0; col < options.cols; col++) {
100
+ const center = {
101
+ x: xStart + col * options.pitchX,
102
+ y: yStart + row * options.pitchY
103
+ };
104
+ const pad1 = options.orientation === "horizontal" ? { x: center.x - padOffset, y: center.y } : { x: center.x, y: center.y - padOffset };
105
+ const pad2 = options.orientation === "horizontal" ? { x: center.x + padOffset, y: center.y } : { x: center.x, y: center.y + padOffset };
106
+ const jumperId = `jumper_r${row}_c${col}`;
107
+ jumpers.push({
108
+ jumperId,
109
+ center,
110
+ orientation: options.orientation,
111
+ padCenters: [pad1, pad2]
112
+ });
113
+ vias.push(
114
+ { center: pad1, diameter: options.viaDiameter },
115
+ { center: pad2, diameter: options.viaDiameter }
116
+ );
117
+ }
118
+ }
119
+ const bounds = {
120
+ minX: xStart - padOffset - options.padWidth / 2 - options.boundsPadding,
121
+ maxX: xStart + (options.cols - 1) * options.pitchX + padOffset + options.padWidth / 2 + options.boundsPadding,
122
+ minY: yStart - padOffset - options.padHeight / 2 - options.boundsPadding,
123
+ maxY: yStart + (options.rows - 1) * options.pitchY + padOffset + options.padHeight / 2 + options.boundsPadding
124
+ };
125
+ return { jumpers, vias, bounds };
126
+ };
127
+
128
+ // lib/patterns/staggered0603.ts
129
+ var computeBoundsFromJumpers = (jumpers, options) => {
130
+ let minX = Number.POSITIVE_INFINITY;
131
+ let minY = Number.POSITIVE_INFINITY;
132
+ let maxX = Number.NEGATIVE_INFINITY;
133
+ let maxY = Number.NEGATIVE_INFINITY;
134
+ for (const jumper of jumpers) {
135
+ for (const pad of jumper.padCenters) {
136
+ const xHalf = options.orientation === "horizontal" ? options.padWidth / 2 : options.padHeight / 2;
137
+ const yHalf = options.orientation === "horizontal" ? options.padHeight / 2 : options.padWidth / 2;
138
+ if (pad.x - xHalf < minX) minX = pad.x - xHalf;
139
+ if (pad.y - yHalf < minY) minY = pad.y - yHalf;
140
+ if (pad.x + xHalf > maxX) maxX = pad.x + xHalf;
141
+ if (pad.y + yHalf > maxY) maxY = pad.y + yHalf;
142
+ }
143
+ }
144
+ return {
145
+ minX: minX - options.boundsPadding,
146
+ minY: minY - options.boundsPadding,
147
+ maxX: maxX + options.boundsPadding,
148
+ maxY: maxY + options.boundsPadding
149
+ };
150
+ };
151
+ var generate0603StaggeredPattern = (options) => {
152
+ const jumpers = [];
153
+ const vias = [];
154
+ const padOffset = options.padGap / 2 + options.padWidth / 2;
155
+ const xStart = -((options.cols - 1) * options.pitchX) / 2;
156
+ const yStart = -((options.rows - 1) * options.pitchY) / 2;
157
+ for (let row = 0; row < options.rows; row++) {
158
+ const rowOffset = options.staggerAxis === "x" && row % 2 === 1 ? options.staggerOffset : 0;
159
+ for (let col = 0; col < options.cols; col++) {
160
+ const colOffset = options.staggerAxis === "y" && col % 2 === 1 ? options.staggerOffset : 0;
161
+ const center = {
162
+ x: xStart + col * options.pitchX + rowOffset,
163
+ y: yStart + row * options.pitchY + colOffset
164
+ };
165
+ const pad1 = options.orientation === "horizontal" ? { x: center.x - padOffset, y: center.y } : { x: center.x, y: center.y - padOffset };
166
+ const pad2 = options.orientation === "horizontal" ? { x: center.x + padOffset, y: center.y } : { x: center.x, y: center.y + padOffset };
167
+ jumpers.push({
168
+ jumperId: `jumper_r${row}_c${col}`,
169
+ center,
170
+ orientation: options.orientation,
171
+ padCenters: [pad1, pad2]
172
+ });
173
+ vias.push(
174
+ { center: pad1, diameter: options.viaDiameter },
175
+ { center: pad2, diameter: options.viaDiameter }
176
+ );
177
+ }
178
+ }
179
+ return {
180
+ jumpers,
181
+ vias,
182
+ bounds: computeBoundsFromJumpers(jumpers, options)
183
+ };
184
+ };
185
+
186
+ // lib/patterns/index.ts
187
+ var generate0603Pattern = (options) => {
188
+ if (options.pattern === "staggered") {
189
+ return generate0603StaggeredPattern(options);
190
+ }
191
+ return generate0603GridPattern(options);
192
+ };
193
+
194
+ // lib/geometry.ts
195
+ var toBounds = (poly) => {
196
+ let minX = Number.POSITIVE_INFINITY;
197
+ let minY = Number.POSITIVE_INFINITY;
198
+ let maxX = Number.NEGATIVE_INFINITY;
199
+ let maxY = Number.NEGATIVE_INFINITY;
200
+ for (const p of poly) {
201
+ if (p.x < minX) minX = p.x;
202
+ if (p.y < minY) minY = p.y;
203
+ if (p.x > maxX) maxX = p.x;
204
+ if (p.y > maxY) maxY = p.y;
205
+ }
206
+ return { minX, minY, maxX, maxY };
207
+ };
208
+ var toCenter = (bounds) => ({
209
+ x: (bounds.minX + bounds.maxX) / 2,
210
+ y: (bounds.minY + bounds.maxY) / 2
211
+ });
212
+ var boundsToPolygon = (bounds) => [
213
+ { x: bounds.minX, y: bounds.minY },
214
+ { x: bounds.maxX, y: bounds.minY },
215
+ { x: bounds.maxX, y: bounds.maxY },
216
+ { x: bounds.minX, y: bounds.maxY }
217
+ ];
218
+ var almostEqual = (a, b, tolerance) => Math.abs(a - b) <= tolerance;
219
+ var pointsEqual = (a, b, tolerance) => almostEqual(a.x, b.x, tolerance) && almostEqual(a.y, b.y, tolerance);
220
+
221
+ // lib/topology.ts
222
+ var createTopLayerRegions = (convexRegions) => convexRegions.map((polygon, index) => {
223
+ const bounds = toBounds(polygon);
224
+ return {
225
+ regionId: `top_${index}`,
226
+ ports: [],
227
+ d: {
228
+ bounds,
229
+ center: toCenter(bounds),
230
+ polygon,
231
+ isPad: false
232
+ }
233
+ };
234
+ });
235
+ var createJumperRegions = (jumpers, options) => jumpers.map((jumper) => {
236
+ const [p1, p2] = jumper.padCenters;
237
+ const bounds = options.orientation === "horizontal" ? {
238
+ minX: p1.x - options.padWidth / 2,
239
+ maxX: p2.x + options.padWidth / 2,
240
+ minY: jumper.center.y - options.padHeight / 2,
241
+ maxY: jumper.center.y + options.padHeight / 2
242
+ } : {
243
+ minX: jumper.center.x - options.padHeight / 2,
244
+ maxX: jumper.center.x + options.padHeight / 2,
245
+ minY: p1.y - options.padWidth / 2,
246
+ maxY: p2.y + options.padWidth / 2
247
+ };
248
+ return {
249
+ regionId: jumper.jumperId,
250
+ ports: [],
251
+ d: {
252
+ bounds,
253
+ center: jumper.center,
254
+ polygon: boundsToPolygon(bounds),
255
+ isPad: true,
256
+ isThroughJumper: true
257
+ }
258
+ };
259
+ });
260
+ var createTopLayerPorts = (topLayerRegions, portSpacing, tolerance = 1e-5) => {
261
+ const ports = [];
262
+ let nextPort = 0;
263
+ for (let i = 0; i < topLayerRegions.length; i++) {
264
+ const regionA = topLayerRegions[i];
265
+ if (!regionA?.d.polygon) continue;
266
+ for (let j = i + 1; j < topLayerRegions.length; j++) {
267
+ const regionB = topLayerRegions[j];
268
+ if (!regionB?.d.polygon) continue;
269
+ const sharedEdges = getSharedBoundaryEdges(regionA, regionB, tolerance);
270
+ for (const edge of sharedEdges) {
271
+ const edgeLength = getSegmentLength(edge);
272
+ const intervalCount = Math.floor(edgeLength / portSpacing);
273
+ const portCount = intervalCount - 1;
274
+ if (portCount < 1) continue;
275
+ for (let k = 0; k < portCount; k++) {
276
+ const t = (k + 1) / (portCount + 1);
277
+ const point = pointAlongSegment(edge, t);
278
+ ports.push({
279
+ portId: `tp_${nextPort++}`,
280
+ region1: regionA,
281
+ region2: regionB,
282
+ d: {
283
+ x: point.x,
284
+ y: point.y
285
+ }
286
+ });
287
+ }
288
+ }
289
+ }
290
+ }
291
+ return ports;
292
+ };
293
+ var getSegmentLength = (segment) => {
294
+ const dx = segment.end.x - segment.start.x;
295
+ const dy = segment.end.y - segment.start.y;
296
+ return Math.hypot(dx, dy);
297
+ };
298
+ var pointAlongSegment = (segment, t) => ({
299
+ x: segment.start.x + (segment.end.x - segment.start.x) * t,
300
+ y: segment.start.y + (segment.end.y - segment.start.y) * t
301
+ });
302
+ var getSharedBoundaryEdges = (regionA, regionB, tolerance) => {
303
+ const edges = [];
304
+ if (!regionA.d.polygon || !regionB.d.polygon) return edges;
305
+ for (let ai = 0; ai < regionA.d.polygon.length; ai++) {
306
+ const a1 = regionA.d.polygon[ai];
307
+ const a2 = regionA.d.polygon[(ai + 1) % regionA.d.polygon.length];
308
+ if (!a1 || !a2) continue;
309
+ for (let bi = 0; bi < regionB.d.polygon.length; bi++) {
310
+ const b1 = regionB.d.polygon[bi];
311
+ const b2 = regionB.d.polygon[(bi + 1) % regionB.d.polygon.length];
312
+ if (!b1 || !b2) continue;
313
+ const overlap = getCollinearOverlap(
314
+ { start: a1, end: a2 },
315
+ { start: b1, end: b2 },
316
+ tolerance
317
+ );
318
+ if (!overlap) continue;
319
+ const alreadyPresent = edges.some(
320
+ (edge) => segmentsEqual(edge, overlap, tolerance)
321
+ );
322
+ if (!alreadyPresent) {
323
+ edges.push(overlap);
324
+ }
325
+ }
326
+ }
327
+ return edges;
328
+ };
329
+ var segmentsEqual = (a, b, tolerance) => pointsEqual(a.start, b.start, tolerance) && pointsEqual(a.end, b.end, tolerance) || pointsEqual(a.start, b.end, tolerance) && pointsEqual(a.end, b.start, tolerance);
330
+ var getCollinearOverlap = (a, b, tolerance) => {
331
+ const av = {
332
+ x: a.end.x - a.start.x,
333
+ y: a.end.y - a.start.y
334
+ };
335
+ const bv = {
336
+ x: b.end.x - b.start.x,
337
+ y: b.end.y - b.start.y
338
+ };
339
+ const aLenSq = av.x ** 2 + av.y ** 2;
340
+ if (aLenSq <= tolerance ** 2) return null;
341
+ const crossAB = av.x * bv.y - av.y * bv.x;
342
+ if (!almostEqual(crossAB, 0, tolerance)) return null;
343
+ const bStartOffset = {
344
+ x: b.start.x - a.start.x,
345
+ y: b.start.y - a.start.y
346
+ };
347
+ const bEndOffset = {
348
+ x: b.end.x - a.start.x,
349
+ y: b.end.y - a.start.y
350
+ };
351
+ const crossStart = av.x * bStartOffset.y - av.y * bStartOffset.x;
352
+ const crossEnd = av.x * bEndOffset.y - av.y * bEndOffset.x;
353
+ if (!almostEqual(crossStart, 0, tolerance) || !almostEqual(crossEnd, 0, tolerance)) {
354
+ return null;
355
+ }
356
+ const tBStart = (bStartOffset.x * av.x + bStartOffset.y * av.y) / aLenSq;
357
+ const tBEnd = (bEndOffset.x * av.x + bEndOffset.y * av.y) / aLenSq;
358
+ const overlapStart = Math.max(0, Math.min(tBStart, tBEnd));
359
+ const overlapEnd = Math.min(1, Math.max(tBStart, tBEnd));
360
+ const minParamLength = tolerance / Math.sqrt(aLenSq);
361
+ if (overlapEnd - overlapStart <= minParamLength) return null;
362
+ return {
363
+ start: pointAlongSegment(a, overlapStart),
364
+ end: pointAlongSegment(a, overlapEnd)
365
+ };
366
+ };
367
+ var createJumperPorts = (jumperRegions, topLayerRegions, tolerance = 1e-5) => {
368
+ const ports = [];
369
+ let nextPort = 0;
370
+ for (const jumperRegion of jumperRegions) {
371
+ for (const topRegion of topLayerRegions) {
372
+ const sharedEdges = getSharedBoundaryEdges(
373
+ jumperRegion,
374
+ topRegion,
375
+ tolerance
376
+ );
377
+ for (const edge of sharedEdges) {
378
+ const midpoint = pointAlongSegment(edge, 0.5);
379
+ ports.push({
380
+ portId: `jp_${nextPort++}`,
381
+ region1: jumperRegion,
382
+ region2: topRegion,
383
+ d: {
384
+ x: midpoint.x,
385
+ y: midpoint.y
386
+ }
387
+ });
388
+ }
389
+ }
390
+ }
391
+ return ports;
392
+ };
393
+ var attachPortsToRegions = (ports) => {
394
+ for (const port of ports) {
395
+ port.region1.ports.push(port);
396
+ port.region2.ports.push(port);
397
+ }
398
+ };
399
+
400
+ // lib/generate0603JumperHyperGraph.ts
401
+ var generate0603JumperHyperGraph = (input) => {
402
+ const options = resolve0603GridOptions(input);
403
+ const pattern = generate0603Pattern(options);
404
+ const padWidth = options.orientation === "horizontal" ? options.padWidth : options.padHeight;
405
+ const padHeight = options.orientation === "horizontal" ? options.padHeight : options.padWidth;
406
+ const rects = pattern.jumpers.flatMap(
407
+ (jumper) => jumper.padCenters.map((center) => ({
408
+ center,
409
+ width: padWidth,
410
+ height: padHeight,
411
+ ccwRotation: 0
412
+ }))
413
+ );
414
+ const convexSolver = new ConvexRegionsSolver({
415
+ bounds: pattern.bounds,
416
+ rects,
417
+ clearance: 0,
418
+ concavityTolerance: options.concavityTolerance
419
+ });
420
+ convexSolver.solve();
421
+ const convex = convexSolver.getOutput();
422
+ if (!convex) {
423
+ throw new Error(convexSolver.error ?? "ConvexRegionsSolver failed");
424
+ }
425
+ const topLayerRegions = createTopLayerRegions(convex.regions);
426
+ const jumperRegions = createJumperRegions(pattern.jumpers, {
427
+ orientation: options.orientation,
428
+ padWidth: options.padWidth,
429
+ padHeight: options.padHeight
430
+ });
431
+ const topLayerPorts = createTopLayerPorts(
432
+ topLayerRegions,
433
+ options.portSpacing
434
+ );
435
+ const jumperPorts = createJumperPorts(jumperRegions, topLayerRegions);
436
+ const ports = [...topLayerPorts, ...jumperPorts];
437
+ attachPortsToRegions(ports);
438
+ return {
439
+ regions: [...topLayerRegions, ...jumperRegions],
440
+ ports,
441
+ jumperLocations: pattern.jumpers.map((jumper, index) => ({
442
+ center: jumper.center,
443
+ orientation: jumper.orientation,
444
+ padRegions: [jumperRegions[index]].filter(
445
+ (region) => Boolean(region)
446
+ )
447
+ })),
448
+ topLayerRegions,
449
+ jumperRegions,
450
+ jumpers: pattern.jumpers,
451
+ vias: pattern.vias,
452
+ bounds: pattern.bounds
453
+ };
454
+ };
455
+
456
+ // lib/visualizeJumperHyperGraph.ts
457
+ var visualizeJumperHyperGraph = (graph) => {
458
+ const topPolygons = graph.topLayerRegions.map((region) => region.d.polygon).filter((polygon) => Array.isArray(polygon));
459
+ const jumperPolygons = graph.jumperRegions.map((region) => region.d.polygon).filter((polygon) => Array.isArray(polygon));
460
+ return {
461
+ title: "0603 Jumper HyperGraph Topology",
462
+ polygons: [
463
+ ...topPolygons.map((points, index) => ({
464
+ points,
465
+ fill: "rgba(87, 164, 255, 0.15)",
466
+ stroke: "#2f78bd",
467
+ strokeWidth: 0.04,
468
+ label: `top_${index}`
469
+ })),
470
+ ...jumperPolygons.map((points, index) => ({
471
+ points,
472
+ fill: "rgba(255, 167, 38, 0.22)",
473
+ stroke: "#cf6a00",
474
+ strokeWidth: 0.06,
475
+ label: `jumper_${index}`
476
+ }))
477
+ ],
478
+ circles: graph.vias.map((via) => ({
479
+ center: via.center,
480
+ radius: via.diameter / 2,
481
+ fill: "rgba(178, 178, 178, 0.5)",
482
+ stroke: "#6e6e6e",
483
+ label: "pad-as-via"
484
+ })),
485
+ points: graph.ports.map((port) => ({
486
+ x: port.d.x,
487
+ y: port.d.y,
488
+ color: "#1f1f1f",
489
+ label: port.portId
490
+ })),
491
+ rects: [
492
+ {
493
+ center: {
494
+ x: (graph.bounds.minX + graph.bounds.maxX) / 2,
495
+ y: (graph.bounds.minY + graph.bounds.maxY) / 2
496
+ },
497
+ width: graph.bounds.maxX - graph.bounds.minX,
498
+ height: graph.bounds.maxY - graph.bounds.minY,
499
+ stroke: "#6f6f6f"
500
+ }
501
+ ]
502
+ };
503
+ };
504
+ export {
505
+ generate0603GridPattern,
506
+ generate0603JumperHyperGraph,
507
+ generate0603Pattern,
508
+ generate0603StaggeredPattern,
509
+ resolve0603GridOptions,
510
+ visualizeJumperHyperGraph
511
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@tscircuit/jumper-topology-generator",
3
+ "main": "dist/index.js",
4
+ "type": "module",
5
+ "version": "0.0.1",
6
+ "scripts": {
7
+ "start": "cosmos",
8
+ "build": "tsup ./lib/index.ts --format esm --dts --outDir dist",
9
+ "cosmos:export": "cosmos-export"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "devDependencies": {
15
+ "@vitejs/plugin-react": "^4.7.0",
16
+ "@tscircuit/hypergraph": "^0.0.25",
17
+ "@types/bun": "latest",
18
+ "@types/react": "^19.1.12",
19
+ "@types/react-dom": "^19.1.9",
20
+ "bun-match-svg": "^0.0.15",
21
+ "graphics-debug": "^0.0.85",
22
+ "react": "^19.1.1",
23
+ "react-cosmos": "^7.1.1",
24
+ "react-cosmos-plugin-vite": "^7.1.1",
25
+ "react-dom": "^19.1.1",
26
+ "tsup": "^8.5.1",
27
+ "vite": "^7.1.5"
28
+ },
29
+ "peerDependencies": {
30
+ "typescript": "^5"
31
+ },
32
+ "dependencies": {
33
+ "@tscircuit/find-convex-regions": "^0.0.3"
34
+ }
35
+ }