Skip to content

Commit

Permalink
Merge pull request #60 from tscircuit/new-fp-gen-mech
Browse files Browse the repository at this point in the history
New Footprint tsx Generation Mechanism for more stable units, support for <hole />
  • Loading branch information
seveibar authored Oct 12, 2024
2 parents 25af6d7 + e4d27ec commit 3bbcdd3
Show file tree
Hide file tree
Showing 13 changed files with 107 additions and 94 deletions.
2 changes: 1 addition & 1 deletion benchmark/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async function benchmark() {
await convertEasyEdaJsonToVariousFormats({
jlcpcbPartNumberOrFilepath: partnumber,
outputFilename: "temp.tsx",
formatType: "tsx",
outputFormat: "tsx",
})
successes++
} catch (error) {
Expand Down
Binary file modified bun.lockb
Binary file not shown.
7 changes: 3 additions & 4 deletions cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import { convertEasyEdaJsonToTscircuitSoupJson } from "../lib/convert-easyeda-js
import fs from "fs/promises"
import packageJson from "../package.json"
import { EasyEdaJsonSchema } from "lib/schemas/easy-eda-json-schema"
import { convertRawEasyEdaToTs } from "lib/convert-to-typescript-component"
import { convertRawEasyToTsx } from "lib/convert-to-typescript-component"
import * as path from "path"
import { normalizeManufacturerPartNumber } from "lib"
import { convertEasyEdaJsonToVariousFormats } from "lib/convert-easyeda-json-to-various-formats"
import { perfectCli } from "perfect-cli"

const program = new Command()

Expand Down Expand Up @@ -57,5 +56,5 @@ program
}
})

// program.parse(process.argv)
perfectCli(program, process.argv)
program.parse(process.argv)
// perfectCli(program, process.argv)
15 changes: 10 additions & 5 deletions lib/convert-easyeda-json-to-tscircuit-soup-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from "circuit-json"
import * as Soup from "circuit-json"
import { generateArcFromSweep, generateArcPathWithMid } from "./math/arc-utils"
import { transformPCBElements } from "@tscircuit/soup-util"
import { findBoundsAndCenter, transformPCBElements } from "@tscircuit/soup-util"
import { compose, scale, translate } from "transformation-matrix"
import { computeCenterOffset } from "./compute-center-offset"
import { mm } from "@tscircuit/mm"
Expand Down Expand Up @@ -123,10 +123,10 @@ export const convertEasyEdaJsonToCircuitJson = (
source_component_id: "source_component_1",
name: "U1",
ftype: "simple_bug",
width: 0, // TODO compute width
height: 0, // TODO compute height
width: 0, // we update this at the end
height: 0, // we update this at the end
rotation: 0,
center: { x: centerOffset.x, y: centerOffset.y },
center: { x: 0, y: 0 },
layer: "top",
} as PcbComponentInput)

Expand Down Expand Up @@ -255,9 +255,14 @@ export const convertEasyEdaJsonToCircuitJson = (
}

if (shouldRecenter) {
const bounds = findBoundsAndCenter(
// exclude the pcb_component because it's center is currently incorrect,
// we set it to (0,0)
soupElements.filter((e) => e.type !== "pcb_component"),
)
transformPCBElements(
soupElements,
compose(translate(-centerOffset.x, centerOffset.y), scale(1, -1)),
compose(translate(-bounds.center.x, bounds.center.y), scale(1, -1)),
)
}

Expand Down
4 changes: 2 additions & 2 deletions lib/convert-easyeda-json-to-various-formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { fetchEasyEDAComponent } from "../lib/fetch-easyeda-json"
import { convertEasyEdaJsonToTscircuitSoupJson } from "../lib/convert-easyeda-json-to-tscircuit-soup-json"
import fs from "fs/promises"
import { EasyEdaJsonSchema } from "lib/schemas/easy-eda-json-schema"
import { convertRawEasyEdaToTs } from "lib/convert-to-typescript-component"
import { convertRawEasyToTsx } from "lib/convert-to-typescript-component"
import * as path from "path"
import { normalizeManufacturerPartNumber } from "lib"

