@tscircuit/schematic-viewer 2.0.33 → 2.0.35
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/dist/index.d.ts +2 -1
- package/dist/index.js +122 -36
- package/dist/index.js.map +1 -1
- package/examples/example13-disablegroups.fixture.tsx +30 -0
- package/lib/components/EditIcon.tsx +7 -1
- package/lib/components/GridIcon.tsx +7 -1
- package/lib/components/SchematicViewer.tsx +42 -16
- package/lib/hooks/useComponentDragging.ts +57 -24
- package/lib/hooks/useLocalStorage.ts +63 -0
- package/package.json +1 -1
|
@@ -4,9 +4,15 @@ export const EditIcon = ({
|
|
|
4
4
|
onClick,
|
|
5
5
|
active,
|
|
6
6
|
}: { onClick: () => void; active: boolean }) => {
|
|
7
|
+
const handleInteraction = (e: React.MouseEvent | React.TouchEvent) => {
|
|
8
|
+
e.preventDefault()
|
|
9
|
+
onClick()
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
return (
|
|
8
13
|
<div
|
|
9
|
-
onClick={
|
|
14
|
+
onClick={handleInteraction}
|
|
15
|
+
onTouchEnd={handleInteraction}
|
|
10
16
|
style={{
|
|
11
17
|
position: "absolute",
|
|
12
18
|
top: "16px",
|
|
@@ -4,9 +4,15 @@ export const GridIcon = ({
|
|
|
4
4
|
onClick,
|
|
5
5
|
active,
|
|
6
6
|
}: { onClick: () => void; active: boolean }) => {
|
|
7
|
+
const handleInteraction = (e: React.MouseEvent | React.TouchEvent) => {
|
|
8
|
+
e.preventDefault()
|
|
9
|
+
onClick()
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
return (
|
|
8
13
|
<div
|
|
9
|
-
onClick={
|
|
14
|
+
onClick={handleInteraction}
|
|
15
|
+
onTouchEnd={handleInteraction}
|
|
10
16
|
style={{
|
|
11
17
|
position: "absolute",
|
|
12
18
|
top: "56px",
|
|
@@ -26,6 +26,7 @@ import { SpiceSimulationOverlay } from "./SpiceSimulationOverlay"
|
|
|
26
26
|
import { zIndexMap } from "../utils/z-index-map"
|
|
27
27
|
import { useSpiceSimulation } from "../hooks/useSpiceSimulation"
|
|
28
28
|
import { getSpiceFromCircuitJson } from "../utils/spice-utils"
|
|
29
|
+
import { getStoredBoolean, setStoredBoolean } from "lib/hooks/useLocalStorage"
|
|
29
30
|
|
|
30
31
|
interface Props {
|
|
31
32
|
circuitJson: CircuitJson
|
|
@@ -39,6 +40,7 @@ interface Props {
|
|
|
39
40
|
clickToInteractEnabled?: boolean
|
|
40
41
|
colorOverrides?: ColorOverrides
|
|
41
42
|
spiceSimulationEnabled?: boolean
|
|
43
|
+
disableGroups?: boolean
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export const SchematicViewer = ({
|
|
@@ -53,6 +55,7 @@ export const SchematicViewer = ({
|
|
|
53
55
|
clickToInteractEnabled = false,
|
|
54
56
|
colorOverrides,
|
|
55
57
|
spiceSimulationEnabled = false,
|
|
58
|
+
disableGroups = false,
|
|
56
59
|
}: Props) => {
|
|
57
60
|
if (debug) {
|
|
58
61
|
enableDebug()
|
|
@@ -102,7 +105,10 @@ export const SchematicViewer = ({
|
|
|
102
105
|
!clickToInteractEnabled,
|
|
103
106
|
)
|
|
104
107
|
const [showViewMenu, setShowViewMenu] = useState(false)
|
|
105
|
-
const [showSchematicGroups, setShowSchematicGroups] = useState(
|
|
108
|
+
const [showSchematicGroups, setShowSchematicGroups] = useState(() => {
|
|
109
|
+
if (disableGroups) return false
|
|
110
|
+
return getStoredBoolean("schematic_viewer_show_groups", false)
|
|
111
|
+
})
|
|
106
112
|
const svgDivRef = useRef<HTMLDivElement>(null)
|
|
107
113
|
const touchStartRef = useRef<{ x: number; y: number } | null>(null)
|
|
108
114
|
|
|
@@ -207,18 +213,21 @@ export const SchematicViewer = ({
|
|
|
207
213
|
return [...unappliedEditEvents, ...internalEditEvents]
|
|
208
214
|
}, [unappliedEditEvents, internalEditEvents])
|
|
209
215
|
|
|
210
|
-
const {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
const {
|
|
217
|
+
handleMouseDown,
|
|
218
|
+
handleTouchStart: handleComponentTouchStart,
|
|
219
|
+
isDragging,
|
|
220
|
+
activeEditEvent,
|
|
221
|
+
} = useComponentDragging({
|
|
222
|
+
onEditEvent: handleEditEvent,
|
|
223
|
+
cancelDrag,
|
|
224
|
+
realToSvgProjection,
|
|
225
|
+
svgToScreenProjection,
|
|
226
|
+
circuitJson,
|
|
227
|
+
editEvents: editEventsWithUnappliedEditEvents,
|
|
228
|
+
enabled: editModeEnabled && isInteractionEnabled && !showSpiceOverlay,
|
|
229
|
+
snapToGrid,
|
|
230
|
+
})
|
|
222
231
|
|
|
223
232
|
useChangeSchematicComponentLocationsInSvg({
|
|
224
233
|
svgDivRef,
|
|
@@ -240,7 +249,7 @@ export const SchematicViewer = ({
|
|
|
240
249
|
svgDivRef,
|
|
241
250
|
circuitJson,
|
|
242
251
|
circuitJsonKey,
|
|
243
|
-
showGroups: showSchematicGroups,
|
|
252
|
+
showGroups: showSchematicGroups && !disableGroups,
|
|
244
253
|
})
|
|
245
254
|
|
|
246
255
|
const svgDiv = useMemo(
|
|
@@ -255,11 +264,23 @@ export const SchematicViewer = ({
|
|
|
255
264
|
: "auto",
|
|
256
265
|
transformOrigin: "0 0",
|
|
257
266
|
}}
|
|
267
|
+
onTouchStart={(e) => {
|
|
268
|
+
if (editModeEnabled && isInteractionEnabled && !showSpiceOverlay) {
|
|
269
|
+
handleComponentTouchStart(e)
|
|
270
|
+
}
|
|
271
|
+
}}
|
|
258
272
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
|
|
259
273
|
dangerouslySetInnerHTML={{ __html: svgString }}
|
|
260
274
|
/>
|
|
261
275
|
),
|
|
262
|
-
[
|
|
276
|
+
[
|
|
277
|
+
svgString,
|
|
278
|
+
isInteractionEnabled,
|
|
279
|
+
clickToInteractEnabled,
|
|
280
|
+
editModeEnabled,
|
|
281
|
+
showSpiceOverlay,
|
|
282
|
+
handleComponentTouchStart,
|
|
283
|
+
],
|
|
263
284
|
)
|
|
264
285
|
|
|
265
286
|
return (
|
|
@@ -367,7 +388,12 @@ export const SchematicViewer = ({
|
|
|
367
388
|
isVisible={showViewMenu}
|
|
368
389
|
onClose={() => setShowViewMenu(false)}
|
|
369
390
|
showGroups={showSchematicGroups}
|
|
370
|
-
onToggleGroups={
|
|
391
|
+
onToggleGroups={(value) => {
|
|
392
|
+
if (!disableGroups) {
|
|
393
|
+
setShowSchematicGroups(value)
|
|
394
|
+
setStoredBoolean("schematic_viewer_show_groups", value)
|
|
395
|
+
}
|
|
396
|
+
}}
|
|
371
397
|
/>
|
|
372
398
|
{spiceSimulationEnabled && (
|
|
373
399
|
<SpiceSimulationIcon onClick={() => setShowSpiceOverlay(true)} />
|
|
@@ -32,6 +32,7 @@ export const useComponentDragging = ({
|
|
|
32
32
|
snapToGrid?: boolean
|
|
33
33
|
}): {
|
|
34
34
|
handleMouseDown: (e: React.MouseEvent) => void
|
|
35
|
+
handleTouchStart: (e: React.TouchEvent) => void
|
|
35
36
|
isDragging: boolean
|
|
36
37
|
activeEditEvent: EditSchematicComponentLocationEventWithElement | null
|
|
37
38
|
} => {
|
|
@@ -74,45 +75,37 @@ export const useComponentDragging = ({
|
|
|
74
75
|
})
|
|
75
76
|
}, [editEvents])
|
|
76
77
|
|
|
77
|
-
const
|
|
78
|
-
(
|
|
79
|
-
if (!enabled) return
|
|
78
|
+
const startDrag = useCallback(
|
|
79
|
+
(clientX: number, clientY: number, target: Element) => {
|
|
80
|
+
if (!enabled) return false
|
|
80
81
|
|
|
81
|
-
const target = e.target as SVGElement
|
|
82
82
|
const componentGroup = target.closest(
|
|
83
83
|
'[data-circuit-json-type="schematic_component"]',
|
|
84
84
|
)
|
|
85
|
-
if (!componentGroup) return
|
|
85
|
+
if (!componentGroup) return false
|
|
86
86
|
|
|
87
87
|
const schematic_component_id = componentGroup.getAttribute(
|
|
88
88
|
"data-schematic-component-id",
|
|
89
89
|
)
|
|
90
|
-
if (!schematic_component_id) return
|
|
90
|
+
if (!schematic_component_id) return false
|
|
91
91
|
|
|
92
92
|
if (cancelDrag) cancelDrag()
|
|
93
93
|
|
|
94
94
|
const schematic_component = su(circuitJson).schematic_component.get(
|
|
95
95
|
schematic_component_id,
|
|
96
96
|
)
|
|
97
|
-
if (!schematic_component) return
|
|
97
|
+
if (!schematic_component) return false
|
|
98
98
|
|
|
99
|
-
dragStartPosRef.current = {
|
|
100
|
-
x: e.clientX,
|
|
101
|
-
y: e.clientY,
|
|
102
|
-
}
|
|
99
|
+
dragStartPosRef.current = { x: clientX, y: clientY }
|
|
103
100
|
|
|
104
|
-
// Get the current position of the component
|
|
105
|
-
// Check if we're already tracking this component
|
|
106
101
|
let current_position: { x: number; y: number }
|
|
107
102
|
const trackedPosition = componentPositionsRef.current.get(
|
|
108
103
|
schematic_component_id,
|
|
109
104
|
)
|
|
110
105
|
|
|
111
106
|
if (trackedPosition) {
|
|
112
|
-
// Use the tracked position from previous edits
|
|
113
107
|
current_position = { ...trackedPosition }
|
|
114
108
|
} else {
|
|
115
|
-
// Calculate position based on component data and edit events
|
|
116
109
|
const editEventOffset = getComponentOffsetDueToEvents({
|
|
117
110
|
editEvents,
|
|
118
111
|
schematic_component_id: schematic_component_id,
|
|
@@ -123,7 +116,6 @@ export const useComponentDragging = ({
|
|
|
123
116
|
y: schematic_component.center.y + editEventOffset.y,
|
|
124
117
|
}
|
|
125
118
|
|
|
126
|
-
// Store this initial position
|
|
127
119
|
componentPositionsRef.current.set(schematic_component_id, {
|
|
128
120
|
...current_position,
|
|
129
121
|
})
|
|
@@ -142,17 +134,36 @@ export const useComponentDragging = ({
|
|
|
142
134
|
|
|
143
135
|
activeEditEventRef.current = newEditEvent
|
|
144
136
|
setActiveEditEvent(newEditEvent)
|
|
137
|
+
return true
|
|
145
138
|
},
|
|
146
139
|
[cancelDrag, enabled, circuitJson, editEvents],
|
|
147
140
|
)
|
|
148
141
|
|
|
149
|
-
const
|
|
150
|
-
(e: MouseEvent) => {
|
|
142
|
+
const handleMouseDown = useCallback(
|
|
143
|
+
(e: React.MouseEvent) => {
|
|
144
|
+
startDrag(e.clientX, e.clientY, e.target as Element)
|
|
145
|
+
},
|
|
146
|
+
[startDrag],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
const handleTouchStart = useCallback(
|
|
150
|
+
(e: React.TouchEvent) => {
|
|
151
|
+
if (e.touches.length !== 1) return
|
|
152
|
+
const touch = e.touches[0]
|
|
153
|
+
if (startDrag(touch.clientX, touch.clientY, e.target as Element)) {
|
|
154
|
+
e.preventDefault()
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
[startDrag],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const updateDragPosition = useCallback(
|
|
161
|
+
(clientX: number, clientY: number) => {
|
|
151
162
|
if (!activeEditEventRef.current || !dragStartPosRef.current) return
|
|
152
163
|
|
|
153
164
|
const screenDelta = {
|
|
154
|
-
x:
|
|
155
|
-
y:
|
|
165
|
+
x: clientX - dragStartPosRef.current.x,
|
|
166
|
+
y: clientY - dragStartPosRef.current.y,
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
const mmDelta = {
|
|
@@ -180,19 +191,33 @@ export const useComponentDragging = ({
|
|
|
180
191
|
[realToScreenProjection, snapToGrid],
|
|
181
192
|
)
|
|
182
193
|
|
|
183
|
-
const
|
|
194
|
+
const handleMouseMove = useCallback(
|
|
195
|
+
(e: MouseEvent) => updateDragPosition(e.clientX, e.clientY),
|
|
196
|
+
[updateDragPosition],
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const handleTouchMove = useCallback(
|
|
200
|
+
(e: TouchEvent) => {
|
|
201
|
+
if (e.touches.length !== 1 || !activeEditEventRef.current) return
|
|
202
|
+
e.preventDefault()
|
|
203
|
+
const touch = e.touches[0]
|
|
204
|
+
updateDragPosition(touch.clientX, touch.clientY)
|
|
205
|
+
},
|
|
206
|
+
[updateDragPosition],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
const endDrag = useCallback(() => {
|
|
184
210
|
if (!activeEditEventRef.current) return
|
|
185
211
|
const finalEvent = {
|
|
186
212
|
...activeEditEventRef.current,
|
|
187
213
|
in_progress: false,
|
|
188
214
|
}
|
|
189
215
|
|
|
190
|
-
// Update our stored position for this component
|
|
191
216
|
componentPositionsRef.current.set(finalEvent.schematic_component_id, {
|
|
192
217
|
...finalEvent.new_center,
|
|
193
218
|
})
|
|
194
219
|
|
|
195
|
-
debug("
|
|
220
|
+
debug("endDrag calling onEditEvent with new edit event", {
|
|
196
221
|
newEditEvent: finalEvent,
|
|
197
222
|
})
|
|
198
223
|
if (onEditEvent) onEditEvent(finalEvent)
|
|
@@ -201,17 +226,25 @@ export const useComponentDragging = ({
|
|
|
201
226
|
setActiveEditEvent(null)
|
|
202
227
|
}, [onEditEvent])
|
|
203
228
|
|
|
229
|
+
const handleMouseUp = useCallback(() => endDrag(), [endDrag])
|
|
230
|
+
const handleTouchEnd = useCallback(() => endDrag(), [endDrag])
|
|
231
|
+
|
|
204
232
|
useEffect(() => {
|
|
205
233
|
window.addEventListener("mousemove", handleMouseMove)
|
|
206
234
|
window.addEventListener("mouseup", handleMouseUp)
|
|
235
|
+
window.addEventListener("touchmove", handleTouchMove, { passive: false })
|
|
236
|
+
window.addEventListener("touchend", handleTouchEnd)
|
|
207
237
|
return () => {
|
|
208
238
|
window.removeEventListener("mousemove", handleMouseMove)
|
|
209
239
|
window.removeEventListener("mouseup", handleMouseUp)
|
|
240
|
+
window.removeEventListener("touchmove", handleTouchMove)
|
|
241
|
+
window.removeEventListener("touchend", handleTouchEnd)
|
|
210
242
|
}
|
|
211
|
-
}, [handleMouseMove, handleMouseUp])
|
|
243
|
+
}, [handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd])
|
|
212
244
|
|
|
213
245
|
return {
|
|
214
246
|
handleMouseDown,
|
|
247
|
+
handleTouchStart,
|
|
215
248
|
isDragging: !!activeEditEventRef.current,
|
|
216
249
|
activeEditEvent: activeEditEvent,
|
|
217
250
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback } from "react"
|
|
2
|
+
|
|
3
|
+
export const STORAGE_KEYS = {
|
|
4
|
+
IS_SHOWING_SCHEMATIC_GROUPS: "schematic_viewer_show_groups",
|
|
5
|
+
} as const
|
|
6
|
+
|
|
7
|
+
export const getStoredBoolean = (
|
|
8
|
+
key: string,
|
|
9
|
+
defaultValue: boolean,
|
|
10
|
+
): boolean => {
|
|
11
|
+
if (typeof window === "undefined") return defaultValue
|
|
12
|
+
try {
|
|
13
|
+
const stored = localStorage.getItem(key)
|
|
14
|
+
return stored !== null ? JSON.parse(stored) : defaultValue
|
|
15
|
+
} catch {
|
|
16
|
+
return defaultValue
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const setStoredBoolean = (key: string, value: boolean): void => {
|
|
21
|
+
if (typeof window === "undefined") return
|
|
22
|
+
try {
|
|
23
|
+
localStorage.setItem(key, JSON.stringify(value))
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const useLocalStorage = () => {
|
|
28
|
+
const getBoolean = useCallback(
|
|
29
|
+
(key: string, defaultValue: boolean): boolean => {
|
|
30
|
+
return getStoredBoolean(key, defaultValue)
|
|
31
|
+
},
|
|
32
|
+
[],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const setBoolean = useCallback((key: string, value: boolean): void => {
|
|
36
|
+
setStoredBoolean(key, value)
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
getBoolean,
|
|
41
|
+
setBoolean,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const useLocalStorageValue = (key: string, defaultValue: boolean) => {
|
|
46
|
+
const { getBoolean, setBoolean } = useLocalStorage()
|
|
47
|
+
|
|
48
|
+
const getValue = useCallback(() => {
|
|
49
|
+
return getBoolean(key, defaultValue)
|
|
50
|
+
}, [getBoolean, key, defaultValue])
|
|
51
|
+
|
|
52
|
+
const setValue = useCallback(
|
|
53
|
+
(value: boolean) => {
|
|
54
|
+
setBoolean(key, value)
|
|
55
|
+
},
|
|
56
|
+
[setBoolean, key],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
getValue,
|
|
61
|
+
setValue,
|
|
62
|
+
}
|
|
63
|
+
}
|