Skip to content

Commit

Permalink
Merge pull request #57 from tscircuit/ref1
Browse files Browse the repository at this point in the history
Segments approach for trace rendering, support multi-layer traces
  • Loading branch information
seveibar authored Sep 9, 2024
2 parents 61a1226 + af74cc3 commit 1011a5e
Show file tree
Hide file tree
Showing 16 changed files with 1,711 additions and 1,355 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,5 @@ dist
*storybook.log
.vscode
.vercel
*.diff.png
*.diff.png
storybook-static
6 changes: 3 additions & 3 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { StorybookConfig } from "@storybook/react-vite";
import type { StorybookConfig } from "@storybook/react-vite"

const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
Expand All @@ -13,5 +13,5 @@ const config: StorybookConfig = {
name: "@storybook/react-vite",
options: {},
},
};
export default config;
}
export default config
Binary file modified bun.lockb
Binary file not shown.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "circuit-to-svg",
"type": "module",
"version": "0.0.20",
"description": "Convert Circuit JSON to SVG",
"main": "dist/index.js",
Expand All @@ -8,7 +9,7 @@
],
"scripts": {
"prepublish": "npm run build",
"build": "tsup ./src/index.ts --dts --sourcemap",
"build": "tsup-node ./src/index.ts --format esm --dts --sourcemap",
"format": "biome format . --write",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
Expand All @@ -30,7 +31,8 @@
"esbuild": "^0.20.2",
"storybook": "^8.2.5",
"tsup": "^8.0.2",
"typescript": "^5.4.5"
"typescript": "^5.4.5",
"vite-tsconfig-paths": "^5.0.1"
},
"peerDependencies": {
"@tscircuit/soup": "^0.0.55"
Expand Down
133 changes: 33 additions & 100 deletions src/lib/circuit-to-pcb-svg.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import type { AnySoupElement } from "@tscircuit/soup"
import { type INode, stringify } from "svgson"
import { applyToPoint, compose, scale, translate } from "transformation-matrix"

interface SvgObject {
name: string
type: "element" | "text"
attributes?: { [key: string]: string }
children?: SvgObject[]
value?: string
}
import type { AnySoupElement, PCBTrace } from "@tscircuit/soup"
import { type INode as SvgObject, stringify } from "svgson"
import {
applyToPoint,
compose,
scale,
translate,
type Matrix,
} from "transformation-matrix"
import { createSvgObjectsFromPcbTrace } from "./svg-object-fns/create-svg-object-from-pcb-trace"

interface PointObjectNotation {
x: number
Expand Down Expand Up @@ -64,18 +63,15 @@ function circuitJsonToPcbSvg(soup: AnySoupElement[]): string {

const traceElements = soup
.filter((item) => item.type === "pcb_trace")
.map((item) => createSvgElement(item, transform))
.filter((element) => element !== null)
.flatMap((item) => createSvgObjects(item, transform))

const holeElements = soup
.filter((item) => item.type === "pcb_plated_hole")
.map((item) => createSvgElement(item, transform))
.filter((element) => element !== null)
.flatMap((item) => createSvgObjects(item, transform))

const silkscreenElements = soup
.filter((item) => item.type === "pcb_silkscreen_path")
.map((item) => createPcbSilkscreenPath(item, transform))
.filter((element) => element !== null)
.flatMap((item) => createPcbSilkscreenPath(item, transform))

const otherElements = soup
.filter(
Expand All @@ -84,8 +80,7 @@ function circuitJsonToPcbSvg(soup: AnySoupElement[]): string {
item.type,
),
)
.map((item) => createSvgElement(item, transform))
.filter((element) => element !== null)
.flatMap((item) => createSvgObjects(item, transform))

let strokeWidth = String(0.05 * scaleFactor)

Expand All @@ -104,6 +99,7 @@ function circuitJsonToPcbSvg(soup: AnySoupElement[]): string {
width: svgWidth.toString(),
height: svgHeight.toString(),
},
value: "",
children: [
{
name: "style",
Expand All @@ -113,7 +109,7 @@ function circuitJsonToPcbSvg(soup: AnySoupElement[]): string {
type: "text",
value: `
.pcb-board { fill: #000; }
.pcb-trace { stroke: rgb(200, 52, 52); stroke-width: ${strokeWidth}; fill: none; }
.pcb-trace { fill: none; }
.pcb-hole-outer { fill: rgb(200, 52, 52); }
.pcb-hole-inner { fill: rgb(255, 38, 226); }
.pcb-pad { fill: rgb(200, 52, 52); }
Expand All @@ -136,7 +132,7 @@ function circuitJsonToPcbSvg(soup: AnySoupElement[]): string {
height: svgHeight.toString(),
},
},
createPcbBoundary(transform, minX, minY, maxX, maxY),
createSvgObjectFromPcbBoundary(transform, minX, minY, maxX, maxY),
{
name: "g",
type: "element",
Expand All @@ -161,11 +157,11 @@ function circuitJsonToPcbSvg(soup: AnySoupElement[]): string {
attributes: { id: "silkscreen" },
children: silkscreenElements,
},
].filter((child) => child !== null),
].filter((child): child is SvgObject => child !== null),
}

try {
return stringify(svgObject as INode)
return stringify(svgObject as SvgObject)
} catch (error) {
console.error("Error stringifying SVG object:", error)
throw error
Expand All @@ -190,22 +186,22 @@ function circuitJsonToPcbSvg(soup: AnySoupElement[]): string {
}
}

function createSvgElement(item: AnySoupElement, transform: any): any {
switch (item.type) {
function createSvgObjects(elm: AnySoupElement, transform: Matrix): SvgObject[] {
switch (elm.type) {
case "pcb_component":
return createPcbComponent(item, transform)
return [createSvgObjectsFromPcbComponent(elm, transform)].filter(Boolean)
case "pcb_trace":
return createPcbTrace(item, transform)
return createSvgObjectsFromPcbTrace(elm, transform)
case "pcb_plated_hole":
return createPcbHole(item, transform)
return [createSvgObjectsFromPcbHole(elm, transform)].filter(Boolean)
case "pcb_smtpad":
return createPcbSMTPad(item, transform)
return [createSvgObjectsFromSmtPad(elm, transform)].filter(Boolean)
default:
return null
return []
}
}

function createPcbComponent(component: any, transform: any): any {
function createSvgObjectsFromPcbComponent(component: any, transform: any): any {
const { center, width, height, rotation = 0 } = component
const [x, y] = applyToPoint(transform, [center.x, center.y])
const scaledWidth = width * Math.abs(transform.a)
Expand Down Expand Up @@ -243,7 +239,7 @@ function createPcbComponent(component: any, transform: any): any {
}
}

function createPcbHole(hole: any, transform: any): any {
function createSvgObjectsFromPcbHole(hole: any, transform: any): any {
const [x, y] = applyToPoint(transform, [hole.x, hole.y])
const scaledOuterRadius = (hole.outer_diameter / 2) * Math.abs(transform.a)
const scaledInnerRadius = (hole.hole_diameter / 2) * Math.abs(transform.a)
Expand Down Expand Up @@ -275,7 +271,7 @@ function createPcbHole(hole: any, transform: any): any {
}
}

function createPcbSMTPad(pad: any, transform: any): any {
function createSvgObjectsFromSmtPad(pad: any, transform: any): any {
const [x, y] = applyToPoint(transform, [pad.x, pad.y])
const width = pad.width * Math.abs(transform.a)
const height = pad.height * Math.abs(transform.d)
Expand All @@ -292,71 +288,6 @@ function createPcbSMTPad(pad: any, transform: any): any {
}
}

function createPcbTrace(trace: any, transform: any): any {
if (!trace.route || !Array.isArray(trace.route) || trace.route.length < 2)
return null

const cornerRadius = 0.2 // Adjust this value to change the roundness of corners
const pathCommands: string[] = []
const transformedPoints = trace.route.map((point: any) =>
applyToPoint(transform, [point.x, point.y]),
)

// Start path
pathCommands.push(`M ${transformedPoints[0][0]} ${transformedPoints[0][1]}`)

for (let i = 1; i < transformedPoints.length - 1; i++) {
const prev = transformedPoints[i - 1]
const curr = transformedPoints[i]
const next = transformedPoints[i + 1]

// Calculate vectors
const v1 = curr && prev ? [curr[0] - prev[0], curr[1] - prev[1]] : [0, 0]
const v2 = next && curr ? [next[0] - curr[0], next[1] - curr[1]] : [0, 0]

// Normalize vectors
const l1 = Math.sqrt((v1[0] as number) * (v1[0] as number) + (v1[1] as number) * (v1[1] as number));
const l2 = Math.sqrt((v2[0] as number) * (v2[0] as number) + (v2[1] as number) * (v2[1] as number));
if (l1 !== 0) {
v1[0]! /= l1;
v1[1]! /= l1;
}
if (l2 !== 0) {
v2[0]! /= l2;
v2[1]! /= l2;
}

// Calculate the corner points
const radius = Math.min(cornerRadius, Math.min(l1, l2) / 2)
const p1 = [curr[0] - v1[0]! * radius, curr[1] - v1[1]! * radius]
const p2 = [curr[0] + v2[0]! * radius, curr[1] + v2[1]! * radius]

// Add line to the start of the corner
pathCommands.push(`L ${p1[0]} ${p1[1]}`)

// Add the arc
pathCommands.push(`A ${radius} ${radius} 0 0 1 ${p2[0]} ${p2[1]}`)
}

// Add final line to last point
const lastPoint = transformedPoints[transformedPoints.length - 1]
pathCommands.push(`L ${lastPoint[0]} ${lastPoint[1]}`)

return {
name: "path",
type: "element",
attributes: {
class: "pcb-trace",
d: pathCommands.join(" "),
"stroke-width": trace.stroke_width
? (trace.stroke_width * Math.abs(transform.a)).toString()
: "0.3",
"stroke-linecap": "round",
"stroke-linejoin": "round",
},
}
}

function createPcbSilkscreenPath(silkscreenPath: any, transform: any): any {
if (!silkscreenPath.route || !Array.isArray(silkscreenPath.route)) return null

Expand Down Expand Up @@ -389,13 +320,13 @@ function createPcbSilkscreenPath(silkscreenPath: any, transform: any): any {
}
}

function createPcbBoundary(
function createSvgObjectFromPcbBoundary(
transform: any,
minX: number,
minY: number,
maxX: number,
maxY: number,
): any {
): SvgObject {
const [x1, y1] = applyToPoint(transform, [minX, minY])
const [x2, y2] = applyToPoint(transform, [maxX, maxY])
const width = Math.abs(x2 - x1)
Expand All @@ -405,6 +336,8 @@ function createPcbBoundary(
return {
name: "rect",
type: "element",
value: "",
children: [],
attributes: {
class: "pcb-boundary",
x: x.toString(),
Expand Down
Loading

0 comments on commit 1011a5e

Please sign in to comment.