@tscircuit/schematic-viewer 2.0.26 → 2.0.28

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.
@@ -4,6 +4,7 @@ import {
4
4
  } from "circuit-to-svg"
5
5
  import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSchematicComponentLocationsInSvg"
6
6
  import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents"
7
+ import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay"
7
8
  import { enableDebug } from "lib/utils/debug"
8
9
  import { useEffect, useMemo, useRef, useState } from "react"
9
10
  import {
@@ -17,6 +18,8 @@ import { useComponentDragging } from "../hooks/useComponentDragging"
17
18
  import type { ManualEditEvent } from "../types/edit-events"
18
19
  import { EditIcon } from "./EditIcon"
19
20
  import { GridIcon } from "./GridIcon"
21
+ import { ViewMenuIcon } from "./ViewMenuIcon"
22
+ import { ViewMenu } from "./ViewMenu"
20
23
  import type { CircuitJson } from "circuit-json"
21
24
  import { SpiceSimulationIcon } from "./SpiceSimulationIcon"
22
25
  import { SpiceSimulationOverlay } from "./SpiceSimulationOverlay"
@@ -87,6 +90,8 @@ export const SchematicViewer = ({
87
90
  const [isInteractionEnabled, setIsInteractionEnabled] = useState<boolean>(
88
91
  !clickToInteractEnabled,
89
92
  )
93
+ const [showViewMenu, setShowViewMenu] = useState(false)
94
+ const [showSchematicGroups, setShowSchematicGroups] = useState(false)
90
95
  const svgDivRef = useRef<HTMLDivElement>(null)
91
96
  const touchStartRef = useRef<{ x: number; y: number } | null>(null)
92
97
 
@@ -219,6 +224,14 @@ export const SchematicViewer = ({
219
224
  editEvents: editEventsWithUnappliedEditEvents,
220
225
  })
221
226
 
227
+ // Add group overlays when enabled
228
+ useSchematicGroupsOverlay({
229
+ svgDivRef,
230
+ circuitJson,
231
+ circuitJsonKey,
232
+ showGroups: showSchematicGroups,
233
+ })
234
+
222
235
  const svgDiv = useMemo(
223
236
  () => (
224
237
  <div
@@ -321,6 +334,10 @@ export const SchematicViewer = ({
321
334
  </div>
322
335
  </div>
323
336
  )}
337
+ <ViewMenuIcon
338
+ active={showViewMenu}
339
+ onClick={() => setShowViewMenu(!showViewMenu)}
340
+ />
324
341
  {editingEnabled && (
325
342
  <EditIcon
326
343
  active={editModeEnabled}
@@ -333,6 +350,14 @@ export const SchematicViewer = ({
333
350
  onClick={() => setSnapToGrid(!snapToGrid)}
334
351
  />
335
352
  )}
353
+ <ViewMenu
354
+ circuitJson={circuitJson}
355
+ circuitJsonKey={circuitJsonKey}
356
+ isVisible={showViewMenu}
357
+ onClose={() => setShowViewMenu(false)}
358
+ showGroups={showSchematicGroups}
359
+ onToggleGroups={setShowSchematicGroups}
360
+ />
336
361
  {spiceSimulationEnabled && (
337
362
  <SpiceSimulationIcon onClick={() => setShowSpiceOverlay(true)} />
338
363
  )}
@@ -12,7 +12,7 @@ export const SpiceSimulationIcon = ({
12
12
  style={{
13
13
  position: "absolute",
14
14
  top: "16px",
15
- right: "56px",
15
+ right: "112px",
16
16
  backgroundColor: "#fff",
17
17
  color: "#000",
18
18
  padding: "8px",
@@ -0,0 +1,147 @@
1
+ import { useMemo } from "react"
2
+ import { su } from "@tscircuit/soup-util"
3
+ import type { CircuitJson } from "circuit-json"
4
+ import { zIndexMap } from "../utils/z-index-map"
5
+
6
+ interface ViewMenuProps {
7
+ circuitJson: CircuitJson
8
+ circuitJsonKey: string
9
+ isVisible: boolean
10
+ onClose: () => void
11
+ showGroups: boolean
12
+ onToggleGroups: (show: boolean) => void
13
+ }
14
+
15
+ export const ViewMenu = ({
16
+ circuitJson,
17
+ circuitJsonKey,
18
+ isVisible,
19
+ onClose,
20
+ showGroups,
21
+ onToggleGroups,
22
+ }: ViewMenuProps) => {
23
+ const hasGroups = useMemo(() => {
24
+ if (!circuitJson || circuitJson.length === 0) return false
25
+
26
+ try {
27
+ // Check if there are explicit groups
28
+ const sourceGroups = su(circuitJson).source_group?.list() || []
29
+ if (sourceGroups.length > 0) return true
30
+
31
+ // Check if we can create virtual groups by component type
32
+ const schematicComponents =
33
+ su(circuitJson).schematic_component?.list() || []
34
+ if (schematicComponents.length > 1) {
35
+ const componentTypes = new Set()
36
+ for (const comp of schematicComponents) {
37
+ const sourceComp = su(circuitJson).source_component.get(
38
+ comp.source_component_id,
39
+ )
40
+ if (sourceComp?.ftype) {
41
+ componentTypes.add(sourceComp.ftype)
42
+ }
43
+ }
44
+ return componentTypes.size > 1 // Only show if there are multiple types
45
+ }
46
+
47
+ return false
48
+ } catch (error) {
49
+ console.error("Error checking for groups:", error)
50
+ return false
51
+ }
52
+ }, [circuitJsonKey])
53
+
54
+ if (!isVisible) return null
55
+
56
+ return (
57
+ <>
58
+ {/* Backdrop */}
59
+ <div
60
+ onClick={onClose}
61
+ style={{
62
+ position: "absolute",
63
+ inset: 0,
64
+ backgroundColor: "transparent",
65
+ zIndex: zIndexMap.viewMenuBackdrop,
66
+ }}
67
+ />
68
+
69
+ {/* Menu */}
70
+ <div
71
+ style={{
72
+ position: "absolute",
73
+ top: "56px",
74
+ right: "16px",
75
+ backgroundColor: "#ffffff",
76
+ color: "#000000",
77
+ border: "1px solid #ccc",
78
+ borderRadius: "4px",
79
+ boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
80
+ minWidth: "200px",
81
+ zIndex: zIndexMap.viewMenu,
82
+ }}
83
+ >
84
+ {/* Groups Toggle Option */}
85
+ <div
86
+ onClick={() => {
87
+ if (hasGroups) {
88
+ onToggleGroups(!showGroups)
89
+ }
90
+ }}
91
+ style={{
92
+ padding: "8px 12px",
93
+ cursor: hasGroups ? "pointer" : "not-allowed",
94
+ opacity: hasGroups ? 1 : 0.5,
95
+ fontSize: "13px",
96
+ color: "#000000",
97
+ fontFamily: "sans-serif",
98
+ display: "flex",
99
+ alignItems: "center",
100
+ gap: "8px",
101
+ }}
102
+ onMouseEnter={(e) => {
103
+ if (hasGroups) {
104
+ e.currentTarget.style.backgroundColor = "#f0f0f0"
105
+ }
106
+ }}
107
+ onMouseLeave={(e) => {
108
+ if (hasGroups) {
109
+ e.currentTarget.style.backgroundColor = "transparent"
110
+ }
111
+ }}
112
+ >
113
+ <div
114
+ style={{
115
+ width: "16px",
116
+ height: "16px",
117
+ border: "2px solid #000",
118
+ borderRadius: "2px",
119
+ backgroundColor: "transparent",
120
+ display: "flex",
121
+ alignItems: "center",
122
+ justifyContent: "center",
123
+ fontSize: "10px",
124
+ fontWeight: "bold",
125
+ }}
126
+ >
127
+ {showGroups && "✓"}
128
+ </div>
129
+ View Schematic Groups
130
+ </div>
131
+
132
+ {!hasGroups && (
133
+ <div
134
+ style={{
135
+ padding: "8px 12px",
136
+ fontSize: "11px",
137
+ color: "#666",
138
+ fontStyle: "italic",
139
+ }}
140
+ >
141
+ No groups found in this schematic
142
+ </div>
143
+ )}
144
+ </div>
145
+ </>
146
+ )
147
+ }
@@ -0,0 +1,40 @@
1
+ import { zIndexMap } from "../utils/z-index-map"
2
+
3
+ export const ViewMenuIcon = ({
4
+ onClick,
5
+ active,
6
+ }: { onClick: () => void; active: boolean }) => {
7
+ return (
8
+ <div
9
+ onClick={onClick}
10
+ style={{
11
+ position: "absolute",
12
+ top: "16px",
13
+ right: "16px",
14
+ backgroundColor: active ? "#4CAF50" : "#fff",
15
+ color: active ? "#fff" : "#000",
16
+ padding: "8px",
17
+ borderRadius: "4px",
18
+ cursor: "pointer",
19
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
20
+ display: "flex",
21
+ alignItems: "center",
22
+ gap: "4px",
23
+ zIndex: zIndexMap.viewMenuIcon,
24
+ }}
25
+ >
26
+ <svg
27
+ width="16"
28
+ height="16"
29
+ viewBox="0 0 24 24"
30
+ fill="none"
31
+ stroke="currentColor"
32
+ strokeWidth="2"
33
+ >
34
+ <circle cx="12" cy="12" r="1" />
35
+ <circle cx="12" cy="5" r="1" />
36
+ <circle cx="12" cy="19" r="1" />
37
+ </svg>
38
+ </div>
39
+ )
40
+ }
@@ -0,0 +1,223 @@
1
+ import { useEffect } from "react"
2
+ import { su } from "@tscircuit/soup-util"
3
+ import type { CircuitJson } from "circuit-json"
4
+
5
+ interface UseSchematicGroupsOverlayOptions {
6
+ svgDivRef: React.RefObject<HTMLDivElement | null>
7
+ circuitJson: CircuitJson
8
+ circuitJsonKey: string
9
+ showGroups: boolean
10
+ }
11
+
12
+ export const useSchematicGroupsOverlay = (
13
+ options: UseSchematicGroupsOverlayOptions,
14
+ ) => {
15
+ const { svgDivRef, circuitJson, circuitJsonKey, showGroups } = options
16
+
17
+ useEffect(() => {
18
+ if (
19
+ !svgDivRef.current ||
20
+ !showGroups ||
21
+ !circuitJson ||
22
+ circuitJson.length === 0
23
+ ) {
24
+ // Remove any existing group overlays
25
+ if (svgDivRef.current) {
26
+ const existingOverlays = svgDivRef.current.querySelectorAll(
27
+ ".schematic-group-overlay",
28
+ )
29
+ existingOverlays.forEach((overlay) => overlay.remove())
30
+ }
31
+ return
32
+ }
33
+
34
+ const svg = svgDivRef.current.querySelector("svg")
35
+ if (!svg) {
36
+ return
37
+ }
38
+
39
+ // Remove existing overlays first
40
+ const existingOverlays = svg.querySelectorAll(".schematic-group-overlay")
41
+ existingOverlays.forEach((overlay) => overlay.remove())
42
+
43
+ try {
44
+ // Get explicit groups first
45
+ const sourceGroups = su(circuitJson).source_group?.list() || []
46
+ const schematicComponents =
47
+ su(circuitJson).schematic_component?.list() || []
48
+
49
+ let groupsToRender: Array<{
50
+ id: string
51
+ name: string
52
+ components: any[]
53
+ color: string
54
+ }> = []
55
+
56
+ // Check if we have meaningful explicit groups (not just auto-generated default groups)
57
+ const hasMeaningfulGroups = sourceGroups.length > 0 &&
58
+ sourceGroups.some(group => group.name && group.name.trim() !== "")
59
+
60
+ if (hasMeaningfulGroups) {
61
+ // Use explicit groups
62
+ const groupMap = new Map<string, any[]>()
63
+
64
+ for (const comp of schematicComponents) {
65
+ const sourceComp = su(circuitJson).source_component.get(
66
+ comp.source_component_id,
67
+ )
68
+ if (sourceComp?.source_group_id) {
69
+ if (!groupMap.has(sourceComp.source_group_id)) {
70
+ groupMap.set(sourceComp.source_group_id, [])
71
+ }
72
+ groupMap.get(sourceComp.source_group_id)!.push(comp)
73
+ }
74
+ }
75
+
76
+ groupsToRender = Array.from(groupMap.entries()).map(
77
+ ([groupId, components], index) => {
78
+ const group = sourceGroups.find(
79
+ (g) => g.source_group_id === groupId,
80
+ )
81
+ return {
82
+ id: groupId,
83
+ name: group?.name || `Group ${index + 1}`,
84
+ components,
85
+ color: getGroupColor(index),
86
+ }
87
+ },
88
+ )
89
+ } else {
90
+ // Create virtual groups by component type
91
+ const componentTypeGroups = new Map<string, any[]>()
92
+
93
+ for (const comp of schematicComponents) {
94
+ const sourceComp = su(circuitJson).source_component.get(
95
+ comp.source_component_id,
96
+ )
97
+ if (sourceComp) {
98
+ const componentType = sourceComp.ftype || "other"
99
+ if (!componentTypeGroups.has(componentType)) {
100
+ componentTypeGroups.set(componentType, [])
101
+ }
102
+ componentTypeGroups.get(componentType)!.push(comp)
103
+ }
104
+ }
105
+
106
+ groupsToRender = Array.from(componentTypeGroups.entries()).map(
107
+ ([type, components], index) => ({
108
+ id: `type_${type}`,
109
+ name: `${type.charAt(0).toUpperCase() + type.slice(1)}s`,
110
+ components,
111
+ color: getGroupColor(index),
112
+ }),
113
+ )
114
+ }
115
+
116
+ // Render group overlays
117
+ groupsToRender.forEach((group, groupIndex) => {
118
+ if (group.components.length === 0) return
119
+
120
+ // Calculate bounding box for the group
121
+ const groupBounds = calculateGroupBounds(group.components, svg)
122
+ if (!groupBounds) return
123
+
124
+ // Create group overlay rectangle
125
+ const groupOverlay = document.createElementNS(
126
+ "http://www.w3.org/2000/svg",
127
+ "rect",
128
+ )
129
+ groupOverlay.setAttribute("class", "schematic-group-overlay")
130
+ groupOverlay.setAttribute("x", (groupBounds.minX - 25).toString())
131
+ groupOverlay.setAttribute("y", (groupBounds.minY - 25).toString())
132
+ groupOverlay.setAttribute(
133
+ "width",
134
+ (groupBounds.maxX - groupBounds.minX + 50).toString(),
135
+ )
136
+ groupOverlay.setAttribute(
137
+ "height",
138
+ (groupBounds.maxY - groupBounds.minY + 50).toString(),
139
+ )
140
+ groupOverlay.setAttribute("fill", "none")
141
+ groupOverlay.setAttribute("stroke", group.color)
142
+ groupOverlay.setAttribute("stroke-width", "3")
143
+ groupOverlay.setAttribute("stroke-dasharray", "8,4")
144
+ groupOverlay.setAttribute("opacity", "0.8")
145
+ groupOverlay.setAttribute("rx", "4")
146
+ groupOverlay.setAttribute("ry", "4")
147
+
148
+ // Create group label
149
+ const groupLabel = document.createElementNS(
150
+ "http://www.w3.org/2000/svg",
151
+ "text",
152
+ )
153
+ groupLabel.setAttribute("class", "schematic-group-overlay")
154
+ groupLabel.setAttribute("x", (groupBounds.minX - 10).toString())
155
+ groupLabel.setAttribute("y", (groupBounds.minY - 8).toString())
156
+ groupLabel.setAttribute("fill", group.color)
157
+ groupLabel.setAttribute("font-size", "14")
158
+ groupLabel.setAttribute("font-family", "Arial, sans-serif")
159
+ groupLabel.setAttribute("font-weight", "bold")
160
+ groupLabel.setAttribute("stroke", "#fff")
161
+ groupLabel.setAttribute("stroke-width", "0.5")
162
+ groupLabel.setAttribute("paint-order", "stroke fill")
163
+ groupLabel.textContent = group.name
164
+
165
+ // Add overlays to the SVG (use appendChild to ensure they're on top)
166
+ svg.appendChild(groupOverlay)
167
+ svg.appendChild(groupLabel)
168
+ })
169
+ } catch (error) {
170
+ console.error("Error creating group overlays:", error)
171
+ }
172
+ }, [svgDivRef, circuitJsonKey, showGroups])
173
+ }
174
+
175
+ function getGroupColor(index: number): string {
176
+ const colors = [
177
+ "#FF6B6B", // Red
178
+ "#4ECDC4", // Teal
179
+ "#45B7D1", // Blue
180
+ "#96CEB4", // Green
181
+ "#FF8C42", // Orange
182
+ "#DDA0DD", // Plum
183
+ "#98D8C8", // Mint
184
+ "#F7DC6F", // Light Yellow
185
+ ]
186
+ return colors[index % colors.length]
187
+ }
188
+
189
+ function calculateGroupBounds(components: any[], svg: SVGElement) {
190
+ let minX = Infinity,
191
+ minY = Infinity,
192
+ maxX = -Infinity,
193
+ maxY = -Infinity
194
+
195
+ for (const component of components) {
196
+ // Look for the component group element (based on circuit-to-svg documentation)
197
+ let componentElement = svg.querySelector(
198
+ `g[data-schematic-component-id="${component.schematic_component_id}"]`,
199
+ )
200
+
201
+ if (!componentElement) {
202
+ // Fallback to any element with the data attribute
203
+ componentElement = svg.querySelector(
204
+ `[data-schematic-component-id="${component.schematic_component_id}"]`,
205
+ )
206
+ }
207
+
208
+ if (componentElement) {
209
+ const bbox = (componentElement as SVGGraphicsElement).getBBox()
210
+ minX = Math.min(minX, bbox.x)
211
+ minY = Math.min(minY, bbox.y)
212
+ maxX = Math.max(maxX, bbox.x + bbox.width)
213
+ maxY = Math.max(maxY, bbox.y + bbox.height)
214
+ }
215
+ }
216
+
217
+ if (minX === Infinity) {
218
+ return null
219
+ }
220
+
221
+ const bounds = { minX, minY, maxX, maxY }
222
+ return bounds
223
+ }
@@ -2,5 +2,8 @@ export const zIndexMap = {
2
2
  schematicEditIcon: 50,
3
3
  schematicGridIcon: 49,
4
4
  spiceSimulationIcon: 51,
5
+ viewMenuIcon: 48,
6
+ viewMenu: 55,
7
+ viewMenuBackdrop: 54,
5
8
  clickToInteractOverlay: 100,
6
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-viewer",
3
- "version": "2.0.26",
3
+ "version": "2.0.28",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {