Skip to main content
Scripting

Measuring circuit size using scripts

Automating board design often starts with a question: how much space does this group of parts actually need? This guide shows how to render groups inside a script, capture their dimensions, and store metadata that other steps in your pipeline can read.

  1. Keep each reusable group in a design-groups/ directory so it can be evaluated on its own.
  2. Run a script that loads every group, renders it with tscircuit, and saves a metadata JSON file containing width and height.
  3. When you build the final board component, import the metadata file to decide which carrier template has enough room.

Separating generated metadata from hand-authored TSX keeps the process debuggable. If you do write metadata back into a .tsx file, wrap the generated block in clearly marked comments so humans and tools know where automation can safely write.

Directory layout

my-project/
├─ design-groups/
│ ├─ esp32-breakout.tsx # Exports the group you want to analyze
│ └─ esp32-breakout.metadata.json # Width/height baked by the script
├─ carriers/
│ └─ esp32-breakout-carrier.tsx # Imports metadata when building the final board
└─ scripts/
└─ bake-group-metadata.ts # Script that renders the group and writes JSON

Each group file should export a React component that renders a <group name="..."> inside a <board> or within the structure you normally use. The script can then import it, render it in isolation, and evaluate the results.

design-groups/esp32-breakout.tsx
export const Esp32Breakout = () => (
<group name="esp32-breakout">
{/* Components, nets, and layout props */}
</group>
)

Rendering a group and measuring its bounding box

The snippet below renders a group, waits for placement to settle, and then extracts the PCB group bounds from Circuit JSON.

import { RootCircuit } from "tscircuit"
import { Esp32Breakout } from "../design-groups/esp32-breakout"

const circuit = new RootCircuit()

circuit.add(
<Esp32Breakout />
)

await circuit.renderUntilSettled();

const circuitJson = await circuit.getCircuitJson();

const rootGroupOrBoard = circuitJson.find(
(item) =>
item.type === "pcb_board" ||
(item.type === "pcb_group" && item.is_subcircuit),
);

Bun.write("../design-groups/esp32-breakout.metadata.json", JSON.stringify(rootGroupOrBoard, null, 2));

You can do this dynamically for every file in the design-groups directory:

import { RootCircuit } from "tscircuit"

for (const file of await Bun.glob("design-groups/*.tsx")) {
const { default: GroupComponent } = await import(`../${file}`);
const groupName = file.split("/").pop()?.replace(".tsx", "");
const circuit = new RootCircuit();
circuit.add(<GroupComponent />);
await circuit.renderUntilSettled();
const circuitJson = await circuit.getCircuitJson();

const rootGroupOrBoard = circuitJson.find(
(item) =>
item.type === "pcb_board" ||
(item.type === "pcb_group" && item.is_subcircuit),
);

await Bun.write(
`../design-groups/${groupName}.metadata.json`,
JSON.stringify(rootGroupOrBoard, null, 2)
);
}

Detecting packing failures programmatically

Packing issues (such as overlaps or components outside the board) are surfaced in Circuit JSON as elements whose type ends in _error, for example pcb_placement_error, pcb_footprint_overlap_error, and pcb_component_outside_board_error. You can scan for them before trusting the bounding box.

function getPackingErrors(json: CircuitJson) {
return json.filter(
(element) =>
element.type.startsWith("pcb_") && element.type.endsWith("_error"),
)
}

If getPackingErrors returns any elements, skip writing metadata and log the error messages so you can debug the group in isolation. You can also persist the raw JSON to disk for later inspection or feed it into tools like circuitjson.com.

Trying candidate board sizes

Once you can render a group programmatically, you can try it against a list of candidate board footprints and pick the smallest one that succeeds. Render the group inside a <board> that has the candidate size, check for packing errors, and return the first success.

const CANDIDATE_SIZES = [
{ name: "SMALL", width: 21, height: 51 },
{ name: "MEDIUM", width: 24, height: 58 },
]

async function findSmallestBoard(load: () => Promise<React.FC>) {
for (const size of CANDIDATE_SIZES) {
const circuit = new RootCircuit()
const Circuit = await load()

circuit.add(
<board width={`${size.width}mm`} height={`${size.height}mm`}>
<Circuit />
</board>,
)

await circuit.renderUntilSettled()
const json = (await circuit.getCircuitJson()) as CircuitJson

if (getPackingErrors(json).length === 0) {
return { size, json }
}
}

throw new Error("No candidate board size could pack the circuit")
}

With this helper you can run multiple passes: one to bake metadata for inspection, and another to select the best board footprint automatically. Store the selected board ID alongside the size metadata so later steps (such as generating headers or enclosure geometry) can read the decision without re-running the analysis.

Using the baked metadata in a board component

The final board component can import the JSON and pick a template accordingly. Because the metadata is static, you can safely load it during module evaluation.

import metadata from "../design-groups/esp32-breakout.metadata.json" assert { type: "json" }

const TEMPLATE_OPTIONS = [
{ id: "SMALL", width: 21, height: 51 },
{ id: "MEDIUM", width: 24, height: 58 },
]

const selectedTemplate = TEMPLATE_OPTIONS.find(
(option) =>
option.width >= metadata.width && option.height >= metadata.height,
)

if (!selectedTemplate) {
throw new Error("No board template can fit the baked group bounds")
}

export const Esp32BreakoutCarrier = ({ children }: { children: React.ReactNode }) => (
<board width={`${selectedTemplate.width}mm`} height={`${selectedTemplate.height}mm`}>
{children}
</board>
)

When automation needs to update the layout (for example, after rerunning packing with AI assistance), rerun the baking script to regenerate the JSON and let the board component pick a new template automatically. This keeps generated numbers out of your hand-authored TSX while remaining easy to audit.

You can add a script inside your package.json file to run the baking script:

{
"scripts": {
"bake-metadata": "bun run scripts/bake-metadata.ts"
}
}