@tscircuit/schematic-viewer 2.0.56 → 2.0.57

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.
Files changed (69) hide show
  1. package/dist/index.js +4 -1
  2. package/dist/index.js.map +1 -1
  3. package/package.json +4 -1
  4. package/.github/workflows/bun-formatcheck.yml +0 -26
  5. package/.github/workflows/bun-pver-release.yml +0 -59
  6. package/.github/workflows/bun-typecheck.yml +0 -26
  7. package/.github/workflows/on-merge-inform-release-tracker.yml +0 -24
  8. package/CLAUDE.md +0 -1
  9. package/biome.json +0 -56
  10. package/bun.lockb +0 -0
  11. package/cosmos.config.json +0 -3
  12. package/cosmos.decorator.tsx +0 -3
  13. package/docs/circuit-to-svg-metadata.md +0 -151
  14. package/docs/dragndrop-spec.md +0 -39
  15. package/examples/example1-resistor-and-capacitor.fixture.tsx +0 -16
  16. package/examples/example10-groups-view-schematic-groups.fixture.tsx +0 -76
  17. package/examples/example11-automatic-grouping-view-schematic-groups.fixture.tsx +0 -109
  18. package/examples/example12-spice-boost-converter.fixture.tsx +0 -78
  19. package/examples/example13-disablegroups.fixture.tsx +0 -30
  20. package/examples/example14-schematic-component-click.fixture.tsx +0 -46
  21. package/examples/example15-analog-simulation-viewer.fixture.tsx +0 -145
  22. package/examples/example16-no-analog-simulation.fixture.tsx +0 -13
  23. package/examples/example17-schematic-ports.fixture.tsx +0 -49
  24. package/examples/example18-live-toggle.fixture.tsx +0 -60
  25. package/examples/example2-small-circuit.fixture.tsx +0 -48
  26. package/examples/example3-small-circuit-without-debug-grid.fixture.tsx +0 -44
  27. package/examples/example4-reset-edit-events.fixture.tsx +0 -57
  28. package/examples/example5-circuit-json-rerender.fixture.tsx +0 -110
  29. package/examples/example6-click-to-interact.fixture.tsx +0 -36
  30. package/examples/example7-schematic-viewer-fix-snapping.fixture.tsx +0 -123
  31. package/examples/example8-color-overrides.fixture.tsx +0 -52
  32. package/examples/example9-spice-rc-charging-voltage-divider.fixture.tsx +0 -77
  33. package/index.html +0 -12
  34. package/lib/components/AnalogSimulationViewer.tsx +0 -300
  35. package/lib/components/ControlledSchematicViewer.tsx +0 -40
  36. package/lib/components/EditIcon.tsx +0 -46
  37. package/lib/components/GridIcon.tsx +0 -45
  38. package/lib/components/MouseTracker.tsx +0 -257
  39. package/lib/components/SchematicComponentMouseTarget.tsx +0 -189
  40. package/lib/components/SchematicPortMouseTarget.tsx +0 -224
  41. package/lib/components/SchematicViewer.tsx +0 -583
  42. package/lib/components/SpiceIcon.tsx +0 -14
  43. package/lib/components/SpicePlot.tsx +0 -221
  44. package/lib/components/SpiceSimulationIcon.tsx +0 -32
  45. package/lib/components/SpiceSimulationOverlay.tsx +0 -250
  46. package/lib/components/ViewMenu.tsx +0 -218
  47. package/lib/components/ViewMenuIcon.tsx +0 -47
  48. package/lib/dev/render-to-circuit-json.ts +0 -8
  49. package/lib/hooks/use-resize-handling.ts +0 -35
  50. package/lib/hooks/useChangeSchematicComponentLocationsInSvg.ts +0 -117
  51. package/lib/hooks/useChangeSchematicTracesForMovedComponents.ts +0 -121
  52. package/lib/hooks/useComponentDragging.ts +0 -251
  53. package/lib/hooks/useLocalStorage.ts +0 -63
  54. package/lib/hooks/useMouseEventsOverBoundingBox.ts +0 -74
  55. package/lib/hooks/useSchematicGroupsOverlay.ts +0 -364
  56. package/lib/hooks/useSpiceSimulation.ts +0 -149
  57. package/lib/index.ts +0 -4
  58. package/lib/types/edit-events.ts +0 -16
  59. package/lib/types/eecircuit-engine.d.ts +0 -147
  60. package/lib/utils/debug.ts +0 -9
  61. package/lib/utils/get-component-offset-due-to-events.ts +0 -43
  62. package/lib/utils/spice-utils.ts +0 -128
  63. package/lib/utils/z-index-map.ts +0 -11
  64. package/lib/workers/spice-simulation.worker.ts +0 -51
  65. package/scripts/build-worker-blob-url.ts +0 -55
  66. package/src/main.tsx +0 -21
  67. package/tsconfig.json +0 -33
  68. package/tsup-webworker.config.ts +0 -13
  69. package/vite.config.js +0 -15
