calculate-packing 0.0.71 → 0.0.72

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/dist/index.d.ts CHANGED
@@ -29,6 +29,14 @@ interface OutputPad extends InputPad {
29
29
  y: number;
30
30
  };
31
31
  }
32
+ interface ComponentCourtyard {
33
+ offsetFromCenter: {
34
+ x: number;
35
+ y: number;
36
+ };
37
+ width: number;
38
+ height: number;
39
+ }
32
40
  interface InputComponent {
33
41
  componentId: string;
34
42
  /** Components marked as static are not moved by the packer */
@@ -45,6 +53,8 @@ interface InputComponent {
45
53
  /** Preconfigured rotation (degrees CCW) for static components */
46
54
  ccwRotationOffset?: number;
47
55
  pads: InputPad[];
56
+ /** Optional courtyard defining the component's physical boundary */
57
+ courtyard?: ComponentCourtyard;
48
58
  }
49
59
  interface PackedComponent extends InputComponent {
50
60
  center: {
@@ -588,4 +598,4 @@ declare class PackSolver2 extends BaseSolver$1 {
588
598
  };
589
599
  }
590
600
 
591
- export { type ComponentId, type GlobalBounds, type InputComponent, type InputObstacle, type InputPad, LargestRectOutsideOutlineFromPointSolver, type NetworkId, type OutputPad, type PackInput, type PackOutput, type PackPlacementStrategy, PackSolver2, type PackedComponent, type PadId, type Point$1 as Point, type Rect, convertCircuitJsonToPackOutput, convertPackOutputToPackInput, getGraphicsFromPackOutput, pack };
601
+ export { type ComponentCourtyard, type ComponentId, type GlobalBounds, type InputComponent, type InputObstacle, type InputPad, LargestRectOutsideOutlineFromPointSolver, type NetworkId, type OutputPad, type PackInput, type PackOutput, type PackPlacementStrategy, PackSolver2, type PackedComponent, type PadId, type Point$1 as Point, type Rect, convertCircuitJsonToPackOutput, convertPackOutputToPackInput, getGraphicsFromPackOutput, pack };
package/dist/index.js CHANGED
@@ -68,6 +68,30 @@ var combineBounds = (bounds) => {
68
68
  return { minX, minY, maxX, maxY };
69
69
  };
70
70
 
71
+ // lib/geometry/expandRotatedRectIntoBounds.ts
72
+ function expandRotatedRectIntoBounds(opts) {
73
+ const { bounds, center, width, height, angleRad } = opts;
74
+ const tx = opts.translate?.x ?? 0;
75
+ const ty = opts.translate?.y ?? 0;
76
+ const hw = width / 2;
77
+ const hh = height / 2;
78
+ const corners = [
79
+ { x: center.x - hw, y: center.y - hh },
80
+ { x: center.x + hw, y: center.y - hh },
81
+ { x: center.x + hw, y: center.y + hh },
82
+ { x: center.x - hw, y: center.y + hh }
83
+ ];
84
+ for (const corner of corners) {
85
+ const rotated = rotatePoint(corner, angleRad);
86
+ const x = rotated.x + tx;
87
+ const y = rotated.y + ty;
88
+ bounds.minX = Math.min(bounds.minX, x);
89
+ bounds.maxX = Math.max(bounds.maxX, x);
90
+ bounds.minY = Math.min(bounds.minY, y);
91
+ bounds.maxY = Math.max(bounds.maxY, y);
92
+ }
93
+ }
94
+
71
95
  // lib/geometry/getComponentBounds.ts
72
96
  var getComponentBounds = (component, minGap = 0) => {
73
97
  const bounds = {
@@ -76,28 +100,27 @@ var getComponentBounds = (component, minGap = 0) => {
76
100
  minY: Infinity,
77
101
  maxY: -Infinity
78
102
  };
79
- component.pads.forEach((pad) => {
80
- const hw = pad.size.x / 2;
81
- const hh = pad.size.y / 2;
82
- const localCorners = [
83
- { x: pad.offset.x - hw, y: pad.offset.y - hh },
84
- { x: pad.offset.x + hw, y: pad.offset.y - hh },
85
- { x: pad.offset.x + hw, y: pad.offset.y + hh },
86
- { x: pad.offset.x - hw, y: pad.offset.y + hh }
87
- ];
88
- localCorners.forEach((corner) => {
89
- const world = rotatePoint(
90
- corner,
91
- component.ccwRotationOffset * Math.PI / 180
92
- );
93
- const x = world.x + component.center.x;
94
- const y = world.y + component.center.y;
95
- bounds.minX = Math.min(bounds.minX, x);
96
- bounds.maxX = Math.max(bounds.maxX, x);
97
- bounds.minY = Math.min(bounds.minY, y);
98
- bounds.maxY = Math.max(bounds.maxY, y);
103
+ const angleRad = component.ccwRotationOffset * Math.PI / 180;
104
+ for (const pad of component.pads) {
105
+ expandRotatedRectIntoBounds({
106
+ bounds,
107
+ center: pad.offset,
108
+ width: pad.size.x,
109
+ height: pad.size.y,
110
+ angleRad,
111
+ translate: component.center
99
112
  });
100
- });
113
+ }
114
+ if (component.courtyard) {
115
+ expandRotatedRectIntoBounds({
116
+ bounds,
117
+ center: component.courtyard.offsetFromCenter,
118
+ width: component.courtyard.width,
119
+ height: component.courtyard.height,
120
+ angleRad,
121
+ translate: component.center
122
+ });
123
+ }
101
124
  return {
102
125
  minX: bounds.minX - minGap,
103
126
  maxX: bounds.maxX + minGap,
@@ -251,6 +274,50 @@ function parseFlattenPolygonSegments(polygon) {
251
274
  }
252
275
 
253
276
  // lib/constructOutlinesFromPackedComponents.ts
277
+ var createCourtyardPolygon = (opts) => {
278
+ const { component, courtyard, minGap } = opts;
279
+ const hw = courtyard.width / 2 + minGap;
280
+ const hh = courtyard.height / 2 + minGap;
281
+ const localCorners = [
282
+ {
283
+ x: courtyard.offsetFromCenter.x - hw,
284
+ y: courtyard.offsetFromCenter.y - hh
285
+ },
286
+ {
287
+ x: courtyard.offsetFromCenter.x + hw,
288
+ y: courtyard.offsetFromCenter.y - hh
289
+ },
290
+ {
291
+ x: courtyard.offsetFromCenter.x + hw,
292
+ y: courtyard.offsetFromCenter.y + hh
293
+ },
294
+ {
295
+ x: courtyard.offsetFromCenter.x - hw,
296
+ y: courtyard.offsetFromCenter.y + hh
297
+ }
298
+ ];
299
+ const worldCorners = localCorners.map((corner) => {
300
+ const rotated = rotatePoint(
301
+ corner,
302
+ component.ccwRotationOffset * Math.PI / 180
303
+ );
304
+ return {
305
+ x: rotated.x + component.center.x,
306
+ y: rotated.y + component.center.y
307
+ };
308
+ });
309
+ const arr = worldCorners.map(({ x, y }) => [x, y]);
310
+ const poly = new Flatten.Polygon(arr);
311
+ const xs = worldCorners.map((p) => p.x);
312
+ const ys = worldCorners.map((p) => p.y);
313
+ const bbox = {
314
+ minX: Math.min(...xs),
315
+ minY: Math.min(...ys),
316
+ maxX: Math.max(...xs),
317
+ maxY: Math.max(...ys)
318
+ };
319
+ return { poly, bbox };
320
+ };
254
321
  var createPadPolygons = (component, minGap) => {
255
322
  return component.pads.map((pad) => {
256
323
  const hw = pad.size.x / 2 + minGap;
@@ -322,8 +389,18 @@ var constructOutlinesFromPackedComponents = (components, opts = {}) => {
322
389
  const bounds = combineBounds([...componentBounds, ...obstacleBounds]);
323
390
  const allPadShapes = [];
324
391
  for (const component of components) {
325
- const padShapes = createPadPolygons(component, minGap);
326
- allPadShapes.push(...padShapes);
392
+ if (component.courtyard) {
393
+ allPadShapes.push(
394
+ createCourtyardPolygon({
395
+ component,
396
+ courtyard: component.courtyard,
397
+ minGap
398
+ })
399
+ );
400
+ } else {
401
+ const padShapes = createPadPolygons(component, minGap);
402
+ allPadShapes.push(...padShapes);
403
+ }
327
404
  }
328
405
  const obstacleShapes = createObstaclePolygons(obstacles, minGap);
329
406
  allPadShapes.push(...obstacleShapes);
@@ -1363,31 +1440,26 @@ var getInputComponentBounds = (component, { rotationDegrees = 0 }) => {
1363
1440
  minY: Infinity,
1364
1441
  maxY: -Infinity
1365
1442
  };
1443
+ const angleRad = rotationDegrees * Math.PI / 180;
1366
1444
  for (const pad of component.pads) {
1367
- const hw = pad.size.x / 2;
1368
- const hh = pad.size.y / 2;
1369
- const localCorners = [
1370
- { x: pad.offset.x - hw, y: pad.offset.y - hh },
1371
- { x: pad.offset.x + hw, y: pad.offset.y - hh },
1372
- { x: pad.offset.x + hw, y: pad.offset.y + hh },
1373
- { x: pad.offset.x - hw, y: pad.offset.y + hh }
1374
- ];
1375
- for (const corner of localCorners) {
1376
- const world = rotatePoint(corner, rotationDegrees * Math.PI / 180);
1377
- const x = world.x;
1378
- const y = world.y;
1379
- bounds.minX = Math.min(bounds.minX, x);
1380
- bounds.maxX = Math.max(bounds.maxX, x);
1381
- bounds.minY = Math.min(bounds.minY, y);
1382
- bounds.maxY = Math.max(bounds.maxY, y);
1383
- }
1445
+ expandRotatedRectIntoBounds({
1446
+ bounds,
1447
+ center: pad.offset,
1448
+ width: pad.size.x,
1449
+ height: pad.size.y,
1450
+ angleRad
1451
+ });
1384
1452
  }
1385
- return {
1386
- minX: bounds.minX,
1387
- maxX: bounds.maxX,
1388
- minY: bounds.minY,
1389
- maxY: bounds.maxY
1390
- };
1453
+ if (component.courtyard) {
1454
+ expandRotatedRectIntoBounds({
1455
+ bounds,
1456
+ center: component.courtyard.offsetFromCenter,
1457
+ width: component.courtyard.width,
1458
+ height: component.courtyard.height,
1459
+ angleRad
1460
+ });
1461
+ }
1462
+ return bounds;
1391
1463
  };
1392
1464
 
1393
1465
  // lib/math/expandSegment.ts
@@ -1757,7 +1829,8 @@ var OutlineSegmentCandidatePointSolver = class extends BaseSolver3 {
1757
1829
  y: center.y + rotatedOffset.y
1758
1830
  }
1759
1831
  };
1760
- })
1832
+ }),
1833
+ courtyard: this.componentToPack.courtyard
1761
1834
  };
1762
1835
  }
1763
1836
  /**
@@ -2114,28 +2187,54 @@ var getGraphicsFromPackOutput = (packOutput) => {
2114
2187
 
2115
2188
  // lib/PackSolver2/checkOverlapWithPackedComponents.ts
2116
2189
  import { computeDistanceBetweenBoxes } from "@tscircuit/math-utils";
2190
+
2191
+ // lib/PackSolver2/getComponentCollisionBoxes.ts
2192
+ function getComponentCollisionBoxes(component) {
2193
+ if (component.courtyard) {
2194
+ const courtyard = component.courtyard;
2195
+ const angleRad = component.ccwRotationOffset * Math.PI / 180;
2196
+ const rotatedOffset = rotatePoint(courtyard.offsetFromCenter, angleRad);
2197
+ const normalizedDeg = (component.ccwRotationOffset % 360 + 360) % 360;
2198
+ const swapDims = normalizedDeg === 90 || normalizedDeg === 270;
2199
+ let width = courtyard.width;
2200
+ let height = courtyard.height;
2201
+ if (swapDims) {
2202
+ width = courtyard.height;
2203
+ height = courtyard.width;
2204
+ }
2205
+ return [
2206
+ {
2207
+ center: {
2208
+ x: component.center.x + rotatedOffset.x,
2209
+ y: component.center.y + rotatedOffset.y
2210
+ },
2211
+ width,
2212
+ height
2213
+ }
2214
+ ];
2215
+ }
2216
+ return component.pads.map((p) => ({
2217
+ center: { x: p.absoluteCenter.x, y: p.absoluteCenter.y },
2218
+ width: p.size.x,
2219
+ height: p.size.y
2220
+ }));
2221
+ }
2222
+
2223
+ // lib/PackSolver2/checkOverlapWithPackedComponents.ts
2117
2224
  function checkOverlapWithPackedComponents({
2118
2225
  component,
2119
2226
  packedComponents,
2120
2227
  minGap
2121
2228
  }) {
2122
- const allPackedPadBoxes = packedComponents.flatMap(
2123
- (c) => c.pads.map((p) => ({
2124
- center: { x: p.absoluteCenter.x, y: p.absoluteCenter.y },
2125
- width: p.size.x,
2126
- height: p.size.y
2127
- }))
2229
+ const allPackedBoxes = packedComponents.flatMap(
2230
+ (c) => getComponentCollisionBoxes(c)
2128
2231
  );
2129
- const newComponentPadBoxes = component.pads.map((p) => ({
2130
- center: { x: p.absoluteCenter.x, y: p.absoluteCenter.y },
2131
- width: p.size.x,
2132
- height: p.size.y
2133
- }));
2134
- for (const newComponentPadBox of newComponentPadBoxes) {
2135
- for (const packedPadBox of allPackedPadBoxes) {
2232
+ const newComponentBoxes = getComponentCollisionBoxes(component);
2233
+ for (const newBox of newComponentBoxes) {
2234
+ for (const packedBox of allPackedBoxes) {
2136
2235
  const { distance: boxDist } = computeDistanceBetweenBoxes(
2137
- newComponentPadBox,
2138
- packedPadBox
2236
+ newBox,
2237
+ packedBox
2139
2238
  );
2140
2239
  if (boxDist + 1e-6 < minGap) {
2141
2240
  return {
@@ -2216,19 +2315,15 @@ var SingleComponentPackSolver = class extends BaseSolver4 {
2216
2315
  const position = { x: 0, y: 0 };
2217
2316
  const rotation = availableRotations2[0] ?? 0;
2218
2317
  const candidate = this.createPackedComponent(position, rotation);
2318
+ const candidateBoxes = getComponentCollisionBoxes(candidate);
2219
2319
  const tooCloseToObstacles = (this.obstacles ?? []).some((obs) => {
2220
2320
  const obsBox = {
2221
2321
  center: { x: obs.absoluteCenter.x, y: obs.absoluteCenter.y },
2222
2322
  width: obs.width,
2223
2323
  height: obs.height
2224
2324
  };
2225
- return candidate.pads.some((p) => {
2226
- const padBox = {
2227
- center: { x: p.absoluteCenter.x, y: p.absoluteCenter.y },
2228
- width: p.size.x,
2229
- height: p.size.y
2230
- };
2231
- const { distance } = computeDistanceBetweenBoxes2(padBox, obsBox);
2325
+ return candidateBoxes.some((box) => {
2326
+ const { distance } = computeDistanceBetweenBoxes2(box, obsBox);
2232
2327
  return distance + 1e-6 < this.minGap;
2233
2328
  });
2234
2329
  });
@@ -2333,19 +2428,15 @@ var SingleComponentPackSolver = class extends BaseSolver4 {
2333
2428
  minGap: this.minGap
2334
2429
  });
2335
2430
  let minObstacleGapDistance = Infinity;
2431
+ const candidateCollisionBoxes = getComponentCollisionBoxes(candidateComponent);
2336
2432
  const tooCloseToObstacles = (this.obstacles ?? []).some((obs) => {
2337
2433
  const obsBox = {
2338
2434
  center: { x: obs.absoluteCenter.x, y: obs.absoluteCenter.y },
2339
2435
  width: obs.width,
2340
2436
  height: obs.height
2341
2437
  };
2342
- return candidateComponent.pads.some((p) => {
2343
- const padBox = {
2344
- center: { x: p.absoluteCenter.x, y: p.absoluteCenter.y },
2345
- width: p.size.x,
2346
- height: p.size.y
2347
- };
2348
- const { distance: distance2 } = computeDistanceBetweenBoxes2(padBox, obsBox);
2438
+ return candidateCollisionBoxes.some((box) => {
2439
+ const { distance: distance2 } = computeDistanceBetweenBoxes2(box, obsBox);
2349
2440
  minObstacleGapDistance = Math.min(minObstacleGapDistance, distance2);
2350
2441
  return distance2 + 1e-6 < this.minGap;
2351
2442
  });
@@ -2795,19 +2886,15 @@ var PackSolver2 = class extends BaseSolver5 {
2795
2886
  };
2796
2887
  setPackedComponentPadCenters(newPackedComponent);
2797
2888
  const obstacles = this.packInput.obstacles ?? [];
2889
+ const newComponentBoxes = getComponentCollisionBoxes(newPackedComponent);
2798
2890
  const tooCloseToObstacles = obstacles.some((obs) => {
2799
2891
  const obsBox = {
2800
2892
  center: { x: obs.absoluteCenter.x, y: obs.absoluteCenter.y },
2801
2893
  width: obs.width,
2802
2894
  height: obs.height
2803
2895
  };
2804
- return newPackedComponent.pads.some((p) => {
2805
- const padBox = {
2806
- center: { x: p.absoluteCenter.x, y: p.absoluteCenter.y },
2807
- width: p.size.x,
2808
- height: p.size.y
2809
- };
2810
- const { distance } = computeDistanceBetweenBoxes3(padBox, obsBox);
2896
+ return newComponentBoxes.some((box) => {
2897
+ const { distance } = computeDistanceBetweenBoxes3(box, obsBox);
2811
2898
  return distance + 1e-6 < this.packInput.minGap;
2812
2899
  });
2813
2900
  });
@@ -3234,6 +3321,62 @@ var getObstacleFromElement = (element) => {
3234
3321
  };
3235
3322
 
3236
3323
  // lib/plumbing/convertCircuitJsonToPackOutput.ts
3324
+ var extractCourtyardForComponent = (opts) => {
3325
+ const { db, pcbComponentIds, componentCenter } = opts;
3326
+ const idSet = new Set(pcbComponentIds);
3327
+ let minX = Infinity;
3328
+ let minY = Infinity;
3329
+ let maxX = -Infinity;
3330
+ let maxY = -Infinity;
3331
+ let found = false;
3332
+ for (const rect of db.pcb_courtyard_rect.list()) {
3333
+ if (!idSet.has(rect.pcb_component_id)) continue;
3334
+ minX = Math.min(minX, rect.center.x - rect.width / 2);
3335
+ maxX = Math.max(maxX, rect.center.x + rect.width / 2);
3336
+ minY = Math.min(minY, rect.center.y - rect.height / 2);
3337
+ maxY = Math.max(maxY, rect.center.y + rect.height / 2);
3338
+ found = true;
3339
+ }
3340
+ for (const polygon of db.pcb_courtyard_polygon.list()) {
3341
+ if (!idSet.has(polygon.pcb_component_id)) continue;
3342
+ for (const pt of polygon.points) {
3343
+ minX = Math.min(minX, pt.x);
3344
+ maxX = Math.max(maxX, pt.x);
3345
+ minY = Math.min(minY, pt.y);
3346
+ maxY = Math.max(maxY, pt.y);
3347
+ }
3348
+ found = true;
3349
+ }
3350
+ for (const outline of db.pcb_courtyard_outline.list()) {
3351
+ if (!idSet.has(outline.pcb_component_id)) continue;
3352
+ for (const pt of outline.outline) {
3353
+ minX = Math.min(minX, pt.x);
3354
+ maxX = Math.max(maxX, pt.x);
3355
+ minY = Math.min(minY, pt.y);
3356
+ maxY = Math.max(maxY, pt.y);
3357
+ }
3358
+ found = true;
3359
+ }
3360
+ for (const circle of db.pcb_courtyard_circle.list()) {
3361
+ if (!idSet.has(circle.pcb_component_id)) continue;
3362
+ minX = Math.min(minX, circle.center.x - circle.radius);
3363
+ maxX = Math.max(maxX, circle.center.x + circle.radius);
3364
+ minY = Math.min(minY, circle.center.y - circle.radius);
3365
+ maxY = Math.max(maxY, circle.center.y + circle.radius);
3366
+ found = true;
3367
+ }
3368
+ if (!found) return void 0;
3369
+ const courtyardCenterX = (minX + maxX) / 2;
3370
+ const courtyardCenterY = (minY + maxY) / 2;
3371
+ return {
3372
+ offsetFromCenter: {
3373
+ x: courtyardCenterX - componentCenter.x,
3374
+ y: courtyardCenterY - componentCenter.y
3375
+ },
3376
+ width: maxX - minX,
3377
+ height: maxY - minY
3378
+ };
3379
+ };
3237
3380
  var buildPackedComponent = (pcbComponents, componentId, db, getNetworkId, shouldAddInnerObstacles, sourcePortToPadIds = /* @__PURE__ */ new Map(), chipMarginsMap = {}, isStatic = false) => {
3238
3381
  const padInfos = pcbComponents.flatMap((pc) => {
3239
3382
  const pads2 = extractPadInfos(pc, db, getNetworkId);
@@ -3292,12 +3435,18 @@ var buildPackedComponent = (pcbComponents, componentId, db, getNetworkId, should
3292
3435
  };
3293
3436
  pads.push(innerPad);
3294
3437
  }
3438
+ const courtyard = extractCourtyardForComponent({
3439
+ db,
3440
+ pcbComponentIds: pcbComponents.map((pc) => pc.pcb_component_id),
3441
+ componentCenter: center
3442
+ });
3295
3443
  return {
3296
3444
  componentId,
3297
3445
  isStatic,
3298
3446
  center,
3299
3447
  ccwRotationOffset: 0,
3300
- pads
3448
+ pads,
3449
+ courtyard
3301
3450
  };
3302
3451
  };
3303
3452
  var collectPcbComponents = (node, db) => {
@@ -3474,7 +3623,8 @@ var convertPackOutputToPackInput = (packed) => {
3474
3623
  componentId: pc.componentId,
3475
3624
  availableRotationDegrees: pc.availableRotationDegrees,
3476
3625
  // Preserve rotation constraints
3477
- pads: pc.pads.map(({ absoluteCenter: _ac, ...rest }) => rest)
3626
+ pads: pc.pads.map(({ absoluteCenter: _ac, ...rest }) => rest),
3627
+ courtyard: pc.courtyard
3478
3628
  }
3479
3629
  }));
3480
3630
  return {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "calculate-packing",
3
3
  "main": "dist/index.js",
4
4
  "type": "module",
5
- "version": "0.0.71",
5
+ "version": "0.0.72",
6
6
  "description": "Calculate a packing layout with support for different strategy configurations",
7
7
  "scripts": {
8
8
  "start": "cosmos",
@@ -19,16 +19,16 @@
19
19
  "@biomejs/biome": "^2.1.1",
20
20
  "@flatten-js/core": "^1.6.2",
21
21
  "@react-hook/resize-observer": "^2.0.2",
22
- "@tscircuit/circuit-json-util": "^0.0.66",
22
+ "@tscircuit/circuit-json-util": "^0.0.94",
23
23
  "@tscircuit/footprinter": "^0.0.203",
24
- "@tscircuit/math-utils": "^0.0.25",
24
+ "@tscircuit/math-utils": "^0.0.36",
25
25
  "@tscircuit/solver-utils": "^0.0.14",
26
26
  "@types/bun": "latest",
27
27
  "@types/react": "^19.1.8",
28
28
  "@types/react-dom": "^19.1.6",
29
29
  "@vitejs/plugin-react": "^5.0.0",
30
30
  "bun-match-svg": "^0.0.12",
31
- "circuit-json": "^0.0.309",
31
+ "circuit-json": "^0.0.421",
32
32
  "framer-motion": "^12.23.12",
33
33
  "graphics-debug": "^0.0.78",
34
34
  "react": "^19.1.0",