@tscircuit/footprinter 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.
@@ -0,0 +1,33 @@
1
+ import { AnySoupElement } from '@tscircuit/soup';
2
+
3
+ type FootprinterParamsBuilder<K extends string> = {
4
+ [P in K | "params" | "soup"]: P extends "params" | "soup" ? Footprinter[P] : (v?: number | string) => FootprinterParamsBuilder<K>;
5
+ };
6
+ type CommonPassiveOptionKey = "metric" | "imperial" | "tht" | "p" | "pw" | "ph" | "w" | "h";
7
+ type Footprinter = {
8
+ dip: (num_pins: number) => FootprinterParamsBuilder<"w" | "p" | "id" | "od">;
9
+ cap: () => FootprinterParamsBuilder<CommonPassiveOptionKey>;
10
+ res: () => FootprinterParamsBuilder<CommonPassiveOptionKey>;
11
+ diode: () => FootprinterParamsBuilder<CommonPassiveOptionKey>;
12
+ led: () => FootprinterParamsBuilder<CommonPassiveOptionKey>;
13
+ lr: (num_pins: number) => FootprinterParamsBuilder<"w" | "l" | "pl" | "pr">;
14
+ quad: (num_pins: number) => FootprinterParamsBuilder<"w" | "l" | "square" | "pl" | "pr" | "pb" | "pt" | "p" | "pw" | "ph">;
15
+ bga: (num_pins: number) => FootprinterParamsBuilder<"grid" | "p" | "w" | "h" | "ball" | "pad" | "missing">;
16
+ params: () => any;
17
+ soup: () => AnySoupElement[];
18
+ };
19
+ declare const string: (def: string) => Footprinter;
20
+ declare const footprinter: {
21
+ (): Footprinter & {
22
+ string: typeof string;
23
+ };
24
+ string: (def: string) => Footprinter;
25
+ };
26
+ declare const fp: {
27
+ (): Footprinter & {
28
+ string: typeof string;
29
+ };
30
+ string: (def: string) => Footprinter;
31
+ };
32
+
33
+ export { type Footprinter, type FootprinterParamsBuilder, footprinter, fp, string };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@tscircuit/footprinter",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "test": "ava",
9
+ "build": "tsup ./src/index.ts --dts --sourcemap"
10
+ },
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "devDependencies": {
15
+ "@tscircuit/soup": "^0.0.17",
16
+ "ava": "^6.1.3",
17
+ "esbuild": "^0.21.4",
18
+ "esbuild-register": "^3.5.0",
19
+ "tsup": "^8.0.2",
20
+ "typescript": "^5.4.5"
21
+ },
22
+ "dependencies": {
23
+ "@tscircuit/mm": "^0.0.6"
24
+ }
25
+ }
package/src/fn/bga.ts ADDED
@@ -0,0 +1,71 @@
1
+ import type { AnySoupElement, PCBSMTPad } from "@tscircuit/soup"
2
+ import { rectpad } from "../helpers/rectpad"
3
+ import { PassiveDef, passive } from "../helpers/passive-fn"
4
+ import { BgaDef, BgaDefInput, bga_def } from "../helpers/zod/bga-def"
5
+
6
+ export const bga = (params: BgaDefInput): AnySoupElement[] => {
7
+ const bga_params = bga_def.parse(params)
8
+ let { num_pins, grid, p, w, h, ball, pad, missing } = bga_params
9
+
10
+ ball ??= (0.75 / 1.27) * p
11
+
12
+ pad ??= ball * 0.8
13
+
14
+ const pads: PCBSMTPad[] = []
15
+
16
+ const missing_pin_nums = (missing ?? []).filter((a) => typeof a === "number")
17
+ const num_pins_missing = grid.x * grid.y - num_pins
18
+
19
+ if (missing?.includes("center")) {
20
+ // Find the largest square that's square is less than
21
+ // the number of missing pins
22
+ const square_size = Math.floor(Math.sqrt(num_pins_missing))
23
+
24
+ // Find the top left coordinate of the inner square, keep
25
+ // in mind the full grid size is grid.x x grid.y
26
+ const inner_square_x = Math.floor((grid.x - square_size) / 2)
27
+ const inner_square_y = Math.floor((grid.y - square_size) / 2)
28
+
29
+ // Add all the missing square pin numbers to missing_pin_nums
30
+ for (let y = inner_square_y; y < inner_square_y + square_size; y++) {
31
+ for (let x = inner_square_x; x < inner_square_x + square_size; x++) {
32
+ missing_pin_nums.push(y * grid.x + x + 1)
33
+ }
34
+ }
35
+ }
36
+
37
+ if (missing?.includes("topleft")) {
38
+ missing_pin_nums.push(1)
39
+ }
40
+
41
+ if (num_pins_missing !== missing_pin_nums.length) {
42
+ throw new Error(
43
+ `not able to create bga component, unable to determine missing pins (try specifying them with "missing+1+2+..."\n\n${JSON.stringify(
44
+ bga_params,
45
+ null,
46
+ " "
47
+ )}`
48
+ )
49
+ }
50
+
51
+ const missing_pin_nums_set = new Set(missing_pin_nums)
52
+
53
+ let missing_pins_passed = 0
54
+ for (let y = 0; y < grid.y; y++) {
55
+ for (let x = 0; x < grid.x; x++) {
56
+ let pin_num = y * grid.x + x + 1
57
+ if (missing_pin_nums_set.has(pin_num)) {
58
+ missing_pins_passed++
59
+ continue
60
+ }
61
+ pin_num -= missing_pins_passed
62
+
63
+ const pad_x = (x - (grid.x - 1) / 2) * p
64
+ const pad_y = -(y - (grid.y - 1) / 2) * p
65
+
66
+ pads.push(rectpad(pin_num, pad_x, pad_y, pad, pad))
67
+ }
68
+ }
69
+
70
+ return [...pads]
71
+ }
package/src/fn/cap.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { AnySoupElement } from "@tscircuit/soup"
2
+ import { rectpad } from "../helpers/rectpad"
3
+ import { PassiveDef, passive } from "../helpers/passive-fn"
4
+
5
+ export const cap = (params: PassiveDef): AnySoupElement[] => {
6
+ return passive(params)
7
+ }
@@ -0,0 +1,8 @@
1
+ import type { AnySoupElement } from "@tscircuit/soup"
2
+
3
+ export const diode = (params: {
4
+ tht: boolean
5
+ p: number
6
+ }): AnySoupElement[] => {
7
+ return []
8
+ }
package/src/fn/dip.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { AnySoupElement } from "@tscircuit/soup"
2
+ import { platedhole } from "../helpers/platedhole"
3
+
4
+ export const getCcwDipCoords = (
5
+ pinCount: number,
6
+ pn: number,
7
+ w: number,
8
+ p: number
9
+ ) => {
10
+ /** pin height */
11
+ const ph = pinCount / 2
12
+ const isLeft = pn <= ph
13
+
14
+ /** Number of gaps between pins on each side, e.g. 4 pins = 3 spaces */
15
+ const leftPinGaps = ph - 1
16
+
17
+ /** gap size (pitch) */
18
+ const gs = p
19
+
20
+ const h = gs * leftPinGaps
21
+
22
+ if (isLeft) {
23
+ // The y position starts at h/2, then goes down by gap size
24
+ // for each pin
25
+ return { x: -w / 2, y: h / 2 - (pn - 1) * gs }
26
+ } else {
27
+ // The y position starts at -h/2, then goes up by gap size
28
+ return { x: w / 2, y: -h / 2 + (pn - ph - 1) * gs }
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Returns the plated holes for a DIP package.
34
+ */
35
+ export const dip = (params: {
36
+ dip: true
37
+ num_pins: number
38
+ w: number
39
+ p?: number
40
+ id?: string | number
41
+ od?: string | number
42
+ }): AnySoupElement[] => {
43
+ const platedHoles: AnySoupElement[] = []
44
+ for (let i = 0; i < params.num_pins; i++) {
45
+ const { x, y } = getCcwDipCoords(
46
+ params.num_pins,
47
+ i + 1,
48
+ params.w,
49
+ params.p ?? 2.54
50
+ )
51
+ platedHoles.push(
52
+ platedhole(i + 1, x, y, params.id ?? "0.8mm", params.od ?? "1mm")
53
+ )
54
+ }
55
+ return platedHoles
56
+ }
package/src/fn/led.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { AnySoupElement } from "@tscircuit/soup"
2
+ import { PassiveDef, passive } from "../helpers/passive-fn"
3
+
4
+ export const led = (params: PassiveDef): AnySoupElement[] => {
5
+ return passive(params)
6
+ }
package/src/fn/res.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { AnySoupElement } from "@tscircuit/soup"
2
+ import { passive, PassiveDef } from "../helpers/passive-fn"
3
+
4
+ export const res = (params: PassiveDef): AnySoupElement[] => {
5
+ return passive(params)
6
+ }
@@ -0,0 +1,102 @@
1
+ import { dip } from "./fn/dip"
2
+ import { diode } from "./fn/diode"
3
+ import { cap } from "./fn/cap"
4
+ import { led } from "./fn/led"
5
+ import { res } from "./fn/res"
6
+ import { bga } from "./fn/bga"
7
+ import type { AnySoupElement } from "@tscircuit/soup"
8
+
9
+ export type FootprinterParamsBuilder<K extends string> = {
10
+ [P in K | "params" | "soup"]: P extends "params" | "soup"
11
+ ? Footprinter[P]
12
+ : (v?: number | string) => FootprinterParamsBuilder<K>
13
+ }
14
+
15
+ type CommonPassiveOptionKey =
16
+ | "metric"
17
+ | "imperial"
18
+ | "tht"
19
+ | "p"
20
+ | "pw"
21
+ | "ph"
22
+ | "w"
23
+ | "h"
24
+
25
+ export type Footprinter = {
26
+ dip: (num_pins: number) => FootprinterParamsBuilder<"w" | "p" | "id" | "od">
27
+ cap: () => FootprinterParamsBuilder<CommonPassiveOptionKey>
28
+ res: () => FootprinterParamsBuilder<CommonPassiveOptionKey>
29
+ diode: () => FootprinterParamsBuilder<CommonPassiveOptionKey>
30
+ led: () => FootprinterParamsBuilder<CommonPassiveOptionKey>
31
+ lr: (num_pins: number) => FootprinterParamsBuilder<"w" | "l" | "pl" | "pr">
32
+ quad: (
33
+ num_pins: number
34
+ ) => FootprinterParamsBuilder<
35
+ "w" | "l" | "square" | "pl" | "pr" | "pb" | "pt" | "p" | "pw" | "ph"
36
+ >
37
+ bga: (
38
+ num_pins: number
39
+ ) => FootprinterParamsBuilder<
40
+ "grid" | "p" | "w" | "h" | "ball" | "pad" | "missing"
41
+ >
42
+ params: () => any
43
+ soup: () => AnySoupElement[]
44
+ }
45
+
46
+ export const string = (def: string): Footprinter => {
47
+ let fp = footprinter()
48
+
49
+ const def_parts = def.split("_").map((s) => {
50
+ const m = s.match(/([a-z]+)([\(\d\+].*)?/)
51
+ const [_, fn, v] = m ?? []
52
+ return { fn: m?.[1]!, v: m?.[2]! }
53
+ })
54
+
55
+ for (const { fn, v } of def_parts) {
56
+ fp = fp[fn](v)
57
+ }
58
+
59
+ return fp
60
+ }
61
+
62
+ export const footprinter = (): Footprinter & { string: typeof string } => {
63
+ const proxy = new Proxy(
64
+ {},
65
+ {
66
+ get: (target: any, prop) => {
67
+ if (prop === "soup") {
68
+ if ("dip" in target) return () => dip(target)
69
+ if ("diode" in target) return () => diode(target)
70
+ if ("cap" in target) return () => cap(target)
71
+ if ("led" in target) return () => led(target)
72
+ if ("res" in target) return () => res(target)
73
+ if ("bga" in target) return () => bga(target)
74
+
75
+ return () => {
76
+ // TODO improve error
77
+ throw new Error(
78
+ `No function found for footprinter, make sure to specify .dip, .lr, .p, etc. Got \"${prop}\"`
79
+ )
80
+ }
81
+ }
82
+ if (prop === "params") {
83
+ // TODO
84
+ return () => target
85
+ }
86
+ return (v: any) => {
87
+ if (["bga", "lr", "quad", "dip"].includes(prop as string)) {
88
+ target[prop] = true
89
+ target.num_pins = parseFloat(v)
90
+ } else {
91
+ target[prop] = v ?? true
92
+ }
93
+ return proxy
94
+ }
95
+ },
96
+ }
97
+ )
98
+ return proxy as any
99
+ }
100
+ footprinter.string = string
101
+
102
+ export const fp = footprinter
@@ -0,0 +1,175 @@
1
+ import type { AnySoupElement } from "@tscircuit/soup"
2
+ import { rectpad } from "../helpers/rectpad"
3
+ import mm from "@tscircuit/mm"
4
+ import { platedhole } from "./platedhole"
5
+
6
+ type StandardSize = {
7
+ imperial: string
8
+ metric: string
9
+ Z_mm_min: number
10
+ G_mm_min: number
11
+ X_mm_min: number
12
+ C_mm_ref: number
13
+ }
14
+
15
+ // https://www.worthingtonassembly.com/perfect-0201-footprint
16
+ // https://static1.squarespace.com/static/54982a02e4b02e9f5e5d9ca7/t/660c692f69a0d83a4afecdf0/1712089391915/Discrete+Component+Footprints.pdf
17
+ // https://page.venkel.com/hubfs/Resources/Technical/Resistors%20Landing%20Pattern.pdf
18
+ const sizes = [
19
+ {
20
+ imperial: "01005",
21
+ metric: "0402",
22
+ Z_mm_min: 0.58,
23
+ G_mm_min: 0.18,
24
+ X_mm_min: 0.21,
25
+ C_mm_ref: 0.038,
26
+ },
27
+ {
28
+ imperial: "0201",
29
+ metric: "0603",
30
+ Z_mm_min: 0.9,
31
+ G_mm_min: 0.3,
32
+ X_mm_min: 0.3,
33
+ C_mm_ref: 0.6,
34
+ },
35
+ {
36
+ imperial: "0402",
37
+ metric: "1005",
38
+ Z_mm_min: 1.6,
39
+ G_mm_min: 0.4,
40
+ X_mm_min: 0.7,
41
+ C_mm_ref: 1,
42
+ },
43
+ {
44
+ imperial: "0603",
45
+ metric: "1608",
46
+ Z_mm_min: 2.6,
47
+ G_mm_min: 0.6,
48
+ X_mm_min: 1.0,
49
+ C_mm_ref: 1.7,
50
+ },
51
+ {
52
+ imperial: "0805",
53
+ metric: "2012",
54
+ Z_mm_min: 3.0,
55
+ G_mm_min: 0.6,
56
+ X_mm_min: 1.2,
57
+ C_mm_ref: 1.9,
58
+ },
59
+ {
60
+ imperial: "1206",
61
+ metric: "3216",
62
+ Z_mm_min: 4.2,
63
+ G_mm_min: 1.2,
64
+ X_mm_min: 1.4,
65
+ C_mm_ref: 2.8,
66
+ },
67
+ {
68
+ imperial: "1210",
69
+ metric: "3225",
70
+ Z_mm_min: 4.2,
71
+ G_mm_min: 1.2,
72
+ X_mm_min: 2.4,
73
+ C_mm_ref: 2.8,
74
+ },
75
+ {
76
+ imperial: "2010",
77
+ metric: "5025",
78
+ Z_mm_min: 6.0,
79
+ G_mm_min: 2.6,
80
+ X_mm_min: 2.4,
81
+ C_mm_ref: 4.4,
82
+ },
83
+ {
84
+ imperial: "2512",
85
+ metric: "6332",
86
+ Z_mm_min: 7.2,
87
+ G_mm_min: 3.8,
88
+ X_mm_min: 3.0,
89
+ C_mm_ref: 5.6,
90
+ },
91
+ ]
92
+ const metricMap: Record<string, StandardSize> = sizes.reduce((acc: any, s) => {
93
+ acc[s.metric] = s
94
+ return acc
95
+ }, {})
96
+ const imperialMap: Record<string, StandardSize> = sizes.reduce(
97
+ (acc: any, s) => {
98
+ acc[s.imperial] = s
99
+ return acc
100
+ },
101
+ {}
102
+ )
103
+
104
+ export type PassiveDef = {
105
+ tht: boolean
106
+ p: number
107
+ pw?: number
108
+ ph?: number
109
+ metric?: string
110
+ imperial?: string
111
+ w?: number
112
+ h?: number
113
+ }
114
+
115
+ const deriveXFromH = (h: number) => 0.079 * h ** 2 + 0.94 * h - 0.009
116
+ const deriveZFromW = (w: number) => 1.09 * w + 0.6
117
+ const deriveGFromW = (w: number) => 0.59 * w - 0.31
118
+ const deriveCFromW = (w: number) => -0.01 * w ** 2 + 0.94 * w + 0.03
119
+
120
+ export const passive = (params: PassiveDef): AnySoupElement[] => {
121
+ let { tht, p, pw, ph, metric, imperial, w, h } = params
122
+
123
+ if (typeof w === "string") w = mm(w)
124
+ if (typeof h === "string") h = mm(h)
125
+ if (typeof p === "string") p = mm(p)
126
+ if (typeof pw === "string") pw = mm(pw)
127
+ if (typeof ph === "string") ph = mm(ph)
128
+
129
+ if (h! > w!) {
130
+ throw new Error(
131
+ "height cannot be greater than width (rotated footprint not yet implemented)"
132
+ )
133
+ }
134
+
135
+ /** standard size */
136
+ let sz: StandardSize | undefined
137
+ if (metric) {
138
+ sz = metricMap[metric]
139
+ }
140
+
141
+ if (imperial) {
142
+ sz = imperialMap[imperial]
143
+ }
144
+
145
+ if (!sz && w && h && !pw && !ph) {
146
+ sz = {
147
+ imperial: "custom",
148
+ metric: "custom",
149
+ Z_mm_min: deriveZFromW(w),
150
+ G_mm_min: deriveGFromW(w),
151
+ X_mm_min: deriveXFromH(h),
152
+ C_mm_ref: deriveCFromW(w),
153
+ }
154
+ }
155
+
156
+ if (sz) {
157
+ w = sz.Z_mm_min
158
+ h = sz.X_mm_min
159
+ p = sz.C_mm_ref
160
+ pw = (sz.Z_mm_min - sz.G_mm_min) / 2
161
+ ph = (sz.Z_mm_min - sz.G_mm_min) / 2
162
+ }
163
+
164
+ if (pw === undefined) throw new Error(`could not infer pad width`)
165
+ if (ph === undefined) throw new Error(`could not infer pad width`)
166
+
167
+ if (tht) {
168
+ return [
169
+ platedhole(1, -p / 2, 0, pw, (pw * 1) / 0.8),
170
+ platedhole(2, -p / 2, 0, pw, (pw * 1) / 0.8),
171
+ ]
172
+ } else {
173
+ return [rectpad(1, -p / 2, 0, pw, ph), rectpad(2, p / 2, 0, pw, ph)]
174
+ }
175
+ }
@@ -0,0 +1,21 @@
1
+ import type { PCBPlatedHole, PCBSMTPad } from "@tscircuit/soup"
2
+ import { mm } from "@tscircuit/mm"
3
+
4
+ export const platedhole = (
5
+ pn: number,
6
+ x: number,
7
+ y: number,
8
+ id: number | string,
9
+ od: number | string
10
+ ): PCBPlatedHole => {
11
+ return {
12
+ type: "pcb_plated_hole",
13
+ x,
14
+ y,
15
+ hole_diameter: mm(id),
16
+ outer_diameter: mm(od),
17
+ pcb_port_id: "",
18
+ layers: ["top", "bottom"],
19
+ port_hints: [pn.toString()],
20
+ }
21
+ }
@@ -0,0 +1,20 @@
1
+ import type { PCBSMTPad } from "@tscircuit/soup"
2
+ export const rectpad = (
3
+ pn: number,
4
+ x: number,
5
+ y: number,
6
+ w: number,
7
+ h: number
8
+ ): PCBSMTPad => {
9
+ return {
10
+ type: "pcb_smtpad",
11
+ x,
12
+ y,
13
+ width: w,
14
+ height: h,
15
+ layer: "top",
16
+ shape: "rect",
17
+ pcb_smtpad_id: "",
18
+ port_hints: [pn.toString()],
19
+ }
20
+ }
@@ -0,0 +1,48 @@
1
+ import { z } from "zod"
2
+ import { length, distance } from "@tscircuit/soup"
3
+ import { dim2d } from "./dim-2d"
4
+ import { function_call } from "./function-call"
5
+
6
+ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
7
+
8
+ export const bga_def = z
9
+ .object({
10
+ num_pins: z.number(),
11
+ grid: dim2d,
12
+ p: distance,
13
+ w: length,
14
+ h: length,
15
+ ball: length.optional().describe("ball diameter"),
16
+ pad: length.optional().describe("pad width/height"),
17
+
18
+ tlorigin: z.boolean().optional(),
19
+ blorigin: z.boolean().optional(),
20
+ trorigin: z.boolean().optional(),
21
+ brorigin: z.boolean().optional(),
22
+
23
+ missing: function_call,
24
+ })
25
+ .transform((a) => {
26
+ let origin: "tl" | "bl" | "tr" | "br" = "tl"
27
+ if (a.blorigin) origin = "bl"
28
+ if (a.trorigin) origin = "tr"
29
+ if (a.brorigin) origin = "br"
30
+
31
+ if (a.missing) {
32
+ a.missing = a.missing.map((s) => {
33
+ if (typeof s === "number") return s
34
+ if (s === "center") return "center"
35
+ if (s === "topleft") return "topleft"
36
+ const m = s.match(/([A-Z]+)(\d+)/)
37
+ if (!m) return s
38
+ let Y = ALPHABET.indexOf(m[1])
39
+ let X = parseInt(m[2]) - 1
40
+ return Y * a.grid.x + X + 1
41
+ })
42
+ }
43
+
44
+ return { ...a, origin }
45
+ })
46
+
47
+ export type BgaDefInput = z.input<typeof bga_def>
48
+ export type BgaDef = z.infer<typeof bga_def>
@@ -0,0 +1,17 @@
1
+ import { z } from "zod"
2
+
3
+ export const dim2d = z
4
+ .string()
5
+ .transform((a) => {
6
+ const [x, y] = a.split(/[x ]/)
7
+ return {
8
+ x: parseFloat(x),
9
+ y: parseFloat(y),
10
+ }
11
+ })
12
+ .pipe(
13
+ z.object({
14
+ x: z.number(),
15
+ y: z.number(),
16
+ })
17
+ )
@@ -0,0 +1,16 @@
1
+ import { z } from "zod"
2
+
3
+ export const function_call = z
4
+ .string()
5
+ .or(z.array(z.any()))
6
+ .transform((a) => {
7
+ if (Array.isArray(a)) return a
8
+ if (a.startsWith("(") && a.endsWith(")")) {
9
+ a = a.slice(1, -1)
10
+ }
11
+ return a.split(",").map((v) => {
12
+ const numVal = Number(v)
13
+ return isNaN(numVal) ? v : numVal
14
+ })
15
+ })
16
+ .pipe(z.array(z.string().or(z.number())))
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./footprinter"
@@ -0,0 +1,49 @@
1
+ import test from "ava"
2
+ import { fp } from "../src/footprinter"
3
+ import { AnySoupElement } from "@tscircuit/soup"
4
+ import { toPinPositionString } from "./fixtures"
5
+
6
+ test("bga footprint", (t) => {
7
+ const soup = fp()
8
+ .bga(8)
9
+ .w("4mm")
10
+ .h("4mm")
11
+ .grid("3x3")
12
+ .missing("center")
13
+ .p(1)
14
+ .soup()
15
+ // 16pins, 4mm x 4mm, 8x8 grid, 1.27mm pitch
16
+ const ps = toPinPositionString(soup)
17
+
18
+ t.is(
19
+ ps,
20
+ `
21
+ 1 : -1.00 1.00
22
+ 2 : 0.00 1.00
23
+ 3 : 1.00 1.00
24
+ 4 : -1.00 0.00
25
+ 5 : 1.00 0.00
26
+ 6 : -1.00 -1.00
27
+ 7 : 0.00 -1.00
28
+ 8 : 1.00 -1.00
29
+ `.trim()
30
+ )
31
+ })
32
+
33
+ test("bga7_w8_h8_grid3x3_p1_missing(center,B1)", (t) => {
34
+ const soup = fp.string("bga7_w8_h8_grid3x3_p1_missing(center,B1)").soup()
35
+ const ps = toPinPositionString(soup)
36
+
37
+ t.is(
38
+ ps,
39
+ `
40
+ 1 : -1.00 1.00
41
+ 2 : 0.00 1.00
42
+ 3 : 1.00 1.00
43
+ 4 : 1.00 0.00
44
+ 5 : -1.00 -1.00
45
+ 6 : 0.00 -1.00
46
+ 7 : 1.00 -1.00
47
+ `.trim()
48
+ )
49
+ })