@@ -1,45 +0,0 @@
1
- import { zIndexMap } from "../utils/z-index-map"
2
-
3
- export const GridIcon = ({
4
- onClick,
5
- active,
6
- }: { onClick: () => void; active: boolean }) => {
7
- const handleInteraction = (e: React.MouseEvent | React.TouchEvent) => {
8
- e.preventDefault()
9
- onClick()
10
- }
11
-
12
- return (
13
- <div
14
- onClick={handleInteraction}
15
- onTouchEnd={handleInteraction}
16
- title={active ? "Hide grid" : "Show grid"}
17
- style={{
18
- position: "absolute",
19
- top: "56px",
20
- right: "64px",
21
- backgroundColor: active ? "#4CAF50" : "#fff",
22
- color: active ? "#fff" : "#000",
23
- padding: "8px",
24
- borderRadius: "4px",
25
- cursor: "pointer",
26
- boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
27
- display: "flex",
28
- alignItems: "center",
29
- gap: "4px",
30
- zIndex: zIndexMap.schematicGridIcon,
31
- }}
32
- >
33
- <svg
34
- width="16"
35
- height="16"
36
- viewBox="0 0 24 24"
37
- fill="none"
38
- stroke="currentColor"
39
- strokeWidth="2"
40
- >
41
- <path d="M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z" />
42
- </svg>
43
- </div>
44
- )
45
- }
@@ -1,257 +0,0 @@
1
- import {
2
- createContext,
3
- useCallback,
4
- useContext,
5
- useEffect,
6
- useMemo,
7
- useRef,
8
- type ReactNode,
9
- } from "react"
10
-
11
- export interface BoundingBoxBounds {
12
- minX: number
13
- maxX: number
14
- minY: number
15
- maxY: number
16
- }
17
-
18
- interface BoundingBoxRegistration {
19
- bounds: BoundingBoxBounds | null
20
- onClick?: ((event: MouseEvent) => void) | undefined
21
- }
22
-
23
- export interface MouseTrackerContextValue {
24
- registerBoundingBox: (
25
- id: string,
26
- registration: BoundingBoxRegistration,
27
- ) => void
28
- updateBoundingBox: (id: string, registration: BoundingBoxRegistration) => void
29
- unregisterBoundingBox: (id: string) => void
30
- subscribe: (listener: () => void) => () => void
31
- isHovering: (id: string) => boolean
32
- }
33
-
34
- export const MouseTrackerContext =
35
- createContext<MouseTrackerContextValue | null>(null)
36
-
37
- const DRAG_THRESHOLD_PX = 5
38
-
39
- const boundsAreEqual = (
40
- a: BoundingBoxBounds | null | undefined,
41
- b: BoundingBoxBounds | null | undefined,
42
- ) => {
43
- if (!a && !b) return true
44
- if (!a || !b) return false
45
- return (
46
- a.minX === b.minX &&
47
- a.maxX === b.maxX &&
48
- a.minY === b.minY &&
49
- a.maxY === b.maxY
50
- )
51
- }
52
-
53
- export const MouseTracker = ({ children }: { children: ReactNode }) => {
54
- const existingContext = useContext(MouseTrackerContext)
55
-
56
- if (existingContext) {
57
- return <>{children}</>
58
- }
59
-
60
- const storeRef = useRef({
61
- pointer: null as { x: number; y: number } | null,
62
- boundingBoxes: new Map<string, BoundingBoxRegistration>(),
63
- hoveringIds: new Set<string>(),
64
- subscribers: new Set<() => void>(),
65
- mouseDownPosition: null as { x: number; y: number } | null,
66
- })
67
-
68
- const notifySubscribers = useCallback(() => {
69
- for (const callback of storeRef.current.subscribers) {
70
- callback()
71
- }
72
- }, [])
73
-
74
- const updateHovering = useCallback(() => {
75
- const pointer = storeRef.current.pointer
76
- const newHovering = new Set<string>()
77
-
78
- if (pointer) {
79
- for (const [id, registration] of storeRef.current.boundingBoxes) {
80
- const bounds = registration.bounds
81
- if (!bounds) continue
82
- if (
83
- pointer.x >= bounds.minX &&
84
- pointer.x <= bounds.maxX &&
85
- pointer.y >= bounds.minY &&
86
- pointer.y <= bounds.maxY
87
- ) {
88
- newHovering.add(id)
89
- }
90
- }
91
- }
92
-
93
- const prevHovering = storeRef.current.hoveringIds
94
- if (
95
- newHovering.size === prevHovering.size &&
96
- [...newHovering].every((id) => prevHovering.has(id))
97
- ) {
98
- return
99
- }
100
-
101
- storeRef.current.hoveringIds = newHovering
102
- notifySubscribers()
103
- }, [notifySubscribers])
104
-
105
- const registerBoundingBox = useCallback(
106
- (id: string, registration: BoundingBoxRegistration) => {
107
- storeRef.current.boundingBoxes.set(id, registration)
108
- updateHovering()
109
- },
110
- [updateHovering],
111
- )
112
-
113
- const updateBoundingBox = useCallback(
114
- (id: string, registration: BoundingBoxRegistration) => {
115
- const existing = storeRef.current.boundingBoxes.get(id)
116
- if (
117
- existing &&
118
- boundsAreEqual(existing.bounds, registration.bounds) &&
119
- existing.onClick === registration.onClick
120
- ) {
121
- return
122
- }
123
- storeRef.current.boundingBoxes.set(id, registration)
124
- updateHovering()
125
- },
126
- [updateHovering],
127
- )
128
-
129
- const unregisterBoundingBox = useCallback(
130
- (id: string) => {
131
- const removed = storeRef.current.boundingBoxes.delete(id)
132
- if (removed) {
133
- updateHovering()
134
- }
135
- },
136
- [updateHovering],
137
- )
138
-
139
- const subscribe = useCallback((listener: () => void) => {
140
- storeRef.current.subscribers.add(listener)
141
- return () => {
142
- storeRef.current.subscribers.delete(listener)
143
- }
144
- }, [])
145
-
146
- const isHovering = useCallback((id: string) => {
147
- return storeRef.current.hoveringIds.has(id)
148
- }, [])
149
-
150
- useEffect(() => {
151
- const handlePointerPosition = (event: PointerEvent | MouseEvent) => {
152
- const { clientX, clientY } = event
153
- const pointer = storeRef.current.pointer
154
- if (pointer && pointer.x === clientX && pointer.y === clientY) {
155
- return
156
- }
157
- storeRef.current.pointer = { x: clientX, y: clientY }
158
- updateHovering()
159
- }
160
-
161
- const handlePointerLeave = () => {
162
- if (storeRef.current.pointer === null) return
163
- storeRef.current.pointer = null
164
- storeRef.current.mouseDownPosition = null
165
- updateHovering()
166
- }
167
-
168
- const handleMouseDown = (event: MouseEvent) => {
169
- storeRef.current.mouseDownPosition = {
170
- x: event.clientX,
171
- y: event.clientY,
172
- }
173
- }
174
-
175
- const handleClick = (event: MouseEvent) => {
176
- const { clientX, clientY } = event
177
- const mouseDownPos = storeRef.current.mouseDownPosition
178
-
179
- // Check if this was a drag (movement > threshold)
180
- if (mouseDownPos) {
181
- const distance = Math.sqrt(
182
- Math.pow(clientX - mouseDownPos.x, 2) +
183
- Math.pow(clientY - mouseDownPos.y, 2),
184
- )
185
- if (distance > DRAG_THRESHOLD_PX) {
186
- // This was a drag, not a click - don't trigger onClick
187
- storeRef.current.mouseDownPosition = null
188
- return
189
- }
190
- }
191
-
192
- storeRef.current.mouseDownPosition = null
193
-
194
- for (const registration of storeRef.current.boundingBoxes.values()) {
195
- const bounds = registration.bounds
196
- if (!bounds) continue
197
- if (
198
- clientX >= bounds.minX &&
199
- clientX <= bounds.maxX &&
200
- clientY >= bounds.minY &&
201
- clientY <= bounds.maxY
202
- ) {
203
- registration.onClick?.(event)
204
- }
205
- }
206
- }
207
-
208
- window.addEventListener("pointermove", handlePointerPosition, {
209
- passive: true,
210
- })
211
- window.addEventListener("pointerdown", handlePointerPosition, {
212
- passive: true,
213
- })
214
- window.addEventListener("pointerup", handlePointerPosition, {
215
- passive: true,
216
- })
217
- window.addEventListener("pointerleave", handlePointerLeave)
218
- window.addEventListener("pointercancel", handlePointerLeave)
219
- window.addEventListener("blur", handlePointerLeave)
220
- window.addEventListener("mousedown", handleMouseDown, { passive: true })
221
- window.addEventListener("click", handleClick, { passive: true })
222
-
223
- return () => {
224
- window.removeEventListener("pointermove", handlePointerPosition)
225
- window.removeEventListener("pointerdown", handlePointerPosition)
226
- window.removeEventListener("pointerup", handlePointerPosition)
227
- window.removeEventListener("pointerleave", handlePointerLeave)
228
- window.removeEventListener("pointercancel", handlePointerLeave)
229
- window.removeEventListener("blur", handlePointerLeave)
230
- window.removeEventListener("mousedown", handleMouseDown)
231
- window.removeEventListener("click", handleClick)
232
- }
233
- }, [updateHovering])
234
-
235
- const value = useMemo<MouseTrackerContextValue>(
236
- () => ({
237
- registerBoundingBox,
238
- updateBoundingBox,
239
- unregisterBoundingBox,
240
- subscribe,
241
- isHovering,
242
- }),
243
- [
244
- registerBoundingBox,
245
- updateBoundingBox,
246
- unregisterBoundingBox,
247
- subscribe,
248
- isHovering,
249
- ],
250
- )
251
-
252
- return (
253
- <MouseTrackerContext.Provider value={value}>
254
- {children}
255
- </MouseTrackerContext.Provider>
256
- )
257
- }
@@ -1,189 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from "react"
2
- import { useMouseEventsOverBoundingBox } from "../hooks/useMouseEventsOverBoundingBox"
3
- import type { BoundingBoxBounds } from "./MouseTracker"
4
- import { zIndexMap } from "../utils/z-index-map"
5
-
6
- interface RelativeRect {
7
- left: number
8
- top: number
9
- width: number
10
- height: number
11
- }
12
-
13
- interface Measurement {
14
- bounds: BoundingBoxBounds
15
- rect: RelativeRect
16
- }
17
-
18
- const areMeasurementsEqual = (a: Measurement | null, b: Measurement | null) => {
19
- if (!a && !b) return true
20
- if (!a || !b) return false
21
- return (
22
- Math.abs(a.bounds.minX - b.bounds.minX) < 0.5 &&
23
- Math.abs(a.bounds.maxX - b.bounds.maxX) < 0.5 &&
24
- Math.abs(a.bounds.minY - b.bounds.minY) < 0.5 &&
25
- Math.abs(a.bounds.maxY - b.bounds.maxY) < 0.5 &&
26
- Math.abs(a.rect.left - b.rect.left) < 0.5 &&
27
- Math.abs(a.rect.top - b.rect.top) < 0.5 &&
28
- Math.abs(a.rect.width - b.rect.width) < 0.5 &&
29
- Math.abs(a.rect.height - b.rect.height) < 0.5
30
- )
31
- }
32
-
33
- interface Props {
34
- componentId: string
35
- svgDivRef: React.RefObject<HTMLDivElement | null>
36
- containerRef: React.RefObject<HTMLDivElement | null>
37
- onComponentClick?: (componentId: string, event: MouseEvent) => void
38
- onHoverChange?: (componentId: string, isHovering: boolean) => void
39
- showOutline: boolean
40
- circuitJsonKey: string
41
- }
42
-
43
- export const SchematicComponentMouseTarget = ({
44
- componentId,
45
- svgDivRef,
46
- containerRef,
47
- onComponentClick,
48
- onHoverChange,
49
- showOutline,
50
- circuitJsonKey,
51
- }: Props) => {
52
- const [measurement, setMeasurement] = useState<Measurement | null>(null)
53
- const frameRef = useRef<number | null>(null)
54
-
55
- const measure = useCallback(() => {
56
- frameRef.current = null
57
- const svgDiv = svgDivRef.current
58
- const container = containerRef.current
59
- if (!svgDiv || !container) {
60
- setMeasurement((prev) => (prev ? null : prev))
61
- return
62
- }
63
- const element = svgDiv.querySelector<SVGGraphicsElement | HTMLElement>(
64
- `[data-schematic-component-id="${componentId}"]`,
65
- )
66
- if (!element) {
67
- setMeasurement((prev) => (prev ? null : prev))
68
- return
69
- }
70
-
71
- const elementRect = element.getBoundingClientRect()
72
- const containerRect = container.getBoundingClientRect()
73
-
74
- const nextMeasurement: Measurement = {
75
- bounds: {
76
- minX: elementRect.left,
77
- maxX: elementRect.right,
78
- minY: elementRect.top,
79
- maxY: elementRect.bottom,
80
- },
81
- rect: {
82
- left: elementRect.left - containerRect.left,
83
- top: elementRect.top - containerRect.top,
84
- width: elementRect.width,
85
- height: elementRect.height,
86
- },
87
- }
88
-
89
- setMeasurement((prev) =>
90
- areMeasurementsEqual(prev, nextMeasurement) ? prev : nextMeasurement,
91
- )
92
- }, [componentId, containerRef, svgDivRef])
93
-
94
- const scheduleMeasure = useCallback(() => {
95
- if (frameRef.current !== null) return
96
- frameRef.current = window.requestAnimationFrame(measure)
97
- }, [measure])
98
-
99
- useEffect(() => {
100
- scheduleMeasure()
101
- }, [scheduleMeasure, circuitJsonKey])
102
-
103
- useEffect(() => {
104
- scheduleMeasure()
105
- const svgDiv = svgDivRef.current
106
- const container = containerRef.current
107
- if (!svgDiv || !container) return
108
-
109
- const resizeObserver =
110
- typeof ResizeObserver !== "undefined"
111
- ? new ResizeObserver(() => {
112
- scheduleMeasure()
113
- })
114
- : null
115
- resizeObserver?.observe(container)
116
- resizeObserver?.observe(svgDiv)
117
-
118
- const mutationObserver =
119
- typeof MutationObserver !== "undefined"
120
- ? new MutationObserver(() => {
121
- scheduleMeasure()
122
- })
123
- : null
124
- mutationObserver?.observe(svgDiv, {
125
- attributes: true,
126
- attributeFilter: ["style", "transform"],
127
- subtree: true,
128
- childList: true,
129
- })
130
-
131
- window.addEventListener("scroll", scheduleMeasure, true)
132
- window.addEventListener("resize", scheduleMeasure)
133
-
134
- return () => {
135
- resizeObserver?.disconnect()
136
- mutationObserver?.disconnect()
137
- window.removeEventListener("scroll", scheduleMeasure, true)
138
- window.removeEventListener("resize", scheduleMeasure)
139
- if (frameRef.current !== null) {
140
- cancelAnimationFrame(frameRef.current)
141
- frameRef.current = null
142
- }
143
- }
144
- }, [scheduleMeasure, svgDivRef, containerRef])
145
-
146
- const handleClick = useCallback(
147
- (event: MouseEvent) => {
148
- if (onComponentClick) {
149
- onComponentClick(componentId, event)
150
- }
151
- },
152
- [componentId, onComponentClick],
153
- )
154
-
155
- const bounds = measurement?.bounds ?? null
156
-
157
- const { hovering } = useMouseEventsOverBoundingBox({
158
- bounds,
159
- onClick: onComponentClick ? handleClick : undefined,
160
- })
161
-
162
- // Notify parent of hover state changes
163
- useEffect(() => {
164
- if (onHoverChange) {
165
- onHoverChange(componentId, hovering)
166
- }
167
- }, [hovering, componentId, onHoverChange])
168
-
169
- if (!measurement || !hovering || !showOutline) {
170
- return null
171
- }
172
-
173
- const rect = measurement.rect
174
-
175
- return (
176
- <div
177
- style={{
178
- position: "absolute",
179
- left: rect.left,
180
- top: rect.top,
181
- width: rect.width,
182
- height: rect.height,
183
- border: "1.5px solid rgba(51, 153, 255, 0.9)",
184
- pointerEvents: "none",
185
- zIndex: zIndexMap.schematicComponentHoverOutline,
186
- }}
187
- />
188
- )
189
- }
@@ -1,224 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from "react"
2
- import { useMouseEventsOverBoundingBox } from "../hooks/useMouseEventsOverBoundingBox"
3
- import type { BoundingBoxBounds } from "./MouseTracker"
4
- import { zIndexMap } from "../utils/z-index-map"
5
-
6
- interface RelativeRect {
7
- left: number
8
- top: number
9
- width: number
10
- height: number
11
- }
12
-
13
- interface Measurement {
14
- bounds: BoundingBoxBounds
15
- rect: RelativeRect
16
- }
17
-
18
- const areMeasurementsEqual = (a: Measurement | null, b: Measurement | null) => {
19
- if (!a && !b) return true
20
- if (!a || !b) return false
21
- return (
22
- Math.abs(a.bounds.minX - b.bounds.minX) < 0.5 &&
23
- Math.abs(a.bounds.maxX - b.bounds.maxX) < 0.5 &&
24
- Math.abs(a.bounds.minY - b.bounds.minY) < 0.5 &&
25
- Math.abs(a.bounds.maxY - b.bounds.maxY) < 0.5 &&
26
- Math.abs(a.rect.left - b.rect.left) < 0.5 &&
27
- Math.abs(a.rect.top - b.rect.top) < 0.5 &&
28
- Math.abs(a.rect.width - b.rect.width) < 0.5 &&
29
- Math.abs(a.rect.height - b.rect.height) < 0.5
30
- )
31
- }
32
-
33
- interface Props {
34
- portId: string
35
- portLabel?: string
36
- svgDivRef: React.RefObject<HTMLDivElement | null>
37
- containerRef: React.RefObject<HTMLDivElement | null>
38
- onPortClick?: (portId: string, event: MouseEvent) => void
39
- onHoverChange?: (portId: string, isHovering: boolean) => void
40
- showOutline: boolean
41
- circuitJsonKey: string
42
- }
43
-
44
- export const SchematicPortMouseTarget = ({
45
- portId,
46
- portLabel,
47
- svgDivRef,
48
- containerRef,
49
- onPortClick,
50
- onHoverChange,
51
- showOutline,
52
- circuitJsonKey,
53
- }: Props) => {
54
- const [measurement, setMeasurement] = useState<Measurement | null>(null)
55
- const frameRef = useRef<number | null>(null)
56
-
57
- const measure = useCallback(() => {
58
- frameRef.current = null
59
- const svgDiv = svgDivRef.current
60
- const container = containerRef.current
61
- if (!svgDiv || !container) {
62
- setMeasurement((prev) => (prev ? null : prev))
63
- return
64
- }
65
- const element = svgDiv.querySelector<SVGGraphicsElement | HTMLElement>(
66
- `[data-schematic-port-id="${portId}"]`,
67
- )
68
- if (!element) {
69
- setMeasurement((prev) => (prev ? null : prev))
70
- return
71
- }
72
-
73
- const elementRect = element.getBoundingClientRect()
74
- const containerRect = container.getBoundingClientRect()
75
-
76
- // Add some padding around the port for easier interaction
77
- const padding = 4
78
-
79
- const nextMeasurement: Measurement = {
80
- bounds: {
81
- minX: elementRect.left - padding,
82
- maxX: elementRect.right + padding,
83
- minY: elementRect.top - padding,
84
- maxY: elementRect.bottom + padding,
85
- },
86
- rect: {
87
- left: elementRect.left - containerRect.left - padding,
88
- top: elementRect.top - containerRect.top - padding,
89
- width: elementRect.width + padding * 2,
90
- height: elementRect.height + padding * 2,
91
- },
92
- }
93
-
94
- setMeasurement((prev) =>
95
- areMeasurementsEqual(prev, nextMeasurement) ? prev : nextMeasurement,
96
- )
97
- }, [portId, containerRef, svgDivRef])
98
-
99
- const scheduleMeasure = useCallback(() => {
100
- if (frameRef.current !== null) return
101
- frameRef.current = window.requestAnimationFrame(measure)
102
- }, [measure])
103
-
104
- useEffect(() => {
105
- scheduleMeasure()
106
- }, [scheduleMeasure, circuitJsonKey])
107
-
108
- useEffect(() => {
109
- scheduleMeasure()
110
- const svgDiv = svgDivRef.current
111
- const container = containerRef.current
112
- if (!svgDiv || !container) return
113
-
114
- const resizeObserver =
115
- typeof ResizeObserver !== "undefined"
116
- ? new ResizeObserver(() => {
117
- scheduleMeasure()
118
- })
119
- : null
120
- resizeObserver?.observe(container)
121
- resizeObserver?.observe(svgDiv)
122
-
123
- const mutationObserver =
124
- typeof MutationObserver !== "undefined"
125
- ? new MutationObserver(() => {
126
- scheduleMeasure()
127
- })
128
- : null
129
- mutationObserver?.observe(svgDiv, {
130
- attributes: true,
131
- attributeFilter: ["style", "transform"],
132
- subtree: true,
133
- childList: true,
134
- })
135
-
136
- window.addEventListener("scroll", scheduleMeasure, true)
137
- window.addEventListener("resize", scheduleMeasure)
138
-
139
- return () => {
140
- resizeObserver?.disconnect()
141
- mutationObserver?.disconnect()
142
- window.removeEventListener("scroll", scheduleMeasure, true)
143
- window.removeEventListener("resize", scheduleMeasure)
144
- if (frameRef.current !== null) {
145
- cancelAnimationFrame(frameRef.current)
146
- frameRef.current = null
147
- }
148
- }
149
- }, [scheduleMeasure, svgDivRef, containerRef])
150
-
151
- const handleClick = useCallback(
152
- (event: MouseEvent) => {
153
- if (onPortClick) {
154
- onPortClick(portId, event)
155
- }
156
- },
157
- [portId, onPortClick],
158
- )
159
-
160
- const bounds = measurement?.bounds ?? null
161
-
162
- const { hovering } = useMouseEventsOverBoundingBox({
163
- bounds,
164
- onClick: onPortClick ? handleClick : undefined,
165
- })
166
-
167
- // Notify parent of hover state changes
168
- useEffect(() => {
169
- if (onHoverChange) {
170
- onHoverChange(portId, hovering)
171
- }
172
- }, [hovering, portId, onHoverChange])
173
-
174
- if (!measurement || !showOutline) {
175
- return null
176
- }
177
-
178
- const rect = measurement.rect
179
-
180
- return (
181
- <>
182
- <div
183
- style={{
184
- position: "absolute",
185
- left: rect.left,
186
- top: rect.top,
187
- width: rect.width,
188
- height: rect.height,
189
- border: hovering
190
- ? "1.5px solid rgba(255, 153, 51, 0.9)"
191
- : "1.5px solid rgba(255, 153, 51, 0.3)",
192
- backgroundColor: hovering
193
- ? "rgba(255, 153, 51, 0.15)"
194
- : "rgba(255, 153, 51, 0.05)",
195
- borderRadius: "50%",
196
- pointerEvents: "none",
197
- zIndex: zIndexMap.schematicPortHoverOutline,
198
- transition: "border-color 0.15s, background-color 0.15s",
199
- }}
200
- />
201
- {hovering && portLabel && (
202
- <div
203
- style={{
204
- position: "absolute",
205
- left: rect.left + rect.width / 2,
206
- top: rect.top - 24,
207
- transform: "translateX(-50%)",
208
- backgroundColor: "rgba(0, 0, 0, 0.85)",
209
- color: "white",
210
- padding: "4px 8px",
211
- borderRadius: "4px",
212
- fontSize: "12px",
213
- fontFamily: "monospace",
214
- whiteSpace: "nowrap",
215
- pointerEvents: "none",
216
- zIndex: zIndexMap.schematicPortHoverOutline + 1,
217
- }}
218
- >
219
- {portLabel}
220
- </div>
221
- )}
222
- </>
223
- )
224
- }