Skip to content
Closed
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
148 changes: 7 additions & 141 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 2 additions & 4 deletions lib/components/BomTable/BomTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import importer from "@tscircuit/internal-dynamic-import"
import { useEffect, useState } from "react"
import type React from "react"
import { loadBomCsvConverter } from "../../optional-features/exporting/dynamic-converters"
import { getBomCellDescriptors, getBomMetadata } from "./bom-table.columns"
import type { BomRow, BomTableProps } from "./bom-table.types"

Expand All @@ -16,9 +16,7 @@ export const BomTable: React.FC<BomTableProps> = ({ circuitJson }) => {
setError(null)
setRows(null)

const { convertCircuitJsonToBomRows } = await importer(
"circuit-json-to-bom-csv",
)
const { convertCircuitJsonToBomRows } = await loadBomCsvConverter()
const bomRows = await convertCircuitJsonToBomRows({
circuitJson,
})
Expand Down
16 changes: 12 additions & 4 deletions lib/components/BomTable/bom-table.types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import type { AnyCircuitElement } from "circuit-json"
import type { convertCircuitJsonToBomRows } from "circuit-json-to-bom-csv"
import type React from "react"

export interface BomTableProps {
circuitJson: AnyCircuitElement[]
}

export type BomRow = Awaited<
ReturnType<typeof convertCircuitJsonToBomRows>
>[number]
export type BomRow = {
designator?: string
comment?: string
value?: string
footprint?: string
supplier_part_number_columns?: Record<string, string>
extra_columns?: Record<string, string>
manufacturer_mpn_pairs?: Array<{
manufacturer: string
mpn: string
}>
}

export type BomMetadata = {
extraColumnNames: string[]
Expand Down
204 changes: 204 additions & 0 deletions lib/optional-features/exporting/dynamic-converters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* Centralized dynamic converter loader.
*
* Instead of bundling heavy converter packages, each converter is lazy-loaded
* on demand from the jsdelivr CDN when the user actually requests an export.
* Modules are cached after first successful load; failed loads are cleared
* so a retry can succeed.
*/

// ---------------------------------------------------------------------------
// Module type declarations
// ---------------------------------------------------------------------------

type GerberConverterModule = {
stringifyGerberCommandLayers: (...args: any[]) => Record<string, string>
convertSoupToGerberCommands: (...args: any[]) => any
convertSoupToExcellonDrillCommands: (...args: any[]) => any
stringifyExcellonDrill: (...args: any[]) => string
}

type BomCsvConverterModule = {
convertCircuitJsonToBomRows: (...args: any[]) => Promise<any[]> | any[]
convertBomRowsToCsv: (...args: any[]) => Promise<string> | string
}

type PnpCsvConverterModule = {
convertCircuitJsonToPickAndPlaceCsv: (
...args: any[]
) => Promise<string> | string
}

type KicadConverterModule = {
CircuitJsonToKicadPcbConverter: new (
...args: any[]
) => {
runUntilFinished: () => void
getOutputString: () => string
getModel3dSourcePaths: () => string[]
}
CircuitJsonToKicadSchConverter: new (
...args: any[]
) => {
runUntilFinished: () => void
getOutputString: () => string
}
CircuitJsonToKicadProConverter: new (
...args: any[]
) => {
runUntilFinished: () => void
getOutputString: () => string
}
CircuitJsonToKicadLibraryConverter: new (
...args: any[]
) => {
runUntilFinished: () => void
getOutput: () => {
kicadSymString: string
footprints: Array<{ footprintName: string; kicadModString: string }>
model3dSourcePaths: string[]
fpLibTableString: string
symLibTableString: string
}
}
resolveAndLoadKicad3dModelFiles: (...args: any[]) => Promise<void>
}

type GltfConverterModule = {
convertCircuitJsonToGltf: (
...args: any[]
) => Promise<ArrayBuffer> | ArrayBuffer
}

type StepConverterModule = {
circuitJsonToStep: (...args: any[]) => Promise<string> | string
}

type LbrnConverterModule = {
convertCircuitJsonToLbrn: (...args: any[]) => Promise<{ toXml: () => string }>
}

// ---------------------------------------------------------------------------
// Package → type mapping
// ---------------------------------------------------------------------------

type ConverterModules = {
"circuit-json-to-gerber": GerberConverterModule
"circuit-json-to-bom-csv": BomCsvConverterModule
"circuit-json-to-pnp-csv": PnpCsvConverterModule
"circuit-json-to-kicad": KicadConverterModule
"circuit-json-to-gltf": GltfConverterModule
"circuit-json-to-step": StepConverterModule
"circuit-json-to-lbrn": LbrnConverterModule
}

export type ConverterPackageName = keyof ConverterModules

// ---------------------------------------------------------------------------
// Pinned CDN URLs (jsdelivr ESM)
// ---------------------------------------------------------------------------

const CONVERTER_CDN_URLS: Record<ConverterPackageName, string> = {
"circuit-json-to-gerber":
"https://cdn.jsdelivr.net/npm/circuit-json-to-gerber@0.0.76/+esm",
"circuit-json-to-bom-csv":
"https://cdn.jsdelivr.net/npm/circuit-json-to-bom-csv@0.0.9/+esm",
"circuit-json-to-pnp-csv":
"https://cdn.jsdelivr.net/npm/circuit-json-to-pnp-csv@0.0.8/+esm",
"circuit-json-to-kicad":
"https://cdn.jsdelivr.net/npm/circuit-json-to-kicad@0.0.147/+esm",
"circuit-json-to-gltf":
"https://cdn.jsdelivr.net/npm/circuit-json-to-gltf@0.0.102/+esm",
"circuit-json-to-step":
"https://cdn.jsdelivr.net/npm/circuit-json-to-step@0.0.33/+esm",
"circuit-json-to-lbrn":
"https://cdn.jsdelivr.net/npm/circuit-json-to-lbrn@0.0.82/+esm",
}

// ---------------------------------------------------------------------------
// Loader with singleton-promise cache + failure retry
// ---------------------------------------------------------------------------

export type ConverterModuleImporter = <TModule>(
specifier: string,
) => Promise<TModule>

declare global {
var tscircuitDynamicModules: Record<string, unknown> | undefined
}

const converterModulePromises = new Map<
ConverterPackageName,
Promise<ConverterModules[ConverterPackageName]>
>()

const importConverterModule: ConverterModuleImporter = (specifier) =>
import(/* @vite-ignore */ specifier)

export const getConverterCdnUrl = (packageName: ConverterPackageName) =>
CONVERTER_CDN_URLS[packageName]

export const clearConverterModuleCache = () => {
converterModulePromises.clear()
}

const registerConverterModule = <TName extends ConverterPackageName>(
packageName: TName,
module: ConverterModules[TName],
) => {
globalThis.tscircuitDynamicModules ??= {}
globalThis.tscircuitDynamicModules[packageName] = module
return module
}

export const loadConverterModule = async <TName extends ConverterPackageName>(
packageName: TName,
importer: ConverterModuleImporter = importConverterModule,
): Promise<ConverterModules[TName]> => {
let modulePromise = converterModulePromises.get(packageName) as
| Promise<ConverterModules[TName]>
| undefined

if (!modulePromise) {
modulePromise = importer<ConverterModules[TName]>(
getConverterCdnUrl(packageName),
)
.then((module) => registerConverterModule(packageName, module))
.catch((error) => {
// Clear cache on failure so next call retries
converterModulePromises.delete(packageName)
throw error
})
converterModulePromises.set(
packageName,
modulePromise as Promise<ConverterModules[ConverterPackageName]>,
)
}

return modulePromise
}

// ---------------------------------------------------------------------------
// Convenience loaders (one per converter)
// ---------------------------------------------------------------------------

export const loadGerberConverter = () =>
loadConverterModule("circuit-json-to-gerber")

export const loadBomCsvConverter = () =>
loadConverterModule("circuit-json-to-bom-csv")

export const loadPnpCsvConverter = () =>
loadConverterModule("circuit-json-to-pnp-csv")

export const loadKicadConverter = () =>
loadConverterModule("circuit-json-to-kicad")

export const loadGltfConverter = () =>
loadConverterModule("circuit-json-to-gltf")

export const loadStepConverter = () =>
loadConverterModule("circuit-json-to-step")

export const loadLbrnConverter = () =>
loadConverterModule("circuit-json-to-lbrn")
36 changes: 19 additions & 17 deletions lib/optional-features/exporting/formats/export-fabrication-files.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import importer from "@tscircuit/internal-dynamic-import"
import type { AnyCircuitElement } from "circuit-json"
import JSZip from "jszip"
import { toast } from "lib/utils/toast"
import {
loadGerberConverter,
loadBomCsvConverter,
loadPnpCsvConverter,
} from "../dynamic-converters"
import { openForDownload } from "../open-for-download"

export const exportFabricationFiles = async ({
Expand All @@ -15,20 +19,17 @@ export const exportFabricationFiles = async ({
(async () => {
const zip = new JSZip()

const [
{
stringifyGerberCommandLayers,
convertSoupToGerberCommands,
convertSoupToExcellonDrillCommands,
stringifyExcellonDrill,
},
{ convertCircuitJsonToBomRows, convertBomRowsToCsv },
{ convertCircuitJsonToPickAndPlaceCsv },
] = await Promise.all([
importer("circuit-json-to-gerber"),
importer("circuit-json-to-bom-csv"),
importer("circuit-json-to-pnp-csv"),
const [gerberMod, bomMod, pnpMod] = await Promise.all([
loadGerberConverter(),
loadBomCsvConverter(),
loadPnpCsvConverter(),
])
const {
stringifyGerberCommandLayers,
convertSoupToGerberCommands,
convertSoupToExcellonDrillCommands,
stringifyExcellonDrill,
} = gerberMod

// Filter out error and warning elements for gerber/drill generation
const filteredCircuitJson = circuitJson.filter(
Expand Down Expand Up @@ -57,12 +58,13 @@ export const exportFabricationFiles = async ({
zip.file("gerber/drill.drl", drillFileContents)

// Generate BOM CSV
const bomRows = await convertCircuitJsonToBomRows({ circuitJson })
const bomCsv = await convertBomRowsToCsv(bomRows)
const bomRows = await bomMod.convertCircuitJsonToBomRows({ circuitJson })
const bomCsv = await bomMod.convertBomRowsToCsv(bomRows)
zip.file("bom.csv", bomCsv)

// Generate Pick and Place CSV
const pnpCsv = await convertCircuitJsonToPickAndPlaceCsv(circuitJson)
const pnpCsv =
await pnpMod.convertCircuitJsonToPickAndPlaceCsv(circuitJson)
zip.file("pick_and_place.csv", pnpCsv)

// Generate and download the zip file
Expand Down
4 changes: 2 additions & 2 deletions lib/optional-features/exporting/formats/export-glb.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CircuitJson } from "circuit-json"
import { openForDownload } from "../open-for-download"
import importer from "@tscircuit/internal-dynamic-import"
import { loadGltfConverter } from "../dynamic-converters"

export const exportGlb = async ({
circuitJson,
Expand All @@ -11,7 +11,7 @@ export const exportGlb = async ({
}) => {
let blob: Blob
try {
const { convertCircuitJsonToGltf } = await importer("circuit-json-to-gltf")
const { convertCircuitJsonToGltf } = await loadGltfConverter()

console.log("convertCircuitJsonToGltf", convertCircuitJsonToGltf)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import importer from "@tscircuit/internal-dynamic-import"
import type { CircuitJson } from "circuit-json"
import JSZip from "jszip"
import { toast } from "lib/utils/toast"
import { loadKicadConverter } from "../dynamic-converters"
import { openForDownload } from "../open-for-download"

export const createKicadLibraryZip = async ({
Expand All @@ -11,9 +11,7 @@ export const createKicadLibraryZip = async ({
circuitJson: CircuitJson
libraryName: string
}) => {
const { CircuitJsonToKicadLibraryConverter } = await importer(
"circuit-json-to-kicad",
)
const { CircuitJsonToKicadLibraryConverter } = await loadKicadConverter()
const libConverter = new CircuitJsonToKicadLibraryConverter(
circuitJson as any,
{
Expand Down
14 changes: 10 additions & 4 deletions lib/optional-features/exporting/formats/export-kicad-project.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import importer from "@tscircuit/internal-dynamic-import"
import type { CircuitJson } from "circuit-json"
import JSZip from "jszip"
import { toast } from "lib/utils/toast"
import { loadKicadConverter } from "../dynamic-converters"
import { openForDownload } from "../open-for-download"

export const createKicadProjectZip = async ({
Expand All @@ -16,7 +16,7 @@ export const createKicadProjectZip = async ({
CircuitJsonToKicadSchConverter,
CircuitJsonToKicadProConverter,
resolveAndLoadKicad3dModelFiles,
} = await importer("circuit-json-to-kicad")
} = await loadKicadConverter()
const schConverter = new CircuitJsonToKicadSchConverter(circuitJson as any)
schConverter.runUntilFinished()
const schContent = schConverter.getOutputString()
Expand Down Expand Up @@ -46,10 +46,16 @@ export const createKicadProjectZip = async ({
projectName,
model3dSourcePaths: pcbConverter.getModel3dSourcePaths(),
fetch,
onModelFile: ({ outputPath, content }) => {
onModelFile: ({
outputPath,
content,
}: {
outputPath: string
content: string | ArrayBuffer | Blob | Uint8Array
}) => {
zip.file(outputPath, content)
},
onError: ({ sourcePath }) => {
onError: ({ sourcePath }: { sourcePath: string }) => {
console.warn(`Failed to load 3D model from ${sourcePath}`)
},
})
Expand Down
Loading