@tscircuit/pcb-viewer 1.11.407 → 1.11.409

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,6 +76,7 @@ The PCBViewer component accepts these props:
76
76
  - `editEvents`: Array of edit events to apply
77
77
  - `onEditEventsChanged`: Callback when edit events change
78
78
  - `onBoundsSelected`: Callback when the Bounds tool completes a rectangle selection. Receives `{ minX, minY, maxX, maxY }`.
79
+ - `onViewSchematicComponent`: Optional callback for the pad context menu action `↗︎ U1 on Schematic`. Receives `{ source_component_id, pcb_component_id, refdes }`. Omit it when the host schematic tab is disabled.
79
80
  - `initialState`: Initial state for the viewer
80
81
 
81
82
  ### Features
@@ -123,3 +124,25 @@ The WebGPU renderer is installed as a bundled devDependency from
123
124
  `https://jscdn.tscircuit.com/@tscircuit/circuit-json-webgpu/0.0.3.tgz`.
124
125
  The renderer repository includes a TypeScript local render command and 597
125
126
  SVG-left/WebGPU-right feature snapshot tests.
127
+
128
+ ### Navigate from a pad to a schematic
129
+
130
+ Click or right-click an SMT pad or plated hole to see its component's manufacturer
131
+ part number in a disabled row at the bottom of the context menu, when available.
132
+ Passing `onViewSchematicComponent` also enables the schematic action:
133
+
134
+ ```tsx
135
+ <PCBViewer
136
+ circuitJson={circuitJson}
137
+ onViewSchematicComponent={schematicTabEnabled ? ({ source_component_id }) => {
138
+ // The host switches tabs and focuses the matching schematic component.
139
+ showSchematicComponent(source_component_id)
140
+ } : undefined}
141
+ />
142
+ ```
143
+
144
+ The callback uses the source component ID so hosts can resolve the corresponding
145
+ schematic component even when reference designators repeat across subcircuits.
146
+ RunFrame and CircuitJsonViewer can forward this callback from their shared PCB
147
+ preview when the schematic tab is enabled. Tab switching and schematic zooming
148
+ are implemented by the host; PCBViewer does not require schematic elements.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,13 @@ import { GraphicsObject } from 'graphics-debug';
4
4
  import { ManualEditEvent } from '@tscircuit/props';
5
5
  import { Matrix } from 'transformation-matrix';
6
6
 
