Skip to content
Merged
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
89 changes: 80 additions & 9 deletions src/convert-kicad-json-to-tscircuit-soup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ const getAxisAlignedRectFromPoints = (
}
}

const fpPolyHasFill = (fill?: string) => {
if (!fill) return false
const normalized = fill.toLowerCase()
return (
normalized !== "no" && normalized !== "none" && normalized !== "outline"
)
}

const getRotationDeg = (at: number[] | undefined) => {
if (!at) return 0
if (Array.isArray(at) && at.length >= 3 && typeof at[2] === "number") {
Expand Down Expand Up @@ -574,9 +582,52 @@ export const convertKicadJsonToTsCircuitSoup = async (

if (fp_polys) {
for (const fp_poly of fp_polys) {
const route = fp_poly.pts.map((p) => ({ x: p[0], y: -p[1] }))
const route: Array<{ x: number; y: number }> = []
const pushRoutePoint = (point: { x: number; y: number }) => {
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
return
}
route.push(point)
}
for (const segment of fp_poly.pts) {
if (Array.isArray(segment)) {
pushRoutePoint({ x: segment[0], y: -segment[1] })
continue
}
if (segment && typeof segment === "object" && "kind" in segment) {
if (segment.kind === "arc") {
const start = makePoint(segment.start)
const mid = makePoint(segment.mid)
const end = makePoint(segment.end)
const arcLength = getArcLength(start, mid, end)
const numPoints = Math.max(8, Math.ceil(arcLength))
const adjustedNumPoints = Math.max(2, Math.ceil(arcLength / 0.1))
const arcPoints = generateArcPath(
start,
mid,
end,
adjustedNumPoints,
).map((p) => ({
x: p.x,
y: -p.y,
}))
for (const point of arcPoints) {
pushRoutePoint(point)
}
}
continue
}
}
const routePoints = route
const isClosed =
routePoints.length > 2 &&
routePoints[0]!.x === routePoints[routePoints.length - 1]!.x &&
routePoints[0]!.y === routePoints[routePoints.length - 1]!.y
const polygonPoints = isClosed ? routePoints.slice(0, -1) : routePoints
if (routePoints.length === 0) continue
const strokeWidth = fp_poly.stroke?.width ?? 0
if (fp_poly.layer.endsWith(".Cu")) {
const rect = getAxisAlignedRectFromPoints(route)
const rect = getAxisAlignedRectFromPoints(polygonPoints)
if (rect) {
circuitJson.push({
type: "pcb_smtpad",
Expand All @@ -589,14 +640,34 @@ export const convertKicadJsonToTsCircuitSoup = async (
layer: convertKicadLayerToTscircuitLayer(fp_poly.layer)!,
pcb_component_id,
} as any)
} else {
} else if (fpPolyHasFill(fp_poly.fill)) {
if (polygonPoints.length >= 3) {
circuitJson.push({
type: "pcb_smtpad",
pcb_smtpad_id: `pcb_smtpad_${smtpadId++}`,
shape: "polygon",
points: polygonPoints,
layer: convertKicadLayerToTscircuitLayer(fp_poly.layer)!,
pcb_component_id,
} as any)
} else if (polygonPoints.length >= 2) {
circuitJson.push({
type: "pcb_trace",
pcb_trace_id: `pcb_trace_${traceId++}`,
pcb_component_id,
layer: convertKicadLayerToTscircuitLayer(fp_poly.layer)!,
route: polygonPoints,
thickness: strokeWidth,
} as any)
}
} else if (polygonPoints.length >= 2) {
circuitJson.push({
type: "pcb_trace",
pcb_trace_id: `pcb_trace_${traceId++}`,
pcb_component_id,
layer: convertKicadLayerToTscircuitLayer(fp_poly.layer)!,
route,
thickness: fp_poly.stroke.width,
route: polygonPoints,
thickness: strokeWidth,
} as any)
}
} else if (fp_poly.layer.endsWith(".SilkS")) {
Expand All @@ -605,17 +676,17 @@ export const convertKicadJsonToTsCircuitSoup = async (
pcb_silkscreen_path_id: `pcb_silkscreen_path_${silkPathId++}`,
pcb_component_id,
layer: convertKicadLayerToTscircuitLayer(fp_poly.layer)!,
route,
stroke_width: fp_poly.stroke.width,
route: routePoints,
stroke_width: strokeWidth,
} as any)
} else if (fp_poly.layer.endsWith(".Fab")) {
circuitJson.push({
type: "pcb_fabrication_note_path",
fabrication_note_path_id: `fabrication_note_path_${fabPathId++}`,
pcb_component_id,
layer: convertKicadLayerToTscircuitLayer(fp_poly.layer)!,
route,
stroke_width: fp_poly.stroke.width,
route: polygonPoints,
stroke_width: strokeWidth,
port_hints: [],
} as any)
} else {
Expand Down
22 changes: 18 additions & 4 deletions src/get-attr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,24 @@ export const formatAttr = (val: any, attrKey: string) => {
return effects_def.parse(effectsObj)
}
if (attrKey === "pts") {
// val is like [ [ 'xy', -1.25, -0.625 ], [ 'xy', 1.25, -0.625 ], ... ]
return val.map((xy_pair: any[]) =>
xy_pair.slice(1).map((n: any) => Number.parseFloat(n.valueOf())),
)
// val can include coordinate tuples as well as arc definitions.
return val.map((segment: any[]) => {
const segmentType = segment[0]?.valueOf?.() ?? segment[0]
if (segmentType === "xy") {
return segment.slice(1).map((n: any) => Number.parseFloat(n.valueOf()))
}
if (segmentType === "arc") {
const arcObj: Record<string, any> = { kind: "arc" }
for (const arcAttr of segment.slice(1)) {
const key = arcAttr[0].valueOf()
arcObj[key] = arcAttr
.slice(1)
.map((n: any) => Number.parseFloat(n.valueOf()))
}
return arcObj
}
return segment
})
}
if (attrKey === "stroke") {
const strokeObj: any = {}
Expand Down
12 changes: 11 additions & 1 deletion src/kicad-zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ export const point = z.union([point2, point3])

type MakeRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>

export const fp_poly_arc_segment_def = z.object({
kind: z.literal("arc"),
start: point2,
mid: point2,
end: point2,
})

export const fp_poly_point_def = z.union([point2, fp_poly_arc_segment_def])

export const attributes_def = z
.object({
at: point,
Expand Down Expand Up @@ -185,7 +194,7 @@ export const fp_circle_def = z.object({

export const fp_poly_def = z
.object({
pts: z.array(point2),
pts: z.array(fp_poly_point_def),
stroke: z
.object({
width: z.number(),
Expand All @@ -195,6 +204,7 @@ export const fp_poly_def = z
width: z.number().optional(),
layer: z.string(),
uuid: z.string().optional(),
fill: z.string().optional(),
})
// Old kicad versions don't have "stroke"
.transform((data) => {
Expand Down
38 changes: 31 additions & 7 deletions src/math/arc-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ export interface Point {
y: number
}

const TWO_PI = Math.PI * 2

const normalizeAngle = (angle: number) => {
let result = angle % TWO_PI
if (result < 0) result += TWO_PI
return result
}

const directedAngleCCW = (start: number, target: number) => {
const startNorm = normalizeAngle(start)
let targetNorm = normalizeAngle(target)
let delta = targetNorm - startNorm
if (delta < 0) delta += TWO_PI
return delta
}

export function calculateCenter(start: Point, mid: Point, end: Point): Point {
const mid1 = { x: (start.x + mid.x) / 2, y: (start.y + mid.y) / 2 }
const mid2 = { x: (mid.x + end.x) / 2, y: (mid.y + end.y) / 2 }
Expand Down Expand Up @@ -30,14 +46,18 @@ export const getArcLength = (start: Point, mid: Point, end: Point) => {
const radius = calculateRadius(center, start)

const angleStart = calculateAngle(center, start)
const angleMid = calculateAngle(center, mid)
const angleEnd = calculateAngle(center, end)

let angleDelta = angleEnd - angleStart
if (angleDelta < 0) {
angleDelta += 2 * Math.PI
const ccwToMid = directedAngleCCW(angleStart, angleMid)
const ccwToEnd = directedAngleCCW(angleStart, angleEnd)

let angleDelta = ccwToEnd
if (ccwToMid > ccwToEnd) {
angleDelta = ccwToEnd - TWO_PI
}

return radius * angleDelta
return Math.abs(radius * angleDelta)
}

export function generateArcPath(
Expand All @@ -50,11 +70,15 @@ export function generateArcPath(
const radius = calculateRadius(center, start)

const angleStart = calculateAngle(center, start)
const angleMid = calculateAngle(center, mid)
const angleEnd = calculateAngle(center, end)

let angleDelta = angleEnd - angleStart
if (angleDelta < 0) {
angleDelta += 2 * Math.PI
const ccwToMid = directedAngleCCW(angleStart, angleMid)
const ccwToEnd = directedAngleCCW(angleStart, angleEnd)

let angleDelta = ccwToEnd
if (ccwToMid > ccwToEnd) {
angleDelta = ccwToEnd - TWO_PI
}

const path: Point[] = []
Expand Down
17 changes: 15 additions & 2 deletions src/parse-kicad-mod-to-kicad-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,27 @@ export const parseKicadModToKicadJson = (fileContent: string): KicadModJson => {
for (const fp_poly_row of fp_polys_rows) {
const pts = getAttr(fp_poly_row, "pts")
const stroke = getAttr(fp_poly_row, "stroke")
const width = getAttr(fp_poly_row, "width")
const layer = getAttr(fp_poly_row, "layer")
const uuid = getAttr(fp_poly_row, "uuid")

const fill = getAttr(fp_poly_row, "fill")
let normalizedStroke = stroke
if (!normalizedStroke && typeof width === "number") {
normalizedStroke = { width, type: "solid" }
} else if (
normalizedStroke &&
typeof normalizedStroke === "object" &&
typeof width === "number" &&
normalizedStroke.width === undefined
) {
normalizedStroke = { ...normalizedStroke, width }
}
fp_polys.push({
pts,
stroke,
stroke: normalizedStroke,
layer,
uuid,
fill,
})
}

Expand Down
2 changes: 1 addition & 1 deletion tests/__snapshots__/poly.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions tests/data/viaGrid-pacman-1.kicad_mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
(footprint "viaGrid-pacman-1"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(property "Reference" "REF**"
(at 0 -0.5 0)
(unlocked yes)
(layer "F.SilkS")
(hide yes)
(uuid "cbfffe61-1e92-4daa-bd46-ae32e0dbad92")
(effects
(font
(size 0.5 0.5)
(thickness 0.1)
)
)
)
(property "Value" "viaGrid-pacman-1"
(at 0 1 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "60d7fc8a-65be-4914-a046-de64475a184c")
(effects
(font
(size 0.5 0.5)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "1143702b-768c-463c-8287-7c8084dfab91")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "515226c9-0c16-4433-b704-16fa13d28a16")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(attr smd)
(fp_poly
(pts
(xy 2.5 0.2)
(xy -0.2 0.2)
(xy -0.199996 -2.5)
(arc
(start -0.199999 -2.5)
(mid -1.773415 1.773415)
(end 2.5 0.199999)
)
)
(stroke
(width 0)
(type solid)
)
(fill yes)
(layer "F.Cu")
(uuid "f37eddc0-cfe3-474b-b8ab-9a4c0ef62518")
)
(embedded_fonts no)
)
Loading