diff --git a/bun.lockb b/bun.lockb index 0161d3b..7346269 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..25b636f --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./tests/fixtures/preload.ts"] \ No newline at end of file diff --git a/lib/Project.ts b/lib/Project.ts index 3390705..3ea90e9 100644 --- a/lib/Project.ts +++ b/lib/Project.ts @@ -11,6 +11,8 @@ export class Project { children: PrimitiveComponent[] db: SoupUtilObjects + _hasRenderedAtleastOnce = false + constructor() { this.children = [] this.db = su([]) @@ -64,9 +66,11 @@ export class Project { rootComponent.setProject(this) rootComponent.runRenderCycle() + this._hasRenderedAtleastOnce = true } getSoup(): AnySoupElement[] { + if (!this._hasRenderedAtleastOnce) this.render() return this.db.toArray() } @@ -74,6 +78,31 @@ export class Project { return this.getSoup() } + async getSvg(options: { view: "pcb"; layer?: string }): Promise { + const circuitToSvg = await import("circuit-to-svg").catch((e) => { + throw new Error( + `To use project.getSvg, you must install the "circuit-to-svg" package.\n\n"${e.message}"`, + ) + }) + + return circuitToSvg.circuitJsonToPcbSvg(this.getSoup()) + } + + async preview( + previewNameOrOpts: + | string + | { + previewName: string + tscircuitApiKey?: string + }, + ) { + const previewOpts = + typeof previewNameOrOpts === "object" + ? previewNameOrOpts + : { previewName: previewNameOrOpts } + throw new Error("project.preview is not yet implemented") + } + computeGlobalSchematicTransform(): Matrix { return identity() } diff --git a/lib/components/base-components/NormalComponent.ts b/lib/components/base-components/NormalComponent.ts index 761af61..2f7e554 100644 --- a/lib/components/base-components/NormalComponent.ts +++ b/lib/components/base-components/NormalComponent.ts @@ -87,6 +87,22 @@ export class NormalComponent< const portsFromFootprint = this.getPortsFromFootprint() this.addAll(portsFromFootprint) + + const pinLabels: Record | undefined = + this._parsedProps.pinLabels + if (pinLabels) { + for (let [pinNumber, label] of Object.entries(pinLabels)) { + pinNumber = pinNumber.replace("pin", "") + const port = this.selectOne(`port[pinNumber='${pinNumber}']`) + if (!port) { + throw new Error( + `Could not find port for pin number ${pinNumber} in chip ${this.getString()}`, + ) + } + port.externallyAddedAliases.push(label) + port.props.name = label + } + } } _addChildrenFromStringFootprint() { diff --git a/lib/components/base-components/PrimitiveComponent.ts b/lib/components/base-components/PrimitiveComponent.ts index 5436845..2f939a9 100644 --- a/lib/components/base-components/PrimitiveComponent.ts +++ b/lib/components/base-components/PrimitiveComponent.ts @@ -46,6 +46,8 @@ export abstract class PrimitiveComponent< componentName = "" lowercaseComponentName = "" + externallyAddedAliases: string[] + source_group_id: string | null = null source_component_id: string | null = null schematic_component_id: string | null = null @@ -57,6 +59,7 @@ export abstract class PrimitiveComponent< this.children = [] this.childrenPendingRemoval = [] this.props = props ?? {} + this.externallyAddedAliases = [] this._parsedProps = this.config.zodProps.parse( props ?? {}, ) as z.infer diff --git a/lib/components/index.ts b/lib/components/index.ts index 4a01ac1..d3d556b 100644 --- a/lib/components/index.ts +++ b/lib/components/index.ts @@ -13,3 +13,5 @@ export { Trace } from "./primitive-components/Trace" export { TraceHint } from "./primitive-components/TraceHint" export { Group } from "./primitive-components/Group" export { Chip } from "./normal-components/Chip" +export { Jumper } from "./normal-components/Jumper" +export { SilkscreenPath } from "./primitive-components/SilkscreenPath" diff --git a/lib/components/normal-components/Chip.ts b/lib/components/normal-components/Chip.ts index c2f4040..10dd7a4 100644 --- a/lib/components/normal-components/Chip.ts +++ b/lib/components/normal-components/Chip.ts @@ -21,25 +21,6 @@ export class Chip extends NormalComponent< } } - initPorts() { - super.initPorts() - - const { _parsedProps: props } = this - - if (props.pinLabels) { - for (const [pinNumber, label] of Object.entries(props.pinLabels)) { - const port = this.selectOne(`port[pinNumber='${pinNumber}']`) - if (!port) { - throw new Error( - `Could not find port for pin number ${pinNumber} in chip ${this.getString()}`, - ) - } - port.props.aliases.push(port.props.name) - port.props.name = label - } - } - } - doInitialSourceRender(): void { const { db } = this.project! const { _parsedProps: props } = this diff --git a/lib/components/normal-components/Jumper.ts b/lib/components/normal-components/Jumper.ts new file mode 100644 index 0000000..b2dea0f --- /dev/null +++ b/lib/components/normal-components/Jumper.ts @@ -0,0 +1,101 @@ +import { NormalComponent } from "lib/components/base-components/NormalComponent" +import { jumperProps } from "@tscircuit/props" +import { Port } from "../primitive-components/Port" +import type { BaseSymbolName } from "lib/utils/constants" +import { + getAllDimensionsForSchematicBox, + type SchematicBoxDimensions, +} from "lib/utils/schematic/getAllDimensionsForSchematicBox" +import { underscorifyPortArrangement } from "lib/soup/underscorifyPortArrangement" +import { underscorifyPinStyles } from "lib/soup/underscorifyPinStyles" + +export class Jumper extends NormalComponent< + typeof jumperProps, + PinLabels +> { + schematicDimensions: SchematicBoxDimensions | null = null + + get config() { + return { + zodProps: jumperProps, + } + } + + doInitialSourceRender(): void { + const { db } = this.project! + const { _parsedProps: props } = this + + const source_component = db.source_component.insert({ + ftype: "simple_chip", // TODO unknown or jumper + name: props.name, + manufacturer_part_number: props.manufacturerPartNumber, + supplier_part_numbers: props.supplierPartNumbers, + }) + + this.source_component_id = source_component.source_component_id! + } + + doInitialSchematicComponentRender() { + const { db } = this.project! + const { _parsedProps: props } = this + + const ports = this.children.filter((child) => child instanceof Port) + + const pinSpacing = props.schPinSpacing ?? 0.2 + + const dimensions = getAllDimensionsForSchematicBox({ + schWidth: props.schWidth, + schHeight: props.schHeight, + schPinSpacing: pinSpacing, + schPinStyle: props.schPinStyle, + + pinCount: ports.length, + + // @ts-ignore there's a subtley in the definition difference with + // leftSide/rightSide/topSide/bottomSide in how the direction is defined + // that doesn't really matter + schPortArrangement: { + // TODO use schematic direction or schPortArrangement + rightSize: ports.length, + }, + }) + this.schematicDimensions = dimensions + + const schematic_component = db.schematic_component.insert({ + center: { x: props.schX ?? 0, y: props.schY ?? 0 }, + rotation: props.schRotation ?? 0, + size: dimensions.getSize(), + + port_arrangement: underscorifyPortArrangement( + props.schPortArrangement as any, + ), + + pin_spacing: pinSpacing, + + // @ts-ignore soup needs to support distance for pin_styles + pin_styles: underscorifyPinStyles(props.schPinStyle), + + port_labels: props.pinLabels, + + source_component_id: this.source_component_id!, + }) + + this.schematic_component_id = schematic_component.schematic_component_id + } + + doInitialPcbComponentRender() { + const { db } = this.project! + const { _parsedProps: props } = this + + const pcb_component = db.pcb_component.insert({ + center: { x: props.pcbX ?? 0, y: props.pcbY ?? 0 }, + width: 2, // Default width, adjust as needed + height: 3, // Default height, adjust as needed + layer: props.layer ?? "top", + rotation: props.pcbRotation ?? 0, + source_component_id: this.source_component_id!, + }) + + this.pcb_component_id = pcb_component.pcb_component_id + } +} diff --git a/lib/components/primitive-components/Port.ts b/lib/components/primitive-components/Port.ts index b59ea22..4ee513c 100644 --- a/lib/components/primitive-components/Port.ts +++ b/lib/components/primitive-components/Port.ts @@ -82,6 +82,7 @@ export class Port extends PrimitiveComponent { ...(typeof props.pinNumber === "number" ? [`pin${props.pinNumber}`, props.pinNumber.toString()] : []), + ...this.externallyAddedAliases, ]), ) as string[] } diff --git a/lib/components/primitive-components/SilkscreenPath.ts b/lib/components/primitive-components/SilkscreenPath.ts new file mode 100644 index 0000000..3f1e630 --- /dev/null +++ b/lib/components/primitive-components/SilkscreenPath.ts @@ -0,0 +1,35 @@ +import { silkscreenPathProps } from "@tscircuit/props" +import { PrimitiveComponent } from "../base-components/PrimitiveComponent" + +export class SilkscreenPath extends PrimitiveComponent< + typeof silkscreenPathProps +> { + pcb_silkscreen_path_id: string | null = null + + get config() { + return { + zodProps: silkscreenPathProps, + } + } + + doInitialPcbPrimitiveRender(): void { + const { db } = this.project! + const { _parsedProps: props } = this + + const layer = props.layer ?? "top" + if (layer !== "top" && layer !== "bottom") { + throw new Error( + `Invalid layer "${layer}" for SilkscreenPath. Must be "top" or "bottom".`, + ) + } + + const pcb_silkscreen_path = db.pcb_silkscreen_path.insert({ + pcb_component_id: this.parent?.pcb_component_id!, + layer, + route: props.route, + stroke_width: props.strokeWidth ?? 0.1, + }) + + this.pcb_silkscreen_path_id = pcb_silkscreen_path.pcb_silkscreen_path_id + } +} diff --git a/lib/fiber/intrinsic-jsx.ts b/lib/fiber/intrinsic-jsx.ts index bfce9b7..51b050a 100644 --- a/lib/fiber/intrinsic-jsx.ts +++ b/lib/fiber/intrinsic-jsx.ts @@ -9,6 +9,7 @@ declare global { diode: Props.DiodeProps led: Props.LedProps board: Props.BoardProps + jumper: Props.JumperProps bug: Props.ChipProps // TODO use ChipProps once it gets merged in @tscircuit/props chip: Props.ChipProps diff --git a/lib/utils/createComponentsFromSoup.ts b/lib/utils/createComponentsFromSoup.ts index dd345f9..b9b38d9 100644 --- a/lib/utils/createComponentsFromSoup.ts +++ b/lib/utils/createComponentsFromSoup.ts @@ -1,6 +1,7 @@ import type { AnySoupElement } from "@tscircuit/soup" import type { PrimitiveComponent } from "../components/base-components/PrimitiveComponent" import { SmtPad } from "lib/components/primitive-components/SmtPad" +import { SilkscreenPath } from "lib/components/primitive-components/SilkscreenPath" export const createComponentsFromSoup = ( soup: AnySoupElement[], @@ -30,57 +31,15 @@ export const createComponentsFromSoup = ( portHints: elm.port_hints, }), ) + } else if (elm.type === "pcb_silkscreen_path") { + components.push( + new SilkscreenPath({ + layer: elm.layer, + route: elm.route, + strokeWidth: elm.stroke_width, + }), + ) } } return components - // if (elm.type === "pcb_smtpad") { - // this.add("smtpad", (pb) => pb.setProps(elm)) - // } else if (elm.type === "pcb_plated_hole") { - // this.add("platedhole", (pb) => pb.setProps(elm)) - // } else if (elm.type === "pcb_hole") { - // this.add("hole", (pb) => pb.setProps(elm)) - // } else if (elm.type === "pcb_silkscreen_circle") { - // this.add("silkscreencircle", (pb) => - // pb.setProps({ - // ...elm, - // pcbX: elm.center.x, - // pcbY: elm.center.y, - // }) - // ) - // } else if (elm.type === "pcb_silkscreen_line") { - // this.add("silkscreenline", (pb) => - // pb.setProps({ - // ...elm, - // strokeWidth: elm.stroke_width, - // }) - // ) - // } else if (elm.type === "pcb_silkscreen_path") { - // this.add("silkscreenpath", (pb) => - // pb.setProps({ - // ...elm, - // strokeWidth: elm.stroke_width, - // }) - // ) - // } else if (elm.type === "pcb_silkscreen_rect") { - // this.add("silkscreenrect", (pb) => - // pb.setProps({ - // ...elm, - // pcbX: elm.center.x, - // pcbY: elm.center.y, - // // TODO silkscreen rect isFilled, isOutline etc. - // }) - // ) - // } else if (elm.type === "pcb_fabrication_note_path") { - // this.add("fabricationnotepath", (pb) => pb.setProps(elm)) - // } else if (elm.type === "pcb_fabrication_note_text") { - // this.add("fabricationnotetext", (pb) => - // pb.setProps({ - // ...elm, - // pcbX: elm.anchor_position.x, - // pcbY: elm.anchor_position.y, - // anchorAlignment: elm.anchor_alignment, - // fontSize: elm.font_size, - // }) - // ) - // } } diff --git a/package.json b/package.json index 1fc72c0..51f70ae 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,13 @@ }, "devDependencies": { "@biomejs/biome": "^1.8.3", + "@tscircuit/layout": "^0.0.27", "@tscircuit/log-soup": "^1.0.2", "@types/bun": "latest", "@types/react": "^18.3.3", "@types/react-reconciler": "^0.28.8", - "circuit-to-svg": "^0.0.3", + "bun-match-svg": "0.0.2", + "circuit-to-svg": "^0.0.13", "howfat": "^0.3.8", "looks-same": "^9.0.1", "tsup": "^8.2.4" @@ -30,7 +32,7 @@ }, "dependencies": { "@tscircuit/infgrid-ijump-astar": "0.0.5", - "@tscircuit/props": "^0.0.46", + "@tscircuit/props": "^0.0.49", "@tscircuit/soup": "^0.0.58", "@tscircuit/soup-util": "0.0.18", "footprinter": "^0.0.44", diff --git a/tests/__snapshots__/example1.snap.svg b/tests/__snapshots__/example1.snap.svg new file mode 100644 index 0000000..3d9ac17 --- /dev/null +++ b/tests/__snapshots__/example1.snap.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/tests/components/normal-components/chip-pin-selector.test.tsx b/tests/components/normal-components/chip-pin-selector.test.tsx new file mode 100644 index 0000000..eb52949 --- /dev/null +++ b/tests/components/normal-components/chip-pin-selector.test.tsx @@ -0,0 +1,20 @@ +import { test, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("should be able to select a pin/port by label on a chip", () => { + const { project } = getTestFixture() + project.add( + + + , + ) + + project.render() + + const pwr = project.selectOne(".U1 .PWR") + expect(pwr).toBeTruthy() +}) diff --git a/tests/components/primitive-components/__snapshots__/trace-hint-pcb.snap.svg b/tests/components/primitive-components/__snapshots__/trace-hint-pcb.snap.svg index 3e91265..8a496c4 100644 --- a/tests/components/primitive-components/__snapshots__/trace-hint-pcb.snap.svg +++ b/tests/components/primitive-components/__snapshots__/trace-hint-pcb.snap.svg @@ -1,30 +1,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/tests/components/primitive-components/__snapshots__/trace-hint-schematic.snap.svg b/tests/components/primitive-components/__snapshots__/trace-hint-schematic.snap.svg index 3fad545..c11e7b4 100644 --- a/tests/components/primitive-components/__snapshots__/trace-hint-schematic.snap.svg +++ b/tests/components/primitive-components/__snapshots__/trace-hint-schematic.snap.svg @@ -1,62 +1,7 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/tests/components/primitive-components/__snapshots__/trace-pcb.snap.svg b/tests/components/primitive-components/__snapshots__/trace-pcb.snap.svg index f3642e1..f92a18c 100644 --- a/tests/components/primitive-components/__snapshots__/trace-pcb.snap.svg +++ b/tests/components/primitive-components/__snapshots__/trace-pcb.snap.svg @@ -1,30 +1,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/tests/components/primitive-components/__snapshots__/trace-schematic.snap.svg b/tests/components/primitive-components/__snapshots__/trace-schematic.snap.svg index 3fad545..c11e7b4 100644 --- a/tests/components/primitive-components/__snapshots__/trace-schematic.snap.svg +++ b/tests/components/primitive-components/__snapshots__/trace-schematic.snap.svg @@ -1,62 +1,7 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/tests/examples/example1.test.tsx b/tests/examples/example1.test.tsx new file mode 100644 index 0000000..0c6451f --- /dev/null +++ b/tests/examples/example1.test.tsx @@ -0,0 +1,44 @@ +import { test, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("example1", async () => { + const { project } = getTestFixture() + project.add( + + + + + + + {/* + + + */} + , + ) + + project.render() + + await expect( + project.getSvg({ + view: "pcb", + layer: "top", + }), + ).toMatchSvgSnapshot(import.meta.dir, "example1") +}) diff --git a/tests/fixtures/__snapshots__/circuit-snapshot-pcb.snap.svg b/tests/fixtures/__snapshots__/circuit-snapshot-pcb.snap.svg index babdd01..99b70d5 100644 --- a/tests/fixtures/__snapshots__/circuit-snapshot-pcb.snap.svg +++ b/tests/fixtures/__snapshots__/circuit-snapshot-pcb.snap.svg @@ -1,21 +1,7 @@ - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/tests/fixtures/extend-expect-circuit-snapshot.ts b/tests/fixtures/extend-expect-circuit-snapshot.ts index f1c686a..3aaedbb 100644 --- a/tests/fixtures/extend-expect-circuit-snapshot.ts +++ b/tests/fixtures/extend-expect-circuit-snapshot.ts @@ -1,4 +1,4 @@ -import { pcbSoupToSvg, soupToSvg as schematicSoupToSvg } from "circuit-to-svg" +import { circuitJsonToPcbSvg, circuitJsonToSchematicSvg } from "circuit-to-svg" import { it, expect, type CustomMatcher, type MatcherResult } from "bun:test" import * as fs from "node:fs" import * as path from "node:path" @@ -22,7 +22,8 @@ async function saveSnapshotOfSoup({ const snapshotName = `${path.basename(testPath || "")}-${mode}.snap.svg` const filePath = path.join(snapshotDir, snapshotName) - const svg = mode === "pcb" ? pcbSoupToSvg(soup) : schematicSoupToSvg(soup) + const svg = + mode === "pcb" ? circuitJsonToPcbSvg(soup) : circuitJsonToSchematicSvg(soup) if (!fs.existsSync(snapshotDir)) { fs.mkdirSync(snapshotDir, { recursive: true }) diff --git a/tests/fixtures/preload.ts b/tests/fixtures/preload.ts new file mode 100644 index 0000000..ee67a60 --- /dev/null +++ b/tests/fixtures/preload.ts @@ -0,0 +1 @@ +import "bun-match-svg"