Skip to content
Merged
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
182 changes: 182 additions & 0 deletions docs/guides/running-tscircuit/scripting/measuring-circuit-size.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
title: Measuring circuit size using scripts
description: Use @tscircuit/core to evaluate groups, extract bounding boxes, and pick board templates that fit
---

import CircuitPreview from "@site/src/components/CircuitPreview"

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.

## Recommended workflow

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

```text
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.

```tsx title="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.

```tsx
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:

```tsx
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();
Comment on lines +73 to +78

Choose a reason for hiding this comment

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

P1 Badge Use matching export type when glob-loading groups

The globbed loading example destructures default from each module, but the doc’s preceding snippet defines groups as named exports (export const Esp32Breakout ...). For files that follow the named-export guidance, GroupComponent becomes undefined and the call to circuit.add(<GroupComponent />) throws. Either import a named export or update the recommended group files to export a default component.

Useful? React with 👍 / 👎.

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.

```ts
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.

```tsx
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.

```tsx
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:

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