calculate-packing 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 +96 -0
- package/dist/index.d.ts +153 -0
- package/dist/index.js +712 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 tscircuit
|
|
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,96 @@
|
|
|
1
|
+
# calculate-packing
|
|
2
|
+
|
|
3
|
+
**calculate-packing** is a small TypeScript library that ships the placement /
|
|
4
|
+
packing algorithm used by the [tscircuit tool-chain](https://github.com/tscircuit/tscircuit) for automatically laying out PCB components.
|
|
5
|
+
|
|
6
|
+
The solver turns a user-supplied `PackInput` (components, pads & strategy
|
|
7
|
+
settings) into a collision-free `PackOutput` while
|
|
8
|
+
|
|
9
|
+
- honouring a configurable clearance (`minGap`)
|
|
10
|
+
- keeping pads that share the same `networkId` close together
|
|
11
|
+
- minimising overall trace length
|
|
12
|
+
|
|
13
|
+
Internally the algorithm:
|
|
14
|
+
|
|
15
|
+
1. sorts components (largest → smallest)
|
|
16
|
+
2. keeps an outline (union of inflated component AABBs) of the already packed
|
|
17
|
+
island(s)
|
|
18
|
+
3. probes outline segments for the point with the shortest distance to any pad
|
|
19
|
+
on the same network
|
|
20
|
+
4. evaluates the four orthogonal rotations of the candidate component and
|
|
21
|
+
chooses the cheapest non-overlapping one
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun add calculate-packing # or npm i / yarn add
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { pack, PackInput } from "calculate-packing"
|
|
33
|
+
|
|
34
|
+
const input: PackInput = {
|
|
35
|
+
components: [
|
|
36
|
+
{
|
|
37
|
+
componentId: "C1",
|
|
38
|
+
pads: [
|
|
39
|
+
{
|
|
40
|
+
padId: "C1_1",
|
|
41
|
+
networkId: "GND",
|
|
42
|
+
type: "rect",
|
|
43
|
+
offset: { x: -0.6, y: 0 },
|
|
44
|
+
size: { x: 1.2, y: 1 },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
padId: "C1_2",
|
|
48
|
+
networkId: "VCC",
|
|
49
|
+
type: "rect",
|
|
50
|
+
offset: { x: +0.6, y: 0 },
|
|
51
|
+
size: { x: 1.2, y: 1 },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
/* …more components… */
|
|
56
|
+
],
|
|
57
|
+
minGap: 0.25,
|
|
58
|
+
packOrderStrategy: "largest_to_smallest",
|
|
59
|
+
packPlacementStrategy: "shortest_connection_along_outline",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = pack(input)
|
|
63
|
+
console.log(result.components) // → positioned & rotated components
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See `tests/` for more elaborate examples (SVG snapshots, circuit-json fixtures).
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bun install # install deps
|
|
72
|
+
bun test # run unit & SVG snapshot tests
|
|
73
|
+
bunx tsc --noEmit # type-check
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Repo layout
|
|
77
|
+
|
|
78
|
+
• `lib/PackSolver` – high-level packing solver
|
|
79
|
+
• `lib/geometry` – computational-geometry helpers
|
|
80
|
+
• `lib/math` – low-level math utilities
|
|
81
|
+
• `tests/` – unit & snapshot tests
|
|
82
|
+
|
|
83
|
+
## Public API
|
|
84
|
+
|
|
85
|
+
| export | purpose |
|
|
86
|
+
| ---------------------------------- | ----------------------------------------- |
|
|
87
|
+
| `pack()` | run the solver |
|
|
88
|
+
| `convertPackOutputToPackInput()` | strip solver-only fields |
|
|
89
|
+
| `convertCircuitJsonToPackOutput()` | circuit-json → PackOutput helper |
|
|
90
|
+
| `getGraphicsFromPackOutput()` | build a `graphics-debug` scene for review |
|
|
91
|
+
|
|
92
|
+
Everything else is internal and may change without notice.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT – see [LICENSE](./LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { GraphicsObject, Point } from 'graphics-debug';
|
|
2
|
+
import { CircuitJson } from 'circuit-json';
|
|
3
|
+
|
|
4
|
+
type ComponentId = string;
|
|
5
|
+
type PadId = string;
|
|
6
|
+
type NetworkId = string;
|
|
7
|
+
interface InputPad {
|
|
8
|
+
padId: string;
|
|
9
|
+
networkId: string;
|
|
10
|
+
type: "rect";
|
|
11
|
+
offset: {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
};
|
|
15
|
+
size: {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
interface OutputPad extends InputPad {
|
|
21
|
+
absoluteCenter: {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
interface InputComponent {
|
|
27
|
+
componentId: string;
|
|
28
|
+
pads: InputPad[];
|
|
29
|
+
}
|
|
30
|
+
interface PackedComponent extends InputComponent {
|
|
31
|
+
center: {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
};
|
|
35
|
+
ccwRotationOffset: number;
|
|
36
|
+
pads: OutputPad[];
|
|
37
|
+
}
|
|
38
|
+
interface PackInput {
|
|
39
|
+
components: InputComponent[];
|
|
40
|
+
minGap: number;
|
|
41
|
+
packOrderStrategy: "largest_to_smallest";
|
|
42
|
+
packPlacementStrategy: "shortest_connection_along_outline";
|
|
43
|
+
disconnectedPackDirection?: "left" | "right" | "up" | "down";
|
|
44
|
+
packFirst?: ComponentId[];
|
|
45
|
+
}
|
|
46
|
+
interface PackOutput extends PackInput {
|
|
47
|
+
components: PackedComponent[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The pack algorithm performs the following steps:
|
|
52
|
+
* 1. Sort the components using the packOrderStrategy
|
|
53
|
+
* 2. Select the next component to pack
|
|
54
|
+
* 3. If the first component, pack at center (0,0) and go to step 2
|
|
55
|
+
* 4. Compute the outline of all packed components with a gap of minGap + max(pad.width, pad.height)/2
|
|
56
|
+
* 5. Find the point along the outline that minimizes the distance of the pad
|
|
57
|
+
* centers within a networkId. If no shared pads, pack to the defaultPackDirection
|
|
58
|
+
* 6. Add the component at the selected point, with it's pad at the position
|
|
59
|
+
* minimizing the distance between the pad centers
|
|
60
|
+
* 7. To determine the component rotation, find the minimum distance between pad
|
|
61
|
+
* centers for the remaining pads at each possible rotation (making sure that
|
|
62
|
+
* we never pack such that two pads overlap)
|
|
63
|
+
* 8. Go to step 2 until all components are packed
|
|
64
|
+
*/
|
|
65
|
+
declare const pack: (input: PackInput) => PackOutput;
|
|
66
|
+
|
|
67
|
+
declare class BaseSolver {
|
|
68
|
+
MAX_ITERATIONS: number;
|
|
69
|
+
solved: boolean;
|
|
70
|
+
failed: boolean;
|
|
71
|
+
iterations: number;
|
|
72
|
+
progress: number;
|
|
73
|
+
error: string | null;
|
|
74
|
+
activeSubSolver?: BaseSolver | null;
|
|
75
|
+
failedSubSolvers?: BaseSolver[];
|
|
76
|
+
timeToSolve?: number;
|
|
77
|
+
stats: Record<string, any>;
|
|
78
|
+
_setupDone: boolean;
|
|
79
|
+
setup(): void;
|
|
80
|
+
/** DO NOT OVERRIDE! Override _step() instead */
|
|
81
|
+
step(): void;
|
|
82
|
+
_setup(): void;
|
|
83
|
+
_step(): void;
|
|
84
|
+
getConstructorParams(): void;
|
|
85
|
+
solve(): void;
|
|
86
|
+
visualize(): GraphicsObject;
|
|
87
|
+
/**
|
|
88
|
+
* Called when the solver is about to fail, but we want to see if we have an
|
|
89
|
+
* "acceptable" or "passable" solution. Mostly used for optimizers that
|
|
90
|
+
* have an aggressive early stopping criterion.
|
|
91
|
+
*/
|
|
92
|
+
tryFinalAcceptance(): void;
|
|
93
|
+
/**
|
|
94
|
+
* A lightweight version of the visualize method that can be used to stream
|
|
95
|
+
* progress
|
|
96
|
+
*/
|
|
97
|
+
preview(): GraphicsObject;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The pack algorithm performs the following steps:
|
|
102
|
+
* 1. Sort the components using the packOrderStrategy
|
|
103
|
+
* 2. Select the next component to pack
|
|
104
|
+
* 3. If the first component, pack at center (0,0) and go to step 2
|
|
105
|
+
* 4. Compute the outline of all packed components with a gap of minGap + max(pad.width, pad.height)/2
|
|
106
|
+
* 5. Find the point along the outline that minimizes the distance of the pad
|
|
107
|
+
* centers within a networkId. If no shared pads, pack to the defaultPackDirection
|
|
108
|
+
* 6. Add the component at the selected point, with it's pad at the position
|
|
109
|
+
* minimizing the distance between the pad centers
|
|
110
|
+
* 7. To determine the component rotation, find the minimum distance between pad
|
|
111
|
+
* centers for the remaining pads at each possible rotation (making sure that
|
|
112
|
+
* we never pack such that two pads overlap)
|
|
113
|
+
* 8. Go to step 2 until all components are packed
|
|
114
|
+
*/
|
|
115
|
+
declare class PackSolver extends BaseSolver {
|
|
116
|
+
packInput: PackInput;
|
|
117
|
+
unpackedComponentQueue: InputComponent[];
|
|
118
|
+
packedComponents: PackedComponent[];
|
|
119
|
+
lastBestPointsResult?: {
|
|
120
|
+
bestPoints: (Point & {
|
|
121
|
+
networkId: NetworkId;
|
|
122
|
+
})[];
|
|
123
|
+
distance: number;
|
|
124
|
+
};
|
|
125
|
+
constructor(input: PackInput);
|
|
126
|
+
_setup(): void;
|
|
127
|
+
_step(): void;
|
|
128
|
+
getConstructorParams(): PackInput[];
|
|
129
|
+
/** Visualize the current packing state – components are omitted, only the outline is shown. */
|
|
130
|
+
visualize(): GraphicsObject;
|
|
131
|
+
getResult(): PackedComponent[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
declare const convertCircuitJsonToPackOutput: (circuitJson: CircuitJson) => PackOutput;
|
|
135
|
+
|
|
136
|
+
declare const getGraphicsFromPackOutput: (packOutput: PackOutput) => GraphicsObject;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Strip all “output only” properties (those added by the pack() solver)
|
|
140
|
+
* so the result can be fed back into pack() again or compared against an
|
|
141
|
+
* original PackInput. Everything else must be preserved verbatim.
|
|
142
|
+
*
|
|
143
|
+
* NOTE:
|
|
144
|
+
* – PackInput.components is an array of **InputComponent**,
|
|
145
|
+
* while PackOutput.components is an array of **PackedComponent**.
|
|
146
|
+
* We therefore have to:
|
|
147
|
+
* • copy componentId
|
|
148
|
+
* • copy each pad but drop `absoluteCenter`
|
|
149
|
+
* • drop `center` and `ccwRotationOffset`
|
|
150
|
+
*/
|
|
151
|
+
declare const convertPackOutputToPackInput: (packed: PackOutput) => PackInput;
|
|
152
|
+
|
|
153
|
+
export { type ComponentId, type InputComponent, type InputPad, type NetworkId, type OutputPad, type PackInput, type PackOutput, PackSolver, type PackedComponent, type PadId, convertCircuitJsonToPackOutput, convertPackOutputToPackInput, getGraphicsFromPackOutput, pack };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
// lib/solver-utils/BaseSolver.ts
|
|
2
|
+
var BaseSolver = class {
|
|
3
|
+
MAX_ITERATIONS = 1e3;
|
|
4
|
+
solved = false;
|
|
5
|
+
failed = false;
|
|
6
|
+
iterations = 0;
|
|
7
|
+
progress = 0;
|
|
8
|
+
error = null;
|
|
9
|
+
activeSubSolver;
|
|
10
|
+
failedSubSolvers;
|
|
11
|
+
timeToSolve;
|
|
12
|
+
stats = {};
|
|
13
|
+
_setupDone = false;
|
|
14
|
+
setup() {
|
|
15
|
+
if (this._setupDone) return;
|
|
16
|
+
this._setup();
|
|
17
|
+
this._setupDone = true;
|
|
18
|
+
}
|
|
19
|
+
/** DO NOT OVERRIDE! Override _step() instead */
|
|
20
|
+
step() {
|
|
21
|
+
if (!this._setupDone) {
|
|
22
|
+
this.setup();
|
|
23
|
+
}
|
|
24
|
+
if (this.solved) return;
|
|
25
|
+
if (this.failed) return;
|
|
26
|
+
this.iterations++;
|
|
27
|
+
try {
|
|
28
|
+
this._step();
|
|
29
|
+
} catch (e) {
|
|
30
|
+
this.error = `${this.constructor.name} error: ${e}`;
|
|
31
|
+
console.error(this.error);
|
|
32
|
+
this.failed = true;
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
if (!this.solved && this.iterations > this.MAX_ITERATIONS) {
|
|
36
|
+
this.tryFinalAcceptance();
|
|
37
|
+
}
|
|
38
|
+
if (!this.solved && this.iterations > this.MAX_ITERATIONS) {
|
|
39
|
+
this.error = `${this.constructor.name} ran out of iterations`;
|
|
40
|
+
console.error(this.error);
|
|
41
|
+
this.failed = true;
|
|
42
|
+
}
|
|
43
|
+
if ("computeProgress" in this) {
|
|
44
|
+
this.progress = this.computeProgress();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
_setup() {
|
|
48
|
+
}
|
|
49
|
+
_step() {
|
|
50
|
+
}
|
|
51
|
+
getConstructorParams() {
|
|
52
|
+
throw new Error("getConstructorParams not implemented");
|
|
53
|
+
}
|
|
54
|
+
solve() {
|
|
55
|
+
const startTime = Date.now();
|
|
56
|
+
while (!this.solved && !this.failed) {
|
|
57
|
+
this.step();
|
|
58
|
+
}
|
|
59
|
+
const endTime = Date.now();
|
|
60
|
+
this.timeToSolve = endTime - startTime;
|
|
61
|
+
}
|
|
62
|
+
visualize() {
|
|
63
|
+
return {
|
|
64
|
+
lines: [],
|
|
65
|
+
points: [],
|
|
66
|
+
rects: [],
|
|
67
|
+
circles: []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Called when the solver is about to fail, but we want to see if we have an
|
|
72
|
+
* "acceptable" or "passable" solution. Mostly used for optimizers that
|
|
73
|
+
* have an aggressive early stopping criterion.
|
|
74
|
+
*/
|
|
75
|
+
tryFinalAcceptance() {
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A lightweight version of the visualize method that can be used to stream
|
|
79
|
+
* progress
|
|
80
|
+
*/
|
|
81
|
+
preview() {
|
|
82
|
+
return {
|
|
83
|
+
lines: [],
|
|
84
|
+
points: [],
|
|
85
|
+
rects: [],
|
|
86
|
+
circles: []
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// lib/math/rotatePoint.ts
|
|
92
|
+
var rotatePoint = (point, angle, origin = { x: 0, y: 0 }) => {
|
|
93
|
+
const cos = Math.cos(angle);
|
|
94
|
+
const sin = Math.sin(angle);
|
|
95
|
+
const dx = point.x - origin.x;
|
|
96
|
+
const dy = point.y - origin.y;
|
|
97
|
+
return {
|
|
98
|
+
x: origin.x + dx * cos - dy * sin,
|
|
99
|
+
y: origin.y + dx * sin + dy * cos
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// lib/geometry/getComponentBounds.ts
|
|
104
|
+
var getComponentBounds = (component, minGap = 0) => {
|
|
105
|
+
const bounds = {
|
|
106
|
+
minX: Infinity,
|
|
107
|
+
maxX: -Infinity,
|
|
108
|
+
minY: Infinity,
|
|
109
|
+
maxY: -Infinity
|
|
110
|
+
};
|
|
111
|
+
component.pads.forEach((pad) => {
|
|
112
|
+
const hw = pad.size.x / 2;
|
|
113
|
+
const hh = pad.size.y / 2;
|
|
114
|
+
const localCorners = [
|
|
115
|
+
{ x: pad.offset.x - hw, y: pad.offset.y - hh },
|
|
116
|
+
{ x: pad.offset.x + hw, y: pad.offset.y - hh },
|
|
117
|
+
{ x: pad.offset.x + hw, y: pad.offset.y + hh },
|
|
118
|
+
{ x: pad.offset.x - hw, y: pad.offset.y + hh }
|
|
119
|
+
];
|
|
120
|
+
localCorners.forEach((corner) => {
|
|
121
|
+
const world = rotatePoint(corner, component.ccwRotationOffset);
|
|
122
|
+
const x = world.x + component.center.x;
|
|
123
|
+
const y = world.y + component.center.y;
|
|
124
|
+
bounds.minX = Math.min(bounds.minX, x);
|
|
125
|
+
bounds.maxX = Math.max(bounds.maxX, x);
|
|
126
|
+
bounds.minY = Math.min(bounds.minY, y);
|
|
127
|
+
bounds.maxY = Math.max(bounds.maxY, y);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
minX: bounds.minX - minGap,
|
|
132
|
+
maxX: bounds.maxX + minGap,
|
|
133
|
+
minY: bounds.minY - minGap,
|
|
134
|
+
maxY: bounds.maxY + minGap
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// lib/constructOutlinesFromPackedComponents.ts
|
|
139
|
+
import Flatten from "@flatten-js/core";
|
|
140
|
+
var constructOutlinesFromPackedComponents = (components, opts = {}) => {
|
|
141
|
+
const { minGap = 0 } = opts;
|
|
142
|
+
if (components.length === 0) return [];
|
|
143
|
+
const rectPolys = components.map((c) => {
|
|
144
|
+
const b = getComponentBounds(c, minGap);
|
|
145
|
+
return new Flatten.Polygon([
|
|
146
|
+
[b.minX, b.minY],
|
|
147
|
+
[b.maxX, b.minY],
|
|
148
|
+
[b.maxX, b.maxY],
|
|
149
|
+
[b.minX, b.maxY]
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
if (rectPolys.length === 0) return [];
|
|
153
|
+
let union = rectPolys[0];
|
|
154
|
+
for (let i = 1; i < rectPolys.length; i++) {
|
|
155
|
+
union = Flatten.BooleanOperations.unify(union, rectPolys[i]);
|
|
156
|
+
}
|
|
157
|
+
const outlines = [];
|
|
158
|
+
for (const face of union.faces) {
|
|
159
|
+
if (face.isHole) continue;
|
|
160
|
+
const outline = [];
|
|
161
|
+
let edge = face.first;
|
|
162
|
+
if (!edge) continue;
|
|
163
|
+
do {
|
|
164
|
+
const shp = edge.shape;
|
|
165
|
+
const ps = shp.start ?? shp.ps;
|
|
166
|
+
const pe = shp.end ?? shp.pe;
|
|
167
|
+
outline.push([
|
|
168
|
+
{ x: ps.x, y: ps.y },
|
|
169
|
+
{ x: pe.x, y: pe.y }
|
|
170
|
+
]);
|
|
171
|
+
edge = edge.next;
|
|
172
|
+
} while (edge !== face.first);
|
|
173
|
+
outlines.push(outline);
|
|
174
|
+
}
|
|
175
|
+
return outlines;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// lib/testing/createColorMapFromStrings.ts
|
|
179
|
+
var createColorMapFromStrings = (strings) => {
|
|
180
|
+
const colorMap = {};
|
|
181
|
+
for (let i = 0; i < strings.length; i++) {
|
|
182
|
+
colorMap[strings[i]] = `hsl(${i * 300 / strings.length}, 100%, 50%)`;
|
|
183
|
+
}
|
|
184
|
+
return colorMap;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// lib/testing/getGraphicsFromPackOutput.ts
|
|
188
|
+
var getGraphicsFromPackOutput = (packOutput) => {
|
|
189
|
+
const rects = [];
|
|
190
|
+
const lines = [];
|
|
191
|
+
const allNetworkIds = Array.from(
|
|
192
|
+
new Set(
|
|
193
|
+
packOutput.components.flatMap((c) => c.pads.map((p) => p.networkId))
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
const colorMap = createColorMapFromStrings(allNetworkIds);
|
|
197
|
+
for (const component of packOutput.components) {
|
|
198
|
+
const bounds = getComponentBounds(component);
|
|
199
|
+
const width = bounds.maxX - bounds.minX;
|
|
200
|
+
const height = bounds.maxY - bounds.minY;
|
|
201
|
+
const rect = {
|
|
202
|
+
center: { x: component.center.x, y: component.center.y },
|
|
203
|
+
width,
|
|
204
|
+
height,
|
|
205
|
+
fill: "rgba(0,0,0,0.25)",
|
|
206
|
+
label: component.componentId
|
|
207
|
+
};
|
|
208
|
+
rects.push(rect);
|
|
209
|
+
for (const pad of component.pads) {
|
|
210
|
+
const { absoluteCenter, size, padId, networkId } = pad;
|
|
211
|
+
const padRect = {
|
|
212
|
+
center: { x: absoluteCenter.x, y: absoluteCenter.y },
|
|
213
|
+
width: size.x,
|
|
214
|
+
height: size.y,
|
|
215
|
+
label: `${padId} ${networkId}`,
|
|
216
|
+
fill: "rgba(255,0,0,0.8)"
|
|
217
|
+
};
|
|
218
|
+
rects.push(padRect);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const netId of allNetworkIds) {
|
|
222
|
+
const padsOnNet = packOutput.components.flatMap(
|
|
223
|
+
(c) => c.pads.filter((p) => p.networkId === netId)
|
|
224
|
+
);
|
|
225
|
+
for (let i = 0; i < padsOnNet.length; i++) {
|
|
226
|
+
for (let j = i + 1; j < padsOnNet.length; j++) {
|
|
227
|
+
lines.push({
|
|
228
|
+
points: [padsOnNet[i].absoluteCenter, padsOnNet[j].absoluteCenter],
|
|
229
|
+
strokeColor: colorMap[netId]
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
coordinateSystem: "cartesian",
|
|
236
|
+
rects,
|
|
237
|
+
lines
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// lib/PackSolver/setPackedComponentPadCenters.ts
|
|
242
|
+
var setPackedComponentPadCenters = (packedComponent) => {
|
|
243
|
+
packedComponent.pads = packedComponent.pads.map((pad) => ({
|
|
244
|
+
...pad,
|
|
245
|
+
absoluteCenter: (() => {
|
|
246
|
+
const rotated = rotatePoint(pad.offset, packedComponent.ccwRotationOffset);
|
|
247
|
+
return {
|
|
248
|
+
x: packedComponent.center.x + rotated.x,
|
|
249
|
+
y: packedComponent.center.y + rotated.y
|
|
250
|
+
};
|
|
251
|
+
})()
|
|
252
|
+
}));
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// lib/PackSolver/getSegmentsFromPad.ts
|
|
256
|
+
var getSegmentsFromPad = (pad, { padding = 0 } = {}) => {
|
|
257
|
+
const segments = [];
|
|
258
|
+
const { x, y } = pad.absoluteCenter;
|
|
259
|
+
const { x: w, y: h } = pad.size;
|
|
260
|
+
segments.push([
|
|
261
|
+
{ x: x - w / 2 - padding, y: y - h / 2 - padding },
|
|
262
|
+
{ x: x + w / 2 + padding, y: y - h / 2 - padding }
|
|
263
|
+
]);
|
|
264
|
+
segments.push([
|
|
265
|
+
{ x: x + w / 2 + padding, y: y - h / 2 - padding },
|
|
266
|
+
{ x: x + w / 2 + padding, y: y + h / 2 + padding }
|
|
267
|
+
]);
|
|
268
|
+
segments.push([
|
|
269
|
+
{ x: x + w / 2 + padding, y: y + h / 2 + padding },
|
|
270
|
+
{ x: x - w / 2 - padding, y: y + h / 2 + padding }
|
|
271
|
+
]);
|
|
272
|
+
segments.push([
|
|
273
|
+
{ x: x - w / 2 - padding, y: y + h / 2 + padding },
|
|
274
|
+
{ x: x - w / 2 - padding, y: y - h / 2 - padding }
|
|
275
|
+
]);
|
|
276
|
+
return segments;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// lib/math/computeNearestPointOnSegmentForSegmentSet.ts
|
|
280
|
+
var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
281
|
+
var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
282
|
+
var mul = (a, s) => ({ x: a.x * s, y: a.y * s });
|
|
283
|
+
var dot = (a, b) => a.x * b.x + a.y * b.y;
|
|
284
|
+
var clamp = (v, lo = 0, hi = 1) => Math.max(lo, Math.min(hi, v));
|
|
285
|
+
function closestPointOnSegAToSegB(segA, segB) {
|
|
286
|
+
const [p, q] = segA;
|
|
287
|
+
const [r, s] = segB;
|
|
288
|
+
const u = sub(q, p);
|
|
289
|
+
const v = sub(s, r);
|
|
290
|
+
const w0 = sub(p, r);
|
|
291
|
+
const a = dot(u, u);
|
|
292
|
+
const b = dot(u, v);
|
|
293
|
+
const c = dot(v, v);
|
|
294
|
+
const d = dot(u, w0);
|
|
295
|
+
const e = dot(v, w0);
|
|
296
|
+
const EPS = 1e-12;
|
|
297
|
+
const D = a * c - b * b;
|
|
298
|
+
let sN;
|
|
299
|
+
let tN;
|
|
300
|
+
let sD = D;
|
|
301
|
+
let tD = D;
|
|
302
|
+
if (D < EPS) {
|
|
303
|
+
sN = 0;
|
|
304
|
+
sD = 1;
|
|
305
|
+
tN = e;
|
|
306
|
+
tD = c;
|
|
307
|
+
} else {
|
|
308
|
+
sN = b * e - c * d;
|
|
309
|
+
tN = a * e - b * d;
|
|
310
|
+
if (sN < 0) {
|
|
311
|
+
sN = 0;
|
|
312
|
+
tN = e;
|
|
313
|
+
tD = c;
|
|
314
|
+
} else if (sN > sD) {
|
|
315
|
+
sN = sD;
|
|
316
|
+
tN = e + b;
|
|
317
|
+
tD = c;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (tN < 0) {
|
|
321
|
+
tN = 0;
|
|
322
|
+
sN = clamp(-d, 0, a);
|
|
323
|
+
sD = a;
|
|
324
|
+
} else if (tN > tD) {
|
|
325
|
+
tN = tD;
|
|
326
|
+
sN = clamp(-d + b, 0, a);
|
|
327
|
+
sD = a;
|
|
328
|
+
}
|
|
329
|
+
const sParam = sD > EPS ? sN / sD : 0;
|
|
330
|
+
const closestA = add(p, mul(u, sParam));
|
|
331
|
+
const tParam = tD > EPS ? tN / tD : 0;
|
|
332
|
+
const closestB = add(r, mul(v, tParam));
|
|
333
|
+
const diff = sub(closestA, closestB);
|
|
334
|
+
return { pointA: closestA, paramA: sParam, dist2: dot(diff, diff) };
|
|
335
|
+
}
|
|
336
|
+
function computeNearestPointOnSegmentForSegmentSet(segmentA, segmentSet) {
|
|
337
|
+
if (!segmentSet.length)
|
|
338
|
+
throw new Error("segmentSet must contain at least one segment");
|
|
339
|
+
let bestPoint = segmentA[0];
|
|
340
|
+
let bestDist2 = Number.POSITIVE_INFINITY;
|
|
341
|
+
for (const segB of segmentSet) {
|
|
342
|
+
const { pointA, dist2 } = closestPointOnSegAToSegB(segmentA, segB);
|
|
343
|
+
if (dist2 < bestDist2) {
|
|
344
|
+
bestDist2 = dist2;
|
|
345
|
+
bestPoint = pointA;
|
|
346
|
+
if (bestDist2 === 0) break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return { nearestPoint: bestPoint, dist: Math.sqrt(bestDist2) };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// lib/PackSolver/PackSolver.ts
|
|
353
|
+
var PackSolver = class extends BaseSolver {
|
|
354
|
+
packInput;
|
|
355
|
+
unpackedComponentQueue;
|
|
356
|
+
packedComponents;
|
|
357
|
+
lastBestPointsResult;
|
|
358
|
+
constructor(input) {
|
|
359
|
+
super();
|
|
360
|
+
this.packInput = input;
|
|
361
|
+
}
|
|
362
|
+
_setup() {
|
|
363
|
+
const { components, packOrderStrategy } = this.packInput;
|
|
364
|
+
this.unpackedComponentQueue = [...components].sort((a, b) => {
|
|
365
|
+
if (packOrderStrategy === "largest_to_smallest") {
|
|
366
|
+
return b.pads.length - a.pads.length;
|
|
367
|
+
}
|
|
368
|
+
return a.pads.length - b.pads.length;
|
|
369
|
+
});
|
|
370
|
+
this.packedComponents = [];
|
|
371
|
+
}
|
|
372
|
+
_step() {
|
|
373
|
+
if (this.solved) return;
|
|
374
|
+
const { minGap = 0, disconnectedPackDirection = "right" } = this.packInput;
|
|
375
|
+
if (this.unpackedComponentQueue.length === 0) {
|
|
376
|
+
this.solved = true;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const next = this.unpackedComponentQueue.shift();
|
|
380
|
+
if (!next) {
|
|
381
|
+
this.solved = true;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const newPackedComponent = {
|
|
385
|
+
...next,
|
|
386
|
+
center: { x: 0, y: 0 },
|
|
387
|
+
ccwRotationOffset: 0,
|
|
388
|
+
pads: next.pads.map((p) => ({
|
|
389
|
+
...p,
|
|
390
|
+
absoluteCenter: { x: 0, y: 0 }
|
|
391
|
+
}))
|
|
392
|
+
};
|
|
393
|
+
if (this.packedComponents.length === 0) {
|
|
394
|
+
newPackedComponent.center = { x: 0, y: 0 };
|
|
395
|
+
setPackedComponentPadCenters(newPackedComponent);
|
|
396
|
+
this.packedComponents.push(newPackedComponent);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const padMargins = newPackedComponent.pads.map(
|
|
400
|
+
(p) => Math.max(p.size.x, p.size.y) / 2
|
|
401
|
+
);
|
|
402
|
+
const additionalGap = Math.max(...padMargins);
|
|
403
|
+
const outlines = constructOutlinesFromPackedComponents(
|
|
404
|
+
this.packedComponents,
|
|
405
|
+
{ minGap: minGap + additionalGap }
|
|
406
|
+
);
|
|
407
|
+
const networkIdsInPackedComponents = new Set(
|
|
408
|
+
this.packedComponents.flatMap((c) => c.pads.map((p) => p.networkId))
|
|
409
|
+
);
|
|
410
|
+
const networkIdsInNewPackedComponent = new Set(
|
|
411
|
+
newPackedComponent.pads.map((p) => p.networkId)
|
|
412
|
+
);
|
|
413
|
+
const sharedNetworkIds = new Set(
|
|
414
|
+
[...networkIdsInPackedComponents].filter(
|
|
415
|
+
(id) => networkIdsInNewPackedComponent.has(id)
|
|
416
|
+
)
|
|
417
|
+
);
|
|
418
|
+
if (sharedNetworkIds.size === 0) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
"Use the disconnectedPackDirection to place the new component"
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const networkIdToAlreadyPackedSegments = /* @__PURE__ */ new Map();
|
|
424
|
+
for (const sharedNetworkId of sharedNetworkIds) {
|
|
425
|
+
networkIdToAlreadyPackedSegments.set(sharedNetworkId, []);
|
|
426
|
+
for (const packedComponent of this.packedComponents) {
|
|
427
|
+
for (const pad of packedComponent.pads) {
|
|
428
|
+
if (pad.networkId !== sharedNetworkId) continue;
|
|
429
|
+
const segments = getSegmentsFromPad(pad);
|
|
430
|
+
networkIdToAlreadyPackedSegments.set(sharedNetworkId, segments);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
let smallestDistance = Number.POSITIVE_INFINITY;
|
|
435
|
+
let bestPoints = [];
|
|
436
|
+
for (const outline of outlines) {
|
|
437
|
+
for (const outlineSegment of outline) {
|
|
438
|
+
for (const sharedNetworkId of sharedNetworkIds) {
|
|
439
|
+
const alreadyPackedSegments = networkIdToAlreadyPackedSegments.get(sharedNetworkId);
|
|
440
|
+
if (!alreadyPackedSegments) continue;
|
|
441
|
+
const {
|
|
442
|
+
nearestPoint: nearestPointOnOutlineToAlreadyPackedSegments,
|
|
443
|
+
dist: outlineToAlreadyPackedSegmentsDist
|
|
444
|
+
} = computeNearestPointOnSegmentForSegmentSet(
|
|
445
|
+
outlineSegment,
|
|
446
|
+
alreadyPackedSegments
|
|
447
|
+
);
|
|
448
|
+
if (outlineToAlreadyPackedSegmentsDist < smallestDistance + 1e-6) {
|
|
449
|
+
if (outlineToAlreadyPackedSegmentsDist < smallestDistance - 1e-6) {
|
|
450
|
+
bestPoints = [
|
|
451
|
+
{
|
|
452
|
+
...nearestPointOnOutlineToAlreadyPackedSegments,
|
|
453
|
+
networkId: sharedNetworkId
|
|
454
|
+
}
|
|
455
|
+
];
|
|
456
|
+
smallestDistance = outlineToAlreadyPackedSegmentsDist;
|
|
457
|
+
} else {
|
|
458
|
+
bestPoints.push({
|
|
459
|
+
...nearestPointOnOutlineToAlreadyPackedSegments,
|
|
460
|
+
networkId: sharedNetworkId
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
this.lastBestPointsResult = {
|
|
468
|
+
bestPoints,
|
|
469
|
+
distance: smallestDistance
|
|
470
|
+
};
|
|
471
|
+
for (const bestPoint of bestPoints) {
|
|
472
|
+
const networkId = bestPoint.networkId;
|
|
473
|
+
const newPadsConnectedToNetworkId = newPackedComponent.pads.filter(
|
|
474
|
+
(p) => p.networkId === networkId
|
|
475
|
+
);
|
|
476
|
+
const candidateAngles = [0, Math.PI / 2, Math.PI, 3 * Math.PI / 2];
|
|
477
|
+
let bestCandidate = null;
|
|
478
|
+
const packedPads = this.packedComponents.flatMap((c) => c.pads);
|
|
479
|
+
for (const angle of candidateAngles) {
|
|
480
|
+
const firstPad = newPadsConnectedToNetworkId[0];
|
|
481
|
+
if (!firstPad) continue;
|
|
482
|
+
const rotatedOffset = rotatePoint(firstPad.offset, angle);
|
|
483
|
+
const candidateCenter = {
|
|
484
|
+
x: bestPoint.x - rotatedOffset.x,
|
|
485
|
+
y: bestPoint.y - rotatedOffset.y
|
|
486
|
+
};
|
|
487
|
+
const transformedPads = newPackedComponent.pads.map((p) => {
|
|
488
|
+
const ro = rotatePoint(p.offset, angle);
|
|
489
|
+
return {
|
|
490
|
+
...p,
|
|
491
|
+
absoluteCenter: {
|
|
492
|
+
x: candidateCenter.x + ro.x,
|
|
493
|
+
y: candidateCenter.y + ro.y
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
const tempComponent = {
|
|
498
|
+
...newPackedComponent,
|
|
499
|
+
center: candidateCenter,
|
|
500
|
+
ccwRotationOffset: angle
|
|
501
|
+
};
|
|
502
|
+
const candBounds = getComponentBounds(tempComponent, 0);
|
|
503
|
+
const overlapsWithPackedComponent = this.packedComponents.some((pc) => {
|
|
504
|
+
const pcBounds = getComponentBounds(pc, 0);
|
|
505
|
+
return candBounds.minX < pcBounds.maxX && candBounds.maxX > pcBounds.minX && candBounds.minY < pcBounds.maxY && candBounds.maxY > pcBounds.minY;
|
|
506
|
+
});
|
|
507
|
+
if (overlapsWithPackedComponent) continue;
|
|
508
|
+
let cost = 0;
|
|
509
|
+
for (const tp of transformedPads) {
|
|
510
|
+
const sameNetPads = packedPads.filter(
|
|
511
|
+
(pp) => pp.networkId === tp.networkId
|
|
512
|
+
);
|
|
513
|
+
if (!sameNetPads.length) continue;
|
|
514
|
+
let bestD = Infinity;
|
|
515
|
+
for (const pp of sameNetPads) {
|
|
516
|
+
const dx = tp.absoluteCenter.x - pp.absoluteCenter.x;
|
|
517
|
+
const dy = tp.absoluteCenter.y - pp.absoluteCenter.y;
|
|
518
|
+
const d = Math.hypot(dx, dy);
|
|
519
|
+
if (d < bestD) bestD = d;
|
|
520
|
+
}
|
|
521
|
+
cost += bestD;
|
|
522
|
+
}
|
|
523
|
+
if (!bestCandidate || cost < bestCandidate.cost) {
|
|
524
|
+
bestCandidate = { center: candidateCenter, angle, cost };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (bestCandidate) {
|
|
528
|
+
newPackedComponent.center = bestCandidate.center;
|
|
529
|
+
newPackedComponent.ccwRotationOffset = bestCandidate.angle;
|
|
530
|
+
} else {
|
|
531
|
+
console.log("no valid rotation found");
|
|
532
|
+
const firstPad = newPadsConnectedToNetworkId[0];
|
|
533
|
+
const candidateCenter = {
|
|
534
|
+
x: bestPoint.x - firstPad.offset.x,
|
|
535
|
+
y: bestPoint.y - firstPad.offset.y
|
|
536
|
+
};
|
|
537
|
+
newPackedComponent.center = candidateCenter;
|
|
538
|
+
newPackedComponent.ccwRotationOffset = 0;
|
|
539
|
+
}
|
|
540
|
+
setPackedComponentPadCenters(newPackedComponent);
|
|
541
|
+
}
|
|
542
|
+
setPackedComponentPadCenters(newPackedComponent);
|
|
543
|
+
this.packedComponents.push(newPackedComponent);
|
|
544
|
+
}
|
|
545
|
+
getConstructorParams() {
|
|
546
|
+
return [this.packInput];
|
|
547
|
+
}
|
|
548
|
+
/** Visualize the current packing state – components are omitted, only the outline is shown. */
|
|
549
|
+
visualize() {
|
|
550
|
+
const graphics = getGraphicsFromPackOutput({
|
|
551
|
+
components: this.packedComponents ?? [],
|
|
552
|
+
minGap: this.packInput.minGap,
|
|
553
|
+
packOrderStrategy: this.packInput.packOrderStrategy,
|
|
554
|
+
packPlacementStrategy: this.packInput.packPlacementStrategy,
|
|
555
|
+
disconnectedPackDirection: this.packInput.disconnectedPackDirection
|
|
556
|
+
});
|
|
557
|
+
graphics.points ??= [];
|
|
558
|
+
graphics.lines ??= [];
|
|
559
|
+
const outlines = constructOutlinesFromPackedComponents(
|
|
560
|
+
this.packedComponents ?? [],
|
|
561
|
+
{
|
|
562
|
+
minGap: this.packInput.minGap
|
|
563
|
+
}
|
|
564
|
+
);
|
|
565
|
+
graphics.lines.push(
|
|
566
|
+
...outlines.flatMap(
|
|
567
|
+
(outline) => outline.map(
|
|
568
|
+
([p1, p2]) => ({
|
|
569
|
+
points: [p1, p2],
|
|
570
|
+
stroke: "#ff4444"
|
|
571
|
+
})
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
if (this.lastBestPointsResult) {
|
|
576
|
+
for (const bestPoint of this.lastBestPointsResult.bestPoints) {
|
|
577
|
+
graphics.points.push({
|
|
578
|
+
x: bestPoint.x,
|
|
579
|
+
y: bestPoint.y,
|
|
580
|
+
label: `bestPoint
|
|
581
|
+
networkId: ${bestPoint.networkId}
|
|
582
|
+
d=${this.lastBestPointsResult.distance}`
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return graphics;
|
|
587
|
+
}
|
|
588
|
+
getResult() {
|
|
589
|
+
return this.packedComponents;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// lib/pack.ts
|
|
594
|
+
var pack = (input) => {
|
|
595
|
+
console.log("pack", input);
|
|
596
|
+
const solver = new PackSolver(input);
|
|
597
|
+
solver.solve();
|
|
598
|
+
return {
|
|
599
|
+
...input,
|
|
600
|
+
components: solver.packedComponents
|
|
601
|
+
};
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// lib/testing/convertCircuitJsonToPackOutput.ts
|
|
605
|
+
import { cju } from "@tscircuit/circuit-json-util";
|
|
606
|
+
var convertCircuitJsonToPackOutput = (circuitJson) => {
|
|
607
|
+
const packOutput = {
|
|
608
|
+
components: [],
|
|
609
|
+
minGap: 0,
|
|
610
|
+
packOrderStrategy: "largest_to_smallest",
|
|
611
|
+
packPlacementStrategy: "shortest_connection_along_outline"
|
|
612
|
+
};
|
|
613
|
+
const db = cju(circuitJson);
|
|
614
|
+
const pcbComponents = db.pcb_component.list();
|
|
615
|
+
let unnamedCounter = 0;
|
|
616
|
+
const getNetworkId = (pcbPortId) => {
|
|
617
|
+
if (pcbPortId) {
|
|
618
|
+
const pcbPort = db.pcb_port.get(pcbPortId);
|
|
619
|
+
if (pcbPort) {
|
|
620
|
+
const sourcePort = db.source_port.get(pcbPort.source_port_id);
|
|
621
|
+
if (sourcePort?.subcircuit_connectivity_map_key) {
|
|
622
|
+
return sourcePort.subcircuit_connectivity_map_key;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return `unnamed${unnamedCounter++}`;
|
|
627
|
+
};
|
|
628
|
+
for (const pcbComponent of pcbComponents) {
|
|
629
|
+
const pads = [];
|
|
630
|
+
const platedHoles = db.pcb_plated_hole.list({
|
|
631
|
+
pcb_component_id: pcbComponent.pcb_component_id
|
|
632
|
+
});
|
|
633
|
+
for (const platedHole of platedHoles) {
|
|
634
|
+
const sx = platedHole.rect_pad_width ?? platedHole.outer_diameter ?? platedHole.hole_diameter ?? 0;
|
|
635
|
+
const sy = platedHole.rect_pad_height ?? platedHole.outer_diameter ?? platedHole.hole_diameter ?? 0;
|
|
636
|
+
const networkId = getNetworkId(platedHole.pcb_port_id);
|
|
637
|
+
const pad = {
|
|
638
|
+
padId: platedHole.pcb_plated_hole_id,
|
|
639
|
+
networkId,
|
|
640
|
+
type: "rect",
|
|
641
|
+
offset: {
|
|
642
|
+
x: platedHole.x - pcbComponent.center.x,
|
|
643
|
+
y: platedHole.y - pcbComponent.center.y
|
|
644
|
+
},
|
|
645
|
+
size: { x: sx, y: sy },
|
|
646
|
+
absoluteCenter: {
|
|
647
|
+
x: platedHole.x,
|
|
648
|
+
y: platedHole.y
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
pads.push(pad);
|
|
652
|
+
}
|
|
653
|
+
const smtPads = db.pcb_smtpad.list({
|
|
654
|
+
pcb_component_id: pcbComponent.pcb_component_id
|
|
655
|
+
});
|
|
656
|
+
for (const smtPad of smtPads) {
|
|
657
|
+
if (smtPad.shape === "polygon") {
|
|
658
|
+
throw new Error("Polygon pads are not supported in pack layout yet");
|
|
659
|
+
}
|
|
660
|
+
const networkId = getNetworkId(smtPad.pcb_port_id);
|
|
661
|
+
const pad = {
|
|
662
|
+
padId: smtPad.pcb_smtpad_id,
|
|
663
|
+
networkId,
|
|
664
|
+
type: "rect",
|
|
665
|
+
offset: {
|
|
666
|
+
x: smtPad.x - pcbComponent.center.x,
|
|
667
|
+
y: smtPad.y - pcbComponent.center.y
|
|
668
|
+
},
|
|
669
|
+
size: {
|
|
670
|
+
x: smtPad.width ?? 0,
|
|
671
|
+
y: smtPad.height ?? 0
|
|
672
|
+
},
|
|
673
|
+
absoluteCenter: {
|
|
674
|
+
x: smtPad.x,
|
|
675
|
+
y: smtPad.y
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
pads.push(pad);
|
|
679
|
+
}
|
|
680
|
+
const packedComponent = {
|
|
681
|
+
componentId: pcbComponent.pcb_component_id,
|
|
682
|
+
pads,
|
|
683
|
+
center: {
|
|
684
|
+
x: pcbComponent.center.x,
|
|
685
|
+
y: pcbComponent.center.y
|
|
686
|
+
},
|
|
687
|
+
ccwRotationOffset: 0
|
|
688
|
+
};
|
|
689
|
+
packOutput.components.push(packedComponent);
|
|
690
|
+
}
|
|
691
|
+
return packOutput;
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// lib/plumbing/convertPackOutputToPackInput.ts
|
|
695
|
+
var convertPackOutputToPackInput = (packed) => {
|
|
696
|
+
const strippedComponents = packed.components.map((pc) => ({
|
|
697
|
+
componentId: pc.componentId,
|
|
698
|
+
pads: pc.pads.map(({ absoluteCenter: _ac, ...rest }) => rest)
|
|
699
|
+
}));
|
|
700
|
+
return {
|
|
701
|
+
...packed,
|
|
702
|
+
// overwrite the components field with strippedComponents
|
|
703
|
+
components: strippedComponents
|
|
704
|
+
};
|
|
705
|
+
};
|
|
706
|
+
export {
|
|
707
|
+
PackSolver,
|
|
708
|
+
convertCircuitJsonToPackOutput,
|
|
709
|
+
convertPackOutputToPackInput,
|
|
710
|
+
getGraphicsFromPackOutput,
|
|
711
|
+
pack
|
|
712
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "calculate-packing",
|
|
3
|
+
"main": "dist/index.js",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"description": "Calculate a packing layout with support for different strategy configurations",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "cosmos",
|
|
9
|
+
"format": "biome format --write .",
|
|
10
|
+
"build": "tsup-node lib/index.ts --format esm --dts --outDir dist",
|
|
11
|
+
"build:site": "cosmos-export"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@biomejs/biome": "^2.1.1",
|
|
18
|
+
"@flatten-js/core": "^1.6.2",
|
|
19
|
+
"@react-hook/resize-observer": "^2.0.2",
|
|
20
|
+
"@tscircuit/circuit-json-util": "^0.0.52",
|
|
21
|
+
"@tscircuit/footprinter": "^0.0.203",
|
|
22
|
+
"@tscircuit/math-utils": "^0.0.19",
|
|
23
|
+
"@types/bun": "latest",
|
|
24
|
+
"@types/react": "^19.1.8",
|
|
25
|
+
"@types/react-dom": "^19.1.6",
|
|
26
|
+
"bun-match-svg": "^0.0.12",
|
|
27
|
+
"circuit-json": "^0.0.220",
|
|
28
|
+
"graphics-debug": "^0.0.60",
|
|
29
|
+
"react": "^19.1.0",
|
|
30
|
+
"react-cosmos": "^7.0.0",
|
|
31
|
+
"react-cosmos-plugin-vite": "^7.0.0",
|
|
32
|
+
"react-dom": "^19.1.0",
|
|
33
|
+
"tsup": "^8.5.0",
|
|
34
|
+
"vite": "^7.0.5"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"typescript": "^5",
|
|
38
|
+
"@tscircuit/circuit-json-util": "*"
|
|
39
|
+
}
|
|
40
|
+
}
|