Skip to content

Commit

Permalink
Merge pull request #59 from tscircuit/holefix
Browse files Browse the repository at this point in the history
Fix Hole Creation, Add snapshot testing
  • Loading branch information
seveibar authored Oct 12, 2024
2 parents 78e8f61 + 1877d93 commit a12d9f0
Show file tree
Hide file tree
Showing 11 changed files with 335 additions and 36 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ dist
.vscode
tmp
temp.tsx
tmp.json
tmp*
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[test]
preload = "tests/fixtures/preload.ts"
14 changes: 8 additions & 6 deletions cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import { convertRawEasyEdaToTs } 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()

program
.name("easyeda-converter")
.description("Convert EasyEDA JSON PCB footprints into tscircuit json soup")
.name("easyeda")
.description("Convert EasyEDA JSON PCB footprints into various formats")
.version(packageJson.version)

program
Expand All @@ -24,14 +25,14 @@ program
.option("-i, --input <jlcpcbPartNumber>", "JLCPCB part number")
.option("-o, --output <filename>", "Output filename")
.option(
"-t, --type <type>",
"Output type: soup.json, kicad_mod, raweasy.json, bettereasy.json, tsx",
"-t, --format-type <format>",
"Output format (can be inferred from filename)",
)
.action(async (options) => {
await convertEasyEdaJsonToVariousFormats({
jlcpcbPartNumberOrFilepath: options.input,
outputFilename: options.output,
formatType: options.type,
formatType: options.format,
})
})

Expand All @@ -56,4 +57,5 @@ program
}
})

program.parse(process.argv)
// program.parse(process.argv)
perfectCli(program, process.argv)
104 changes: 74 additions & 30 deletions lib/convert-easyeda-json-to-tscircuit-soup-json.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {
import type {
PadSchema,
TrackSchema,
ArcSchema,
SVGNodeSchema,
HoleSchema,
} from "./schemas/package-detail-shape-schema"
import { z } from "zod"
import type { z } from "zod"
import type { BetterEasyEdaJson } from "./schemas/easy-eda-json-schema"
import type {
AnySoupElement,
Expand All @@ -19,14 +20,31 @@ import {
pcb_smtpad,
pcb_silkscreen_path,
pcb_plated_hole,
pcb_hole,
} from "circuit-json"
import * as Soup from "circuit-json"
import { generateArcFromSweep, generateArcPathWithMid } from "./math/arc-utils"
import { transformPCBElements } from "@tscircuit/soup-util"
import { scale, translate } from "transformation-matrix"
import { compose, scale, translate } from "transformation-matrix"
import { computeCenterOffset } from "./compute-center-offset"
import { mm } from "@tscircuit/mm"

const mil2mm = (mil: number | string) => {
if (typeof mil === "number") return mm(`${mil}mil`)
if (mil.match(/^\d+$/)) return mm(`${mil}mil`)
return mm(mil)
}
/**
* Some components, like paths and "HOLE", seem to use mil*10 as
* their unlabeled unit
*/
const milx10 = (mil10: number | string) => {
if (typeof mil10 === "number") return mil2mm(mil10) * 10
if (mil10.match(/^\d+$/)) return mil2mm(mil10) * 10
// If it has a unit, return the specified unit ignoring the multiplier
return mil2mm(mil10)
}

const handleSilkscreenPath = (
track: z.infer<typeof TrackSchema>,
index: number,
Expand All @@ -36,7 +54,10 @@ const handleSilkscreenPath = (
pcb_silkscreen_path_id: `pcb_silkscreen_path_${index + 1}`,
pcb_component_id: "pcb_component_1",
layer: "top", // Assuming all silkscreen is on top layer
route: track.points.map((point) => ({ x: point.x, y: point.y })),
route: track.points.map((point) => ({
x: milx10(point.x),
y: milx10(point.y),
})),
stroke_width: track.width,
})
}
Expand All @@ -57,9 +78,23 @@ const handleSilkscreenArc = (arc: z.infer<typeof ArcSchema>, index: number) => {
pcb_silkscreen_path_id: `pcb_silkscreen_arc_${index + 1}`,
pcb_component_id: "pcb_component_1",
layer: "top", // Assuming all silkscreen is on top layer
route: arcPath,
stroke_width: arc.width,
})
route: arcPath.map((p) => ({
x: milx10(p.x),
y: milx10(p.y),
})),
stroke_width: mm(arc.width),
} as Soup.PcbSilkscreenPathInput)
}