7
+ /** Stable identity for hosts to locate the component in their schematic. */
8
+ interface ViewSchematicComponentEvent {
9
+ source_component_id: string;
10
+ pcb_component_id: string;
11
+ refdes: string;
12
+ }
13
+
7
14
  interface State {
8
15
  hidden_layer_opacity: number;
9
16
  setHiddenLayerOpacity: (opacity: number) => void;
@@ -81,13 +88,15 @@ type Props = {
81
88
  editEvents?: ManualEditEvent[];
82
89
  initialState?: Partial<StateProps>;
83
90
  onEditEventsChanged?: (editEvents: ManualEditEvent[]) => void;
91
+ /** Omit when the host has no enabled schematic tab. */
92
+ onViewSchematicComponent?: (event: ViewSchematicComponentEvent) => void;
84
93
  onBoundsSelected?: (bounds: BoundsSelection) => void;
85
94
  focusOnHover?: boolean;
86
95
  clickToInteractEnabled?: boolean;
87
96
  debugGraphics?: GraphicsObject | null;
88
97
  disablePcbGroups?: boolean;
89
98
  };
90
- declare const PCBViewer: ({ circuitJson, debugGraphics, height, renderer, initialState, allowEditing, editEvents: editEventsProp, onEditEventsChanged, onBoundsSelected, focusOnHover, clickToInteractEnabled, disablePcbGroups, }: Props) => react_jsx_runtime.JSX.Element;
99
+ declare const PCBViewer: ({ circuitJson, debugGraphics, height, renderer, initialState, allowEditing, editEvents: editEventsProp, onEditEventsChanged, onBoundsSelected, onViewSchematicComponent, focusOnHover, clickToInteractEnabled, disablePcbGroups, }: Props) => react_jsx_runtime.JSX.Element;
91
100
 
92
101
  interface CanvasElementsRendererProps {
93
102
  elements: AnyCircuitElement[];
@@ -100,6 +109,7 @@ interface CanvasElementsRendererProps {
100
109
  grid?: GridConfig;
101
110
  allowEditing: boolean;
102
111
  focusOnHover?: boolean;
112
+ onViewSchematicComponent?: (event: ViewSchematicComponentEvent) => void;
103
113
  onBoundsSelected?: (bounds: BoundsSelection) => void;
104
114
  onContextMenuOpenChange?: (open: boolean) => void;
105
115
  cancelPanDrag: () => void;
@@ -108,4 +118,4 @@ interface CanvasElementsRendererProps {
108
118
  }
109
119
  declare const CanvasElementsRenderer: (props: CanvasElementsRendererProps) => react_jsx_runtime.JSX.Element;
110
120
 
111
- export { CanvasElementsRenderer, PCBViewer };
121
+ export { CanvasElementsRenderer, PCBViewer, type ViewSchematicComponentEvent };
package/dist/index.js CHANGED
@@ -10100,6 +10100,25 @@ var useMouseMatrixTransform = (props = {}) => {
10100
10100
  };
10101
10101
  var src_default = useMouseMatrixTransform;
10102
10102
 
10103
+ // src/lib/get-pad-component.ts
10104
+ var getPadComponent = (pad, elements) => {
10105
+ if (pad?.type !== "pcb_smtpad" && pad?.type !== "pcb_plated_hole") return;
10106
+ const pcbComponent = elements.find(
10107
+ (element) => element.type === "pcb_component" && element.pcb_component_id === pad.pcb_component_id
10108
+ );
10109
+ if (pcbComponent?.type !== "pcb_component") return;
10110
+ const sourceComponent = elements.find(
10111
+ (element) => element.type === "source_component" && element.source_component_id === pcbComponent.source_component_id
10112
+ );
10113
+ if (sourceComponent?.type !== "source_component") return;
10114
+ return {
10115
+ source_component_id: sourceComponent.source_component_id,
10116
+ pcb_component_id: pcbComponent.pcb_component_id,
10117
+ refdes: sourceComponent.name,
10118
+ manufacturer_part_number: sourceComponent.manufacturer_part_number
10119
+ };
10120
+ };
10121
+
10103
10122
  // node_modules/circuit-json-to-connectivity-map/dist/index.js
10104
10123
  import { doesLineIntersectLine } from "@tscircuit/math-utils";
10105
10124
  function findConnectedNetworks(connections) {
@@ -18323,7 +18342,7 @@ import { css as css3 } from "@emotion/css";
18323
18342
  // package.json
18324
18343
  var package_default = {
18325
18344
  name: "@tscircuit/pcb-viewer",
18326
- version: "1.11.406",
18345
+ version: "1.11.408",
18327
18346
  main: "dist/index.js",
18328
18347
  type: "module",
18329
18348
  repository: {
@@ -18517,6 +18536,8 @@ var itemStyle = css2`
18517
18536
  &:hover, &:focus-visible { background: #444; }
18518
18537
  `;
18519
18538
  var VisibilityContextMenu = ({
18539
+ component,
18540
+ onViewSchematicComponent,
18520
18541
  position,
18521
18542
  onClose,
18522
18543
  onXRayNet,
@@ -18584,9 +18605,9 @@ var VisibilityContextMenu = ({
18584
18605
  const button = event.target.closest("button");
18585
18606
  if (!button) return;
18586
18607
  const menu = button.closest('[role="menu"]');
18587
- const items = Array.from(menu.querySelectorAll("button")).filter(
18588
- (item) => item.closest('[role="menu"]') === menu
18589
- );
18608
+ const items = Array.from(
18609
+ menu.querySelectorAll("button:not(:disabled)")
18610
+ ).filter((item) => item.closest('[role="menu"]') === menu);
18590
18611
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
18591
18612
  event.preventDefault();
18592
18613
  const step = event.key === "ArrowDown" ? 1 : -1;
@@ -18829,6 +18850,65 @@ var VisibilityContextMenu = ({
18829
18850
  )
18830
18851
  ]
18831
18852
  }
18853
+ ),
18854
+ component && onViewSchematicComponent && /* @__PURE__ */ jsxs15(
18855
+ "button",
18856
+ {
18857
+ type: "button",
18858
+ role: "menuitem",
18859
+ className: itemStyle,
18860
+ style: { whiteSpace: "normal", overflowWrap: "anywhere" },
18861
+ onClick: () => {
18862
+ onClose();
18863
+ onViewSchematicComponent({
18864
+ source_component_id: component.source_component_id,
18865
+ pcb_component_id: component.pcb_component_id,
18866
+ refdes: component.refdes
18867
+ });
18868
+ },
18869
+ children: [
18870
+ /* @__PURE__ */ jsx20(
18871
+ "svg",
18872
+ {
18873
+ "aria-hidden": "true",
18874
+ width: "12",
18875
+ height: "12",
18876
+ viewBox: "0 0 16 16",
18877
+ fill: "none",
18878
+ stroke: "currentColor",
18879
+ strokeWidth: "2.5",
18880
+ strokeLinecap: "round",
18881
+ strokeLinejoin: "round",
18882
+ style: { verticalAlign: "-1px", marginRight: 5 },
18883
+ children: /* @__PURE__ */ jsx20("path", { d: "M5 11 11 5M5 5h6v6" })
18884
+ }
18885
+ ),
18886
+ component.refdes,
18887
+ " on Schematic"
18888
+ ]
18889
+ }
18890
+ ),
18891
+ component?.manufacturer_part_number && /* @__PURE__ */ jsxs15(
18892
+ "button",
18893
+ {
18894
+ type: "button",
18895
+ role: "menuitem",
18896
+ disabled: true,
18897
+ "aria-label": `Manufacturer part number: ${component.manufacturer_part_number}`,
18898
+ className: itemStyle,
18899
+ style: {
18900
+ marginTop: 4,
18901
+ background: "#292929",
18902
+ color: "#aaa",
18903
+ cursor: "default",
18904
+ whiteSpace: "normal",
18905
+ overflowWrap: "anywhere"
18906
+ },
18907
+ children: [
18908
+ "MPN: ",
18909
+ component.manufacturer_part_number
18910
+ ]
18911
+ }
18832
18912
  )
18833
18913
  ]
18834
18914
  }
@@ -19433,6 +19513,8 @@ var ToolbarOverlay = ({
19433
19513
  elements,
19434
19514
  onContextMenuOpenChange,
19435
19515
  getNetAtPoint,
19516
+ getComponentAtPoint,
19517
+ onViewSchematicComponent,
19436
19518
  xRayNetIds = [],
19437
19519
  onXRayNetsChange
19438
19520
  }) => {
@@ -19501,6 +19583,14 @@ var ToolbarOverlay = ({
19501
19583
  const [contextGroups, setContextGroups] = useState12([]);
19502
19584
  const [contextNetId, setContextNetId] = useState12();
19503
19585
  const [contextNetDisplayName, setContextNetDisplayName] = useState12("Net");
19586
+ const [contextComponent, setContextComponent] = useState12();
19587
+ const componentAtEvent = (event) => {
19588
+ const rect = event.currentTarget.getBoundingClientRect();
19589
+ return getComponentAtPoint?.({
19590
+ x: event.clientX - rect.left,
19591
+ y: event.clientY - rect.top
19592
+ });
19593
+ };
19504
19594
  const pointerStart = useRef14(null);
19505
19595
  const inEditMode = useGlobalStore((s) => s.in_edit_mode);
19506
19596
  const netAtEvent = (event) => {
@@ -19661,15 +19751,17 @@ var ToolbarOverlay = ({
19661
19751
  if (!start || Math.hypot(event.clientX - start.x, event.clientY - start.y) > 4)
19662
19752
  return;
19663
19753
  const hit = netAtEvent(event);
19664
- if (!hit) return;
19665
- const net = hit.netId;
19666
- if (xRayNetIds.includes(net)) {
19754
+ const component = componentAtEvent(event);
19755
+ if (!hit && !component) return;
19756
+ const net = hit?.netId;
19757
+ if (net && xRayNetIds.includes(net)) {
19667
19758
  onXRayNetsChange?.(xRayNetIds.filter((id) => id !== net));
19668
19759
  closeContextMenu();
19669
19760
  } else {
19670
19761
  setContextNetId(net);
19671
- setContextNetDisplayName(hit.displayName);
19672
- setContextGroups(hit.groups);
19762
+ setContextNetDisplayName(hit?.displayName ?? "Net");
19763
+ setContextGroups(hit?.groups ?? []);
19764
+ setContextComponent(component);
19673
19765
  onContextMenuOpenChange?.(true);
19674
19766
  setContextMenuPosition({ x: event.clientX, y: event.clientY });
19675
19767
  }
@@ -19682,6 +19774,7 @@ var ToolbarOverlay = ({
19682
19774
  event.preventDefault();
19683
19775
  event.stopPropagation();
19684
19776
  const hit = netAtEvent(event);
19777
+ setContextComponent(componentAtEvent(event));
19685
19778
  setContextNetId(hit?.netId);
19686
19779
  setContextNetDisplayName(hit?.displayName ?? "Net");
19687
19780
  setContextGroups(hit?.groups ?? []);
@@ -19698,6 +19791,8 @@ var ToolbarOverlay = ({
19698
19791
  VisibilityContextMenu,
19699
19792
  {
19700
19793
  position: contextMenuPosition,
19794
+ component: contextComponent,
19795
+ onViewSchematicComponent,
19701
19796
  onClose: closeContextMenu,
19702
19797
  xRayDisplayName: contextNetDisplayName,
19703
19798
  xRayGroups: contextGroups.filter(
@@ -20182,6 +20277,14 @@ var CanvasElementsRenderer = (props) => {
20182
20277
  );
20183
20278
  const [xRayNetIds, setXRayNetIds] = useState13([]);
20184
20279
  useEffect19(() => setXRayNetIds([]), [connectivityMap]);
20280
+ useEffect19(() => {
20281
+ if (xRayNetIds.length === 0) return;
20282
+ const onKeyDown = (event) => {
20283
+ if (event.key === "Escape") setXRayNetIds([]);
20284
+ };
20285
+ window.addEventListener("keydown", onKeyDown, true);
20286
+ return () => window.removeEventListener("keydown", onKeyDown, true);
20287
+ }, [xRayNetIds.length]);
20185
20288
  const xRayElements = useMemo10(
20186
20289
  () => xRayNetIds.length > 0 ? elementsToRender.filter(
20187
20290
  (element) => isXRayCopper(element) && xRayNetIds.includes(
@@ -20197,6 +20300,21 @@ var CanvasElementsRenderer = (props) => {
20197
20300
  (primitive) => (primitive._element?.type === "pcb_smtpad" || primitive._element?.type === "pcb_plated_hole") && selectedElements.has(primitive._element)
20198
20301
  );
20199
20302
  }, [primitivesWithoutInteractionMetadata, xRayElements]);
20303
+ const getComponentAtPoint = (point2) => {
20304
+ if (!transform) return;
20305
+ const hits = getPrimitivesUnderPoint(
20306
+ primitivesWithoutInteractionMetadata,
20307
+ applyToPoint19(inverse6(transform), point2),
20308
+ transform,
20309
+ selectedLayer
20310
+ ).filter(
20311
+ (p) => p._element?.type === "pcb_smtpad" || p._element?.type === "pcb_plated_hole"
20312
+ );
20313
+ hits.sort(
20314
+ (a, b) => Number(b.layer === selectedLayer) - Number(a.layer === selectedLayer)
20315
+ );
20316
+ return getPadComponent(hits[0]?._element, elements);
20317
+ };
20200
20318
  const getNetAtPoint = (point2) => {
20201
20319
  if (!transform) return void 0;
20202
20320
  const describeNet = (netId, element) => ({
@@ -20380,6 +20498,8 @@ var CanvasElementsRenderer = (props) => {
20380
20498
  xRayNetIds,
20381
20499
  onXRayNetsChange: setXRayNetIds,
20382
20500
  onContextMenuOpenChange: props.onContextMenuOpenChange,
20501
+ getComponentAtPoint,
20502
+ onViewSchematicComponent: props.onViewSchematicComponent,
20383
20503
  children: /* @__PURE__ */ jsx25(ErrorOverlay, { transform, elements, children: /* @__PURE__ */ jsx25(RatsNestOverlay, { transform, soup: elements, children: /* @__PURE__ */ jsx25(
20384
20504
  PcbGroupOverlay,
20385
20505
  {
@@ -20512,6 +20632,7 @@ var PCBViewer = ({
20512
20632
  editEvents: editEventsProp,
20513
20633
  onEditEventsChanged,
20514
20634
  onBoundsSelected,
20635
+ onViewSchematicComponent,
20515
20636
  focusOnHover = false,
20516
20637
  clickToInteractEnabled = false,
20517
20638
  disablePcbGroups = false
@@ -20654,6 +20775,7 @@ var PCBViewer = ({
20654
20775
  allowEditing,
20655
20776
  focusOnHover,
20656
20777
  onBoundsSelected,
20778
+ onViewSchematicComponent,
20657
20779
  cancelPanDrag,
20658
20780
  onContextMenuOpenChange,
20659
20781
  onCreateEditEvent,