Skip to content
85 changes: 82 additions & 3 deletions lib/generate-footprint-tsx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,19 @@ export const generateFootprintTsx = (
const platedHoles = su(circuitJson).pcb_plated_hole.list()
const smtPads = su(circuitJson).pcb_smtpad.list()
const silkscreenPaths = su(circuitJson).pcb_silkscreen_path.list()
const silkscreenCircles = su(circuitJson).pcb_silkscreen_circle.list()
const fabricationNotePaths = su(circuitJson).pcb_fabrication_note_path.list()
const silkscreenTexts = su(circuitJson).pcb_silkscreen_text.list()
// NOTE: soup-util doesn't currently expose all courtyard helpers, so we filter manually
const courtyardCircles = circuitJson.filter(
(e: any) => e?.type === "pcb_courtyard_circle",
) as any[]
const courtyardRects = circuitJson.filter(
(e: any) => e?.type === "pcb_courtyard_rect",
) as any[]
const courtyardOutlines = circuitJson.filter(
(e: any) => e?.type === "pcb_courtyard_outline",
) as any[]
const pcbCutouts = su(circuitJson).pcb_cutout.list()
const noteTexts = su(circuitJson).pcb_note_text.list()
const noteRects = su(circuitJson).pcb_note_rect.list()
Expand Down Expand Up @@ -60,6 +71,19 @@ export const generateFootprintTsx = (
)
}

for (const silkscreenCircle of silkscreenCircles) {
const pcbX = silkscreenCircle.center?.x ?? 0
const pcbY = silkscreenCircle.center?.y ?? 0
const strokeWidth =
silkscreenCircle.stroke_width !== undefined
? ` strokeWidth="${mmStr(silkscreenCircle.stroke_width)}"`
: ""

elementStrings.push(
`<silkscreencircle pcbX="${mmStr(pcbX)}" pcbY="${mmStr(pcbY)}" radius="${mmStr(silkscreenCircle.radius)}"${strokeWidth} />`,
)
}

