From b560fe8f8ce4fe0e12dc49e8d4b83a0dd2de45d2 Mon Sep 17 00:00:00 2001 From: Mukedlii Date: Fri, 20 Feb 2026 18:22:25 +0100 Subject: [PATCH 01/11] support silkscreen circles + courtyard elements in footprint tsx --- lib/generate-footprint-tsx.tsx | 37 +++++++++++ ...st7-support-courtyard-and-circles.test.tsx | 64 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 tests/test7-support-courtyard-and-circles.test.tsx diff --git a/lib/generate-footprint-tsx.tsx b/lib/generate-footprint-tsx.tsx index f03c5b9..56d0aaa 100644 --- a/lib/generate-footprint-tsx.tsx +++ b/lib/generate-footprint-tsx.tsx @@ -9,8 +9,12 @@ 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() + const courtyardCircles = su(circuitJson).pcb_courtyard_circle.list() + const courtyardRects = su(circuitJson).pcb_courtyard_rect.list() + const courtyardOutlines = su(circuitJson).pcb_courtyard_outline.list() const pcbCutouts = su(circuitJson).pcb_cutout.list() const noteTexts = su(circuitJson).pcb_note_text.list() const noteRects = su(circuitJson).pcb_note_rect.list() @@ -60,6 +64,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( + ``, + ) + } + // Map fabrication note paths to silkscreen paths in footprints for (const fabPath of fabricationNotePaths) { elementStrings.push( @@ -82,6 +99,26 @@ export const generateFootprintTsx = ( ) } + // Add courtyard elements + for (const courtyardCircle of courtyardCircles) { + elementStrings.push( + ``, + ) + } + + for (const courtyardRect of courtyardRects) { + const colorAttr = courtyardRect.color ? ` color="${courtyardRect.color}"` : "" + elementStrings.push( + ``, + ) + } + + for (const courtyardOutline of courtyardOutlines) { + elementStrings.push( + ``, + ) + } + // Add cutout elements for (const cutout of pcbCutouts) { if (cutout.shape === "rect") { diff --git a/tests/test7-support-courtyard-and-circles.test.tsx b/tests/test7-support-courtyard-and-circles.test.tsx new file mode 100644 index 0000000..beead7c --- /dev/null +++ b/tests/test7-support-courtyard-and-circles.test.tsx @@ -0,0 +1,64 @@ +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(" Date: Fri, 20 Feb 2026 18:44:04 +0100 Subject: [PATCH 02/11] fix: filter courtyard elements without soup-util helpers --- lib/generate-footprint-tsx.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/generate-footprint-tsx.tsx b/lib/generate-footprint-tsx.tsx index 56d0aaa..e1b8feb 100644 --- a/lib/generate-footprint-tsx.tsx +++ b/lib/generate-footprint-tsx.tsx @@ -12,9 +12,16 @@ export const generateFootprintTsx = ( 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() - const courtyardCircles = su(circuitJson).pcb_courtyard_circle.list() - const courtyardRects = su(circuitJson).pcb_courtyard_rect.list() - const courtyardOutlines = su(circuitJson).pcb_courtyard_outline.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() From 6a80ccfdfb3c4557f143564294ac9ace02f492d7 Mon Sep 17 00:00:00 2001 From: Mukedlii Date: Fri, 20 Feb 2026 18:49:49 +0100 Subject: [PATCH 03/11] fix: format + add required component elements for courtyard test --- lib/generate-footprint-tsx.tsx | 4 +- ...st7-support-courtyard-and-circles.test.tsx | 40 +++++++++++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/lib/generate-footprint-tsx.tsx b/lib/generate-footprint-tsx.tsx index e1b8feb..04fa6c3 100644 --- a/lib/generate-footprint-tsx.tsx +++ b/lib/generate-footprint-tsx.tsx @@ -114,7 +114,9 @@ export const generateFootprintTsx = ( } for (const courtyardRect of courtyardRects) { - const colorAttr = courtyardRect.color ? ` color="${courtyardRect.color}"` : "" + const colorAttr = courtyardRect.color + ? ` color="${courtyardRect.color}"` + : "" elementStrings.push( ``, ) diff --git a/tests/test7-support-courtyard-and-circles.test.tsx b/tests/test7-support-courtyard-and-circles.test.tsx index beead7c..cf3c568 100644 --- a/tests/test7-support-courtyard-and-circles.test.tsx +++ b/tests/test7-support-courtyard-and-circles.test.tsx @@ -22,10 +22,42 @@ test("test7 support courtyards + silkscreen circles", async () => { }) 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_component_0", + pcb_component_id: "pcb_generic_component_0", layer: "top", center: { x: 1, y: 2 }, radius: 2, @@ -34,7 +66,7 @@ const circuitJson: any = [ { type: "pcb_courtyard_circle", pcb_courtyard_circle_id: "pcb_courtyard_circle_0", - pcb_component_id: "pcb_component_0", + pcb_component_id: "pcb_generic_component_0", layer: "top", center: { x: 0, y: 0 }, radius: 3, @@ -42,7 +74,7 @@ const circuitJson: any = [ { type: "pcb_courtyard_rect", pcb_courtyard_rect_id: "pcb_courtyard_rect_0", - pcb_component_id: "pcb_component_0", + pcb_component_id: "pcb_generic_component_0", layer: "top", center: { x: 0, y: 0 }, width: 4, @@ -52,7 +84,7 @@ const circuitJson: any = [ { type: "pcb_courtyard_outline", pcb_courtyard_outline_id: "pcb_courtyard_outline_0", - pcb_component_id: "pcb_component_0", + pcb_component_id: "pcb_generic_component_0", layer: "top", outline: [ { x: -1, y: -1 }, From 95a26f6be16ad3389260b37202f35a3ba7385f96 Mon Sep 17 00:00:00 2001 From: Mukedlii Date: Sat, 21 Feb 2026 05:11:59 +0100 Subject: [PATCH 04/11] feat: emit courtyardrect from pcb_courtyard_outline rectangles --- lib/generate-footprint-tsx.tsx | 37 +++++++++++++++++++ ...st7-support-courtyard-and-circles.test.tsx | 23 ++++++++---- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/lib/generate-footprint-tsx.tsx b/lib/generate-footprint-tsx.tsx index 04fa6c3..4fe6eab 100644 --- a/lib/generate-footprint-tsx.tsx +++ b/lib/generate-footprint-tsx.tsx @@ -122,7 +122,44 @@ export const generateFootprintTsx = ( ) } + 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( + ``, + ) + continue + } + elementStrings.push( ``, ) diff --git a/tests/test7-support-courtyard-and-circles.test.tsx b/tests/test7-support-courtyard-and-circles.test.tsx index cf3c568..5cf748e 100644 --- a/tests/test7-support-courtyard-and-circles.test.tsx +++ b/tests/test7-support-courtyard-and-circles.test.tsx @@ -13,10 +13,10 @@ test("test7 support courtyards + silkscreen circles", async () => { expect(tscircuit).toContain(" Date: Sat, 21 Feb 2026 05:15:00 +0100 Subject: [PATCH 05/11] test: ensure pcb_courtyard_outline emits courtyardoutline when not a rect --- tests/test7-support-courtyard-and-circles.test.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test7-support-courtyard-and-circles.test.tsx b/tests/test7-support-courtyard-and-circles.test.tsx index 5cf748e..2abb6f8 100644 --- a/tests/test7-support-courtyard-and-circles.test.tsx +++ b/tests/test7-support-courtyard-and-circles.test.tsx @@ -91,10 +91,9 @@ const circuitJson: any = [ pcb_component_id: "pcb_generic_component_0", layer: "top", outline: [ - { x: -1, y: -1 }, - { x: 1, y: -1 }, + { x: 0, y: 0 }, + { x: 2, y: 0 }, { x: 1, y: 1 }, - { x: -1, y: 1 }, ], stroke_width: 0.05, is_closed: true, From 6c2f0a26f3ceb35b8f8892b90e5f8e0000b7e629 Mon Sep 17 00:00:00 2001 From: Mukedlii Date: Sat, 21 Feb 2026 05:16:15 +0100 Subject: [PATCH 06/11] style: biome formatting --- lib/generate-footprint-tsx.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/generate-footprint-tsx.tsx b/lib/generate-footprint-tsx.tsx index 4fe6eab..e1d98ba 100644 --- a/lib/generate-footprint-tsx.tsx +++ b/lib/generate-footprint-tsx.tsx @@ -122,9 +122,7 @@ export const generateFootprintTsx = ( ) } - const maybeRectFromOutline = ( - outline: Array<{ x: number; y: number }>, - ) => { + 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) From 38be375aef2aff19529e357c7d0f55bd1ce275de Mon Sep 17 00:00:00 2001 From: Mukedlii Date: Sat, 21 Feb 2026 05:18:50 +0100 Subject: [PATCH 07/11] style: satisfy biome formatting in test7 --- tests/test7-support-courtyard-and-circles.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test7-support-courtyard-and-circles.test.tsx b/tests/test7-support-courtyard-and-circles.test.tsx index 2abb6f8..b45524d 100644 --- a/tests/test7-support-courtyard-and-circles.test.tsx +++ b/tests/test7-support-courtyard-and-circles.test.tsx @@ -99,4 +99,3 @@ const circuitJson: any = [ is_closed: true, }, ] - From 941b205cd7bb58d741c4d4f9a5dee14c297e7734 Mon Sep 17 00:00:00 2001 From: Mukedlii Date: Sat, 21 Feb 2026 05:37:27 +0100 Subject: [PATCH 08/11] fix: emit numeric pcbRotation for cutouts --- lib/generate-footprint-tsx.tsx | 4 +--- tests/test5-pcb-cutout.test.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/generate-footprint-tsx.tsx b/lib/generate-footprint-tsx.tsx index e1d98ba..e5bf4e2 100644 --- a/lib/generate-footprint-tsx.tsx +++ b/lib/generate-footprint-tsx.tsx @@ -171,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( ``, diff --git a/tests/test5-pcb-cutout.test.tsx b/tests/test5-pcb-cutout.test.tsx index b38a360..377c3ac 100644 --- a/tests/test5-pcb-cutout.test.tsx +++ b/tests/test5-pcb-cutout.test.tsx @@ -49,7 +49,7 @@ test("test pcb_cutout conversion - all shapes", async () => { export const ComponentWithCutouts = (props: ChipProps) => ( - + } From d36c819396bb32638f54f4d796674a0c763982ae Mon Sep 17 00:00:00 2001 From: Mukedlii Date: Sat, 21 Feb 2026 06:26:07 +0100 Subject: [PATCH 09/11] fix: null-safe pcb_courtyard_circle center --- lib/generate-footprint-tsx.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/generate-footprint-tsx.tsx b/lib/generate-footprint-tsx.tsx index e5bf4e2..3587632 100644 --- a/lib/generate-footprint-tsx.tsx +++ b/lib/generate-footprint-tsx.tsx @@ -108,8 +108,11 @@ export const generateFootprintTsx = ( // Add courtyard elements for (const courtyardCircle of courtyardCircles) { + const pcbX = courtyardCircle.center?.x ?? 0 + const pcbY = courtyardCircle.center?.y ?? 0 + elementStrings.push( - ``, + ``, ) } From 015b92202c4813868d412aaf0d42632d830d3483 Mon Sep 17 00:00:00 2001 From: Mukedlii Date: Sat, 21 Feb 2026 06:52:08 +0100 Subject: [PATCH 10/11] fix: null-safe pcb_courtyard_rect fields --- lib/generate-footprint-tsx.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/generate-footprint-tsx.tsx b/lib/generate-footprint-tsx.tsx index 3587632..d6cbc91 100644 --- a/lib/generate-footprint-tsx.tsx +++ b/lib/generate-footprint-tsx.tsx @@ -117,11 +117,16 @@ export const generateFootprintTsx = ( } for (const courtyardRect of courtyardRects) { + 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( - ``, + ``, ) } From b5114c18f70d05110e69f7af033dfab14a092d64 Mon Sep 17 00:00:00 2001 From: Mukedlii Date: Sat, 21 Feb 2026 06:55:15 +0100 Subject: [PATCH 11/11] test: increase pcb_cutout timeout for CI --- tests/test5-pcb-cutout.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test5-pcb-cutout.test.tsx b/tests/test5-pcb-cutout.test.tsx index 377c3ac..091eee5 100644 --- a/tests/test5-pcb-cutout.test.tsx +++ b/tests/test5-pcb-cutout.test.tsx @@ -61,7 +61,7 @@ test("test pcb_cutout conversion - all shapes", async () => { const result = await runTscircuitCode(tscircuit) expect(Array.isArray(result)).toBe(true) expect(result).not.toHaveLength(0) -}) +}, 20_000) test("test pcb_cutout conversion - rect without rotation", async () => { const circuitJson: AnyCircuitElement[] = [ @@ -94,7 +94,7 @@ test("test pcb_cutout conversion - rect without rotation", async () => { const result = await runTscircuitCode(tscircuit) expect(Array.isArray(result)).toBe(true) expect(result).not.toHaveLength(0) -}) +}, 20_000) test("test pcb_cutout conversion - mixed with other elements", async () => { const circuitJson: AnyCircuitElement[] = [ @@ -147,4 +147,4 @@ test("test pcb_cutout conversion - mixed with other elements", async () => { const result = await runTscircuitCode(tscircuit) expect(Array.isArray(result)).toBe(true) expect(result).not.toHaveLength(0) -}) +}, 20_000)