From 1f028977fe7edc0ff7aac5cff18ac50f78644e47 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Thu, 6 Nov 2025 14:05:32 -0800 Subject: [PATCH 1/2] Rewrite circuit size guide with preview example --- .../measuring-circuit-size.mdx | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 docs/guides/running-tscircuit/measuring-circuit-size.mdx diff --git a/docs/guides/running-tscircuit/measuring-circuit-size.mdx b/docs/guides/running-tscircuit/measuring-circuit-size.mdx new file mode 100644 index 0000000..3398aeb --- /dev/null +++ b/docs/guides/running-tscircuit/measuring-circuit-size.mdx @@ -0,0 +1,275 @@ +--- +title: Measuring circuit size from 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/core`, 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 `` inside a `` 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 Circuit = () => ( + + {/* Components, nets, and layout props */} + +) +``` + +## 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. + +```ts title="scripts/bake-group-metadata.ts" +import React, { type ComponentType } from "react" +import { RootCircuit } from "@tscircuit/core" +import type { CircuitJson, PcbGroup } from "circuit-json" +import { writeFile } from "node:fs/promises" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const GROUPS = [ + { + name: "esp32-breakout", + load: async () => + (await import("../design-groups/esp32-breakout.tsx")).Circuit, + }, +] + +async function renderGroup(name: string, load: () => Promise) { + const Circuit = await load() + const circuit = new RootCircuit() + + circuit.add( + + + + + , + ) + + await circuit.renderUntilSettled() + return (await circuit.getCircuitJson()) as CircuitJson +} + +function getGroupBounds(json: CircuitJson, groupName: string) { + const pcbGroup = json.find( + (element): element is PcbGroup => + element.type === "pcb_group" && element.name === groupName, + ) + + if (!pcbGroup || pcbGroup.width === undefined || pcbGroup.height === undefined) { + throw new Error( + `Group ${groupName} rendered without width/height. Check the group name and resolve packing issues before baking metadata.`, + ) + } + + return { + width: pcbGroup.width, + height: pcbGroup.height, + center: pcbGroup.center, + } +} + +async function bakeMetadata() { + const scriptDir = path.dirname(fileURLToPath(import.meta.url)) + + for (const entry of GROUPS) { + const circuitJson = await renderGroup(entry.name, entry.load) + const bounds = getGroupBounds(circuitJson, entry.name) + + const outputPath = path.resolve( + scriptDir, + `../design-groups/${entry.name}.metadata.json`, + ) + + await writeFile(outputPath, JSON.stringify(bounds, null, 2)) + console.log(`Wrote ${outputPath}`) + } +} + +bakeMetadata().catch((error) => { + console.error(error) + process.exitCode = 1 +}) +``` + +The `PcbGroup` element in Circuit JSON contains `width`, `height`, and a `center` point for the packed group, making it the most direct source for bounding boxes. If the group fails to pack, the width and height are undefined, so the script throws and you can inspect the failing JSON. + +## 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 `` that has the candidate size, check for packing errors, and return the first success. + +```ts +const CANDIDATE_SIZES = [ + { name: "SMALL", width: 21, height: 51 }, + { name: "MEDIUM", width: 24, height: 58 }, +] + +async function findSmallestBoard(load: () => Promise) { + for (const size of CANDIDATE_SIZES) { + const circuit = new RootCircuit() + const Circuit = await load() + + circuit.add( + + + , + ) + + 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 title="carriers/esp32-breakout-carrier.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 }) => ( + + {children} + +) +``` + +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. + +> ℹ️ If your tooling does not yet support `assert { type: "json" }`, generate a thin `.metadata.ts` file that re-exports the JSON contents so regular TypeScript imports still work. + +## Preview: metadata-driven template selection + +The preview below includes a baked metadata file, a small helper that re-exports the JSON, and a carrier component that chooses the smallest template capable of containing the group. Switch between the files to see how the metadata flows through the project. + + ( + + + + + + + +) +`, + "/design-groups/esp32-breakout.metadata.json": `{ + "width": 18.2, + "height": 18.2 +} +`, + "/design-groups/esp32-breakout.metadata.ts": `import json from "./esp32-breakout.metadata.json" assert { type: "json" } + +export const metadata = json as { width: number; height: number } +`, + "/carriers/templates.tsx": `export const BOARD_TEMPLATES = [ + { id: "SMALL", width: 20, height: 20 }, + { id: "MEDIUM", width: 24, height: 24 }, + { id: "LARGE", width: 28, height: 28 }, +] as const +`, + "/carriers/esp32-breakout-carrier.tsx": `import { BOARD_TEMPLATES } from "./templates" +import { metadata } from "../design-groups/esp32-breakout.metadata" +import { Esp32BreakoutGroup } from "../design-groups/esp32-breakout" + +const selectedTemplate = BOARD_TEMPLATES.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 = () => ( + + + + + +) +`, + "/index.tsx": `import { Esp32BreakoutCarrier } from "./carriers/esp32-breakout-carrier" + +export default () => ( + +) +`, + }} + entrypoint="/index.tsx" + mainComponentPath="/carriers/esp32-breakout-carrier.tsx" +/> From 1654cd285d59e532cc8b0681b1bac6396df01d88 Mon Sep 17 00:00:00 2001 From: seveibar Date: Thu, 6 Nov 2025 23:15:46 -0800 Subject: [PATCH 2/2] add first scripting guide about measuring circuit size --- .../measuring-circuit-size.mdx | 195 +++++------------- 1 file changed, 51 insertions(+), 144 deletions(-) rename docs/guides/running-tscircuit/{ => scripting}/measuring-circuit-size.mdx (50%) diff --git a/docs/guides/running-tscircuit/measuring-circuit-size.mdx b/docs/guides/running-tscircuit/scripting/measuring-circuit-size.mdx similarity index 50% rename from docs/guides/running-tscircuit/measuring-circuit-size.mdx rename to docs/guides/running-tscircuit/scripting/measuring-circuit-size.mdx index 3398aeb..d933071 100644 --- a/docs/guides/running-tscircuit/measuring-circuit-size.mdx +++ b/docs/guides/running-tscircuit/scripting/measuring-circuit-size.mdx @@ -1,5 +1,5 @@ --- -title: Measuring circuit size from scripts +title: Measuring circuit size using scripts description: Use @tscircuit/core to evaluate groups, extract bounding boxes, and pick board templates that fit --- @@ -10,7 +10,7 @@ Automating board design often starts with a question: _how much space does this ## 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/core`, and saves a metadata JSON file containing width and height. +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. @@ -31,7 +31,7 @@ my-project/ Each group file should export a React component that renders a `` inside a `` 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 Circuit = () => ( +export const Esp32Breakout = () => ( {/* Components, nets, and layout props */} @@ -42,82 +42,55 @@ export const Circuit = () => ( The snippet below renders a group, waits for placement to settle, and then extracts the PCB group bounds from Circuit JSON. -```ts title="scripts/bake-group-metadata.ts" -import React, { type ComponentType } from "react" -import { RootCircuit } from "@tscircuit/core" -import type { CircuitJson, PcbGroup } from "circuit-json" -import { writeFile } from "node:fs/promises" -import path from "node:path" -import { fileURLToPath } from "node:url" - -const GROUPS = [ - { - name: "esp32-breakout", - load: async () => - (await import("../design-groups/esp32-breakout.tsx")).Circuit, - }, -] - -async function renderGroup(name: string, load: () => Promise) { - const Circuit = await load() - const circuit = new RootCircuit() - - circuit.add( - - - - - , - ) +```tsx +import { RootCircuit } from "tscircuit" +import { Esp32Breakout } from "../design-groups/esp32-breakout" - await circuit.renderUntilSettled() - return (await circuit.getCircuitJson()) as CircuitJson -} - -function getGroupBounds(json: CircuitJson, groupName: string) { - const pcbGroup = json.find( - (element): element is PcbGroup => - element.type === "pcb_group" && element.name === groupName, - ) +const circuit = new RootCircuit() - if (!pcbGroup || pcbGroup.width === undefined || pcbGroup.height === undefined) { - throw new Error( - `Group ${groupName} rendered without width/height. Check the group name and resolve packing issues before baking metadata.`, - ) - } +circuit.add( + +) - return { - width: pcbGroup.width, - height: pcbGroup.height, - center: pcbGroup.center, - } -} +await circuit.renderUntilSettled(); -async function bakeMetadata() { - const scriptDir = path.dirname(fileURLToPath(import.meta.url)) +const circuitJson = await circuit.getCircuitJson(); - for (const entry of GROUPS) { - const circuitJson = await renderGroup(entry.name, entry.load) - const bounds = getGroupBounds(circuitJson, entry.name) +const rootGroupOrBoard = circuitJson.find( + (item) => + item.type === "pcb_board" || + (item.type === "pcb_group" && item.is_subcircuit), +); - const outputPath = path.resolve( - scriptDir, - `../design-groups/${entry.name}.metadata.json`, - ) +Bun.write("../design-groups/esp32-breakout.metadata.json", JSON.stringify(rootGroupOrBoard, null, 2)); +``` - await writeFile(outputPath, JSON.stringify(bounds, null, 2)) - console.log(`Wrote ${outputPath}`) - } +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(); + 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) + ); } - -bakeMetadata().catch((error) => { - console.error(error) - process.exitCode = 1 -}) ``` -The `PcbGroup` element in Circuit JSON contains `width`, `height`, and a `center` point for the packed group, making it the most direct source for bounding boxes. If the group fails to pack, the width and height are undefined, so the script throws and you can inspect the failing JSON. - ## 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. @@ -137,7 +110,7 @@ If `getPackingErrors` returns any elements, skip writing metadata and log the er 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 `` that has the candidate size, check for packing errors, and return the first success. -```ts +```tsx const CANDIDATE_SIZES = [ { name: "SMALL", width: 21, height: 51 }, { name: "MEDIUM", width: 24, height: 58 }, @@ -172,7 +145,7 @@ With this helper you can run multiple passes: one to bake metadata for inspectio 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 title="carriers/esp32-breakout-carrier.tsx" +```tsx import metadata from "../design-groups/esp32-breakout.metadata.json" assert { type: "json" } const TEMPLATE_OPTIONS = [ @@ -198,78 +171,12 @@ export const Esp32BreakoutCarrier = ({ children }: { children: React.ReactNode } 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. -> ℹ️ If your tooling does not yet support `assert { type: "json" }`, generate a thin `.metadata.ts` file that re-exports the JSON contents so regular TypeScript imports still work. +You can add a script inside your package.json file to run the baking script: -## Preview: metadata-driven template selection - -The preview below includes a baked metadata file, a small helper that re-exports the JSON, and a carrier component that chooses the smallest template capable of containing the group. Switch between the files to see how the metadata flows through the project. - - ( - - - - - - - -) -`, - "/design-groups/esp32-breakout.metadata.json": `{ - "width": 18.2, - "height": 18.2 -} -`, - "/design-groups/esp32-breakout.metadata.ts": `import json from "./esp32-breakout.metadata.json" assert { type: "json" } - -export const metadata = json as { width: number; height: number } -`, - "/carriers/templates.tsx": `export const BOARD_TEMPLATES = [ - { id: "SMALL", width: 20, height: 20 }, - { id: "MEDIUM", width: 24, height: 24 }, - { id: "LARGE", width: 28, height: 28 }, -] as const -`, - "/carriers/esp32-breakout-carrier.tsx": `import { BOARD_TEMPLATES } from "./templates" -import { metadata } from "../design-groups/esp32-breakout.metadata" -import { Esp32BreakoutGroup } from "../design-groups/esp32-breakout" - -const selectedTemplate = BOARD_TEMPLATES.find( - (option) => - option.width >= metadata.width && option.height >= metadata.height, -) - -if (!selectedTemplate) { - throw new Error("No board template can fit the baked group bounds") +```json +{ + "scripts": { + "bake-metadata": "bun run scripts/bake-metadata.ts" + } } - -export const Esp32BreakoutCarrier = () => ( - - - - - -) -`, - "/index.tsx": `import { Esp32BreakoutCarrier } from "./carriers/esp32-breakout-carrier" - -export default () => ( - -) -`, - }} - entrypoint="/index.tsx" - mainComponentPath="/carriers/esp32-breakout-carrier.tsx" -/> +``` \ No newline at end of file