const handleHole = (hole: z.infer<typeof HoleSchema>, index: number) => {
return pcb_hole.parse({
type: "pcb_hole",
x: milx10(hole.center.x),
y: milx10(hole.center.y),
hole_diameter: milx10(hole.radius) * 2,
hole_shape: "circle",
pcb_hole_id: `pcb_hole_${index + 1}`,
} as Soup.PcbHole)
}

interface Options {
Expand Down Expand Up @@ -111,18 +146,18 @@ export const convertEasyEdaJsonToCircuitJson = (
name: portNumber,
})

if (pad.holeRadius !== undefined && mm(pad.holeRadius) !== 0) {
if (pad.holeRadius !== undefined && mil2mm(pad.holeRadius) !== 0) {
// Add pcb_plated_hole
soupElements.push(
pcb_plated_hole.parse({
type: "pcb_plated_hole",
pcb_plated_hole_id: `pcb_plated_hole_${index + 1}`,
shape: "circle",
x: mm(pad.center.x),
y: mm(pad.center.y),
hole_diameter: mm(pad.holeRadius) * 2,
outer_diameter: mm(pad.width),
radius: mm(pad.holeRadius),
x: mil2mm(pad.center.x),
y: mil2mm(pad.center.y),
hole_diameter: mil2mm(pad.holeRadius) * 2,
outer_diameter: mil2mm(pad.width),
radius: mil2mm(pad.holeRadius),
port_hints: [portNumber],
pcb_component_id: "pcb_component_1",
pcb_port_id: `pcb_port_${index + 1}`,
Expand All @@ -144,25 +179,34 @@ export const convertEasyEdaJsonToCircuitJson = (
if (!soupShape) {
throw new Error(`unknown pad.shape: "${pad.shape}"`)
}
soupElements.push(
pcb_smtpad.parse({
type: "pcb_smtpad",
pcb_smtpad_id: `pcb_smtpad_${index + 1}`,
shape: soupShape,
x: mm(pad.center.x),
y: mm(pad.center.y),
...(soupShape === "rect"
? { width: mm(pad.width), height: mm(pad.height) }
: { radius: Math.min(mm(pad.width), mm(pad.height)) / 2 }),
layer: "top",
port_hints: [portNumber],
pcb_component_id: "pcb_component_1",
pcb_port_id: `pcb_port_${index + 1}`,
} as PCBSMTPad),
)

const parsedPcbSmtpad = pcb_smtpad.parse({
type: "pcb_smtpad",
pcb_smtpad_id: `pcb_smtpad_${index + 1}`,
shape: soupShape,
x: mil2mm(pad.center.x),
y: mil2mm(pad.center.y),
...(soupShape === "rect"
? { width: mil2mm(pad.width), height: mil2mm(pad.height) }
: { radius: Math.min(mil2mm(pad.width), mil2mm(pad.height)) / 2 }),
layer: "top",
port_hints: [portNumber],
pcb_component_id: "pcb_component_1",
pcb_port_id: `pcb_port_${index + 1}`,
} as PCBSMTPad)
soupElements.push(parsedPcbSmtpad)
}
})

// Add holes
easyEdaJson.packageDetail.dataStr.shape
.filter(
(shape): shape is z.infer<typeof HoleSchema> => shape.type === "HOLE",
)
.forEach((h, index) => {
soupElements.push(handleHole(h, index))
})

// Add silkscreen paths and arcs
easyEdaJson.packageDetail.dataStr.shape.forEach((shape, index) => {
if (shape.type === "TRACK") {
Expand Down Expand Up @@ -213,7 +257,7 @@ export const convertEasyEdaJsonToCircuitJson = (
if (shouldRecenter) {
transformPCBElements(
soupElements,
translate(-centerOffset.x, centerOffset.y),
compose(translate(-centerOffset.x, centerOffset.y), scale(1, -1)),
)
}

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"scripts": {
"test": "bun test",
"cli": "bun cli/main.ts",
"build": "tsup lib/index.ts cli/main.ts --format esm --dts --sourcemap",
"aider": "aider",
"format:check": "biome format .",
Expand All @@ -27,7 +28,9 @@
"@tscircuit/props": "^0.0.68",
"@tscircuit/soup-util": "^0.0.38",
"@types/bun": "latest",
"bun-match-svg": "^0.0.6",
"circuit-json": "^0.0.83",
"circuit-to-svg": "^0.0.40",
"tsup": "^8.1.0"
},
"peerDependencies": {
Expand All @@ -36,6 +39,7 @@
"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
Loading

0 comments on commit a12d9f0

Please sign in to comment.