Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ The PCBViewer component accepts these props:
- `allowEditing`: Enable/disable editing capabilities (default: true)
- `editEvents`: Array of edit events to apply
- `onEditEventsChanged`: Callback when edit events change
- `initialState`: Initial state for the viewer
- `viewState`: Controlled subset of view options (layer + visibility toggles)
- `onViewStateChange`: Callback when controlled view state changes
- `initialState`: Initial boolean state for the viewer.

### Features

Expand Down
120 changes: 110 additions & 10 deletions src/PCBViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { applyEditEvents } from "@tscircuit/core"
import { findBoundsAndCenter } from "@tscircuit/circuit-json-util"
import type { AnyCircuitElement, SourceTrace } from "circuit-json"
import type { AnyCircuitElement, LayerRef, SourceTrace } from "circuit-json"
import { ContextProviders } from "./components/ContextProviders"
import type { StateProps } from "./global-store"
import type { ControlledViewState, StateProps } from "./global-store"
import type { GraphicsObject } from "graphics-debug"
import { ToastContainer } from "lib/toast"
import { useEffect, useMemo, useRef, useState } from "react"
Expand All @@ -17,11 +17,92 @@ import { calculateBoardSizeKey } from "lib/calculate-board-size-key"

const defaultTransform = compose(translate(400, 300), scale(40, -40))

export type PCBViewerViewState = {
selectedLayer: LayerRef
isShowingRatsNest: boolean
isShowingMultipleTracesLength: boolean
isShowingAutorouting: boolean
isShowingDrcErrors: boolean
isShowingCopperPours: boolean
isShowingPcbGroups: boolean
isShowingGroupAnchorOffsets: boolean
isShowingSolderMask: boolean
isShowingFabricationNotes: boolean
pcbGroupViewMode: "all" | "named_only"
}

const VIEW_STATE_KEY_MAP: Record<
keyof PCBViewerViewState,
keyof ControlledViewState
> = {
selectedLayer: "selected_layer",
isShowingRatsNest: "is_showing_rats_nest",
isShowingMultipleTracesLength: "is_showing_multiple_traces_length",
isShowingAutorouting: "is_showing_autorouting",
isShowingDrcErrors: "is_showing_drc_errors",
isShowingCopperPours: "is_showing_copper_pours",
isShowingPcbGroups: "is_showing_pcb_groups",
isShowingGroupAnchorOffsets: "is_showing_group_anchor_offsets",
isShowingSolderMask: "is_showing_solder_mask",
isShowingFabricationNotes: "is_showing_fabrication_notes",
pcbGroupViewMode: "pcb_group_view_mode",
}

const VIEW_STATE_MAPPINGS = Object.entries(VIEW_STATE_KEY_MAP) as Array<
[keyof PCBViewerViewState, keyof ControlledViewState]
>

const mapViewStateToControlledState = (
viewState?: Partial<PCBViewerViewState>,
): Partial<ControlledViewState> | undefined => {
if (!viewState) return undefined

const controlledState: Partial<ControlledViewState> = {}

for (const [publicKey, controlledKey] of VIEW_STATE_MAPPINGS) {
const value = viewState[publicKey]
if (value !== undefined) {
controlledState[controlledKey] = value as never
}
}

return controlledState
}

const mapControlledStateToViewState = (
controlledViewState: ControlledViewState,
): PCBViewerViewState => {
const viewState = {} as PCBViewerViewState

for (const [publicKey, controlledKey] of VIEW_STATE_MAPPINGS) {
viewState[publicKey] = controlledViewState[controlledKey] as never
}

return viewState
}

const mapViewStateToInitialState = (
viewState?: Partial<PCBViewerViewState>,
): Partial<StateProps> => {
const controlledState = mapViewStateToControlledState(viewState)
if (!controlledState) return {}

const {
selected_layer: _selectedLayer,
pcb_group_view_mode: _pcbGroupViewMode,
...booleanState
} = controlledState

return booleanState as Partial<StateProps>
}

