Skip to content
Open
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
53 changes: 8 additions & 45 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion lib/components/BomTable/BomTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { convertCircuitJsonToBomRows } from "circuit-json-to-bom-csv"
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,6 +16,7 @@ export const BomTable: React.FC<BomTableProps> = ({ circuitJson }) => {
setError(null)
setRows(null)

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
161 changes: 161 additions & 0 deletions lib/optional-features/exporting/dynamic-converters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
type ConverterModuleImporter = <TModule>(specifier: string) => Promise<TModule>

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 RunnableStringConverter = new (
...args: any[]
) => {
runUntilFinished: () => void
getOutputString: () => string
}

type KicadPcbConverter = new (
...args: any[]
) => {
runUntilFinished: () => void
getOutputString: () => string
getModel3dSourcePaths: () => string[]
}

type KicadLibraryConverter = new (
...args: any[]
) => {
runUntilFinished: () => void
getOutput: () => {
kicadSymString: string
footprints: Array<{
footprintName: string
kicadModString: string
}>
model3dSourcePaths: string[]
fpLibTableString: string
symLibTableString: string
}
}

type KicadConverterModule = {
CircuitJsonToKicadPcbConverter: KicadPcbConverter
CircuitJsonToKicadSchConverter: RunnableStringConverter
CircuitJsonToKicadProConverter: RunnableStringConverter
CircuitJsonToKicadLibraryConverter: KicadLibraryConverter
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 }>
}

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

const CONVERTER_CDN_URLS: Record<ConverterPackageName, string> = {
"circuit-json-to-gerber":
"https://cdn.jsdelivr.net/npm/circuit-json-to-gerber@0.0.56/+esm",
"circuit-json-to-bom-csv":
"https://cdn.jsdelivr.net/npm/circuit-json-to-bom-csv@0.0.8/+esm",
"circuit-json-to-pnp-csv":
"https://cdn.jsdelivr.net/npm/circuit-json-to-pnp-csv@0.0.7/+esm",
"circuit-json-to-kicad":
"https://cdn.jsdelivr.net/npm/circuit-json-to-kicad@0.0.137/+esm",
"circuit-json-to-gltf":
"https://cdn.jsdelivr.net/npm/circuit-json-to-gltf@0.0.100/+esm",
"circuit-json-to-step":
"https://cdn.jsdelivr.net/npm/circuit-json-to-step@0.0.28/+esm",
"circuit-json-to-lbrn":
"https://cdn.jsdelivr.net/npm/circuit-json-to-lbrn@0.0.74/+esm",
}

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()
}

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),
).catch((error) => {
converterModulePromises.delete(packageName)
throw error
})
converterModulePromises.set(
packageName,
modulePromise as Promise<ConverterModules[ConverterPackageName]>,
)
}

return modulePromise
}

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")
28 changes: 28 additions & 0 deletions lib/optional-features/exporting/dynamic-converters1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "bun:test"
import { getConverterCdnUrl } from "./dynamic-converters"

describe("dynamic converter URLs", () => {
it("uses pinned jsDelivr ESM URLs", () => {
expect(getConverterCdnUrl("circuit-json-to-gerber")).toBe(
"https://cdn.jsdelivr.net/npm/circuit-json-to-gerber@0.0.56/+esm",
)
expect(getConverterCdnUrl("circuit-json-to-bom-csv")).toBe(
"https://cdn.jsdelivr.net/npm/circuit-json-to-bom-csv@0.0.8/+esm",
)
expect(getConverterCdnUrl("circuit-json-to-pnp-csv")).toBe(
"https://cdn.jsdelivr.net/npm/circuit-json-to-pnp-csv@0.0.7/+esm",
)
expect(getConverterCdnUrl("circuit-json-to-kicad")).toBe(
"https://cdn.jsdelivr.net/npm/circuit-json-to-kicad@0.0.137/+esm",
)
expect(getConverterCdnUrl("circuit-json-to-gltf")).toBe(
"https://cdn.jsdelivr.net/npm/circuit-json-to-gltf@0.0.100/+esm",
)
expect(getConverterCdnUrl("circuit-json-to-step")).toBe(
"https://cdn.jsdelivr.net/npm/circuit-json-to-step@0.0.28/+esm",
)
expect(getConverterCdnUrl("circuit-json-to-lbrn")).toBe(
"https://cdn.jsdelivr.net/npm/circuit-json-to-lbrn@0.0.74/+esm",
)
})
})
56 changes: 56 additions & 0 deletions lib/optional-features/exporting/dynamic-converters2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, expect, it } from "bun:test"
import {
clearConverterModuleCache,
loadConverterModule,
} from "./dynamic-converters"

