@tscircuit/schematic-viewer 2.0.42 → 2.0.43

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.
@@ -2,6 +2,7 @@ import {
2
2
  convertCircuitJsonToSchematicSvg,
3
3
  type ColorOverrides,
4
4
  } from "circuit-to-svg"
5
+ import { su } from "@tscircuit/soup-util"
5
6
  import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSchematicComponentLocationsInSvg"
6
7
  import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents"
7
8
  import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay"
@@ -27,6 +28,8 @@ import { zIndexMap } from "../utils/z-index-map"
27
28
  import { useSpiceSimulation } from "../hooks/useSpiceSimulation"
28
29
  import { getSpiceFromCircuitJson } from "../utils/spice-utils"
29
30
  import { getStoredBoolean, setStoredBoolean } from "lib/hooks/useLocalStorage"
31
+ import { MouseTracker } from "./MouseTracker"
32
+ import { SchematicComponentMouseTarget } from "./SchematicComponentMouseTarget"
30
33
 
31
34
  interface Props {
32
35
  circuitJson: CircuitJson
@@ -41,6 +44,10 @@ interface Props {
41
44
  colorOverrides?: ColorOverrides
42
45
  spiceSimulationEnabled?: boolean
43
46
  disableGroups?: boolean
47
+ onSchematicComponentClicked?: (options: {
48
+ schematicComponentId: string
49
+ event: MouseEvent
50
+ }) => void
44
51
  }
45
52
 
46
53
  export const SchematicViewer = ({
@@ -56,6 +63,7 @@ export const SchematicViewer = ({
56
63
  colorOverrides,
57
64
  spiceSimulationEnabled = false,
58
65
  disableGroups = false,
66
+ onSchematicComponentClicked,
59
67
  }: Props) => {
60
68
  if (debug) {
61
69
  enableDebug()
@@ -118,6 +126,19 @@ export const SchematicViewer = ({
118
126
  const svgDivRef = useRef<HTMLDivElement>(null)
119
127
  const touchStartRef = useRef<{ x: number; y: number } | null>(null)
120
128
 
129
+ const schematicComponentIds = useMemo(() => {
130
+ try {
131
+ return (
132
+ su(circuitJson)
133
+ .schematic_component?.list()
134
+ ?.map((component) => component.schematic_component_id as string) ?? []
135
+ )
136
+ } catch (err) {
137
+ console.error("Failed to derive schematic component ids", err)
138
+ return []
139
+ }
140
+ }, [circuitJsonKey, circuitJson])
141
+
121
142
  const handleTouchStart = (e: React.TouchEvent) => {
122
143
  const touch = e.touches[0]
123
144
  touchStartRef.current = {
@@ -295,137 +316,156 @@ export const SchematicViewer = ({
295
316
  )
296
317
 
297
318
  return (
298
- <div
299
- ref={containerRef}
300
- style={{
301
- position: "relative",
302
- backgroundColor: containerBackgroundColor,
303
- overflow: "hidden",
304
- cursor: showSpiceOverlay
305
- ? "auto"
306
- : isDragging
307
- ? "grabbing"
308
- : clickToInteractEnabled && !isInteractionEnabled
309
- ? "pointer"
310
- : "grab",
311
- minHeight: "300px",
312
- ...containerStyle,
313
- }}
314
- onWheelCapture={(e) => {
315
- if (showSpiceOverlay) {
316
- e.stopPropagation()
317
- }
318
- }}
319
- onMouseDown={(e) => {
320
- if (clickToInteractEnabled && !isInteractionEnabled) {
321
- e.preventDefault()
322
- e.stopPropagation()
323
- return
324
- }
325
- handleMouseDown(e)
326
- }}
327
- onMouseDownCapture={(e) => {
328
- if (clickToInteractEnabled && !isInteractionEnabled) {
329
- e.preventDefault()
330
- e.stopPropagation()
331
- return
332
- }
333
- }}
334
- onTouchStart={(e) => {
335
- if (showSpiceOverlay) return
336
- handleTouchStart(e)
337
- }}
338
- onTouchEnd={(e) => {
339
- if (showSpiceOverlay) return
340
- handleTouchEnd(e)
341
- }}
342
- >
343
- {!isInteractionEnabled && clickToInteractEnabled && (
344
- <div
345
- onClick={(e) => {
319
+ <MouseTracker>
320
+ <div
321
+ ref={containerRef}
322
+ style={{
323
+ position: "relative",
324
+ backgroundColor: containerBackgroundColor,
325
+ overflow: "hidden",
326
+ cursor: showSpiceOverlay
327
+ ? "auto"
328
+ : isDragging
329
+ ? "grabbing"
330
+ : clickToInteractEnabled && !isInteractionEnabled
331
+ ? "pointer"
332
+ : "grab",
333
+ minHeight: "300px",
334
+ ...containerStyle,
335
+ }}
336
+ onWheelCapture={(e) => {
337
+ if (showSpiceOverlay) {
338
+ e.stopPropagation()
339
+ }
340
+ }}
341
+ onMouseDown={(e) => {
342
+ if (clickToInteractEnabled && !isInteractionEnabled) {
346
343
  e.preventDefault()
347
344
  e.stopPropagation()
348
- setIsInteractionEnabled(true)
349
- }}
350
- style={{
351
- position: "absolute",
352
- inset: 0,
353
- cursor: "pointer",
354
- zIndex: zIndexMap.clickToInteractOverlay,
355
- display: "flex",
356
- alignItems: "center",
357
- justifyContent: "center",
358
- pointerEvents: "all",
359
- touchAction: "pan-x pan-y pinch-zoom",
360
- }}
361
- >
345
+ return
346
+ }
347
+ handleMouseDown(e)
348
+ }}
349
+ onMouseDownCapture={(e) => {
350
+ if (clickToInteractEnabled && !isInteractionEnabled) {
351
+ e.preventDefault()
352
+ e.stopPropagation()
353
+ return
354
+ }
355
+ }}
356
+ onTouchStart={(e) => {
357
+ if (showSpiceOverlay) return
358
+ handleTouchStart(e)
359
+ }}
360
+ onTouchEnd={(e) => {
361
+ if (showSpiceOverlay) return
362
+ handleTouchEnd(e)
363
+ }}
364
+ >
365
+ {!isInteractionEnabled && clickToInteractEnabled && (
362
366
  <div
367
+ onClick={(e) => {
368
+ e.preventDefault()
369
+ e.stopPropagation()
370
+ setIsInteractionEnabled(true)
371
+ }}
363
372
  style={{
364
- backgroundColor: "rgba(0, 0, 0, 0.8)",
365
- color: "white",
366
- padding: "12px 24px",
367
- borderRadius: "8px",
368
- fontSize: "16px",
369
- fontFamily: "sans-serif",
370
- pointerEvents: "none",
373
+ position: "absolute",
374
+ inset: 0,
375
+ cursor: "pointer",
376
+ zIndex: zIndexMap.clickToInteractOverlay,
377
+ display: "flex",
378
+ alignItems: "center",
379
+ justifyContent: "center",
380
+ pointerEvents: "all",
381
+ touchAction: "pan-x pan-y pinch-zoom",
371
382
  }}
372
383
  >
373
- {typeof window !== "undefined" &&
374
- ("ontouchstart" in window || navigator.maxTouchPoints > 0)
375
- ? "Touch to Interact"
376
- : "Click to Interact"}
384
+ <div
385
+ style={{
386
+ backgroundColor: "rgba(0, 0, 0, 0.8)",
387
+ color: "white",
388
+ padding: "12px 24px",
389
+ borderRadius: "8px",
390
+ fontSize: "16px",
391
+ fontFamily: "sans-serif",
392
+ pointerEvents: "none",
393
+ }}
394
+ >
395
+ {typeof window !== "undefined" &&
396
+ ("ontouchstart" in window || navigator.maxTouchPoints > 0)
397
+ ? "Touch to Interact"
398
+ : "Click to Interact"}
399
+ </div>
377
400
  </div>
378
- </div>
379
- )}
380
- <ViewMenuIcon
381
- active={showViewMenu}
382
- onClick={() => setShowViewMenu(!showViewMenu)}
383
- />
384
- {editingEnabled && (
385
- <EditIcon
386
- active={editModeEnabled}
387
- onClick={() => setEditModeEnabled(!editModeEnabled)}
388
- />
389
- )}
390
- {editingEnabled && editModeEnabled && (
391
- <GridIcon
392
- active={snapToGrid}
393
- onClick={() => setSnapToGrid(!snapToGrid)}
401
+ )}
402
+ <ViewMenuIcon
403
+ active={showViewMenu}
404
+ onClick={() => setShowViewMenu(!showViewMenu)}
394
405
  />
395
- )}
396
- <ViewMenu
397
- circuitJson={circuitJson}
398
- circuitJsonKey={circuitJsonKey}
399
- isVisible={showViewMenu}
400
- onClose={() => setShowViewMenu(false)}
401
- showGroups={showSchematicGroups}
402
- onToggleGroups={(value) => {
403
- if (!disableGroups) {
404
- setShowSchematicGroups(value)
405
- setStoredBoolean("schematic_viewer_show_groups", value)
406
- }
407
- }}
408
- />
409
- {spiceSimulationEnabled && (
410
- <SpiceSimulationIcon onClick={() => setShowSpiceOverlay(true)} />
411
- )}
412
- {showSpiceOverlay && (
413
- <SpiceSimulationOverlay
414
- spiceString={spiceString}
415
- onClose={() => setShowSpiceOverlay(false)}
416
- plotData={plotData}
417
- nodes={nodes}
418
- isLoading={isSpiceSimLoading}
419
- error={spiceSimError}
420
- simOptions={spiceSimOptions}
421
- onSimOptionsChange={(options) => {
422
- setHasSpiceSimRun(true)
423
- setSpiceSimOptions(options)
406
+ {editingEnabled && (
407
+ <EditIcon
408
+ active={editModeEnabled}
409
+ onClick={() => setEditModeEnabled(!editModeEnabled)}
410
+ />
411
+ )}
412
+ {editingEnabled && editModeEnabled && (
413
+ <GridIcon
414
+ active={snapToGrid}
415
+ onClick={() => setSnapToGrid(!snapToGrid)}
416
+ />
417
+ )}
418
+ <ViewMenu
419
+ circuitJson={circuitJson}
420
+ circuitJsonKey={circuitJsonKey}
421
+ isVisible={showViewMenu}
422
+ onClose={() => setShowViewMenu(false)}
423
+ showGroups={showSchematicGroups}
424
+ onToggleGroups={(value) => {
425
+ if (!disableGroups) {
426
+ setShowSchematicGroups(value)
427
+ setStoredBoolean("schematic_viewer_show_groups", value)
428
+ }
424
429
  }}
425
- hasRun={hasSpiceSimRun}
426
430
  />
427
- )}
428
- {svgDiv}
429
- </div>
431
+ {spiceSimulationEnabled && (
432
+ <SpiceSimulationIcon onClick={() => setShowSpiceOverlay(true)} />
433
+ )}
434
+ {showSpiceOverlay && (
435
+ <SpiceSimulationOverlay
436
+ spiceString={spiceString}
437
+ onClose={() => setShowSpiceOverlay(false)}
438
+ plotData={plotData}
439
+ nodes={nodes}
440
+ isLoading={isSpiceSimLoading}
441
+ error={spiceSimError}
442
+ simOptions={spiceSimOptions}
443
+ onSimOptionsChange={(options) => {
444
+ setHasSpiceSimRun(true)
445
+ setSpiceSimOptions(options)
446
+ }}
447
+ hasRun={hasSpiceSimRun}
448
+ />
449
+ )}
450
+ {onSchematicComponentClicked &&
451
+ schematicComponentIds.map((componentId) => (
452
+ <SchematicComponentMouseTarget
453
+ key={componentId}
454
+ componentId={componentId}
455
+ svgDivRef={svgDivRef}
456
+ containerRef={containerRef}
457
+ showOutline={true}
458
+ circuitJsonKey={circuitJsonKey}
459
+ onComponentClick={(id, event) => {
460
+ onSchematicComponentClicked?.({
461
+ schematicComponentId: id,
462
+ event,
463
+ })
464
+ }}
465
+ />
466
+ ))}
467
+ {svgDiv}
468
+ </div>
469
+ </MouseTracker>
430
470
  )
431
471
  }
@@ -0,0 +1,74 @@
1
+ import {
2
+ useContext,
3
+ useEffect,
4
+ useId,
5
+ useMemo,
6
+ useRef,
7
+ useSyncExternalStore,
8
+ } from "react"
9
+ import {
10
+ MouseTrackerContext,
11
+ type BoundingBoxBounds,
12
+ } from "../components/MouseTracker"
13
+
14
+ interface UseMouseEventsOverBoundingBoxOptions {
15
+ bounds: BoundingBoxBounds | null
16
+ onClick?: (event: MouseEvent) => void
17
+ }
18
+
19
+ export const useMouseEventsOverBoundingBox = (
20
+ options: UseMouseEventsOverBoundingBoxOptions,
21
+ ) => {
22
+ const context = useContext(MouseTrackerContext)
23
+
24
+ if (!context) {
25
+ throw new Error(
26
+ "useMouseEventsOverBoundingBox must be used within a MouseTracker",
27
+ )
28
+ }
29
+
30
+ const id = useId()
31
+ const latestOptionsRef = useRef(options)
32
+ latestOptionsRef.current = options
33
+
34
+ const handleClick = useMemo(
35
+ () => (event: MouseEvent) => {
36
+ latestOptionsRef.current.onClick?.(event)
37
+ },
38
+ [],
39
+ )
40
+
41
+ useEffect(() => {
42
+ context.registerBoundingBox(id, {
43
+ bounds: latestOptionsRef.current.bounds,
44
+ onClick: latestOptionsRef.current.onClick ? handleClick : undefined,
45
+ })
46
+ return () => {
47
+ context.unregisterBoundingBox(id)
48
+ }
49
+ }, [context, handleClick, id])
50
+
51
+ useEffect(() => {
52
+ context.updateBoundingBox(id, {
53
+ bounds: latestOptionsRef.current.bounds,
54
+ onClick: latestOptionsRef.current.onClick ? handleClick : undefined,
55
+ })
56
+ }, [
57
+ context,
58
+ handleClick,
59
+ id,
60
+ options.bounds?.minX,
61
+ options.bounds?.maxX,
62
+ options.bounds?.minY,
63
+ options.bounds?.maxY,
64
+ options.onClick,
65
+ ])
66
+
67
+ const hovering = useSyncExternalStore(
68
+ context.subscribe,
69
+ () => context.isHovering(id),
70
+ () => false,
71
+ )
72
+
73
+ return { hovering }
74
+ }
package/lib/index.ts CHANGED
@@ -1 +1,3 @@
1
1
  export { SchematicViewer } from "./components/SchematicViewer"
2
+ export { MouseTracker } from "./components/MouseTracker"
3
+ export { useMouseEventsOverBoundingBox } from "./hooks/useMouseEventsOverBoundingBox"
@@ -6,4 +6,5 @@ export const zIndexMap = {
6
6
  viewMenu: 55,
7
7
  viewMenuBackdrop: 54,
8
8
  clickToInteractOverlay: 100,
9
+ schematicComponentHoverOutline: 47,
9
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-viewer",
3
- "version": "2.0.42",
3
+ "version": "2.0.43",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {