tscircuit/ti
ti tscircuit library
- Version
- 1.0.198
- License
- unset
- Stars
- 0
system-block-ui/src/rendering/evaluate-schematic.ts
import webWorkerBlobUrl from "@tscircuit/eval/blob-url";
import { createCircuitWebWorker } from "@tscircuit/eval/worker";
import type { AnyCircuitElement } from "circuit-json";
import { assertCircuitJsonHasNoErrors } from "./circuit-json-errors";
import {
type EvaluatedSchematicSheet,
renderSchematicSheets,
type SchematicSvgOptions,
} from "./render-schematic-sheets";
export {
CircuitJsonEvaluationError,
getCircuitJsonErrors,
} from "./circuit-json-errors";
export {
type EvaluatedSchematicSheet,
renderSchematicSheets,
type SchematicSvgOptions,
} from "./render-schematic-sheets";
export const DEFAULT_EVALUATION_TIMEOUT_MS = 30_000;
export const DEFAULT_MAIN_COMPONENT_PATH = "generated-system.tsx";
export const PREVIEW_EVALUATION_PLATFORM = {
pcbDisabled: false,
routingDisabled: true,
partsEngineDisabled: true,
drcChecksDisabled: true,
analogSimulationDisabled: true,
} as const;
const NON_FATAL_PREVIEW_ERROR_TYPES = [
"pcb_missing_footprint_error",
// The schematic remains valid when a remote KiCad library footprint is not
// present in the cache. ECAD export keeps that source component as a
// footprint placeholder, matching the existing missing-footprint behavior.
"external_footprint_load_error",
] as const;
export interface EvaluateGeneratedTsxOptions {
/** Maximum time allowed for worker creation, evaluation, and rendering. */
timeoutMs?: number;
/** Virtual path used for the generated default-exported component. */
mainComponentPath?: string;
/** Extra virtual files needed by relative imports in the generated TSX. */
fsMap?: Readonly<Record<string, string>>;
/** Options forwarded to circuit-to-svg's schematic renderer. */
schematicOptions?: SchematicSvgOptions;
}
export interface EvaluatedSchematic {
circuitJson: AnyCircuitElement[];
sheets: EvaluatedSchematicSheet[];
}
export class SchematicEvaluationTimeoutError extends Error {
readonly timeoutMs: number;
constructor(timeoutMs: number) {
super(`Schematic evaluation timed out after ${timeoutMs}ms`);
this.name = "SchematicEvaluationTimeoutError";
this.timeoutMs = timeoutMs;
}
}
const validateTimeout = (timeoutMs: number): void => {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
throw new RangeError("timeoutMs must be a positive finite number");
}
};
const validateMainComponentPath = (path: string): void => {
if (path.trim().length === 0) {
throw new TypeError("mainComponentPath must not be empty");
}
};
/**
* Evaluates generated, default-exported tscircuit TSX in an isolated worker and
* returns footprint-bearing Circuit JSON plus one schematic SVG per sheet.
*/
export async function evaluateGeneratedTsx(
tsx: string,
options: EvaluateGeneratedTsxOptions = {},
): Promise<EvaluatedSchematic> {
const timeoutMs = options.timeoutMs ?? DEFAULT_EVALUATION_TIMEOUT_MS;
const mainComponentPath =
options.mainComponentPath ?? DEFAULT_MAIN_COMPONENT_PATH;
validateTimeout(timeoutMs);
validateMainComponentPath(mainComponentPath);
let worker: Awaited<ReturnType<typeof createCircuitWebWorker>> | undefined;
let killPromise: Promise<void> | undefined;
let timedOut = false;
const killWorker = (): Promise<void> => {
if (!worker) return Promise.resolve();
killPromise ??= worker.kill().catch(() => undefined);
return killPromise;
};
const evaluation = (async (): Promise<AnyCircuitElement[]> => {
worker = await createCircuitWebWorker({
webWorkerBlobUrl,
// PCB component and pad generation is required by the ECAD exporters.
// Routing, external parts lookup, simulation, and DRC are unnecessary for
// the system preview and are skipped to keep evaluation responsive.
platform: PREVIEW_EVALUATION_PLATFORM,
});
// A worker can finish initializing just after the timeout fires. Kill that
// late worker before it can begin evaluating the generated source.
if (timedOut) {
await killWorker();
throw new SchematicEvaluationTimeoutError(timeoutMs);
}
await worker.executeWithFsMap({
fsMap: {
...options.fsMap,
[mainComponentPath]: tsx,
},
mainComponentPath,
});
await worker.renderUntilSettled();
return await worker.getCircuitJson();
})();
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => {
timedOut = true;
void killWorker();
reject(new SchematicEvaluationTimeoutError(timeoutMs));
}, timeoutMs);
});
try {
const circuitJson = await Promise.race([evaluation, timeout]);
// Schematic-only blocks may not have selected physical packages yet. Keep
// their PCB component records available to exporters without preventing a
// schematic preview; every other evaluation error remains fatal.
assertCircuitJsonHasNoErrors(circuitJson, {
ignoredErrorTypes: NON_FATAL_PREVIEW_ERROR_TYPES,
});
const sheets = renderSchematicSheets(circuitJson, options.schematicOptions);
return { circuitJson, sheets };
} finally {
if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
await killWorker();
}
}