@tscircuit/schematic-viewer 2.0.25 → 2.0.27

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,9 @@ export const SchematicViewer = ({
219
224
  editEvents: editEventsWithUnappliedEditEvents,
220
225
  })
221
226
 
227
+ // Add group overlays when enabled
228
+ useSchematicGroupsOverlay(svgDivRef, circuitJson, circuitJsonKey, showSchematicGroups)
229
+
222
230
  const svgDiv = useMemo(
223
231
  () => (
224
232
  <div
@@ -321,6 +329,10 @@ export const SchematicViewer = ({
321
329
  </div>
322
330
  </div>
323
331
  )}
332
+ <ViewMenuIcon
333
+ active={showViewMenu}
334
+ onClick={() => setShowViewMenu(!showViewMenu)}
335
+ />
324
336
  {editingEnabled && (
325
337
  <EditIcon
326
338
  active={editModeEnabled}
@@ -333,6 +345,14 @@ export const SchematicViewer = ({
333
345
  onClick={() => setSnapToGrid(!snapToGrid)}
334
346
  />
335
347
  )}
348
+ <ViewMenu
349
+ circuitJson={circuitJson}
350
+ circuitJsonKey={circuitJsonKey}
351
+ isVisible={showViewMenu}
352
+ onClose={() => setShowViewMenu(false)}
353
+ showGroups={showSchematicGroups}
354
+ onToggleGroups={setShowSchematicGroups}
355
+ />
336
356
  {spiceSimulationEnabled && (
337
357
  <SpiceSimulationIcon onClick={() => setShowSpiceOverlay(true)} />
338
358
  )}
@@ -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,217 @@
1
+ import { useEffect } from "react"
2
+ import { su } from "@tscircuit/soup-util"
3
+ import type { CircuitJson } from "circuit-json"
4
+
5
+ export const useSchematicGroupsOverlay = (
6
+ svgDivRef: React.RefObject<HTMLDivElement | null>,
7
+ circuitJson: CircuitJson,
8
+ circuitJsonKey: string,
9
+ showGroups: boolean,
10
+ ) => {
11
+ useEffect(() => {
12
+ if (
13
+ !svgDivRef.current ||
14
+ !showGroups ||
15
+ !circuitJson ||
16
+ circuitJson.length === 0
17
+ ) {
18
+ // Remove any existing group overlays
19
+ if (svgDivRef.current) {
20
+ const existingOverlays = svgDivRef.current.querySelectorAll(
21
+ ".schematic-group-overlay",
22
+ )
23
+ existingOverlays.forEach((overlay) => overlay.remove())
24
+ }
25
+ return
26
+ }
27
+
28
+ const svg = svgDivRef.current.querySelector("svg")
29
+ if (!svg) {
30
+ return
31
+ }
32
+
33
+ // Remove existing overlays first
34
+ const existingOverlays = svg.querySelectorAll(".schematic-group-overlay")
35
+ existingOverlays.forEach((overlay) => overlay.remove())
36
+
37
+ try {
38
+ // Get explicit groups first
39
+ const sourceGroups = su(circuitJson).source_group?.list() || []
40
+ const schematicComponents =
41
+ su(circuitJson).schematic_component?.list() || []
42
+
43
+ let groupsToRender: Array<{
44
+ id: string
45
+ name: string
46
+ components: any[]
47
+ color: string
48
+ }> = []
49
+
50
+ // Check if we have meaningful explicit groups (not just auto-generated default groups)
51
+ const hasMeaningfulGroups = sourceGroups.length > 0 &&
52
+ sourceGroups.some(group => group.name && group.name !== "default" && group.name !== "")
53
+
54
+ if (hasMeaningfulGroups) {
55
+ // Use explicit groups
56
+ const groupMap = new Map<string, any[]>()
57
+
58
+ for (const comp of schematicComponents) {
59
+ const sourceComp = su(circuitJson).source_component.get(
60
+ comp.source_component_id,
61
+ )
62
+ if (sourceComp?.source_group_id) {
63
+ if (!groupMap.has(sourceComp.source_group_id)) {
64
+ groupMap.set(sourceComp.source_group_id, [])
65
+ }
66
+ groupMap.get(sourceComp.source_group_id)!.push(comp)
67
+ }
68
+ }
69
+
70
+ groupsToRender = Array.from(groupMap.entries()).map(
71
+ ([groupId, components], index) => {
72
+ const group = sourceGroups.find(
73
+ (g) => g.source_group_id === groupId,
74
+ )
75
+ return {
76
+ id: groupId,
77
+ name: group?.name || `Group ${index + 1}`,
78
+ components,
79
+ color: getGroupColor(index),
80
+ }
81
+ },
82
+ )
83
+ } else {
84
+ // Create virtual groups by component type
85
+ const componentTypeGroups = new Map<string, any[]>()
86
+
87
+ for (const comp of schematicComponents) {
88
+ const sourceComp = su(circuitJson).source_component.get(
89
+ comp.source_component_id,
90
+ )
91
+ if (sourceComp) {
92
+ const componentType = sourceComp.ftype || "other"
93
+ if (!componentTypeGroups.has(componentType)) {
94
+ componentTypeGroups.set(componentType, [])
95
+ }
96
+ componentTypeGroups.get(componentType)!.push(comp)
97
+ }
98
+ }
99
+
100
+ groupsToRender = Array.from(componentTypeGroups.entries()).map(
101
+ ([type, components], index) => ({
102
+ id: `type_${type}`,
103
+ name: `${type.charAt(0).toUpperCase() + type.slice(1)}s`,
104
+ components,
105
+ color: getGroupColor(index),
106
+ }),
107
+ )
108
+ }
109
+
110
+ // Render group overlays
111
+ groupsToRender.forEach((group, groupIndex) => {
112
+ if (group.components.length === 0) return
113
+
114
+ // Calculate bounding box for the group
115
+ const groupBounds = calculateGroupBounds(group.components, svg)
116
+ if (!groupBounds) return
117
+
118
+ // Create group overlay rectangle
119
+ const groupOverlay = document.createElementNS(
120
+ "http://www.w3.org/2000/svg",
121
+ "rect",
122
+ )
123
+ groupOverlay.setAttribute("class", "schematic-group-overlay")
124
+ groupOverlay.setAttribute("x", (groupBounds.minX - 25).toString())
125
+ groupOverlay.setAttribute("y", (groupBounds.minY - 25).toString())
126
+ groupOverlay.setAttribute(
127
+ "width",
128
+ (groupBounds.maxX - groupBounds.minX + 50).toString(),
129
+ )
130
+ groupOverlay.setAttribute(
131
+ "height",
132
+ (groupBounds.maxY - groupBounds.minY + 50).toString(),
133
+ )
134
+ groupOverlay.setAttribute("fill", "none")
135
+ groupOverlay.setAttribute("stroke", group.color)
136
+ groupOverlay.setAttribute("stroke-width", "3")
137
+ groupOverlay.setAttribute("stroke-dasharray", "8,4")
138
+ groupOverlay.setAttribute("opacity", "0.8")
139
+ groupOverlay.setAttribute("rx", "4")
140
+ groupOverlay.setAttribute("ry", "4")
141
+
142
+ // Create group label
143
+ const groupLabel = document.createElementNS(
144
+ "http://www.w3.org/2000/svg",
145
+ "text",
146
+ )
147
+ groupLabel.setAttribute("class", "schematic-group-overlay")
148
+ groupLabel.setAttribute("x", (groupBounds.minX - 10).toString())
149
+ groupLabel.setAttribute("y", (groupBounds.minY - 8).toString())
150
+ groupLabel.setAttribute("fill", group.color)
151
+ groupLabel.setAttribute("font-size", "14")
152
+ groupLabel.setAttribute("font-family", "Arial, sans-serif")
153
+ groupLabel.setAttribute("font-weight", "bold")
154
+ groupLabel.setAttribute("stroke", "#fff")
155
+ groupLabel.setAttribute("stroke-width", "0.5")
156
+ groupLabel.setAttribute("paint-order", "stroke fill")
157
+ groupLabel.textContent = group.name
158
+
159
+ // Add overlays to the SVG (use appendChild to ensure they're on top)
160
+ svg.appendChild(groupOverlay)
161
+ svg.appendChild(groupLabel)
162
+ })
163
+ } catch (error) {
164
+ console.error("Error creating group overlays:", error)
165
+ }
166
+ }, [svgDivRef, circuitJsonKey, showGroups])
167
+ }
168
+
169
+ function getGroupColor(index: number): string {
170
+ const colors = [
171
+ "#FF6B6B", // Red
172
+ "#4ECDC4", // Teal
173
+ "#45B7D1", // Blue
174
+ "#96CEB4", // Green
175
+ "#FF8C42", // Orange
176
+ "#DDA0DD", // Plum
177
+ "#98D8C8", // Mint
178
+ "#F7DC6F", // Light Yellow
179
+ ]
180
+ return colors[index % colors.length]
181
+ }
182
+
183
+ function calculateGroupBounds(components: any[], svg: SVGElement) {
184
+ let minX = Infinity,
185
+ minY = Infinity,
186
+ maxX = -Infinity,
187
+ maxY = -Infinity
188
+
189
+ for (const component of components) {
190
+ // Look for the component group element (based on circuit-to-svg documentation)
191
+ let componentElement = svg.querySelector(
192
+ `g[data-schematic-component-id="${component.schematic_component_id}"]`,
193
+ )
194
+
195
+ if (!componentElement) {
196
+ // Fallback to any element with the data attribute
197
+ componentElement = svg.querySelector(
198
+ `[data-schematic-component-id="${component.schematic_component_id}"]`,
199
+ )
200
+ }
201
+
202
+ if (componentElement) {
203
+ const bbox = (componentElement as SVGGraphicsElement).getBBox()
204
+ minX = Math.min(minX, bbox.x)
205
+ minY = Math.min(minY, bbox.y)
206
+ maxX = Math.max(maxX, bbox.x + bbox.width)
207
+ maxY = Math.max(maxY, bbox.y + bbox.height)
208
+ }
209
+ }
210
+
211
+ if (minX === Infinity) {
212
+ return null
213
+ }
214
+
215
+ const bounds = { minX, minY, maxX, maxY }
216
+ return bounds
217
+ }
@@ -108,26 +108,16 @@ export const useSpiceSimulation = (spiceString: string | null) => {
108
108
  setPlotData([])
109
109
  setNodes([])
110
110
 
111
- let worker: Worker
112
-
113
- // In development, Vite will handle the worker bundling.
114
- // In production, we use a pre-built blob URL.
115
- if (import.meta.env.DEV) {
116
- worker = new Worker(
117
- new URL("../workers/spice-simulation.worker.ts", import.meta.url),
118
- { type: "module" },
119
- )
120
- } else {
121
- const workerUrl = getSpiceSimulationWorkerBlobUrl()
122
-
123
- if (!workerUrl) {
124
- setError("Could not create SPICE simulation worker.")
125
- setIsLoading(false)
126
- return
127
- }
128
- worker = new Worker(workerUrl, { type: "module" })
111
+ const workerUrl = getSpiceSimulationWorkerBlobUrl()
112
+
113
+ if (!workerUrl) {
114
+ setError("Could not create SPICE simulation worker.")
115
+ setIsLoading(false)
116
+ return
129
117
  }
130
118
 
119
+ const worker = new Worker(workerUrl, { type: "module" })
120
+
131
121
  worker.onmessage = (event: MessageEvent<WorkerMessage>) => {
132
122
  if (event.data.type === "result") {
133
123
  try {
@@ -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.25",
3
+ "version": "2.0.27",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {