Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions lib/generate-footprint-tsx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const generateFootprintTsx = (
const fabricationNotePaths = su(circuitJson).pcb_fabrication_note_path.list()
const silkscreenTexts = su(circuitJson).pcb_silkscreen_text.list()
const pcbCutouts = su(circuitJson).pcb_cutout.list()
const noteTexts = su(circuitJson).pcb_note_text.list()
const noteRects = su(circuitJson).pcb_note_rect.list()
const notePaths = su(circuitJson).pcb_note_path.list()
const noteLines = su(circuitJson).pcb_note_line.list()
const noteDimensions = su(circuitJson).pcb_note_dimension.list()

const elementStrings: string[] = []

Expand Down Expand Up @@ -111,6 +116,122 @@ export const generateFootprintTsx = (
}
}

for (const noteText of noteTexts) {
const anchorPosition = noteText.anchor_position ?? { x: 0, y: 0 }
const anchorAlignment = noteText.anchor_alignment ?? "center"
const font = noteText.font ?? "tscircuit2024"
const fontSize = noteText.font_size ?? 0
const colorAttr = noteText.color ? ` color="${noteText.color}"` : ""

const rawText = String(noteText.text ?? "")
const escapedText = rawText.replace(/"/g, '\\"')
Comment on lines +126 to +127
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Backslashes in text are not escaped, which will break the generated TSX syntax. If the text contains a backslash (e.g., "test\value"), the generated code will have text="test\value" where the backslash could escape the closing quote or create invalid escape sequences.

Fix by escaping backslashes before quotes:

const rawText = String(noteText.text ?? "")
const escapedText = rawText.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
Suggested change
const rawText = String(noteText.text ?? "")
const escapedText = rawText.replace(/"/g, '\\"')
const rawText = String(noteText.text ?? "")
const escapedText = rawText.replace(/\\/g, '\\\\').replace(/"/g, '\\"')

Spotted by Graphite Agent

Fix in Graphite


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

This comment came from an experimental review—please leave feedback if it was helpful/unhelpful. Learn more about experimental comments here.


elementStrings.push(
`<pcbnotetext pcbX={${anchorPosition.x}} pcbY={${anchorPosition.y}} anchorAlignment="${anchorAlignment}" font="${font}" fontSize={${fontSize}} text="${escapedText}"${colorAttr} />`,
Comment on lines +119 to +130
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Escape note text contents before embedding into JSX

The conversion logic escapes only double quotes before writing noteText.text into the text="…" attribute, but values such as "A<B" or "A&B" are not escaped. When a note contains </>/&, the generated TSX will be syntactically invalid because the attribute value terminates prematurely or introduces an entity. Consider encoding the string with JSON.stringify or replacing these characters so note texts containing comparison symbols or ampersands still generate valid JSX.

Useful? React with 👍 / 👎.

)
}

for (const noteRect of noteRects) {
const center = noteRect.center ?? { x: 0, y: 0 }
const width = noteRect.width ?? 0
const height = noteRect.height ?? 0
const strokeWidth = noteRect.stroke_width ?? 0

const attrs = [
`pcbX={${center.x}}`,
`pcbY={${center.y}}`,
`width={${width}}`,
`height={${height}}`,
`strokeWidth={${strokeWidth}}`,
]

if (noteRect.is_filled !== undefined) {
attrs.push(`isFilled={${noteRect.is_filled}}`)
}
if (noteRect.has_stroke !== undefined) {
attrs.push(`hasStroke={${noteRect.has_stroke}}`)
}
if (noteRect.is_stroke_dashed !== undefined) {
attrs.push(`isStrokeDashed={${noteRect.is_stroke_dashed}}`)
}
if (noteRect.color !== undefined) {
attrs.push(`color="${noteRect.color}"`)
}

elementStrings.push(`<pcbnoterect ${attrs.join(" ")} />`)
}

for (const notePath of notePaths) {
const routeJson = JSON.stringify(notePath.route ?? [])
const attrs = [`route={${routeJson}}`]

if (notePath.stroke_width !== undefined) {
attrs.push(`strokeWidth={${notePath.stroke_width}}`)
}
if (notePath.color !== undefined) {
attrs.push(`color="${notePath.color}"`)
}

elementStrings.push(`<pcbnotepath ${attrs.join(" ")} />`)
}

for (const noteLine of noteLines) {
const attrs = [
`x1={${noteLine.x1 ?? 0}}`,
`y1={${noteLine.y1 ?? 0}}`,
`x2={${noteLine.x2 ?? 0}}`,
`y2={${noteLine.y2 ?? 0}}`,
]

if (noteLine.stroke_width !== undefined) {
attrs.push(`strokeWidth={${noteLine.stroke_width}}`)
}
if (noteLine.color !== undefined) {
attrs.push(`color="${noteLine.color}"`)
}
if (noteLine.is_dashed !== undefined) {
attrs.push(`isDashed={${noteLine.is_dashed}}`)
}

elementStrings.push(`<pcbnoteline ${attrs.join(" ")} />`)
}

for (const noteDimension of noteDimensions) {
const fromPoint = noteDimension.from ?? { x: 0, y: 0 }
const toPoint = noteDimension.to ?? { x: 0, y: 0 }
const font = noteDimension.font ?? "tscircuit2024"
const fontSize = noteDimension.font_size ?? 0
const arrowSize = noteDimension.arrow_size
const attrs = [
`from={{ x: ${fromPoint.x}, y: ${fromPoint.y} }}`,
`to={{ x: ${toPoint.x}, y: ${toPoint.y} }}`,
`font="${font}"`,
`fontSize={${fontSize}}`,
]

if (arrowSize !== undefined) {
attrs.push(`arrowSize={${arrowSize}}`)
}

if ("offset" in noteDimension) {
const offsetValue = (noteDimension as { offset?: number }).offset
if (offsetValue !== undefined) {
attrs.push(`offset={${offsetValue}}`)
}
}

if (noteDimension.text !== undefined) {
const escapedText = String(noteDimension.text).replace(/"/g, '\\"')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same backslash escaping issue as noteText. Backslashes in dimension text are not escaped before embedding in the generated TSX string.

Fix by escaping backslashes before quotes:

const escapedText = String(noteDimension.text).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
Suggested change
const escapedText = String(noteDimension.text).replace(/"/g, '\\"')
const escapedText = String(noteDimension.text).replace(/\\/g, '\\\\').replace(/"/g, '\\"')

Spotted by Graphite Agent

Fix in Graphite


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

This comment came from an experimental review—please leave feedback if it was helpful/unhelpful. Learn more about experimental comments here.

attrs.push(`text="${escapedText}"`)
Comment on lines +223 to +225
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Escape dimension label text before embedding into JSX

The dimension loop performs the same quote-only escaping for noteDimension.text and embeds it directly into text="…". If a dimension label contains characters like <, > or &, the generated JSX becomes invalid and convertCircuitJsonToTscircuit will emit code that fails to compile. The label should be fully escaped (e.g., via JSON.stringify or manual replacement of <, >, &) before being injected.

Useful? React with 👍 / 👎.

}

if (noteDimension.color !== undefined) {
attrs.push(`color="${noteDimension.color}"`)
}

elementStrings.push(`<pcbnotedimension ${attrs.join(" ")} />`)
}

return `
<footprint>
${elementStrings.join("\n")}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"commander": "^13.1.0",
"@tscircuit/core": "^0.0.792",
"@tscircuit/mm": "^0.0.8",
"@tscircuit/soup-util": "^0.0.41",
"@types/bun": "latest",
"@types/react": "18",
"@types/react-dom": "18",
"circuit-json": "^0.0.279",
"commander": "^13.1.0",
"tscircuit": "^0.0.769",
"tsup": "^8.3.5"
},
"peerDependencies": {
Expand Down
104 changes: 104 additions & 0 deletions tests/test6-support-pcb-notes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { expect, test } from "bun:test"
import { convertCircuitJsonToTscircuit } from "lib"
import { runTscircuitCode } from "tscircuit"

test("test6 support pcb notes", async () => {
const tscircuit = convertCircuitJsonToTscircuit(circuitJson as any, {
componentName: "Test6Component",
})

expect(tscircuit).toMatchInlineSnapshot(`
"import { type ChipProps } from \"tscircuit\"\n export const Test6Component = (props: ChipProps) => (\n <chip\n footprint={<footprint>\n <pcbnotetext pcbX={1} pcbY={2} anchorAlignment=\"top_left\" font=\"tscircuit2024\" fontSize={1.5} text=\"Assembly\" color=\"#ff0000\" />\n <pcbnoterect pcbX={0} pcbY={0} width={3.2} height={1.6} strokeWidth={0.2} isFilled={false} hasStroke={true} isStrokeDashed={true} color=\"#00ff00\" />\n <pcbnotepath route={[{\"x\":-1,\"y\":-1},{\"x\":1,\"y\":-1},{\"x\":1,\"y\":1}]} strokeWidth={0.15} color=\"#0000ff\" />\n <pcbnoteline x1={-0.5} y1={-0.5} x2={0.5} y2={0.5} strokeWidth={0.1} color=\"#123456\" isDashed={true} />\n <pcbnotedimension from={{ x: -2, y: 0 }} to={{ x: 2, y: 0 }} font=\"tscircuit2024\" fontSize={1.2} arrowSize={0.25} text=\"4mm\" color=\"#654321\" />\n </footprint>}\n {...props}\n />\n )"
`)

const result = await runTscircuitCode(tscircuit)

expect(Array.isArray(result)).toBe(true)
expect(result).not.toHaveLength(0)
})

const circuitJson = [
{
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_note_text",
pcb_note_text_id: "pcb_note_text_0",
pcb_component_id: "pcb_generic_component_0",
anchor_position: { x: 1, y: 2 },
anchor_alignment: "top_left",
font: "tscircuit2024",
font_size: 1.5,
text: "Assembly",
color: "#ff0000",
},
{
type: "pcb_note_rect",
pcb_note_rect_id: "pcb_note_rect_0",
pcb_component_id: "pcb_generic_component_0",
center: { x: 0, y: 0 },
width: 3.2,
height: 1.6,
stroke_width: 0.2,
is_filled: false,
has_stroke: true,
is_stroke_dashed: true,
color: "#00ff00",
},
{
type: "pcb_note_path",
pcb_note_path_id: "pcb_note_path_0",
pcb_component_id: "pcb_generic_component_0",
route: [
{ x: -1, y: -1 },
{ x: 1, y: -1 },
{ x: 1, y: 1 },
],
stroke_width: 0.15,
color: "#0000ff",
},
{
type: "pcb_note_line",
pcb_note_line_id: "pcb_note_line_0",
pcb_component_id: "pcb_generic_component_0",
x1: -0.5,
y1: -0.5,
x2: 0.5,
y2: 0.5,
stroke_width: 0.1,
color: "#123456",
is_dashed: true,
},
{
type: "pcb_note_dimension",
pcb_note_dimension_id: "pcb_note_dimension_0",
pcb_component_id: "pcb_generic_component_0",
from: { x: -2, y: 0 },
to: { x: 2, y: 0 },
text: "4mm",
font: "tscircuit2024",
font_size: 1.2,
arrow_size: 0.25,
color: "#654321",
},
]