Skip to content
Closed
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
30 changes: 25 additions & 5 deletions cli/snapshot/register.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Command } from "commander"
import type { VisibleLayerRef } from "circuit-json"
import { CAMERA_PRESET_NAMES, type CameraPreset } from "circuit-json-to-3d-png"
import type { Command } from "commander"
import { snapshotProject } from "lib/shared/snapshot-project"

export const registerSnapshot = (program: Command) => {
Expand All @@ -25,6 +25,10 @@ export const registerSnapshot = (program: Command) => {
)
.option("--disable-parts-engine", "Disable the parts engine")
.option("--show-courtyards", "Show courtyard outlines in PCB snapshots")
.option(
"--component-name <name>",
"Focus on one PCB component (implies --pcb-only and --show-courtyards)",
)
.option(
"--camera-preset <preset>",
`Camera angle preset for 3D snapshots (implies --3d). Valid presets: ${CAMERA_PRESET_NAMES.join(", ")}`,
Expand Down Expand Up @@ -53,6 +57,7 @@ export const registerSnapshot = (program: Command) => {
ci?: boolean
test?: boolean
concurrency?: string
componentName?: string
},
) => {
if (
Expand Down Expand Up @@ -82,6 +87,19 @@ export const registerSnapshot = (program: Command) => {
process.exit(1)
}

if (
options.componentName &&
(options.schematicOnly ||
options.simulationOnly ||
options["3d"] ||
options.cameraPreset)
) {
console.error(
"--component-name cannot be combined with --schematic-only, --simulation-only, --3d, or --camera-preset.",
)
process.exit(1)
}

if (
options.simulationOnly &&
(options.pcbOnly ||
Expand All @@ -97,7 +115,7 @@ export const registerSnapshot = (program: Command) => {
}

let pcbOnly = options.pcbOnly ?? false
if (pcbLayer) {
if (pcbLayer || options.componentName) {
pcbOnly = true
}

Expand All @@ -117,9 +135,11 @@ export const registerSnapshot = (program: Command) => {
platformConfig: options.disablePartsEngine
? { partsEngineDisabled: true }
: undefined,
pcbSnapshotSettingsOverride: options.showCourtyards
? { showCourtyards: true }
: undefined,
pcbSnapshotSettingsOverride:
options.showCourtyards || options.componentName
? { showCourtyards: true }
: undefined,
componentName: options.componentName,
cameraPreset: options.cameraPreset as CameraPreset | undefined,
createDiff: (options.ci ?? false) || (options.test ?? false),
onExit: (code) => process.exit(code),
Expand Down
1 change: 1 addition & 0 deletions cli/snapshot/worker-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const snapshotFilesWithWorkerPool = async (options: {
createDiff: job.options.createDiff,
cameraPreset: job.options.cameraPreset,
pcbLayer: job.options.pcbLayer,
componentName: job.options.componentName,
},
}),
isLogMessage: (message) => message.message_type === "worker_log",
Expand Down
6 changes: 4 additions & 2 deletions cli/snapshot/worker-snapshot-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { PlatformConfig } from "@tscircuit/props"
import type { VisibleLayerRef } from "circuit-json"
import type { CameraPreset } from "circuit-json-to-3d-png"
import { loadRuntimeProjectConfig } from "lib/project-config"
import type { PcbSnapshotSettings } from "lib/project-config/project-config-schema"
import { mergePlatformConfigs } from "lib/shared/platform-config-utils"
import { processSnapshotFile } from "lib/shared/process-snapshot-file"
import { registerStaticAssetLoaders } from "lib/shared/register-static-asset-loaders"
import { loadRuntimeProjectConfig } from "lib/project-config"
import { mergePlatformConfigs } from "lib/shared/platform-config-utils"
import type { SnapshotCompletedMessage } from "./worker-types"

type SnapshotWorkerOptions = {
Expand All @@ -20,6 +20,7 @@ type SnapshotWorkerOptions = {
createDiff: boolean
cameraPreset?: CameraPreset
pcbLayer?: VisibleLayerRef
componentName?: string
}

export const handleSnapshotFile = async (
Expand Down Expand Up @@ -51,6 +52,7 @@ export const handleSnapshotFile = async (
createDiff: options.createDiff,
cameraPreset: options.cameraPreset,
pcbLayer: options.pcbLayer,
componentName: options.componentName,
})

return {
Expand Down
1 change: 1 addition & 0 deletions cli/snapshot/worker-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type SnapshotFileMessage = {
createDiff: boolean
cameraPreset?: CameraPreset
pcbLayer?: VisibleLayerRef
componentName?: string
}
}

Expand Down
126 changes: 126 additions & 0 deletions lib/shared/get-component-pcb-viewport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { AnyCircuitElement } from "circuit-json"

export type PcbViewport = {
minX: number
minY: number
maxX: number
maxY: number
}

const normalizeComponentName = (name: string) => name.replace(/^\./, "")

const addPoint = (bounds: PcbViewport, x: number, y: number) => {
bounds.minX = Math.min(bounds.minX, x)
bounds.minY = Math.min(bounds.minY, y)
bounds.maxX = Math.max(bounds.maxX, x)
bounds.maxY = Math.max(bounds.maxY, y)
}

const addRect = (
bounds: PcbViewport,
center: { x: number; y: number },
width: number,
height: number,
rotation = 0,
) => {
const radians = (rotation * Math.PI) / 180
const halfWidth = width / 2
const halfHeight = height / 2
const rotatedHalfWidth =
Math.abs(Math.cos(radians)) * halfWidth +
Math.abs(Math.sin(radians)) * halfHeight
const rotatedHalfHeight =
Math.abs(Math.sin(radians)) * halfWidth +
Math.abs(Math.cos(radians)) * halfHeight

addPoint(bounds, center.x - rotatedHalfWidth, center.y - rotatedHalfHeight)
addPoint(bounds, center.x + rotatedHalfWidth, center.y + rotatedHalfHeight)
}

export const getComponentPcbViewport = (
circuitJson: AnyCircuitElement[],
requestedComponentName: string,
): PcbViewport => {
const componentName = normalizeComponentName(requestedComponentName)
const sourceComponent = circuitJson.find(
(element) =>
element.type === "source_component" &&
normalizeComponentName(element.name) === componentName,
)

if (!sourceComponent || sourceComponent.type !== "source_component") {
throw new Error(
`Could not find component named "${requestedComponentName}"`,
)
}

const pcbComponent = circuitJson.find(
(element) =>
element.type === "pcb_component" &&
element.source_component_id === sourceComponent.source_component_id,
)

if (!pcbComponent || pcbComponent.type !== "pcb_component") {
throw new Error(
`Component "${requestedComponentName}" does not have a PCB component`,
)
}

const bounds: PcbViewport = {
minX: Number.POSITIVE_INFINITY,
minY: Number.POSITIVE_INFINITY,
maxX: Number.NEGATIVE_INFINITY,
maxY: Number.NEGATIVE_INFINITY,
}

addRect(
bounds,
pcbComponent.center,
pcbComponent.width,
pcbComponent.height,
pcbComponent.rotation,
)

for (const element of circuitJson) {
if (
!("pcb_component_id" in element) ||
element.pcb_component_id !== pcbComponent.pcb_component_id
) {
continue
}

if (element.type === "pcb_courtyard_rect") {
addRect(
bounds,
element.center,
element.width,
element.height,
element.ccw_rotation,
)
} else if (
element.type === "pcb_courtyard_outline" ||
element.type === "pcb_courtyard_polygon"
) {
const points =
element.type === "pcb_courtyard_outline"
? element.outline
: element.points
for (const point of points) addPoint(bounds, point.x, point.y)
} else if (element.type === "pcb_courtyard_circle") {
addRect(bounds, element.center, element.radius * 2, element.radius * 2)
} else if (element.type === "pcb_courtyard_pill") {
addRect(bounds, element.center, element.width, element.height)
}
}

const width = bounds.maxX - bounds.minX
const height = bounds.maxY - bounds.minY
const padding = Math.max(0.5, Math.max(width, height) * 0.1)

return {
minX: bounds.minX - padding,
minY: bounds.minY - padding,
maxX: bounds.maxX + padding,
maxY: bounds.maxY + padding,
}
}
16 changes: 13 additions & 3 deletions lib/shared/process-snapshot-file.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import fs from "node:fs"
import path from "node:path"
import type { PlatformConfig } from "@tscircuit/props"
import type { PcbSnapshotSettings } from "lib/project-config/project-config-schema"
import type { AnyCircuitElement, VisibleLayerRef } from "circuit-json"
import { renderCircuitJsonTo3dPng } from "circuit-json-to-3d-png"
import type { CameraPreset } from "circuit-json-to-3d-png"
import {
convertCircuitJsonToPcbSvg,
convertCircuitJsonToStackedSchematicSheetsSvg,
} from "circuit-to-svg"
import kleur from "kleur"
import type { CameraPreset } from "circuit-json-to-3d-png"
import type { PcbSnapshotSettings } from "lib/project-config/project-config-schema"
import { getOrGenerateCircuitJson } from "lib/shared/get-or-generate-circuit-json"
import { getPlatformConfigWithCliDefaults } from "lib/shared/get-platform-config-with-cli-defaults"
import { getSimulationSvgAssetsFromCircuitJson } from "lib/shared/simulation-svg-assets"
import { compareAndCreateDiff } from "./compare-images"
import { getComponentPcbViewport } from "./get-component-pcb-viewport"
import { isCircuitJsonFile } from "./is-circuit-json-file"

export type ProcessSnapshotFileOptions = {
Expand All @@ -31,6 +32,7 @@ export type ProcessSnapshotFileOptions = {
createDiff: boolean
cameraPreset?: CameraPreset
pcbLayer?: VisibleLayerRef
componentName?: string
}

export type ProcessSnapshotFileResult = {
Expand All @@ -57,6 +59,7 @@ export const processSnapshotFile = async ({
createDiff,
cameraPreset,
pcbLayer,
componentName,
}: ProcessSnapshotFileOptions): Promise<ProcessSnapshotFileResult> => {
const relativeFilePath = path.relative(projectDir, file)
const successPaths: string[] = []
Expand Down Expand Up @@ -104,9 +107,13 @@ export const processSnapshotFile = async ({

if (!simulationOnly) {
try {
const viewport = componentName
? getComponentPcbViewport(circuitJson, componentName)
: undefined
pcbSvg = convertCircuitJsonToPcbSvg(circuitJson, {
...pcbSnapshotSettings,
layer: pcbLayer,
viewport,
})
} catch (error) {
const errorMessage =
Expand Down Expand Up @@ -228,6 +235,9 @@ export const processSnapshotFile = async ({
fs.mkdirSync(snapDir, { recursive: true })

const base = path.basename(file).replace(/\.[^.]+$/, "")
const componentSuffix = componentName
? `-${componentName.replace(/^\./, "").replace(/[^a-zA-Z0-9._-]+/g, "-")}`
: ""
const snapshots: Array<
| {
type: string | VisibleLayerRef
Expand Down Expand Up @@ -283,7 +293,7 @@ export const processSnapshotFile = async ({
const is3d = type === "3d"
const snapPath = path.join(
snapDir,
`${base}-${type}.snap.${is3d ? "png" : "svg"}`,
`${base}${componentSuffix}-${type}.snap.${is3d ? "png" : "svg"}`,
)
const existing = fs.existsSync(snapPath)

Expand Down
7 changes: 6 additions & 1 deletion lib/shared/snapshot-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from "node:fs"
import path from "node:path"
import type { PlatformConfig } from "@tscircuit/props"
import type { VisibleLayerRef } from "circuit-json"
import type { CameraPreset } from "circuit-json-to-3d-png"
import { snapshotFilesWithWorkerPool } from "cli/snapshot/worker-pool"
import kleur from "kleur"
import {
Expand All @@ -10,7 +11,6 @@ import {
loadRuntimeProjectConfig,
} from "lib/project-config"
import type { PcbSnapshotSettings } from "lib/project-config/project-config-schema"
import type { CameraPreset } from "circuit-json-to-3d-png"
import { findBoardFilesAsync } from "lib/shared/find-board-files"
import { mergePlatformConfigs } from "lib/shared/platform-config-utils"
import { processSnapshotFile } from "lib/shared/process-snapshot-file"
Expand Down Expand Up @@ -44,6 +44,8 @@ type SnapshotOptions = {
cameraPreset?: CameraPreset
/** Limit PCB snapshots to one layer */
pcbLayer?: VisibleLayerRef
/** Focus the PCB snapshot on one component */
componentName?: string
/** Number of files to process in parallel (default: 1) */
concurrency?: number
onExit?: (code: number) => void
Expand Down Expand Up @@ -78,6 +80,7 @@ export const snapshotProject = async ({
createDiff = false,
cameraPreset,
pcbLayer,
componentName,
concurrency = 1,
}: SnapshotOptions = {}) => {
// --camera-preset implies --3d
Expand Down Expand Up @@ -188,6 +191,7 @@ export const snapshotProject = async ({
createDiff,
cameraPreset,
pcbLayer,
componentName,
},
stopOnFailure: true,
onLog: (lines) => {
Expand Down Expand Up @@ -232,6 +236,7 @@ export const snapshotProject = async ({
createDiff,
cameraPreset,
pcbLayer,
componentName,
})

processResult(result)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions tests/cli/snapshot/snapshot-component-name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, test } from "bun:test"
import "bun-match-svg"
import { join } from "node:path"
import { getCliTestFixture } from "../../fixtures/get-cli-test-fixture"

test("snapshot --component-name focuses a PCB snapshot on one component", async () => {
const { tmpDir, runCommand } = await getCliTestFixture()

await Bun.write(
join(tmpDir, "test.board.tsx"),
`
export const TestBoard = () => (
<board width="20mm" height="10mm">
<resistor name="R1" resistance="1k" footprint="0402" pcbX={-6} />
<resistor name="R2" resistance="2k" footprint="0402" pcbX={6} />
</board>
)
`,
)

await runCommand("tsci snapshot --update --component-name R1")

const snapshotPath = join(
tmpDir,
"__snapshots__",
"test.board-R1-pcb.snap.svg",
)
const svg = await Bun.file(snapshotPath).text()

expect(svg).toMatchSvgSnapshot(import.meta.path, "component-name-R1")
}, 60_000)
Loading