@tscircuit/footprinter 0.0.5 → 0.0.7

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/footprinter",
3
3
  "type": "module",
4
- "version": "0.0.5",
4
+ "version": "0.0.7",
5
5
  "description": "",
6
6
  "main": "dist/index.cjs",
7
7
  "scripts": {
@@ -23,6 +23,6 @@
23
23
  "typescript": "^5.4.5"
24
24
  },
25
25
  "dependencies": {
26
- "@tscircuit/mm": "^0.0.6"
26
+ "@tscircuit/mm": "^0.0.7"
27
27
  }
28
28
  }
package/src/fn/bga.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { AnySoupElement, PCBSMTPad } from "@tscircuit/soup"
2
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"
3
+ import { type BgaDefInput, bga_def } from "../helpers/zod/bga-def"
4
+ import { ALPHABET } from "../helpers/zod/ALPHABET"
5
5
 
6
6
  export const bga = (params: BgaDefInput): AnySoupElement[] => {
7
7
  const bga_params = bga_def.parse(params)
@@ -16,6 +16,16 @@ export const bga = (params: BgaDefInput): AnySoupElement[] => {
16
16
  const missing_pin_nums = (missing ?? []).filter((a) => typeof a === "number")
17
17
  const num_pins_missing = grid.x * grid.y - num_pins
18
18
 
19
+ if (missing.length === 0 && num_pins_missing > 0) {
20
+ // No missing pins specified, let's see if a squared center works
21
+ // if num_pins_missing is a square
22
+ if (Math.sqrt(num_pins_missing) % 1 === 0) {
23
+ missing.push("center")
24
+ } else if (num_pins_missing === 1) {
25
+ missing.push("topleft")
26
+ }
27
+ }
28
+
19
29
  if (missing?.includes("center")) {
20
30
  // Find the largest square that's square is less than
21
31
  // the number of missing pins
@@ -63,7 +73,10 @@ export const bga = (params: BgaDefInput): AnySoupElement[] => {
63
73
  const pad_x = (x - (grid.x - 1) / 2) * p
64
74
  const pad_y = -(y - (grid.y - 1) / 2) * p
65
75
 
66
- pads.push(rectpad(pin_num, pad_x, pad_y, pad, pad))
76
+ // TODO handle >26 rows
77
+ pads.push(
78
+ rectpad([pin_num, `${ALPHABET[y]}${x + 1}`], pad_x, pad_y, pad, pad)
79
+ )
67
80
  }
68
81
  }
69
82
 
package/src/fn/dip.ts CHANGED
@@ -2,15 +2,42 @@ import type { AnySoupElement } from "@tscircuit/soup"
2
2
  import { platedhole } from "../helpers/platedhole"
3
3
  import { z } from "zod"
4
4
  import { length } from "@tscircuit/soup"
5
+ import type { NowDefined } from "../helpers/zod/now-defined"
5
6
 
6
- const dip_def = z.object({
7
- dip: z.literal(true),
8
- num_pins: z.number(),
9
- w: length,
10
- p: length.optional(),
11
- id: length.optional(),
12
- od: length.optional(),
13
- })
7
+ const dip_def = z
8
+ .object({
9
+ dip: z.literal(true),
10
+ num_pins: z.number(),
11
+ wide: z.boolean().optional(),
12
+ narrow: z.boolean().optional(),
13
+ w: length.optional(),
14
+ p: length.default(length.parse("2.54mm")),
15
+ id: length.optional(),
16
+ od: length.optional(),
17
+ })
18
+ .transform((v) => {
19
+ // Default inner diameter and outer diameter
20
+ if (!v.id && !v.od) {
21
+ v.id = length.parse("1.0mm")
22
+ v.od = length.parse("1.2mm")
23
+ } else if (!v.id) {
24
+ v.id = v.od! * (1.0 / 1.2)
25
+ } else if (!v.od) {
26
+ v.od = v.id! * (1.2 / 1.0)
27
+ }
28
+
29
+ // Default width (TODO high pin counts should probably be wide?)
30
+ if (!v.w) {
31
+ if (v.wide) {
32
+ v.w = length.parse("600mil")
33
+ } else if (v.narrow) {
34
+ v.w = length.parse("300mil")
35
+ } else {
36
+ v.w = length.parse("300mil")
37
+ }
38
+ }
39
+ return v as NowDefined<typeof v, "w" | "p" | "id" | "od">
40
+ })
14
41
 
15
42
  export const getCcwDipCoords = (
16
43
  pinCount: number,
@@ -43,7 +70,7 @@ export const getCcwDipCoords = (
43
70
  /**
44
71
  * Returns the plated holes for a DIP package.
45
72
  */
46
- export const dip = (params: {
73
+ export const dip = (raw_params: {
47
74
  dip: true
48
75
  num_pins: number
49
76
  w: number
@@ -51,7 +78,7 @@ export const dip = (params: {
51
78
  id?: string | number
52
79
  od?: string | number
53
80
  }): AnySoupElement[] => {
54
- params = dip_def.parse(params)
81
+ const params = dip_def.parse(raw_params)
55
82
  const platedHoles: AnySoupElement[] = []
56
83
  for (let i = 0; i < params.num_pins; i++) {
57
84
  const { x, y } = getCcwDipCoords(
@@ -5,6 +5,7 @@ import { led } from "./fn/led"
5
5
  import { res } from "./fn/res"
6
6
  import { bga } from "./fn/bga"
7
7
  import type { AnySoupElement } from "@tscircuit/soup"
8
+ import { isNotNull } from "./helpers/is-not-null"
8
9
 
9
10
  export type FootprinterParamsBuilder<K extends string> = {
10
11
  [P in K | "params" | "soup"]: P extends "params" | "soup"
@@ -23,7 +24,9 @@ type CommonPassiveOptionKey =
23
24
  | "h"
24
25
 
25
26
  export type Footprinter = {
26
- dip: (num_pins: number) => FootprinterParamsBuilder<"w" | "p" | "id" | "od">
27
+ dip: (
28
+ num_pins: number
29
+ ) => FootprinterParamsBuilder<"w" | "p" | "id" | "od" | "wide" | "narrow">
27
30
  cap: () => FootprinterParamsBuilder<CommonPassiveOptionKey>
28
31
  res: () => FootprinterParamsBuilder<CommonPassiveOptionKey>
29
32
  diode: () => FootprinterParamsBuilder<CommonPassiveOptionKey>
@@ -46,11 +49,15 @@ export type Footprinter = {
46
49
  export const string = (def: string): Footprinter => {
47
50
  let fp = footprinter()
48
51
 
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
- })
52
+ const def_parts = def
53
+ .split("_")
54
+ .map((s) => {
55
+ const m = s.match(/([a-z]+)([\(\d\.\+\?].*)?/)
56
+ const [_, fn, v] = m ?? []
57
+ if (v?.includes("?")) return null
58
+ return { fn: m?.[1]!, v: m?.[2]! }
59
+ })
60
+ .filter(isNotNull)
54
61
 
55
62
  for (const { fn, v } of def_parts) {
56
63
  fp = fp[fn](v)
@@ -0,0 +1,3 @@
1
+ export function isNotNull<T>(value: T | null): value is T {
2
+ return value !== null
3
+ }
@@ -1,6 +1,6 @@
1
1
  import type { PCBSMTPad } from "@tscircuit/soup"
2
2
  export const rectpad = (
3
- pn: number,
3
+ pn: number | Array<string | number>,
4
4
  x: number,
5
5
  y: number,
6
6
  w: number,
@@ -15,6 +15,8 @@ export const rectpad = (
15
15
  layer: "top",
16
16
  shape: "rect",
17
17
  pcb_smtpad_id: "",
18
- port_hints: [pn.toString()],
18
+ port_hints: Array.isArray(pn)
19
+ ? pn.map((item) => item.toString())
20
+ : [pn.toString()],
19
21
  }
20
22
  }
@@ -0,0 +1 @@
1
+ export const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@@ -2,16 +2,16 @@ import { z } from "zod"
2
2
  import { length, distance } from "@tscircuit/soup"
3
3
  import { dim2d } from "./dim-2d"
4
4
  import { function_call } from "./function-call"
5
-
6
- const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
5
+ import type { NowDefined } from "./now-defined"
6
+ import { ALPHABET } from "./ALPHABET"
7
7
 
8
8
  export const bga_def = z
9
9
  .object({
10
10
  num_pins: z.number(),
11
- grid: dim2d,
12
- p: distance,
13
- w: length,
14
- h: length,
11
+ grid: dim2d.optional(),
12
+ p: distance.default("0.8mm"),
13
+ w: length.optional(),
14
+ h: length.optional(),
15
15
  ball: length.optional().describe("ball diameter"),
16
16
  pad: length.optional().describe("pad width/height"),
17
17
 
@@ -20,7 +20,7 @@ export const bga_def = z
20
20
  trorigin: z.boolean().optional(),
21
21
  brorigin: z.boolean().optional(),
22
22
 
23
- missing: function_call,
23
+ missing: function_call.default([]),
24
24
  })
25
25
  .transform((a) => {
26
26
  let origin: "tl" | "bl" | "tr" | "br" = "tl"
@@ -28,6 +28,12 @@ export const bga_def = z
28
28
  if (a.trorigin) origin = "tr"
29
29
  if (a.brorigin) origin = "br"
30
30
 
31
+ if (!a.grid) {
32
+ // find the largest square for the number of pins
33
+ const largest_square = Math.ceil(Math.sqrt(a.num_pins))
34
+ a.grid = { x: largest_square, y: largest_square }
35
+ }
36
+
31
37
  if (a.missing) {
32
38
  a.missing = a.missing.map((s) => {
33
39
  if (typeof s === "number") return s
@@ -35,13 +41,15 @@ export const bga_def = z
35
41
  if (s === "topleft") return "topleft"
36
42
  const m = s.match(/([A-Z]+)(\d+)/)
37
43
  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
44
+ let Y = ALPHABET.indexOf(m[1]!)
45
+ let X = parseInt(m[2]!) - 1
46
+ return Y * a.grid!.x + X + 1
41
47
  })
42
48
  }
43
49
 
44
- return { ...a, origin }
50
+ const new_def = { ...a, origin }
51
+
52
+ return new_def as NowDefined<typeof new_def, "w" | "h" | "grid">
45
53
  })
46
54
 
47
55
  export type BgaDefInput = z.input<typeof bga_def>
@@ -0,0 +1,2 @@
1
+ /* Define a utility type to make certain keys required */
2
+ export type NowDefined<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
@@ -10,5 +10,9 @@ export const getTestFixture = async (t: ExecutionContext) => {
10
10
  if (process.env.CI) return
11
11
  return logSoup(`footprinter: ${t.title}`, soup)
12
12
  },
13
+ logSoupWithPrefix: (prefix: string, soup: AnySoupElement[]) => {
14
+ if (process.env.CI) return
15
+ return logSoup(`footprinter: ${t.title} ${prefix}`, soup)
16
+ },
13
17
  }
14
18
  }
@@ -2,17 +2,45 @@ import test from "ava"
2
2
  import { getTestFixture } from "../fixtures"
3
3
  import type { AnySoupElement } from "@tscircuit/soup"
4
4
 
5
- export const SLOP_LIST = ["dip3"]
5
+ /**
6
+ * Slop is an underdefined definition.
7
+ */
8
+ export const SLOP_LIST = [
9
+ "dip3",
10
+ "bga64",
11
+ "bga48",
12
+ "bga48_grid8x8",
13
+ "bga48_p2_pad0.2",
14
+ ]
6
15
 
7
16
  test("slop1", async (t) => {
8
- const { fp, logSoup } = await getTestFixture(t)
17
+ const { fp, logSoupWithPrefix } = await getTestFixture(t)
9
18
 
10
19
  const soups: AnySoupElement[][] = []
20
+ const failures: Array<{
21
+ slop_string: string
22
+ error: any
23
+ }> = []
11
24
 
12
25
  for (const slop of SLOP_LIST) {
13
- const soup = fp.string(slop).soup()
14
- soups.push(soup)
26
+ try {
27
+ const soup = fp.string(slop).soup()
28
+ soups.push(soup)
29
+ if (slop === SLOP_LIST[SLOP_LIST.length - 1]) {
30
+ await logSoupWithPrefix(slop, soup)
31
+ }
32
+ } catch (e: any) {
33
+ failures.push({
34
+ slop_string: slop,
35
+ error: e,
36
+ })
37
+ throw e
38
+ }
15
39
  }
16
40
 
17
- await logSoup(soups[0]!)
41
+ if (failures.length > 0) {
42
+ t.fail(`Failures:\n${failures.map((f) => f.slop_string).join("\n")}`)
43
+ } else {
44
+ t.pass()
45
+ }
18
46
  })