Skip to content

Commit 239db37

Browse files
Support standard=usb_c for connector using parts-engine
1 parent 4ed40bc commit 239db37

10 files changed

+421
-8
lines changed

lib/components/base-components/NormalComponent/NormalComponent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1551,7 +1551,7 @@ export class NormalComponent<
15511551
})
15521552
}
15531553

1554-
private async _getSupplierPartNumbers(
1554+
protected async _getSupplierPartNumbers(
15551555
partsEngine: any,
15561556
source_component: any,
15571557
footprinterString: string | undefined,

lib/components/normal-components/Connector.ts

Lines changed: 151 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { guessCableInsertCenter } from "@tscircuit/infer-cable-insertion-point"
2-
import { chipProps } from "@tscircuit/props"
3-
import type { SourceSimpleConnector } from "circuit-json"
2+
import { chipProps, type ConnectorProps } from "@tscircuit/props"
3+
import type { AnyCircuitElement, SourceSimpleConnector } from "circuit-json"
4+
import { unknown_error_finding_part } from "circuit-json"
5+
import { createComponentsFromCircuitJson } from "lib/utils/createComponentsFromCircuitJson"
46
import { Chip } from "./Chip"
57

68
export class Connector<
@@ -17,19 +19,164 @@ export class Connector<
1719
doInitialSourceRender(): void {
1820
const { db } = this.root!
1921
const { _parsedProps: props } = this
22+
const connectorProps = this.props as ConnectorProps
2023

2124
const source_component = db.source_component.insert({
2225
ftype: "simple_connector",
2326
name: this.name,
24-
manufacturer_part_number: props.manufacturerPartNumber,
27+
manufacturer_part_number: props.manufacturerPartNumber ?? props.mfn,
2528
supplier_part_numbers: props.supplierPartNumbers,
2629
display_name: props.displayName,
27-
standard: (props as any).standard,
30+
standard: connectorProps.standard,
2831
} as SourceSimpleConnector)
2932

3033
this.source_component_id = source_component.source_component_id!
3134
}
3235

36+
private _isUsingStandardPartsEngineCircuitJsonFlow() {
37+
const connectorProps = this.props as ConnectorProps
38+
if (!connectorProps.standard) return false
39+
if (this.getInheritedProperty("partsEngineDisabled")) return false
40+
const partsEngine = this.getInheritedProperty("partsEngine")
41+
return Boolean(partsEngine?.fetchPartCircuitJson)
42+
}
43+
44+
doInitialPcbFootprintStringRender(): void {
45+
const { _parsedProps: props } = this
46+
const connectorProps = this.props as ConnectorProps
47+
const standard = connectorProps.standard
48+
49+
if (standard) {
50+
if (this._hasStartedFootprintUrlLoad) return
51+
if (this.getInheritedProperty("partsEngineDisabled")) {
52+
super.doInitialPcbFootprintStringRender()
53+
return
54+
}
55+
const partsEngine = this.getInheritedProperty("partsEngine")
56+
if (!partsEngine?.fetchPartCircuitJson) {
57+
super.doInitialPcbFootprintStringRender()
58+
return
59+
}
60+
61+
this._hasStartedFootprintUrlLoad = true
62+
63+
// source_component_id is not yet set (SourceRender runs after PcbFootprintStringRender),
64+
// but after the first `await` in the async effect all synchronous render phases
65+
// including SourceRender will have run, making source_component_id available.
66+
const sourceComponentForQuery = {
67+
ftype: "simple_connector",
68+
name: this.name,
69+
manufacturer_part_number: props.manufacturerPartNumber ?? props.mfn,
70+
standard,
71+
}
72+
73+
this._queueAsyncEffect(
74+
"load-standard-connector-circuit-json",
75+
async () => {
76+
const { db } = this.root!
77+
try {
78+
// Step 1: findPart → supplier part numbers
79+
const supplierPartNumbers = await this._getSupplierPartNumbers(
80+
partsEngine,
81+
sourceComponentForQuery,
82+
`standard:${standard}`,
83+
)
84+
85+
if (this.source_component_id) {
86+
db.source_component.update(this.source_component_id, {
87+
supplier_part_numbers: supplierPartNumbers,
88+
})
89+
}
90+
91+
// Step 2: fetchPartCircuitJson with first available supplier part number
92+
let circuitJson: AnyCircuitElement[] | null = null
93+
for (const supplier of Object.keys(supplierPartNumbers ?? {})) {
94+
const nums = supplierPartNumbers[supplier]
95+
if (Array.isArray(nums) && nums.length > 0) {
96+
const maybeCircuitJson =
97+
(await Promise.resolve(
98+
partsEngine.fetchPartCircuitJson({
99+
supplierPartNumber: nums[0],
100+
}),
101+
)) ?? null
102+
if (
103+
Array.isArray(maybeCircuitJson) &&
104+
maybeCircuitJson.length > 0
105+
) {
106+
circuitJson = maybeCircuitJson
107+
break
108+
}
109+
}
110+
}
111+
112+
// Fallback: manufacturer part number
113+
if (
114+
(!circuitJson || circuitJson.length === 0) &&
115+
sourceComponentForQuery.manufacturer_part_number
116+
) {
117+
const maybeCircuitJson =
118+
(await Promise.resolve(
119+
partsEngine.fetchPartCircuitJson({
120+
manufacturerPartNumber:
121+
sourceComponentForQuery.manufacturer_part_number,
122+
}),
123+
)) ?? null
124+
if (
125+
Array.isArray(maybeCircuitJson) &&
126+
maybeCircuitJson.length > 0
127+
) {
128+
circuitJson = maybeCircuitJson
129+
}
130+
}
131+
132+
if (circuitJson && circuitJson.length > 0) {
133+
const fpComponents = createComponentsFromCircuitJson(
134+
{
135+
componentName: this.name,
136+
componentRotation: String(props.pcbRotation ?? 0),
137+
footprinterString: `standard:${standard}`,
138+
pinLabels: props.pinLabels,
139+
pcbPinLabels: props.pcbPinLabels,
140+
},
141+
circuitJson,
142+
)
143+
this.addAll(fpComponents)
144+
this._markDirty("InitializePortsFromChildren")
145+
}
146+
} catch (error: any) {
147+
if (this.source_component_id) {
148+
db.source_component.update(this.source_component_id, {
149+
supplier_part_numbers: {},
150+
})
151+
}
152+
const errorObj = unknown_error_finding_part.parse({
153+
type: "unknown_error_finding_part",
154+
message: `Failed to fetch circuit JSON for ${this.getString()} (standard="${standard}"): ${error.message}`,
155+
source_component_id: this.source_component_id ?? undefined,
156+
subcircuit_id: this.getSubcircuit()?.subcircuit_id,
157+
})
158+
db.unknown_error_finding_part.insert(errorObj)
159+
}
160+
},
161+
)
162+
return
163+
}
164+
165+
super.doInitialPcbFootprintStringRender()
166+
}
167+
168+
doInitialPartsEngineRender(): void {
169+
// For standard connectors, supplier part numbers are already resolved
170+
// during PcbFootprintStringRender via findPart + fetchPartCircuitJson
171+
if (this._isUsingStandardPartsEngineCircuitJsonFlow()) return
172+
super.doInitialPartsEngineRender()
173+
}
174+
175+
updatePartsEngineRender(): void {
176+
if (this._isUsingStandardPartsEngineCircuitJsonFlow()) return
177+
super.updatePartsEngineRender()
178+
}
179+
33180
doInitialPcbComponentSizeCalculation(): void {
34181
super.doInitialPcbComponentSizeCalculation()
35182
if (this.root?.pcbDisabled) return

lib/fiber/intrinsic-jsx.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ export interface TscircuitElements {
7575
constraint: Props.ConstraintProps
7676
constrainedlayout: Props.ConstrainedLayoutProps
7777
battery: Props.BatteryProps
78-
// TODO use ConnectorProps once it gets merged in @tscircuit/props
79-
connector: Props.ChipProps
78+
connector: Props.ConnectorProps
8079
pinheader: Props.PinHeaderProps
8180
resonator: Props.ResonatorProps
8281
subcircuit: Props.SubcircuitGroupProps

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"@tscircuit/math-utils": "^0.0.36",
4747
"@tscircuit/miniflex": "^0.0.4",
4848
"@tscircuit/ngspice-spice-engine": "^0.0.8",
49-
"@tscircuit/props": "^0.0.499",
49+
"@tscircuit/props": "^0.0.500",
5050
"@tscircuit/schematic-match-adapt": "^0.0.16",
5151
"@tscircuit/schematic-trace-solver": "^v0.0.45",
5252
"@tscircuit/solver-utils": "^0.0.3",
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { test, expect } from "bun:test"
2+
import type { PartsEngine } from "@tscircuit/props"
3+
import { getTestFixture } from "tests/fixtures/get-test-fixture"
4+
5+
test("connector usb_c leaves footprint empty when supplier and manufacturer circuit json are not found", async () => {
6+
const { circuit } = getTestFixture()
7+
const calls: Array<{
8+
supplierPartNumber?: string
9+
manufacturerPartNumber?: string
10+
}> = []
11+
12+
const mockPartsEngine: PartsEngine = {
13+
findPart: async () => ({ jlcpcb: ["C165948"] }),
14+
fetchPartCircuitJson: async ({
15+
supplierPartNumber,
16+
manufacturerPartNumber,
17+
}: {
18+
supplierPartNumber?: string
19+
manufacturerPartNumber?: string
20+
}) => {
21+
calls.push({ supplierPartNumber, manufacturerPartNumber })
22+
return undefined
23+
},
24+
}
25+
26+
circuit.add(
27+
<board partsEngine={mockPartsEngine} width="20mm" height="20mm">
28+
<connector
29+
name="USB1"
30+
standard="usb_c"
31+
manufacturerPartNumber="USB4135-GF-A"
32+
/>
33+
</board>,
34+
)
35+
36+
await circuit.renderUntilSettled()
37+
38+
expect(calls).toEqual([
39+
{ supplierPartNumber: "C165948", manufacturerPartNumber: undefined },
40+
{ supplierPartNumber: undefined, manufacturerPartNumber: "USB4135-GF-A" },
41+
])
42+
43+
const sourceComponent = circuit.db.source_component
44+
.list()
45+
.find((c: any) => c.name === "USB1")
46+
expect(sourceComponent).toBeTruthy()
47+
expect(sourceComponent!.supplier_part_numbers).toEqual({
48+
jlcpcb: ["C165948"],
49+
})
50+
51+
expect(circuit.db.pcb_smtpad.list().length).toBe(0)
52+
expect(circuit.db.unknown_error_finding_part.list().length).toBe(0)
53+
})
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { test, expect } from "bun:test"
2+
import type { PartsEngine } from "@tscircuit/props"
3+
import { getTestFixture } from "tests/fixtures/get-test-fixture"
4+
5+
test("connector with standard='usb_c' handles findPart returning 'Not found' without creating footprint", async () => {
6+
const { circuit } = getTestFixture()
7+
let fetchCalls = 0
8+
9+
const mockPartsEngine: PartsEngine = {
10+
findPart: async () => "Not found",
11+
fetchPartCircuitJson: async () => {
12+
fetchCalls++
13+
return undefined
14+
},
15+
} as any
16+
17+
circuit.add(
18+
<board partsEngine={mockPartsEngine} width="20mm" height="20mm">
19+
<connector name="USB1" standard="usb_c" />
20+
</board>,
21+
)
22+
23+
await circuit.renderUntilSettled()
24+
25+
const sourceComponent = circuit.db.source_component
26+
.list()
27+
.find((c: any) => c.name === "USB1")
28+
expect(sourceComponent).toBeTruthy()
29+
expect(sourceComponent!.supplier_part_numbers).toEqual({})
30+
31+
expect(fetchCalls).toBe(0)
32+
expect(circuit.db.pcb_smtpad.list().length).toBe(0)
33+
expect(circuit.db.unknown_error_finding_part.list().length).toBe(0)
34+
})
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { test, expect } from "bun:test"
2+
import type { PartsEngine } from "@tscircuit/props"
3+
import type { AnyCircuitElement } from "circuit-json"
4+
import { getTestFixture } from "tests/fixtures/get-test-fixture"
5+
6+
/**
7+
* When findPart returns no supplier part numbers, core falls back to calling
8+
* fetchPartCircuitJson with the manufacturerPartNumber directly.
9+
*/
10+
test("connector usb_c falls back to manufacturerPartNumber when findPart returns no supplier parts", async () => {
11+
const { circuit } = getTestFixture()
12+
13+
const mockCircuitJson = [
14+
{
15+
type: "pcb_smtpad",
16+
x: 0,
17+
y: 0,
18+
width: 0.6,
19+
height: 0.6,
20+
shape: "rect",
21+
layer: "top",
22+
port_hints: ["1"],
23+
},
24+
{
25+
type: "pcb_smtpad",
26+
x: 1,
27+
y: 0,
28+
width: 0.6,
29+
height: 0.6,
30+
shape: "rect",
31+
layer: "top",
32+
port_hints: ["2"],
33+
},
34+
{
35+
type: "pcb_silkscreen_text",
36+
pcb_silkscreen_text_id: "silk1",
37+
text: "USB",
38+
anchor_position: { x: 0.5, y: -1 },
39+
anchor_alignment: "center",
40+
layer: "top",
41+
font: "tscircuit2024",
42+
font_size: 1,
43+
},
44+
]
45+
46+
const mockPartsEngine: PartsEngine = {
47+
findPart: async () => {
48+
// No supplier part numbers available
49+
return {}
50+
},
51+
fetchPartCircuitJson: async ({
52+
supplierPartNumber,
53+
manufacturerPartNumber,
54+
}: {
55+
supplierPartNumber?: string
56+
manufacturerPartNumber?: string
57+
}) => {
58+
if (manufacturerPartNumber === "USB4135-GF-A") {
59+
return mockCircuitJson as AnyCircuitElement[]
60+
}
61+
return undefined
62+
},
63+
}
64+
65+
circuit.add(
66+
<board partsEngine={mockPartsEngine} width="20mm" height="20mm">
67+
<connector
68+
name="USB1"
69+
standard="usb_c"
70+
manufacturerPartNumber="USB4135-GF-A"
71+
/>
72+
</board>,
73+
)
74+
75+
await circuit.renderUntilSettled()
76+
77+
const sourceComponent = circuit.db.source_component
78+
.list()
79+
.find((c: any) => c.name === "USB1")
80+
expect(sourceComponent).toBeTruthy()
81+
expect((sourceComponent as any).standard).toBe("usb_c")
82+
83+
const pads = circuit.db.pcb_smtpad.list()
84+
expect(pads.length).toBeGreaterThan(0)
85+
expect(circuit).toMatchPcbSnapshot(import.meta.path)
86+
})

0 commit comments

Comments
 (0)