@tscircuit/parts-engine 0.0.9 → 0.0.11
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/lib/footprint-translators/get-footprinter-string-from-kicad.ts +21 -0
- package/lib/footprint-translators/get-jlc-package-from-footprinter-string.ts +12 -0
- package/lib/footprint-translators/index.ts +25 -0
- package/lib/jlc-parts-engine.ts +68 -33
- package/package.json +1 -1
- package/tests/jlc-parts-engine.test.ts +78 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transforms a KiCad footprint string into a generic "footprinter string".
|
|
3
|
+
* For now, this is a simplified conversion to a standard package name.
|
|
4
|
+
* e.g. "kicad:Resistor_SMD:R_0603_1608Metric" -> "0603"
|
|
5
|
+
*/
|
|
6
|
+
export const getFootprinterStringFromKicad = (
|
|
7
|
+
kicadFootprint: string,
|
|
8
|
+
): string | undefined => {
|
|
9
|
+
// kicad:Resistor_SMD:R_0603_1608Metric -> 0603
|
|
10
|
+
let match = kicadFootprint.match(/:[RC]_(\d{4})_/)
|
|
11
|
+
if (match) return match[1]
|
|
12
|
+
|
|
13
|
+
// kicad:Package_SO:SOIC-8_3.9x4.9mm_P1.27mm -> SOIC-8
|
|
14
|
+
// kicad:Package_TO_SOT_SMD:SOT-23 -> SOT-23
|
|
15
|
+
match = kicadFootprint.match(
|
|
16
|
+
/:(SOIC-\d+|SOT-\d+|SOD-\d+|SSOP-\d+|TSSOP-\d+|QFP-\d+|QFN-\d+)/,
|
|
17
|
+
)
|
|
18
|
+
if (match) return match[1]
|
|
19
|
+
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transforms a generic "footprinter string" into a JLC-compatible package name.
|
|
3
|
+
* e.g. "cap0603" -> "0603"
|
|
4
|
+
*/
|
|
5
|
+
export const getJlcPackageFromFootprinterString = (
|
|
6
|
+
footprinterString: string,
|
|
7
|
+
): string => {
|
|
8
|
+
if (footprinterString.includes("cap")) {
|
|
9
|
+
return footprinterString.replace(/cap/g, "")
|
|
10
|
+
}
|
|
11
|
+
return footprinterString
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getFootprinterStringFromKicad } from "./get-footprinter-string-from-kicad"
|
|
2
|
+
import { getJlcPackageFromFootprinterString } from "./get-jlc-package-from-footprinter-string"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get a JLC-compatible package name from a footprint string, which could be
|
|
6
|
+
* a KiCad footprint or a generic "footprinter string".
|
|
7
|
+
*/
|
|
8
|
+
export const getJlcpcbPackageName = (
|
|
9
|
+
footprint: string | undefined,
|
|
10
|
+
): string | undefined => {
|
|
11
|
+
if (!footprint) return undefined
|
|
12
|
+
|
|
13
|
+
if (footprint.startsWith("kicad:")) {
|
|
14
|
+
const footprinterString = getFootprinterStringFromKicad(footprint)
|
|
15
|
+
if (footprinterString) {
|
|
16
|
+
return getJlcPackageFromFootprinterString(footprinterString)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fallback for un-matched KiCad strings
|
|
20
|
+
return footprint
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Not a KiCad string, assume it's a footprinter string
|
|
24
|
+
return getJlcPackageFromFootprinterString(footprint)
|
|
25
|
+
}
|
package/lib/jlc-parts-engine.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PartsEngine, SupplierPartNumbers } from "@tscircuit/props"
|
|
2
|
+
import { getJlcpcbPackageName } from "./footprint-translators/index"
|
|
2
3
|
|
|
3
|
-
const cache = new Map<string, any>()
|
|
4
|
+
export const cache = new Map<string, any>()
|
|
4
5
|
|
|
5
6
|
const getJlcPartsCached = async (name: any, params: any) => {
|
|
6
7
|
const paramString = new URLSearchParams({
|
|
@@ -18,37 +19,47 @@ const getJlcPartsCached = async (name: any, params: any) => {
|
|
|
18
19
|
return responseJson
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
const withBasicPartPreference = (parts: any[] | undefined) => {
|
|
23
|
+
if (!parts) return []
|
|
24
|
+
return [...parts].sort(
|
|
25
|
+
(a, b) => Number(b.is_basic ?? false) - Number(a.is_basic ?? false),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export const jlcPartsEngine: PartsEngine = {
|
|
22
30
|
findPart: async ({
|
|
23
31
|
sourceComponent,
|
|
24
32
|
footprinterString,
|
|
25
33
|
}): Promise<SupplierPartNumbers> => {
|
|
34
|
+
const jlcpcbPackage = getJlcpcbPackageName(footprinterString)
|
|
35
|
+
|
|
26
36
|
if (
|
|
27
37
|
sourceComponent.type === "source_component" &&
|
|
28
38
|
sourceComponent.ftype === "simple_resistor"
|
|
29
39
|
) {
|
|
30
40
|
const { resistors } = await getJlcPartsCached("resistors", {
|
|
31
41
|
resistance: sourceComponent.resistance,
|
|
32
|
-
package:
|
|
42
|
+
package: jlcpcbPackage,
|
|
33
43
|
})
|
|
34
44
|
|
|
35
45
|
return {
|
|
36
|
-
jlcpcb: (resistors
|
|
46
|
+
jlcpcb: withBasicPartPreference(resistors)
|
|
47
|
+
.map((r: any) => `C${r.lcsc}`)
|
|
48
|
+
.slice(0, 3),
|
|
37
49
|
}
|
|
38
50
|
} else if (
|
|
39
51
|
sourceComponent.type === "source_component" &&
|
|
40
52
|
sourceComponent.ftype === "simple_capacitor"
|
|
41
53
|
) {
|
|
42
|
-
if (footprinterString?.includes("cap")) {
|
|
43
|
-
footprinterString = footprinterString.replace("cap", "")
|
|
44
|
-
}
|
|
45
54
|
const { capacitors } = await getJlcPartsCached("capacitors", {
|
|
46
55
|
capacitance: sourceComponent.capacitance,
|
|
47
|
-
package:
|
|
56
|
+
package: jlcpcbPackage,
|
|
48
57
|
})
|
|
49
58
|
|
|
50
59
|
return {
|
|
51
|
-
jlcpcb: (capacitors
|
|
60
|
+
jlcpcb: withBasicPartPreference(capacitors)
|
|
61
|
+
.map((c: any) => `C${c.lcsc}`)
|
|
62
|
+
.slice(0, 3),
|
|
52
63
|
}
|
|
53
64
|
} else if (
|
|
54
65
|
sourceComponent.type === "source_component" &&
|
|
@@ -72,7 +83,9 @@ export const jlcPartsEngine: PartsEngine = {
|
|
|
72
83
|
},
|
|
73
84
|
)
|
|
74
85
|
return {
|
|
75
|
-
jlcpcb: (headers
|
|
86
|
+
jlcpcb: withBasicPartPreference(headers)
|
|
87
|
+
.map((h: any) => `C${h.lcsc}`)
|
|
88
|
+
.slice(0, 3),
|
|
76
89
|
}
|
|
77
90
|
} else if (
|
|
78
91
|
sourceComponent.type === "source_component" &&
|
|
@@ -80,10 +93,10 @@ export const jlcPartsEngine: PartsEngine = {
|
|
|
80
93
|
) {
|
|
81
94
|
const { potentiometers } = await getJlcPartsCached("potentiometers", {
|
|
82
95
|
resistance: sourceComponent.max_resistance,
|
|
83
|
-
package:
|
|
96
|
+
package: jlcpcbPackage,
|
|
84
97
|
})
|
|
85
98
|
return {
|
|
86
|
-
jlcpcb: (potentiometers
|
|
99
|
+
jlcpcb: withBasicPartPreference(potentiometers)
|
|
87
100
|
.map((p: any) => `C${p.lcsc}`)
|
|
88
101
|
.slice(0, 3),
|
|
89
102
|
}
|
|
@@ -92,31 +105,37 @@ export const jlcPartsEngine: PartsEngine = {
|
|
|
92
105
|
sourceComponent.ftype === "simple_diode"
|
|
93
106
|
) {
|
|
94
107
|
const { diodes } = await getJlcPartsCached("diodes", {
|
|
95
|
-
package:
|
|
108
|
+
package: jlcpcbPackage,
|
|
96
109
|
})
|
|
97
110
|
return {
|
|
98
|
-
jlcpcb: (diodes
|
|
111
|
+
jlcpcb: withBasicPartPreference(diodes)
|
|
112
|
+
.map((d: any) => `C${d.lcsc}`)
|
|
113
|
+
.slice(0, 3),
|
|
99
114
|
}
|
|
100
115
|
} else if (
|
|
101
116
|
sourceComponent.type === "source_component" &&
|
|
102
117
|
sourceComponent.ftype === "simple_chip"
|
|
103
118
|
) {
|
|
104
119
|
const { chips } = await getJlcPartsCached("chips", {
|
|
105
|
-
package:
|
|
120
|
+
package: jlcpcbPackage,
|
|
106
121
|
})
|
|
107
122
|
return {
|
|
108
|
-
jlcpcb: (chips
|
|
123
|
+
jlcpcb: withBasicPartPreference(chips)
|
|
124
|
+
.map((c: any) => `C${c.lcsc}`)
|
|
125
|
+
.slice(0, 3),
|
|
109
126
|
}
|
|
110
127
|
} else if (
|
|
111
128
|
sourceComponent.type === "source_component" &&
|
|
112
129
|
sourceComponent.ftype === "simple_transistor"
|
|
113
130
|
) {
|
|
114
131
|
const { transistors } = await getJlcPartsCached("transistors", {
|
|
115
|
-
package:
|
|
132
|
+
package: jlcpcbPackage,
|
|
116
133
|
transistor_type: sourceComponent.transistor_type,
|
|
117
134
|
})
|
|
118
135
|
return {
|
|
119
|
-
jlcpcb: (transistors
|
|
136
|
+
jlcpcb: withBasicPartPreference(transistors)
|
|
137
|
+
.map((t: any) => `C${t.lcsc}`)
|
|
138
|
+
.slice(0, 3),
|
|
120
139
|
}
|
|
121
140
|
} else if (
|
|
122
141
|
sourceComponent.type === "source_component" &&
|
|
@@ -124,10 +143,12 @@ export const jlcPartsEngine: PartsEngine = {
|
|
|
124
143
|
) {
|
|
125
144
|
const { power_sources } = await getJlcPartsCached("power_sources", {
|
|
126
145
|
voltage: sourceComponent.voltage,
|
|
127
|
-
package:
|
|
146
|
+
package: jlcpcbPackage,
|
|
128
147
|
})
|
|
129
148
|
return {
|
|
130
|
-
jlcpcb: (power_sources
|
|
149
|
+
jlcpcb: withBasicPartPreference(power_sources)
|
|
150
|
+
.map((p: any) => `C${p.lcsc}`)
|
|
151
|
+
.slice(0, 3),
|
|
131
152
|
}
|
|
132
153
|
} else if (
|
|
133
154
|
sourceComponent.type === "source_component" &&
|
|
@@ -135,10 +156,12 @@ export const jlcPartsEngine: PartsEngine = {
|
|
|
135
156
|
) {
|
|
136
157
|
const { inductors } = await getJlcPartsCached("inductors", {
|
|
137
158
|
inductance: sourceComponent.inductance,
|
|
138
|
-
package:
|
|
159
|
+
package: jlcpcbPackage,
|
|
139
160
|
})
|
|
140
161
|
return {
|
|
141
|
-
jlcpcb: (inductors
|
|
162
|
+
jlcpcb: withBasicPartPreference(inductors)
|
|
163
|
+
.map((i: any) => `C${i.lcsc}`)
|
|
164
|
+
.slice(0, 3),
|
|
142
165
|
}
|
|
143
166
|
} else if (
|
|
144
167
|
sourceComponent.type === "source_component" &&
|
|
@@ -147,22 +170,26 @@ export const jlcPartsEngine: PartsEngine = {
|
|
|
147
170
|
const { crystals } = await getJlcPartsCached("crystals", {
|
|
148
171
|
frequency: sourceComponent.frequency,
|
|
149
172
|
load_capacitance: sourceComponent.load_capacitance,
|
|
150
|
-
package:
|
|
173
|
+
package: jlcpcbPackage,
|
|
151
174
|
})
|
|
152
175
|
return {
|
|
153
|
-
jlcpcb: (crystals
|
|
176
|
+
jlcpcb: withBasicPartPreference(crystals)
|
|
177
|
+
.map((c: any) => `C${c.lcsc}`)
|
|
178
|
+
.slice(0, 3),
|
|
154
179
|
}
|
|
155
180
|
} else if (
|
|
156
181
|
sourceComponent.type === "source_component" &&
|
|
157
182
|
sourceComponent.ftype === "simple_mosfet"
|
|
158
183
|
) {
|
|
159
184
|
const { mosfets } = await getJlcPartsCached("mosfets", {
|
|
160
|
-
package:
|
|
185
|
+
package: jlcpcbPackage,
|
|
161
186
|
mosfet_mode: sourceComponent.mosfet_mode,
|
|
162
187
|
channel_type: sourceComponent.channel_type,
|
|
163
188
|
})
|
|
164
189
|
return {
|
|
165
|
-
jlcpcb: (mosfets
|
|
190
|
+
jlcpcb: withBasicPartPreference(mosfets)
|
|
191
|
+
.map((m: any) => `C${m.lcsc}`)
|
|
192
|
+
.slice(0, 3),
|
|
166
193
|
}
|
|
167
194
|
} else if (
|
|
168
195
|
sourceComponent.type === "source_component" &&
|
|
@@ -170,10 +197,12 @@ export const jlcPartsEngine: PartsEngine = {
|
|
|
170
197
|
) {
|
|
171
198
|
const { resonators } = await getJlcPartsCached("resonators", {
|
|
172
199
|
frequency: sourceComponent.frequency,
|
|
173
|
-
package:
|
|
200
|
+
package: jlcpcbPackage,
|
|
174
201
|
})
|
|
175
202
|
return {
|
|
176
|
-
jlcpcb: (resonators
|
|
203
|
+
jlcpcb: withBasicPartPreference(resonators)
|
|
204
|
+
.map((r: any) => `C${r.lcsc}`)
|
|
205
|
+
.slice(0, 3),
|
|
177
206
|
}
|
|
178
207
|
} else if (
|
|
179
208
|
sourceComponent.type === "source_component" &&
|
|
@@ -181,30 +210,36 @@ export const jlcPartsEngine: PartsEngine = {
|
|
|
181
210
|
) {
|
|
182
211
|
const { switches } = await getJlcPartsCached("switches", {
|
|
183
212
|
switch_type: sourceComponent.type,
|
|
184
|
-
package:
|
|
213
|
+
package: jlcpcbPackage,
|
|
185
214
|
})
|
|
186
215
|
return {
|
|
187
|
-
jlcpcb: (switches
|
|
216
|
+
jlcpcb: withBasicPartPreference(switches)
|
|
217
|
+
.map((s: any) => `C${s.lcsc}`)
|
|
218
|
+
.slice(0, 3),
|
|
188
219
|
}
|
|
189
220
|
} else if (
|
|
190
221
|
sourceComponent.type === "source_component" &&
|
|
191
222
|
sourceComponent.ftype === "simple_led"
|
|
192
223
|
) {
|
|
193
224
|
const { leds } = await getJlcPartsCached("leds", {
|
|
194
|
-
package:
|
|
225
|
+
package: jlcpcbPackage,
|
|
195
226
|
})
|
|
196
227
|
return {
|
|
197
|
-
jlcpcb: (leds
|
|
228
|
+
jlcpcb: withBasicPartPreference(leds)
|
|
229
|
+
.map((l: any) => `C${l.lcsc}`)
|
|
230
|
+
.slice(0, 3),
|
|
198
231
|
}
|
|
199
232
|
} else if (
|
|
200
233
|
sourceComponent.type === "source_component" &&
|
|
201
234
|
sourceComponent.ftype === "simple_fuse"
|
|
202
235
|
) {
|
|
203
236
|
const { fuses } = await getJlcPartsCached("fuses", {
|
|
204
|
-
package:
|
|
237
|
+
package: jlcpcbPackage,
|
|
205
238
|
})
|
|
206
239
|
return {
|
|
207
|
-
jlcpcb: (fuses
|
|
240
|
+
jlcpcb: withBasicPartPreference(fuses)
|
|
241
|
+
.map((l: any) => `C${l.lcsc}`)
|
|
242
|
+
.slice(0, 3),
|
|
208
243
|
}
|
|
209
244
|
}
|
|
210
245
|
return {}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach } from "bun:test"
|
|
2
|
-
import { jlcPartsEngine } from "../lib/jlc-parts-engine"
|
|
2
|
+
import { jlcPartsEngine, cache } from "../lib/jlc-parts-engine"
|
|
3
3
|
import type { AnySourceComponent } from "circuit-json"
|
|
4
4
|
describe("jlcPartsEngine", () => {
|
|
5
5
|
beforeEach(() => {
|
|
6
|
+
cache.clear()
|
|
6
7
|
// Reset fetch fake between tests
|
|
7
8
|
globalThis.fetch = (async (url: string) => {
|
|
8
9
|
if (url.includes("/resistors/")) {
|
|
@@ -127,6 +128,25 @@ describe("jlcPartsEngine", () => {
|
|
|
127
128
|
})
|
|
128
129
|
})
|
|
129
130
|
|
|
131
|
+
test("should find resistor parts with kicad footprint", async () => {
|
|
132
|
+
const resistor: AnySourceComponent = {
|
|
133
|
+
type: "source_component",
|
|
134
|
+
ftype: "simple_resistor",
|
|
135
|
+
resistance: 10000,
|
|
136
|
+
source_component_id: "source_component_0",
|
|
137
|
+
name: "R1",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const result = await jlcPartsEngine.findPart({
|
|
141
|
+
sourceComponent: resistor,
|
|
142
|
+
footprinterString: "kicad:Resistor_SMD:R_0603_1608Metric",
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(result).toEqual({
|
|
146
|
+
jlcpcb: ["C1234", "C5678", "C9012"],
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
130
150
|
test("should find capacitor parts", async () => {
|
|
131
151
|
const capacitor: AnySourceComponent = {
|
|
132
152
|
type: "source_component",
|
|
@@ -138,7 +158,7 @@ describe("jlcPartsEngine", () => {
|
|
|
138
158
|
|
|
139
159
|
const result = await jlcPartsEngine.findPart({
|
|
140
160
|
sourceComponent: capacitor,
|
|
141
|
-
footprinterString: "
|
|
161
|
+
footprinterString: "cap0603",
|
|
142
162
|
})
|
|
143
163
|
|
|
144
164
|
expect(result).toEqual({
|
|
@@ -222,6 +242,24 @@ describe("jlcPartsEngine", () => {
|
|
|
222
242
|
})
|
|
223
243
|
})
|
|
224
244
|
|
|
245
|
+
test("should find chip parts with kicad footprint", async () => {
|
|
246
|
+
const chip: AnySourceComponent = {
|
|
247
|
+
type: "source_component",
|
|
248
|
+
ftype: "simple_chip",
|
|
249
|
+
source_component_id: "source_component_0",
|
|
250
|
+
name: "U1",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const result = await jlcPartsEngine.findPart({
|
|
254
|
+
sourceComponent: chip,
|
|
255
|
+
footprinterString: "kicad:Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
expect(result).toEqual({
|
|
259
|
+
jlcpcb: ["C5678", "C9012", "C3456"],
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
225
263
|
test("should find transistor parts", async () => {
|
|
226
264
|
const transistor: AnySourceComponent = {
|
|
227
265
|
type: "source_component",
|
|
@@ -394,4 +432,42 @@ describe("jlcPartsEngine", () => {
|
|
|
394
432
|
|
|
395
433
|
expect(result).toEqual({ jlcpcb: [] })
|
|
396
434
|
})
|
|
435
|
+
|
|
436
|
+
test("should prefer basic parts when available", async () => {
|
|
437
|
+
// Override the global fetch mock for this specific test
|
|
438
|
+
globalThis.fetch = (async (url: string) => {
|
|
439
|
+
if (url.includes("/resistors/")) {
|
|
440
|
+
return {
|
|
441
|
+
json: async () => ({
|
|
442
|
+
resistors: [
|
|
443
|
+
{ lcsc: "1111" }, // is_basic is undefined, treated as false
|
|
444
|
+
{ lcsc: "2222", is_basic: true },
|
|
445
|
+
{ lcsc: "3333", is_basic: false },
|
|
446
|
+
{ lcsc: "4444", is_basic: true },
|
|
447
|
+
{ lcsc: "5555" },
|
|
448
|
+
],
|
|
449
|
+
}),
|
|
450
|
+
} as Response
|
|
451
|
+
}
|
|
452
|
+
return {} as Response
|
|
453
|
+
}) as unknown as typeof fetch
|
|
454
|
+
|
|
455
|
+
const resistor: AnySourceComponent = {
|
|
456
|
+
type: "source_component",
|
|
457
|
+
ftype: "simple_resistor",
|
|
458
|
+
resistance: 10000,
|
|
459
|
+
source_component_id: "source_component_0",
|
|
460
|
+
name: "R1",
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const result = await jlcPartsEngine.findPart({
|
|
464
|
+
sourceComponent: resistor,
|
|
465
|
+
footprinterString: "0603",
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
// Expecting basic parts to be prioritized
|
|
469
|
+
expect(result).toEqual({
|
|
470
|
+
jlcpcb: ["C2222", "C4444", "C1111"],
|
|
471
|
+
})
|
|
472
|
+
})
|
|
397
473
|
})
|