@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.
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/.github/workflows/bun-formatcheck.yml +0 -26
- package/.github/workflows/bun-pver-release.yml +0 -59
- package/.github/workflows/bun-typecheck.yml +0 -26
- package/.github/workflows/on-merge-inform-release-tracker.yml +0 -24
- package/CLAUDE.md +0 -1
- package/biome.json +0 -56
- package/bun.lockb +0 -0
- package/cosmos.config.json +0 -3
- package/cosmos.decorator.tsx +0 -3
- package/docs/circuit-to-svg-metadata.md +0 -151
- package/docs/dragndrop-spec.md +0 -39
- package/examples/example1-resistor-and-capacitor.fixture.tsx +0 -16
- package/examples/example10-groups-view-schematic-groups.fixture.tsx +0 -76
- package/examples/example11-automatic-grouping-view-schematic-groups.fixture.tsx +0 -109
- package/examples/example12-spice-boost-converter.fixture.tsx +0 -78
- package/examples/example13-disablegroups.fixture.tsx +0 -30
- package/examples/example14-schematic-component-click.fixture.tsx +0 -46
- package/examples/example15-analog-simulation-viewer.fixture.tsx +0 -145
- package/examples/example16-no-analog-simulation.fixture.tsx +0 -13
- package/examples/example17-schematic-ports.fixture.tsx +0 -49
- package/examples/example18-live-toggle.fixture.tsx +0 -60
- package/examples/example2-small-circuit.fixture.tsx +0 -48
- package/examples/example3-small-circuit-without-debug-grid.fixture.tsx +0 -44
- package/examples/example4-reset-edit-events.fixture.tsx +0 -57
- package/examples/example5-circuit-json-rerender.fixture.tsx +0 -110
- package/examples/example6-click-to-interact.fixture.tsx +0 -36
- package/examples/example7-schematic-viewer-fix-snapping.fixture.tsx +0 -123
- package/examples/example8-color-overrides.fixture.tsx +0 -52
- package/examples/example9-spice-rc-charging-voltage-divider.fixture.tsx +0 -77
- package/index.html +0 -12
- package/lib/components/AnalogSimulationViewer.tsx +0 -300
- package/lib/components/ControlledSchematicViewer.tsx +0 -40
- package/lib/components/EditIcon.tsx +0 -46
- package/lib/components/GridIcon.tsx +0 -45
- package/lib/components/MouseTracker.tsx +0 -257
- package/lib/components/SchematicComponentMouseTarget.tsx +0 -189
- package/lib/components/SchematicPortMouseTarget.tsx +0 -224
- package/lib/components/SchematicViewer.tsx +0 -583
- package/lib/components/SpiceIcon.tsx +0 -14
- package/lib/components/SpicePlot.tsx +0 -221
- package/lib/components/SpiceSimulationIcon.tsx +0 -32
- package/lib/components/SpiceSimulationOverlay.tsx +0 -250
- package/lib/components/ViewMenu.tsx +0 -218
- package/lib/components/ViewMenuIcon.tsx +0 -47
- package/lib/dev/render-to-circuit-json.ts +0 -8
- package/lib/hooks/use-resize-handling.ts +0 -35
- package/lib/hooks/useChangeSchematicComponentLocationsInSvg.ts +0 -117
- package/lib/hooks/useChangeSchematicTracesForMovedComponents.ts +0 -121
- package/lib/hooks/useComponentDragging.ts +0 -251
- package/lib/hooks/useLocalStorage.ts +0 -63
- package/lib/hooks/useMouseEventsOverBoundingBox.ts +0 -74
- package/lib/hooks/useSchematicGroupsOverlay.ts +0 -364
- package/lib/hooks/useSpiceSimulation.ts +0 -149
- package/lib/index.ts +0 -4
- package/lib/types/edit-events.ts +0 -16
- package/lib/types/eecircuit-engine.d.ts +0 -147
- package/lib/utils/debug.ts +0 -9
- package/lib/utils/get-component-offset-due-to-events.ts +0 -43
- package/lib/utils/spice-utils.ts +0 -128
- package/lib/utils/z-index-map.ts +0 -11
- package/lib/workers/spice-simulation.worker.ts +0 -51
- package/scripts/build-worker-blob-url.ts +0 -55
- package/src/main.tsx +0 -21
- package/tsconfig.json +0 -33
- package/tsup-webworker.config.ts +0 -13
- package/vite.config.js +0 -15
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,364 +0,0 @@
|
|
|
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
|
-
const GROUP_COLORS = [
|
|
13
|
-
"#8B0000", // Dark Red
|
|
14
|
-
"#2F4F4F", // Dark Slate Gray
|
|
15
|
-
"#191970", // Midnight Blue
|
|
16
|
-
"#006400", // Dark Green
|
|
17
|
-
"#FF4500", // Dark Orange
|
|
18
|
-
"#800080", // Purple
|
|
19
|
-
"#2E8B57", // Sea Green
|
|
20
|
-
"#B8860B", // Dark Goldenrod
|
|
21
|
-
"#C71585", // Medium Violet Red
|
|
22
|
-
"#008B8B", // Dark Cyan
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
export const useSchematicGroupsOverlay = (
|
|
26
|
-
options: UseSchematicGroupsOverlayOptions,
|
|
27
|
-
) => {
|
|
28
|
-
const { svgDivRef, circuitJson, circuitJsonKey, showGroups } = options
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
// Always clean up existing overlays first
|
|
32
|
-
if (svgDivRef.current) {
|
|
33
|
-
const existingOverlays = svgDivRef.current.querySelectorAll(
|
|
34
|
-
".schematic-group-overlay",
|
|
35
|
-
)
|
|
36
|
-
existingOverlays.forEach((overlay) => overlay.remove())
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
!svgDivRef.current ||
|
|
41
|
-
!showGroups ||
|
|
42
|
-
!circuitJson ||
|
|
43
|
-
circuitJson.length === 0
|
|
44
|
-
) {
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Small delay to ensure SVG is rendered when groups are enabled from localStorage
|
|
49
|
-
const timeoutId = setTimeout(() => {
|
|
50
|
-
if (!svgDivRef.current) return
|
|
51
|
-
|
|
52
|
-
const svg = svgDivRef.current.querySelector("svg")
|
|
53
|
-
if (!svg) {
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const existingOverlays = svg.querySelectorAll(".schematic-group-overlay")
|
|
58
|
-
existingOverlays.forEach((overlay) => overlay.remove())
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const sourceGroups =
|
|
62
|
-
su(circuitJson)
|
|
63
|
-
.source_group?.list()
|
|
64
|
-
.filter((x) => !!!x.is_subcircuit) || []
|
|
65
|
-
const schematicComponents =
|
|
66
|
-
su(circuitJson).schematic_component?.list() || []
|
|
67
|
-
|
|
68
|
-
const sourceGroupHierarchy = new Map<string, string[]>()
|
|
69
|
-
sourceGroups.forEach((group) => {
|
|
70
|
-
const groupWithParent = group as any
|
|
71
|
-
if (groupWithParent.parent_source_group_id) {
|
|
72
|
-
const children =
|
|
73
|
-
sourceGroupHierarchy.get(
|
|
74
|
-
groupWithParent.parent_source_group_id,
|
|
75
|
-
) || []
|
|
76
|
-
children.push(group.source_group_id)
|
|
77
|
-
sourceGroupHierarchy.set(
|
|
78
|
-
groupWithParent.parent_source_group_id,
|
|
79
|
-
children,
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
const getAllDescendantSourceGroups = (
|
|
85
|
-
sourceGroupId: string,
|
|
86
|
-
): string[] => {
|
|
87
|
-
const descendants: string[] = []
|
|
88
|
-
const children = sourceGroupHierarchy.get(sourceGroupId) || []
|
|
89
|
-
for (const child of children) {
|
|
90
|
-
descendants.push(child)
|
|
91
|
-
descendants.push(...getAllDescendantSourceGroups(child))
|
|
92
|
-
}
|
|
93
|
-
return descendants
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const getGroupDepthLevel = (sourceGroupId: string): number => {
|
|
97
|
-
const groupWithParent = sourceGroups.find(
|
|
98
|
-
(g) => g.source_group_id === sourceGroupId,
|
|
99
|
-
) as any
|
|
100
|
-
if (!groupWithParent?.parent_source_group_id) {
|
|
101
|
-
return 0
|
|
102
|
-
}
|
|
103
|
-
return 1 + getGroupDepthLevel(groupWithParent.parent_source_group_id)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const hasMeaningfulGroups =
|
|
107
|
-
sourceGroups.length > 0 &&
|
|
108
|
-
sourceGroups.some((group) => group.name && group.name.trim() !== "")
|
|
109
|
-
|
|
110
|
-
let groupsToRender: Array<{
|
|
111
|
-
id: string
|
|
112
|
-
name: string
|
|
113
|
-
components: any[]
|
|
114
|
-
color: string
|
|
115
|
-
depthLevel: number
|
|
116
|
-
hasChildren: boolean
|
|
117
|
-
sourceGroupId?: string
|
|
118
|
-
}> = []
|
|
119
|
-
|
|
120
|
-
if (hasMeaningfulGroups) {
|
|
121
|
-
const groupMap = new Map<string, any[]>()
|
|
122
|
-
|
|
123
|
-
for (const comp of schematicComponents) {
|
|
124
|
-
const sourceComp = su(circuitJson).source_component.get(
|
|
125
|
-
comp.source_component_id!,
|
|
126
|
-
)
|
|
127
|
-
if (sourceComp?.source_group_id) {
|
|
128
|
-
if (!groupMap.has(sourceComp.source_group_id)) {
|
|
129
|
-
groupMap.set(sourceComp.source_group_id, [])
|
|
130
|
-
}
|
|
131
|
-
groupMap.get(sourceComp.source_group_id)!.push(comp)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
sourceGroups.forEach((group, index) => {
|
|
136
|
-
let groupComponents = groupMap.get(group.source_group_id) || []
|
|
137
|
-
|
|
138
|
-
const descendantGroups = getAllDescendantSourceGroups(
|
|
139
|
-
group.source_group_id,
|
|
140
|
-
)
|
|
141
|
-
for (const descendantGroupId of descendantGroups) {
|
|
142
|
-
const descendantComponents = groupMap.get(descendantGroupId) || []
|
|
143
|
-
groupComponents = [...groupComponents, ...descendantComponents]
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (groupComponents.length > 0) {
|
|
147
|
-
const depthLevel = getGroupDepthLevel(group.source_group_id)
|
|
148
|
-
const hasChildren =
|
|
149
|
-
getAllDescendantSourceGroups(group.source_group_id).length > 0
|
|
150
|
-
|
|
151
|
-
if (group.name?.startsWith("unnamed_board")) return
|
|
152
|
-
groupsToRender.push({
|
|
153
|
-
id: group.source_group_id,
|
|
154
|
-
name: group.name || `Group ${index + 1}`,
|
|
155
|
-
components: groupComponents,
|
|
156
|
-
color: GROUP_COLORS[index % GROUP_COLORS.length],
|
|
157
|
-
depthLevel,
|
|
158
|
-
hasChildren,
|
|
159
|
-
sourceGroupId: group.source_group_id,
|
|
160
|
-
})
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
// else {
|
|
165
|
-
// const componentTypeGroups = new Map<string, any[]>()
|
|
166
|
-
|
|
167
|
-
// for (const comp of schematicComponents) {
|
|
168
|
-
// const sourceComp = su(circuitJson).source_component.get(comp.source_component_id)
|
|
169
|
-
// if (sourceComp) {
|
|
170
|
-
// const componentType = sourceComp.ftype || "other"
|
|
171
|
-
// if (!componentTypeGroups.has(componentType)) {
|
|
172
|
-
// componentTypeGroups.set(componentType, [])
|
|
173
|
-
// }
|
|
174
|
-
// componentTypeGroups.get(componentType)!.push(comp)
|
|
175
|
-
// }
|
|
176
|
-
// }
|
|
177
|
-
// // groupsToRender = Array.from(componentTypeGroups.entries()).map(
|
|
178
|
-
// // ([type, components], index) => ({
|
|
179
|
-
// // id: `type_${type}`,
|
|
180
|
-
// // name: `${type.charAt(0).toUpperCase() + type.slice(1)}s`,
|
|
181
|
-
// // components,
|
|
182
|
-
// // color: GROUP_COLORS[index % GROUP_COLORS.length],
|
|
183
|
-
// // depthLevel: 0,
|
|
184
|
-
// // hasChildren: false,
|
|
185
|
-
// // }),
|
|
186
|
-
// // )
|
|
187
|
-
// }
|
|
188
|
-
|
|
189
|
-
const viewBox = svg.viewBox.baseVal
|
|
190
|
-
const svgRect = svg.getBoundingClientRect()
|
|
191
|
-
const scale =
|
|
192
|
-
Math.min(
|
|
193
|
-
svgRect.width / viewBox.width,
|
|
194
|
-
svgRect.height / viewBox.height,
|
|
195
|
-
) || 1
|
|
196
|
-
|
|
197
|
-
groupsToRender.sort((a, b) => a.depthLevel - b.depthLevel)
|
|
198
|
-
|
|
199
|
-
groupsToRender.forEach((group) => {
|
|
200
|
-
if (group.components.length === 0) return
|
|
201
|
-
|
|
202
|
-
const groupBounds = calculateGroupBounds(group.components, svg)
|
|
203
|
-
if (!groupBounds) return
|
|
204
|
-
|
|
205
|
-
const basePadding = Math.max(
|
|
206
|
-
8,
|
|
207
|
-
Math.min(25, 15 / Math.max(scale, 0.3)),
|
|
208
|
-
)
|
|
209
|
-
const hierarchyPadding = group.hasChildren ? basePadding * 0.6 : 0
|
|
210
|
-
const totalPadding = basePadding + hierarchyPadding
|
|
211
|
-
|
|
212
|
-
const baseStrokeWidth = Math.max(1, 2 / Math.max(scale, 0.5))
|
|
213
|
-
const strokeWidth =
|
|
214
|
-
group.depthLevel === 0 ? baseStrokeWidth : baseStrokeWidth * 0.7
|
|
215
|
-
|
|
216
|
-
const baseDashSize = Math.max(4, 8 / Math.max(scale, 0.5))
|
|
217
|
-
const dashMultiplier = group.hasChildren ? 1.3 : 1
|
|
218
|
-
const dashSize = baseDashSize * dashMultiplier
|
|
219
|
-
const gapSize = dashSize * 0.5
|
|
220
|
-
|
|
221
|
-
const groupOverlay = document.createElementNS(
|
|
222
|
-
"http://www.w3.org/2000/svg",
|
|
223
|
-
"rect",
|
|
224
|
-
)
|
|
225
|
-
groupOverlay.setAttribute("class", "schematic-group-overlay")
|
|
226
|
-
groupOverlay.setAttribute(
|
|
227
|
-
"x",
|
|
228
|
-
(groupBounds.minX - totalPadding).toString(),
|
|
229
|
-
)
|
|
230
|
-
groupOverlay.setAttribute(
|
|
231
|
-
"y",
|
|
232
|
-
(groupBounds.minY - totalPadding).toString(),
|
|
233
|
-
)
|
|
234
|
-
groupOverlay.setAttribute(
|
|
235
|
-
"width",
|
|
236
|
-
(groupBounds.maxX - groupBounds.minX + totalPadding * 2).toString(),
|
|
237
|
-
)
|
|
238
|
-
groupOverlay.setAttribute(
|
|
239
|
-
"height",
|
|
240
|
-
(groupBounds.maxY - groupBounds.minY + totalPadding * 2).toString(),
|
|
241
|
-
)
|
|
242
|
-
groupOverlay.setAttribute("fill", "none")
|
|
243
|
-
groupOverlay.setAttribute("stroke", group.color)
|
|
244
|
-
groupOverlay.setAttribute("stroke-width", strokeWidth.toString())
|
|
245
|
-
groupOverlay.setAttribute(
|
|
246
|
-
"stroke-dasharray",
|
|
247
|
-
`${dashSize},${gapSize}`,
|
|
248
|
-
)
|
|
249
|
-
groupOverlay.setAttribute("opacity", "0.8")
|
|
250
|
-
groupOverlay.setAttribute("rx", "0")
|
|
251
|
-
groupOverlay.setAttribute("ry", "0")
|
|
252
|
-
|
|
253
|
-
const baseFontSize = Math.max(
|
|
254
|
-
6,
|
|
255
|
-
Math.min(20, 14 / Math.max(scale, 0.2)),
|
|
256
|
-
)
|
|
257
|
-
const fontSizeReduction =
|
|
258
|
-
group.depthLevel === 0 || group.depthLevel === 1
|
|
259
|
-
? 0
|
|
260
|
-
: group.depthLevel * 0.2
|
|
261
|
-
const fontSize = baseFontSize * (1 - fontSizeReduction)
|
|
262
|
-
|
|
263
|
-
const labelPadding = Math.max(1, fontSize * 0.2)
|
|
264
|
-
const labelText = group.name
|
|
265
|
-
|
|
266
|
-
const tempText = document.createElementNS(
|
|
267
|
-
"http://www.w3.org/2000/svg",
|
|
268
|
-
"text",
|
|
269
|
-
)
|
|
270
|
-
tempText.setAttribute("font-size", fontSize.toString())
|
|
271
|
-
tempText.setAttribute("font-family", "Arial, sans-serif")
|
|
272
|
-
tempText.textContent = labelText
|
|
273
|
-
svg.appendChild(tempText)
|
|
274
|
-
const textBBox = tempText.getBBox()
|
|
275
|
-
svg.removeChild(tempText)
|
|
276
|
-
|
|
277
|
-
const labelWidth = textBBox.width + labelPadding * 2
|
|
278
|
-
const labelHeight = fontSize + labelPadding * 2
|
|
279
|
-
const labelX = groupBounds.minX - totalPadding
|
|
280
|
-
const labelY = groupBounds.minY - totalPadding - labelHeight
|
|
281
|
-
|
|
282
|
-
const labelBg = document.createElementNS(
|
|
283
|
-
"http://www.w3.org/2000/svg",
|
|
284
|
-
"rect",
|
|
285
|
-
)
|
|
286
|
-
labelBg.setAttribute("class", "schematic-group-overlay")
|
|
287
|
-
labelBg.setAttribute("x", labelX.toString())
|
|
288
|
-
labelBg.setAttribute("y", (labelY - labelHeight).toString())
|
|
289
|
-
labelBg.setAttribute("width", labelWidth.toString())
|
|
290
|
-
labelBg.setAttribute("height", labelHeight.toString())
|
|
291
|
-
labelBg.setAttribute("fill", "transparent")
|
|
292
|
-
labelBg.setAttribute("rx", "0")
|
|
293
|
-
labelBg.setAttribute("ry", "0")
|
|
294
|
-
|
|
295
|
-
const groupLabel = document.createElementNS(
|
|
296
|
-
"http://www.w3.org/2000/svg",
|
|
297
|
-
"text",
|
|
298
|
-
)
|
|
299
|
-
groupLabel.setAttribute("class", "schematic-group-overlay")
|
|
300
|
-
groupLabel.setAttribute("x", (labelX + labelPadding).toString())
|
|
301
|
-
groupLabel.setAttribute(
|
|
302
|
-
"y",
|
|
303
|
-
(labelY + labelHeight - labelPadding).toString(),
|
|
304
|
-
)
|
|
305
|
-
groupLabel.setAttribute("fill", group.color)
|
|
306
|
-
groupLabel.setAttribute("font-size", fontSize.toString())
|
|
307
|
-
groupLabel.setAttribute("font-family", "Arial, sans-serif")
|
|
308
|
-
groupLabel.setAttribute(
|
|
309
|
-
"font-weight",
|
|
310
|
-
group.depthLevel === 0 ? "600" : "500",
|
|
311
|
-
)
|
|
312
|
-
groupLabel.setAttribute("stroke", group.color)
|
|
313
|
-
groupLabel.setAttribute(
|
|
314
|
-
"stroke-width",
|
|
315
|
-
Math.max(0.2, fontSize * 0.02).toString(),
|
|
316
|
-
)
|
|
317
|
-
groupLabel.textContent = labelText
|
|
318
|
-
|
|
319
|
-
svg.appendChild(groupOverlay)
|
|
320
|
-
svg.appendChild(labelBg)
|
|
321
|
-
svg.appendChild(groupLabel)
|
|
322
|
-
})
|
|
323
|
-
} catch (error) {
|
|
324
|
-
console.error("Error creating group overlays:", error)
|
|
325
|
-
}
|
|
326
|
-
}, 10) // Small delay to ensure SVG is ready
|
|
327
|
-
|
|
328
|
-
return () => clearTimeout(timeoutId)
|
|
329
|
-
}, [svgDivRef, circuitJsonKey, showGroups])
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function calculateGroupBounds(components: any[], svg: SVGElement) {
|
|
333
|
-
let minX = Infinity,
|
|
334
|
-
minY = Infinity,
|
|
335
|
-
maxX = -Infinity,
|
|
336
|
-
maxY = -Infinity
|
|
337
|
-
|
|
338
|
-
for (const component of components) {
|
|
339
|
-
let componentElement = svg.querySelector(
|
|
340
|
-
`g[data-schematic-component-id="${component.schematic_component_id}"]`,
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
if (!componentElement) {
|
|
344
|
-
componentElement = svg.querySelector(
|
|
345
|
-
`[data-schematic-component-id="${component.schematic_component_id}"]`,
|
|
346
|
-
)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (componentElement) {
|
|
350
|
-
const bbox = (componentElement as SVGGraphicsElement).getBBox()
|
|
351
|
-
minX = Math.min(minX, bbox.x)
|
|
352
|
-
minY = Math.min(minY, bbox.y)
|
|
353
|
-
maxX = Math.max(maxX, bbox.x + bbox.width)
|
|
354
|
-
maxY = Math.max(maxY, bbox.y + bbox.height)
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (minX === Infinity) {
|
|
359
|
-
return null
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const bounds = { minX, minY, maxX, maxY }
|
|
363
|
-
return bounds
|
|
364
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from "react"
|
|
2
|
-
import type * as EecircuitEngine from "../types/eecircuit-engine"
|
|
3
|
-
// @ts-ignore
|
|
4
|
-
import { getSpiceSimulationWorkerBlobUrl } from "../workers/spice-simulation.worker.blob.js"
|
|
5
|
-
|
|
6
|
-
// Types from eecircuit-engine interface
|
|
7
|
-
type RealDataType = {
|
|
8
|
-
name: string
|
|
9
|
-
type: string
|
|
10
|
-
values: number[]
|
|
11
|
-
}
|
|
12
|
-
type ComplexNumber = {
|
|
13
|
-
real: number
|
|
14
|
-
img: number
|
|
15
|
-
}
|
|
16
|
-
type ComplexDataType = {
|
|
17
|
-
name: string
|
|
18
|
-
type: string
|
|
19
|
-
values: ComplexNumber[]
|
|
20
|
-
}
|
|
21
|
-
type EecEngineResult =
|
|
22
|
-
| {
|
|
23
|
-
header: string
|
|
24
|
-
numVariables: number
|
|
25
|
-
variableNames: string[]
|
|
26
|
-
numPoints: number
|
|
27
|
-
dataType: "real"
|
|
28
|
-
data: RealDataType[]
|
|
29
|
-
}
|
|
30
|
-
| {
|
|
31
|
-
header: string
|
|
32
|
-
numVariables: number
|
|
33
|
-
variableNames: string[]
|
|
34
|
-
numPoints: number
|
|
35
|
-
dataType: "complex"
|
|
36
|
-
data: ComplexDataType[]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface PlotPoint {
|
|
40
|
-
name: string // time or sweep variable
|
|
41
|
-
[key: string]: number | string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const parseEecEngineOutput = (
|
|
45
|
-
result: EecEngineResult,
|
|
46
|
-
): { plotData: PlotPoint[]; nodes: string[] } => {
|
|
47
|
-
const columnData: Record<string, number[]> = {}
|
|
48
|
-
|
|
49
|
-
if (result.dataType === "real") {
|
|
50
|
-
result.data.forEach((col) => {
|
|
51
|
-
columnData[col.name] = col.values
|
|
52
|
-
})
|
|
53
|
-
} else if (result.dataType === "complex") {
|
|
54
|
-
result.data.forEach((col) => {
|
|
55
|
-
// For now, plot the real part of complex numbers
|
|
56
|
-
columnData[col.name] = col.values.map((v) => v.real)
|
|
57
|
-
})
|
|
58
|
-
} else {
|
|
59
|
-
throw new Error("Unsupported data type in simulation result")
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const timeKey = Object.keys(columnData).find(
|
|
63
|
-
(k) => k.toLowerCase() === "time" || k.toLowerCase() === "frequency",
|
|
64
|
-
)
|
|
65
|
-
if (!timeKey) {
|
|
66
|
-
throw new Error("No time or frequency data in simulation result")
|
|
67
|
-
}
|
|
68
|
-
const timeValues = columnData[timeKey]
|
|
69
|
-
const probedVariables = Object.keys(columnData).filter((k) => k !== timeKey)
|
|
70
|
-
const plotableNodes = probedVariables
|
|
71
|
-
|
|
72
|
-
const plotData: PlotPoint[] = timeValues.map((t: number, i: number) => {
|
|
73
|
-
const point: PlotPoint = { name: t.toExponential(2) }
|
|
74
|
-
probedVariables.forEach((variable) => {
|
|
75
|
-
point[variable] = columnData[variable][i]
|
|
76
|
-
})
|
|
77
|
-
return point
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
return { plotData, nodes: plotableNodes }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
type WorkerMessage =
|
|
84
|
-
| {
|
|
85
|
-
type: "result"
|
|
86
|
-
result: EecEngineResult
|
|
87
|
-
}
|
|
88
|
-
| { type: "error"; error: string }
|
|
89
|
-
|
|
90
|
-
export const useSpiceSimulation = (spiceString: string | null) => {
|
|
91
|
-
const [plotData, setPlotData] = useState<PlotPoint[]>([])
|
|
92
|
-
const [nodes, setNodes] = useState<string[]>([])
|
|
93
|
-
const [isLoading, setIsLoading] = useState(true)
|
|
94
|
-
const [error, setError] = useState<string | null>(null)
|
|
95
|
-
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
if (!spiceString) {
|
|
98
|
-
setIsLoading(false)
|
|
99
|
-
setPlotData([])
|
|
100
|
-
setNodes([])
|
|
101
|
-
setError(null)
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
setIsLoading(true)
|
|
105
|
-
setError(null)
|
|
106
|
-
setPlotData([])
|
|
107
|
-
setNodes([])
|
|
108
|
-
|
|
109
|
-
const workerUrl = getSpiceSimulationWorkerBlobUrl()
|
|
110
|
-
|
|
111
|
-
if (!workerUrl) {
|
|
112
|
-
setError("Could not create SPICE simulation worker.")
|
|
113
|
-
setIsLoading(false)
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const worker = new Worker(workerUrl, { type: "module" })
|
|
118
|
-
|
|
119
|
-
worker.onmessage = (event: MessageEvent<WorkerMessage>) => {
|
|
120
|
-
if (event.data.type === "result") {
|
|
121
|
-
try {
|
|
122
|
-
const { plotData: parsedData, nodes: parsedNodes } =
|
|
123
|
-
parseEecEngineOutput(event.data.result)
|
|
124
|
-
setPlotData(parsedData)
|
|
125
|
-
setNodes(parsedNodes)
|
|
126
|
-
} catch (e: any) {
|
|
127
|
-
setError(e.message || "Failed to parse simulation result")
|
|
128
|
-
console.error(e)
|
|
129
|
-
}
|
|
130
|
-
} else if (event.data.type === "error") {
|
|
131
|
-
setError(event.data.error)
|
|
132
|
-
}
|
|
133
|
-
setIsLoading(false)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
worker.onerror = (err) => {
|
|
137
|
-
setError(err.message)
|
|
138
|
-
setIsLoading(false)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
worker.postMessage({ spiceString })
|
|
142
|
-
|
|
143
|
-
return () => {
|
|
144
|
-
worker.terminate()
|
|
145
|
-
}
|
|
146
|
-
}, [spiceString])
|
|
147
|
-
|
|
148
|
-
return { plotData, nodes, isLoading, error }
|
|
149
|
-
}
|
package/lib/index.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export { SchematicViewer } from "./components/SchematicViewer"
|
|
2
|
-
export { MouseTracker } from "./components/MouseTracker"
|
|
3
|
-
export { useMouseEventsOverBoundingBox } from "./hooks/useMouseEventsOverBoundingBox"
|
|
4
|
-
export { AnalogSimulationViewer } from "./components/AnalogSimulationViewer"
|
package/lib/types/edit-events.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BaseManualEditEvent,
|
|
3
|
-
EditSchematicComponentLocationEvent,
|
|
4
|
-
ManualEditEvent,
|
|
5
|
-
} from "@tscircuit/props"
|
|
6
|
-
|
|
7
|
-
export type EditSchematicComponentLocationEventWithElement =
|
|
8
|
-
EditSchematicComponentLocationEvent & {
|
|
9
|
-
_element: SVGElement
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type {
|
|
13
|
-
BaseManualEditEvent,
|
|
14
|
-
EditSchematicComponentLocationEvent,
|
|
15
|
-
ManualEditEvent,
|
|
16
|
-
}
|