@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 +21 -0
- package/README.md +169 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +511 -0
- package/package.json +35 -0
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
|
+

|
|
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
|
+

|
|
134
|
+
|
|
135
|
+
Vertical grid (`cols: 2, rows: 2, orientation: "vertical"`):
|
|
136
|
+
|
|
137
|
+

|
|
138
|
+
|
|
139
|
+
Staggered X (`cols: 4, rows: 3, pattern: "staggered", staggerAxis: "x"`):
|
|
140
|
+
|
|
141
|
+

|
|
142
|
+
|
|
143
|
+
Staggered Y (`cols: 3, rows: 3, pattern: "staggered", staggerAxis: "y"`):
|
|
144
|
+
|
|
145
|
+

|
|
146
|
+
|
|
147
|
+
SVG export fixture (`cols: 2, rows: 1`):
|
|
148
|
+
|
|
149
|
+

|
|
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|