Expand Down Expand Up @@ -83,7 +83,7 @@ export const convertEasyEdaJsonToVariousFormats = async ({
outputFilename.endsWith(".tsx") ||
outputFilename.endsWith(".ts")
) {
const tsComp = await convertRawEasyEdaToTs(rawEasyEdaJson)
const tsComp = await convertRawEasyToTsx(rawEasyEdaJson)
await fs.writeFile(outputFilename, tsComp)
console.log(
`[${jlcpcbPartNumberOrFilepath}] Saved TypeScript component: ${outputFilename}`,
Expand Down
39 changes: 20 additions & 19 deletions lib/convert-to-typescript-component/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,36 @@ import {
} from "lib/schemas/easy-eda-json-schema"
import { su } from "@tscircuit/soup-util"
import { soupTypescriptComponentTemplate } from "./soup-typescript-component-template"
import { convertEasyEdaJsonToTscircuitSoupJson } from "lib/convert-easyeda-json-to-tscircuit-soup-json"
import {
convertEasyEdaJsonToCircuitJson,
convertEasyEdaJsonToTscircuitSoupJson,
} from "lib/convert-easyeda-json-to-tscircuit-soup-json"
import { normalizeManufacturerPartNumber } from "lib/utils/normalize-manufacturer-part-number"

export const convertRawEasyEdaToTs = async (rawEasy: any) => {
const easyeda = EasyEdaJsonSchema.parse(rawEasy)
const soup = convertEasyEdaJsonToTscircuitSoupJson(easyeda, {
useModelCdn: true,
})
const result = await convertToTypescriptComponent({
easyeda,
soup,
export const convertRawEasyToTsx = async (rawEasy: any) => {
const betterEasy = EasyEdaJsonSchema.parse(rawEasy)
const result = await convertBetterEasyToTsx({
betterEasy,
})
return result
}

export const convertToTypescriptComponent = async ({
soup,
easyeda: easyEdaJson,
export const convertBetterEasyToTsx = async ({
betterEasy,
}: {
soup: AnyCircuitElement[]
easyeda: BetterEasyEdaJson
betterEasy: BetterEasyEdaJson
}): Promise<string> => {
const rawPn = easyEdaJson.dataStr.head.c_para["Manufacturer Part"]
const circuitJson = convertEasyEdaJsonToCircuitJson(betterEasy, {
useModelCdn: true,
shouldRecenter: true,
})
const rawPn = betterEasy.dataStr.head.c_para["Manufacturer Part"]
const pn = normalizeManufacturerPartNumber(rawPn)
const [cad_component] = su(soup as any).cad_component.list()
const [cad_component] = su(circuitJson).cad_component.list()

// Derive pinLabels from easyeda json
const pinLabels: Record<string, string> = {}
easyEdaJson.dataStr.shape
betterEasy.dataStr.shape
.filter((shape) => shape.type === "PIN")
.forEach((pin) => {
const isPinLabelNumeric = /^\d+$/.test(pin.label)
Expand All @@ -43,7 +44,7 @@ export const convertToTypescriptComponent = async ({
})

// Derive schPinArrangement from easyeda json
const pins = easyEdaJson.dataStr.shape.filter((shape) => shape.type === "PIN")
const pins = betterEasy.dataStr.shape.filter((shape) => shape.type === "PIN")
const leftPins = pins.filter((pin) => pin.rotation === 180)
const rightPins = pins.filter((pin) => pin.rotation === 0)

Expand Down Expand Up @@ -73,7 +74,7 @@ export const convertToTypescriptComponent = async ({
pinLabels,
schPinArrangement,
objUrl: modelObjUrl,
easyEdaJson,
circuitJson,
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import type { AnySoupElement } from "circuit-json"
import type { AnyCircuitElement, AnySoupElement } from "circuit-json"
import type { ChipProps } from "@tscircuit/props"
import { su } from "@tscircuit/soup-util"
import type { BetterEasyEdaJson } from "../schemas/easy-eda-json-schema"
import { generateFootprintTsx } from "../generate-footprint-tsx"
import { convertEasyEdaJsonToCircuitJson } from "lib/convert-easyeda-json-to-tscircuit-soup-json"

interface Params {
pinLabels: ChipProps["pinLabels"]
componentName: string
schPinArrangement: ChipProps["schPortArrangement"]
objUrl?: string
easyEdaJson: BetterEasyEdaJson
circuitJson: AnyCircuitElement[]
}

export const soupTypescriptComponentTemplate = ({
pinLabels,
componentName,
schPinArrangement,
objUrl,
easyEdaJson,
circuitJson,
}: Params) => {
const footprintTsx = generateFootprintTsx(easyEdaJson)
const footprintTsx = generateFootprintTsx(circuitJson)
return `
import { createUseComponent } from "@tscircuit/core"
import type { CommonLayoutProps } from "@tscircuit/props"
Expand All @@ -39,7 +40,8 @@ export const ${componentName} = (props: Props) => {
${
objUrl
? `cadModel={{
objUrl: "${objUrl}"
objUrl: "${objUrl}",
rotationOffset: { x: 0, y: 0, z: 0 }
}}`
: ""
}
Expand Down
89 changes: 53 additions & 36 deletions lib/generate-footprint-tsx.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,70 @@
import { z } from "zod"
import type { BetterEasyEdaJson } from "./schemas/easy-eda-json-schema"
import { PadSchema } from "./schemas/package-detail-shape-schema"
import type {
HoleSchema,
PadSchema,
} from "./schemas/package-detail-shape-schema"
import { computeCenterOffset } from "./compute-center-offset"
import { mm } from "@tscircuit/mm"
import { mm, mmStr } from "@tscircuit/mm"
import { convertEasyEdaJsonToTscircuitSoupJson } from "./convert-easyeda-json-to-tscircuit-soup-json"
import type { AnyCircuitElement } from "circuit-json"
import { su } from "@tscircuit/soup-util"

export const generateFootprintTsx = (
easyEdaJson: BetterEasyEdaJson,
circuitJson: AnyCircuitElement[],
): string => {
const pads = easyEdaJson.packageDetail.dataStr.shape.filter(
(shape): shape is z.infer<typeof PadSchema> => shape.type === "PAD",
)
const holes = su(circuitJson).pcb_hole.list()
const platedHoles = su(circuitJson).pcb_plated_hole.list()
const smtPads = su(circuitJson).pcb_smtpad.list()
const silkscreenPaths = su(circuitJson).pcb_silkscreen_path.list()

const centerOffset = computeCenterOffset(easyEdaJson)
const centerX = centerOffset.x
const centerY = centerOffset.y
const elementStrings: string[] = []

const footprintElements = pads.map((pad) => {
const { center, width, height, holeRadius, number } = pad
const isPlatedHole = holeRadius !== undefined && mm(holeRadius) > 0
for (const hole of holes) {
if (hole.hole_shape === "circle") {
elementStrings.push(
`<hole pcbX="${mmStr(hole.x)}" pcbY="${mmStr(hole.y)}" diameter="${mmStr(hole.hole_diameter)}" />`,
)
} else if (hole.hole_shape === "oval") {
console.warn("Unhandled oval hole in conversion (needs implementation)")
}
}

// Normalize the position by subtracting the center point
const normalizedX = mm(center.x) - centerX
const normalizedY = mm(center.y) - centerY
for (const platedHole of platedHoles) {
if (platedHole.shape === "oval") {
elementStrings.push(
`<platedhole portHints={${JSON.stringify(platedHole.port_hints)}} pcbX="${mmStr(platedHole.x)}" pcbY="${mmStr(platedHole.y)}" diameter="${mmStr(platedHole.hole_width)}" height="${mmStr(platedHole.hole_height)}" shape="oval" />`,
)
} else if (platedHole.shape === "circle") {
elementStrings.push(
`<platedhole portHints={${JSON.stringify(platedHole.port_hints)}} pcbX="${mmStr(platedHole.x)}" pcbY="${mmStr(platedHole.y)}" diameter="${mmStr(platedHole.hole_diameter)}" shape="circle" />`,
)
} else if (platedHole.shape === "pill") {
console.warn("Unhandled pill hole in conversion (needs implementation)")
}
}

if (isPlatedHole) {
return `
<platedhole
pcbX="${normalizedX.toFixed(2)}mm"
pcbY="${normalizedY.toFixed(2)}mm"
hole_diameter="${mm(holeRadius) * 2}mm"
outer_diameter="${mm(width)}mm"
portHints={["${number}"]}
/>`.replace(/\n/, "")
} else {
return `
<smtpad
pcbX="${normalizedX.toFixed(2)}mm"
pcbY="${normalizedY.toFixed(2)}mm"
width="${mm(width)}mm"
height="${mm(height)}mm"
shape="rect"
portHints={["${number}"]}
/>`.replace(/\n/, "")
for (const smtPad of smtPads) {
if (smtPad.shape === "circle") {
elementStrings.push(
`<smtpad portHints={${JSON.stringify(smtPad.port_hints)}} pcbX="${mmStr(smtPad.x)}" pcbY="${mmStr(smtPad.y)}" radius="${mmStr(smtPad.radius)}" shape="circle" />`,
)
} else if (smtPad.shape === "rect") {
elementStrings.push(
`<smtpad portHints={${JSON.stringify(smtPad.port_hints)}} pcbX="${mmStr(smtPad.x)}" pcbY="${mmStr(smtPad.y)}" width="${mmStr(smtPad.width)}" height="${mmStr(smtPad.height)}" shape="rect" />`,
)
}
})
}

for (const silkscreenPath of silkscreenPaths) {
elementStrings.push(
`<silkscreenpath route={${JSON.stringify(silkscreenPath.route)}} />`,
)
}

return `
<footprint>
${footprintElements.join("\n")}
${elementStrings.join("\n")}
</footprint>
`.trim()
}
2 changes: 1 addition & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from "./convert-easyeda-json-to-tscircuit-soup-json"
export * from "./fetch-easyeda-json"
export { convertRawEasyEdaToTs } from "./convert-to-typescript-component"
export { convertRawEasyToTsx as convertRawEasyEdaToTs } from "./convert-to-typescript-component"
export { normalizeManufacturerPartNumber } from "./utils/normalize-manufacturer-part-number"
export * from "./schemas/easy-eda-json-schema"
export { convertEasyEdaJsonToVariousFormats } from "./convert-easyeda-json-to-various-formats"
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"dependencies": {
"@tscircuit/mm": "^0.0.8",
"commander": "^12.1.0",
"perfect-cli": "^1.0.20",
"transformation-matrix": "^2.16.1",
"zod": "^3.23.8"
}
Expand Down
4 changes: 1 addition & 3 deletions tests/convert-to-soup-tests/convert-usb-c-to-soup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import { convertEasyEdaJsonToCircuitJson } from "lib/convert-easyeda-json-to-tsc
import type { AnySoupElement } from "circuit-json"
import { su } from "@tscircuit/soup-util"

it("should convert a usb-c footprint to tscircuit soup json", async () => {
it.skip("should convert a usb-c footprint to tscircuit soup json", async () => {
const parsedJson = EasyEdaJsonSchema.parse(usbCEasyEdaJson)
const soupElements = convertEasyEdaJsonToCircuitJson(parsedJson) as any

expect(su(soupElements).pcb_component.list()[0]).toBeTruthy()

expect(su(soupElements).cad_component.list().length).toBe(1)

await logSoup("easyeda usb-c to soup", soupElements as AnySoupElement[])
})
14 changes: 5 additions & 9 deletions tests/convert-to-ts/C128415-to-ts.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { it, expect } from "bun:test"
import timerRawEasy from "../assets/C128415.raweasy.json"
import { convertToTypescriptComponent } from "lib/convert-to-typescript-component"
import { convertBetterEasyToTsx } from "lib/convert-to-typescript-component"
import { EasyEdaJsonSchema } from "lib/schemas/easy-eda-json-schema"
import { convertEasyEdaJsonToCircuitJson } from "lib"

it("should convert 555timer into typescript file", async () => {
const easyeda = EasyEdaJsonSchema.parse(timerRawEasy)
const soup = convertEasyEdaJsonToCircuitJson(easyeda, {
useModelCdn: true,
const betterEasy = EasyEdaJsonSchema.parse(timerRawEasy)
const result = await convertBetterEasyToTsx({
betterEasy,
})
const result = await convertToTypescriptComponent({
easyeda,
soup,
})
console.log(result)
// TODO snapshot
})
12 changes: 4 additions & 8 deletions tests/convert-to-ts/C88224-to-ts.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { it, expect } from "bun:test"
import chipRawEasy from "../assets/C88224.raweasy.json"
import { convertToTypescriptComponent } from "lib/convert-to-typescript-component"
import { convertBetterEasyToTsx } from "lib/convert-to-typescript-component"
import { EasyEdaJsonSchema } from "lib/schemas/easy-eda-json-schema"
import { convertEasyEdaJsonToCircuitJson } from "lib"

it("should convert c88224 into typescript file", async () => {
const easyeda = EasyEdaJsonSchema.parse(chipRawEasy)
const soup = convertEasyEdaJsonToCircuitJson(easyeda, {
useModelCdn: true,
})
const result = await convertToTypescriptComponent({
easyeda,
soup,
const betterEasy = EasyEdaJsonSchema.parse(chipRawEasy)
const result = await convertBetterEasyToTsx({
betterEasy,
})

console.log(result)
Expand Down

0 comments on commit 3bbcdd3

Please sign in to comment.