// Map fabrication note paths to silkscreen paths in footprints
for (const fabPath of fabricationNotePaths) {
elementStrings.push(
Expand All @@ -82,6 +106,63 @@ export const generateFootprintTsx = (
)
}

// Add courtyard elements
for (const courtyardCircle of courtyardCircles) {
elementStrings.push(
`<courtyardcircle pcbX="${mmStr(courtyardCircle.center.x)}" pcbY="${mmStr(courtyardCircle.center.y)}" radius="${mmStr(courtyardCircle.radius)}" />`,
)
}
Comment on lines +110 to +117
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null safety for courtyardCircle.center. The code directly accesses courtyardCircle.center.x and courtyardCircle.center.y without optional chaining, which will throw a runtime error if center is undefined or null.

This is inconsistent with the silkscreen circle implementation (lines 75-76) which safely uses silkscreenCircle.center?.x ?? 0.

Fix:

for (const courtyardCircle of courtyardCircles) {
  const pcbX = courtyardCircle.center?.x ?? 0
  const pcbY = courtyardCircle.center?.y ?? 0
  elementStrings.push(
    `<courtyardcircle pcbX="${mmStr(pcbX)}" pcbY="${mmStr(pcbY)}" radius="${mmStr(courtyardCircle.radius)}" />`,
  )
}
Suggested change
for (const courtyardCircle of courtyardCircles) {
elementStrings.push(
`<courtyardcircle pcbX="${mmStr(courtyardCircle.center.x)}" pcbY="${mmStr(courtyardCircle.center.y)}" radius="${mmStr(courtyardCircle.radius)}" />`,
)
}
for (const courtyardCircle of courtyardCircles) {
const pcbX = courtyardCircle.center?.x ?? 0
const pcbY = courtyardCircle.center?.y ?? 0
elementStrings.push(
`<courtyardcircle pcbX="${mmStr(pcbX)}" pcbY="${mmStr(pcbY)}" radius="${mmStr(courtyardCircle.radius)}" />`,
)
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.


for (const courtyardRect of courtyardRects) {
const colorAttr = courtyardRect.color
? ` color="${courtyardRect.color}"`
: ""
elementStrings.push(
`<courtyardrect pcbX="${mmStr(courtyardRect.center.x)}" pcbY="${mmStr(courtyardRect.center.y)}" width="${mmStr(courtyardRect.width)}" height="${mmStr(courtyardRect.height)}"${colorAttr} />`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null-safety checks on courtyardRect.center, width, and height properties. This is inconsistent with the defensive pattern used for circles (e.g., lines 75-76, 111-112 use optional chaining center?.x ?? 0).

If courtyardRect.center is undefined or null, this will throw: Cannot read property 'x' of undefined.

Fix:

const pcbX = courtyardRect.center?.x ?? 0
const pcbY = courtyardRect.center?.y ?? 0
const width = mmStr(courtyardRect.width ?? 0)
const height = mmStr(courtyardRect.height ?? 0)
const colorAttr = courtyardRect.color
  ? ` color="${courtyardRect.color}"`
  : ""
elementStrings.push(
  `<courtyardrect pcbX="${mmStr(pcbX)}" pcbY="${mmStr(pcbY)}" width="${width}" height="${height}"${colorAttr} />`,
)
Suggested change
`<courtyardrect pcbX="${mmStr(courtyardRect.center.x)}" pcbY="${mmStr(courtyardRect.center.y)}" width="${mmStr(courtyardRect.width)}" height="${mmStr(courtyardRect.height)}"${colorAttr} />`,
const pcbX = courtyardRect.center?.x ?? 0
const pcbY = courtyardRect.center?.y ?? 0
const width = mmStr(courtyardRect.width ?? 0)
const height = mmStr(courtyardRect.height ?? 0)
const colorAttr = courtyardRect.color
? ` color="${courtyardRect.color}"`
: ""
elementStrings.push(
`<courtyardrect pcbX="${mmStr(pcbX)}" pcbY="${mmStr(pcbY)}" width="${width}" height="${height}"${colorAttr} />`,
)

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

)
}

const maybeRectFromOutline = (outline: Array<{ x: number; y: number }>) => {
if (!Array.isArray(outline) || outline.length !== 4) return null
const xs = outline.map((p) => p.x)
const ys = outline.map((p) => p.y)
const minX = Math.min(...xs)
const maxX = Math.max(...xs)
const minY = Math.min(...ys)
const maxY = Math.max(...ys)

// axis-aligned rectangle must only contain the 4 corners
const corners = new Set([
`${minX},${minY}`,
`${minX},${maxY}`,
`${maxX},${minY}`,
`${maxX},${maxY}`,
])
for (const p of outline) {
if (!corners.has(`${p.x},${p.y}`)) return null
}

return {
center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
width: Math.abs(maxX - minX),
height: Math.abs(maxY - minY),
}
}

for (const courtyardOutline of courtyardOutlines) {
const rect = maybeRectFromOutline(courtyardOutline.outline)
if (rect) {
elementStrings.push(
`<courtyardrect pcbX="${mmStr(rect.center.x)}" pcbY="${mmStr(rect.center.y)}" width="${mmStr(rect.width)}" height="${mmStr(rect.height)}" />`,
)
continue
}

elementStrings.push(
`<courtyardoutline outline={${JSON.stringify(courtyardOutline.outline)}} />`,
)
}

// Add cutout elements
for (const cutout of pcbCutouts) {
if (cutout.shape === "rect") {
Expand All @@ -90,9 +171,7 @@ export const generateFootprintTsx = (
const width = mmStr(cutout.width)
const height = mmStr(cutout.height)
const rotation =
cutout.rotation !== undefined
? ` pcbRotation="${mmStr(cutout.rotation)}"`
: ""
cutout.rotation !== undefined ? ` pcbRotation={${cutout.rotation}}` : ""

elementStrings.push(
`<cutout shape="rect" pcbX="${mmStr(pcbX)}" pcbY="${mmStr(pcbY)}" width="${width}" height="${height}"${rotation} />`,
Expand Down
2 changes: 1 addition & 1 deletion tests/test5-pcb-cutout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ test("test pcb_cutout conversion - all shapes", async () => {
export const ComponentWithCutouts = (props: ChipProps) => (
<chip
footprint={<footprint>
<cutout shape="rect" pcbX="0mm" pcbY="0mm" width="5mm" height="3mm" pcbRotation="45mm" />
<cutout shape="rect" pcbX="0mm" pcbY="0mm" width="5mm" height="3mm" pcbRotation={45} />
<cutout shape="circle" pcbX="10mm" pcbY="10mm" radius="2.5mm" />
<cutout shape="polygon" points={[{"x":0,"y":0},{"x":5,"y":0},{"x":5,"y":5},{"x":0,"y":5}]} />
</footprint>}
Expand Down
101 changes: 101 additions & 0 deletions tests/test7-support-courtyard-and-circles.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { expect, test } from "bun:test"
import { convertCircuitJsonToTscircuit } from "lib"

test("test7 support courtyards + silkscreen circles", async () => {
const tscircuit = convertCircuitJsonToTscircuit(circuitJson, {
componentName: "Test7Component",
})

expect(tscircuit).toContain("<silkscreencircle")
expect(tscircuit).toContain('radius="2mm"')
expect(tscircuit).toContain('strokeWidth="0.15mm"')

expect(tscircuit).toContain("<courtyardcircle")
expect(tscircuit).toContain('radius="3mm"')

// generated from pcb_courtyard_outline rectangle
expect(tscircuit).toContain("<courtyardrect")
expect(tscircuit).toContain('width="4mm"')
expect(tscircuit).toContain('height="5mm"')

expect(tscircuit).toContain("<courtyardoutline")
})

const circuitJson: any = [
{
type: "source_component",
source_component_id: "generic_0",
supplier_part_numbers: {},
},
{
type: "schematic_component",
schematic_component_id: "schematic_generic_component_0",
source_component_id: "generic_0",
center: {
x: 0,
y: 0,
},
rotation: 0,
size: {
width: 0,
height: 0,
},
},
{
type: "pcb_component",
source_component_id: "generic_0",
pcb_component_id: "pcb_generic_component_0",
layer: "top",
center: {
x: 0,
y: 0,
},
rotation: 0,
width: 1,
height: 1,
},
{
type: "pcb_silkscreen_circle",
pcb_silkscreen_circle_id: "pcb_silkscreen_circle_0",
pcb_component_id: "pcb_generic_component_0",
layer: "top",
center: { x: 1, y: 2 },
radius: 2,
stroke_width: 0.15,
},
{
type: "pcb_courtyard_circle",
pcb_courtyard_circle_id: "pcb_courtyard_circle_0",
pcb_component_id: "pcb_generic_component_0",
layer: "top",
center: { x: 0, y: 0 },
radius: 3,
},
{
type: "pcb_courtyard_outline",
pcb_courtyard_outline_id: "pcb_courtyard_outline_0",
pcb_component_id: "pcb_generic_component_0",
layer: "top",
outline: [
{ x: -2, y: -2.5 },
{ x: 2, y: -2.5 },
{ x: 2, y: 2.5 },
{ x: -2, y: 2.5 },
],
stroke_width: 0.05,
is_closed: true,
},
{
type: "pcb_courtyard_outline",
pcb_courtyard_outline_id: "pcb_courtyard_outline_1",
pcb_component_id: "pcb_generic_component_0",
layer: "top",
outline: [
{ x: 0, y: 0 },
{ x: 2, y: 0 },
{ x: 1, y: 1 },
],
stroke_width: 0.05,
is_closed: true,
},
]