type Props = {
circuitJson?: AnyCircuitElement[]
height?: number
allowEditing?: boolean
editEvents?: ManualEditEvent[]
viewState?: Partial<PCBViewerViewState>
onViewStateChange?: (viewState: PCBViewerViewState) => void
initialState?: Partial<StateProps>
onEditEventsChanged?: (editEvents: ManualEditEvent[]) => void
focusOnHover?: boolean
Expand All @@ -35,6 +116,8 @@ export const PCBViewer = ({
debugGraphics,
height = 600,
initialState,
viewState,
onViewStateChange,
allowEditing = true,
editEvents: editEventsProp,
onEditEventsChanged,
Expand Down Expand Up @@ -74,9 +157,7 @@ export const PCBViewer = ({
const { center, width, height } = elements.some((e) =>
e.type.startsWith("pcb_"),
)
? findBoundsAndCenter(
elements.filter((e) => e.type.startsWith("pcb_")) as any,
)
? findBoundsAndCenter(elements.filter((e) => e.type.startsWith("pcb_")))
: { center: { x: 0, y: 0 }, width: 0.001, height: 0.001 }
const scaleFactor =
Math.min(
Expand Down Expand Up @@ -115,14 +196,14 @@ export const PCBViewer = ({
const pcbElmsPreEdit = useMemo(() => {
return (
circuitJson?.filter(
(e: any) => e.type.startsWith("pcb_") || e.type.startsWith("source_"),
(e) => e.type.startsWith("pcb_") || e.type.startsWith("source_"),
) ?? []
)
}, [circuitJsonKey])

const elements = useMemo(() => {
return applyEditEvents({
circuitJson: pcbElmsPreEdit as any,
circuitJson: pcbElmsPreEdit,
editEvents,
})
}, [pcbElmsPreEdit, editEvents])
Expand All @@ -144,21 +225,40 @@ export const PCBViewer = ({
const mergedInitialState = useMemo(
() => ({
...initialState,
...mapViewStateToInitialState(viewState),
...(disablePcbGroups && { is_showing_pcb_groups: false }),
}),
[initialState, disablePcbGroups],
[initialState, viewState, disablePcbGroups],
)

const controlledViewState = useMemo(
() => mapViewStateToControlledState(viewState),
[viewState],
)

Comment thread
techmannih marked this conversation as resolved.
return (
<div
ref={transformRef as any}
ref={transformRef}
style={{ position: "relative" }}
onContextMenu={(event) => event.preventDefault()}
>
<div ref={ref as any}>
<div
ref={(element) => {
if (element) ref(element)
Comment thread
techmannih marked this conversation as resolved.
}}
>
<ContextProviders
initialState={mergedInitialState}
disablePcbGroups={disablePcbGroups}
controlledViewState={controlledViewState}
onControlledViewStateChange={
onViewStateChange
? (nextControlledViewState) =>
onViewStateChange(
mapControlledStateToViewState(nextControlledViewState),
)
: undefined
}
>
<CanvasElementsRenderer
key={refDimensions.width}
Expand Down
78 changes: 70 additions & 8 deletions src/components/ContextProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,88 @@
import { useMemo } from "react"
import { useEffect, useMemo, useRef } from "react"
import { createContext, useContext } from "react"
Comment thread
techmannih marked this conversation as resolved.
import { createStore, type StateProps } from "../global-store"
import {
createStore,
getControlledViewState,
type ControlledViewState,
type StateProps,
} from "../global-store"

export const StoreContext = createContext(null)
export const StoreContext = createContext<ReturnType<
typeof createStore
> | null>(null)

export const ContextProviders = ({
children,
initialState,
disablePcbGroups,
controlledViewState,
onControlledViewStateChange,
}: {
children?: any
initialState?: Partial<StateProps>
disablePcbGroups?: boolean
controlledViewState?: Partial<ControlledViewState>
onControlledViewStateChange?: (viewState: ControlledViewState) => void
}) => {
const store = useMemo(
() => createStore(initialState, disablePcbGroups),
[disablePcbGroups],
)

return (
<StoreContext.Provider value={store as any}>
{children}
</StoreContext.Provider>
)
const isSyncingFromPropsRef = useRef(false)

useEffect(() => {
if (!controlledViewState) return

Comment thread
techmannih marked this conversation as resolved.
const currentState = store.getState()
let nextPartialState: Partial<ControlledViewState> = {}

const controlledEntries = Object.entries(controlledViewState) as {
[K in keyof ControlledViewState]-?: [
K,
ControlledViewState[K] | undefined,
]
}[keyof ControlledViewState][]

for (const [typedKey, value] of controlledEntries) {
if (value === undefined) continue
if (currentState[typedKey] === value) continue
nextPartialState = {
...nextPartialState,
[typedKey]: value,
}
}

if (Object.keys(nextPartialState).length === 0) return

isSyncingFromPropsRef.current = true
try {
store.setState(nextPartialState)
} finally {
isSyncingFromPropsRef.current = false
}
}, [controlledViewState, store])

useEffect(() => {
if (!onControlledViewStateChange) return

const unsubscribe = store.subscribe((state, previousState) => {
if (isSyncingFromPropsRef.current) return

const currentViewState = getControlledViewState(state)
const prevViewState = getControlledViewState(previousState)

const hasChanged = (
Object.keys(currentViewState) as (keyof ControlledViewState)[]
).some((key) => currentViewState[key] !== prevViewState[key])

if (hasChanged) {
onControlledViewStateChange(currentViewState)
}
})

return () => unsubscribe()
}, [onControlledViewStateChange, store])

return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
}
Loading
Loading