describe("dynamic converter loading", () => {
it("caches successful loads and clears failed loads for retry", async () => {
clearConverterModuleCache()

const importedSpecifiers: string[] = []
const successfulImporter = async <TModule>(specifier: string) => {
importedSpecifiers.push(specifier)
return {
convertCircuitJsonToBomRows: () => [],
convertBomRowsToCsv: () => "",
} as TModule
}

const [firstLoad, secondLoad] = await Promise.all([
loadConverterModule("circuit-json-to-bom-csv", successfulImporter),
loadConverterModule("circuit-json-to-bom-csv", successfulImporter),
])

expect(firstLoad).toBe(secondLoad)
expect(importedSpecifiers).toEqual([
"https://cdn.jsdelivr.net/npm/circuit-json-to-bom-csv@0.0.8/+esm",
])

clearConverterModuleCache()

let attempts = 0
const flakyImporter = async <TModule>() => {
attempts += 1
if (attempts === 1) throw new Error("temporary CDN failure")
return {
convertCircuitJsonToBomRows: () => [],
convertBomRowsToCsv: () => "",
} as TModule
}

await expect(
loadConverterModule("circuit-json-to-bom-csv", flakyImporter),
).rejects.toThrow("temporary CDN failure")

await expect(
loadConverterModule("circuit-json-to-bom-csv", flakyImporter),
).resolves.toEqual({
convertCircuitJsonToBomRows: expect.any(Function),
convertBomRowsToCsv: expect.any(Function),
})
expect(attempts).toBe(2)

clearConverterModuleCache()
})
})
43 changes: 23 additions & 20 deletions lib/optional-features/exporting/formats/export-fabrication-files.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { AnyCircuitElement } from "circuit-json"
import JSZip from "jszip"
import importer from "@tscircuit/internal-dynamic-import"
import {
convertCircuitJsonToBomRows,
convertBomRowsToCsv,
} from "circuit-json-to-bom-csv"
import { convertCircuitJsonToPickAndPlaceCsv } from "circuit-json-to-pnp-csv"
loadBomCsvConverter,
loadGerberConverter,
loadPnpCsvConverter,
} from "../dynamic-converters"
import { openForDownload } from "../open-for-download"

export const exportFabricationFiles = async ({
Expand All @@ -17,44 +16,48 @@ export const exportFabricationFiles = async ({
}) => {
const zip = new JSZip()

const {
stringifyGerberCommandLayers,
convertSoupToGerberCommands,
convertSoupToExcellonDrillCommands,
stringifyExcellonDrill,
} = await importer("circuit-json-to-gerber")
const [gerberConverter, bomCsvConverter, pnpCsvConverter] = await Promise.all(
[loadGerberConverter(), loadBomCsvConverter(), loadPnpCsvConverter()],
)

// Filter out error and warning elements for gerber/drill generation
const filteredCircuitJson = circuitJson.filter(
(element) => !("error_type" in element) && !("warning_type" in element),
) as any

// Generate Gerber files
const gerberLayerCmds = convertSoupToGerberCommands(filteredCircuitJson, {
flip_y_axis: false,
})
const gerberFileContents = stringifyGerberCommandLayers(gerberLayerCmds)
const gerberLayerCmds = gerberConverter.convertSoupToGerberCommands(
filteredCircuitJson,
{
flip_y_axis: false,
},
)
const gerberFileContents =
gerberConverter.stringifyGerberCommandLayers(gerberLayerCmds)

for (const [fileName, fileContents] of Object.entries(gerberFileContents)) {
zip.file(`gerber/${fileName}.gbr`, fileContents)
}

// Generate Drill files
const drillCmds = convertSoupToExcellonDrillCommands({
const drillCmds = gerberConverter.convertSoupToExcellonDrillCommands({
circuitJson: filteredCircuitJson,
is_plated: true,
flip_y_axis: false,
})
const drillFileContents = stringifyExcellonDrill(drillCmds)
const drillFileContents = gerberConverter.stringifyExcellonDrill(drillCmds)
zip.file("gerber/drill.drl", drillFileContents)

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

// Generate Pick and Place CSV
const pnpCsv = await convertCircuitJsonToPickAndPlaceCsv(circuitJson)
const pnpCsv =
await pnpCsvConverter.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
Loading
Loading