@tscircuit/schematic-viewer 2.0.37 → 2.0.38

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.
@@ -28,288 +28,304 @@ export const useSchematicGroupsOverlay = (
28
28
  const { svgDivRef, circuitJson, circuitJsonKey, showGroups } = options
29
29
 
30
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
+
31
39
  if (
32
40
  !svgDivRef.current ||
33
41
  !showGroups ||
34
42
  !circuitJson ||
35
43
  circuitJson.length === 0
36
44
  ) {
37
- if (svgDivRef.current) {
38
- const existingOverlays = svgDivRef.current.querySelectorAll(
39
- ".schematic-group-overlay",
40
- )
41
- existingOverlays.forEach((overlay) => overlay.remove())
42
- }
43
45
  return
44
46
  }
45
47
 
46
- const svg = svgDivRef.current.querySelector("svg")
47
- if (!svg) {
48
- return
49
- }
48
+ // Small delay to ensure SVG is rendered when groups are enabled from localStorage
49
+ const timeoutId = setTimeout(() => {
50
+ if (!svgDivRef.current) return
50
51
 
51
- const existingOverlays = svg.querySelectorAll(".schematic-group-overlay")
52
- existingOverlays.forEach((overlay) => overlay.remove())
53
-
54
- try {
55
- const sourceGroups =
56
- su(circuitJson)
57
- .source_group?.list()
58
- .filter((x) => !!!x.is_subcircuit) || []
59
- const schematicComponents =
60
- su(circuitJson).schematic_component?.list() || []
61
-
62
- const sourceGroupHierarchy = new Map<string, string[]>()
63
- sourceGroups.forEach((group) => {
64
- const groupWithParent = group as any
65
- if (groupWithParent.parent_source_group_id) {
66
- const children =
67
- sourceGroupHierarchy.get(groupWithParent.parent_source_group_id) ||
68
- []
69
- children.push(group.source_group_id)
70
- sourceGroupHierarchy.set(
71
- groupWithParent.parent_source_group_id,
72
- children,
73
- )
74
- }
75
- })
76
-
77
- const getAllDescendantSourceGroups = (
78
- sourceGroupId: string,
79
- ): string[] => {
80
- const descendants: string[] = []
81
- const children = sourceGroupHierarchy.get(sourceGroupId) || []
82
- for (const child of children) {
83
- descendants.push(child)
84
- descendants.push(...getAllDescendantSourceGroups(child))
85
- }
86
- return descendants
52
+ const svg = svgDivRef.current.querySelector("svg")
53
+ if (!svg) {
54
+ return
87
55
  }
88
56
 
89
- const getGroupDepthLevel = (sourceGroupId: string): number => {
90
- const groupWithParent = sourceGroups.find(
91
- (g) => g.source_group_id === sourceGroupId,
92
- ) as any
93
- if (!groupWithParent?.parent_source_group_id) {
94
- return 0
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
95
94
  }
96
- return 1 + getGroupDepthLevel(groupWithParent.parent_source_group_id)
97
- }
98
95
 
99
- const hasMeaningfulGroups =
100
- sourceGroups.length > 0 &&
101
- sourceGroups.some((group) => group.name && group.name.trim() !== "")
102
-
103
- let groupsToRender: Array<{
104
- id: string
105
- name: string
106
- components: any[]
107
- color: string
108
- depthLevel: number
109
- hasChildren: boolean
110
- sourceGroupId?: string
111
- }> = []
112
-
113
- if (hasMeaningfulGroups) {
114
- const groupMap = new Map<string, any[]>()
115
-
116
- for (const comp of schematicComponents) {
117
- const sourceComp = su(circuitJson).source_component.get(
118
- comp.source_component_id,
119
- )
120
- if (sourceComp?.source_group_id) {
121
- if (!groupMap.has(sourceComp.source_group_id)) {
122
- groupMap.set(sourceComp.source_group_id, [])
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)
123
132
  }
124
- groupMap.get(sourceComp.source_group_id)!.push(comp)
125
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
+ })
126
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
127
211
 
128
- sourceGroups.forEach((group, index) => {
129
- let groupComponents = groupMap.get(group.source_group_id) || []
212
+ const baseStrokeWidth = Math.max(1, 2 / Math.max(scale, 0.5))
213
+ const strokeWidth =
214
+ group.depthLevel === 0 ? baseStrokeWidth : baseStrokeWidth * 0.7
130
215
 
131
- const descendantGroups = getAllDescendantSourceGroups(
132
- group.source_group_id,
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",
133
224
  )
134
- for (const descendantGroupId of descendantGroups) {
135
- const descendantComponents = groupMap.get(descendantGroupId) || []
136
- groupComponents = [...groupComponents, ...descendantComponents]
137
- }
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", "4")
251
+ groupOverlay.setAttribute("ry", "4")
138
252
 
139
- if (groupComponents.length > 0) {
140
- const depthLevel = getGroupDepthLevel(group.source_group_id)
141
- const hasChildren =
142
- getAllDescendantSourceGroups(group.source_group_id).length > 0
143
-
144
- if (group.name?.startsWith("unnamed_board")) return
145
- groupsToRender.push({
146
- id: group.source_group_id,
147
- name: group.name || `Group ${index + 1}`,
148
- components: groupComponents,
149
- color: GROUP_COLORS[index % GROUP_COLORS.length],
150
- depthLevel,
151
- hasChildren,
152
- sourceGroupId: group.source_group_id,
153
- })
154
- }
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", "3")
293
+ labelBg.setAttribute("ry", "3")
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)
155
322
  })
323
+ } catch (error) {
324
+ console.error("Error creating group overlays:", error)
156
325
  }
157
- // else {
158
- // const componentTypeGroups = new Map<string, any[]>()
159
-
160
- // for (const comp of schematicComponents) {
161
- // const sourceComp = su(circuitJson).source_component.get(comp.source_component_id)
162
- // if (sourceComp) {
163
- // const componentType = sourceComp.ftype || "other"
164
- // if (!componentTypeGroups.has(componentType)) {
165
- // componentTypeGroups.set(componentType, [])
166
- // }
167
- // componentTypeGroups.get(componentType)!.push(comp)
168
- // }
169
- // }
170
- // // groupsToRender = Array.from(componentTypeGroups.entries()).map(
171
- // // ([type, components], index) => ({
172
- // // id: `type_${type}`,
173
- // // name: `${type.charAt(0).toUpperCase() + type.slice(1)}s`,
174
- // // components,
175
- // // color: GROUP_COLORS[index % GROUP_COLORS.length],
176
- // // depthLevel: 0,
177
- // // hasChildren: false,
178
- // // }),
179
- // // )
180
- // }
181
-
182
- const viewBox = svg.viewBox.baseVal
183
- const svgRect = svg.getBoundingClientRect()
184
- const scale =
185
- Math.min(
186
- svgRect.width / viewBox.width,
187
- svgRect.height / viewBox.height,
188
- ) || 1
189
-
190
- groupsToRender.sort((a, b) => a.depthLevel - b.depthLevel)
191
-
192
- groupsToRender.forEach((group) => {
193
- if (group.components.length === 0) return
194
-
195
- const groupBounds = calculateGroupBounds(group.components, svg)
196
- if (!groupBounds) return
197
-
198
- const basePadding = Math.max(8, Math.min(25, 15 / Math.max(scale, 0.3)))
199
- const hierarchyPadding = group.hasChildren ? basePadding * 0.6 : 0
200
- const totalPadding = basePadding + hierarchyPadding
201
-
202
- const baseStrokeWidth = Math.max(1, 2 / Math.max(scale, 0.5))
203
- const strokeWidth =
204
- group.depthLevel === 0 ? baseStrokeWidth : baseStrokeWidth * 0.7
205
-
206
- const baseDashSize = Math.max(4, 8 / Math.max(scale, 0.5))
207
- const dashMultiplier = group.hasChildren ? 1.3 : 1
208
- const dashSize = baseDashSize * dashMultiplier
209
- const gapSize = dashSize * 0.5
210
-
211
- const groupOverlay = document.createElementNS(
212
- "http://www.w3.org/2000/svg",
213
- "rect",
214
- )
215
- groupOverlay.setAttribute("class", "schematic-group-overlay")
216
- groupOverlay.setAttribute(
217
- "x",
218
- (groupBounds.minX - totalPadding).toString(),
219
- )
220
- groupOverlay.setAttribute(
221
- "y",
222
- (groupBounds.minY - totalPadding).toString(),
223
- )
224
- groupOverlay.setAttribute(
225
- "width",
226
- (groupBounds.maxX - groupBounds.minX + totalPadding * 2).toString(),
227
- )
228
- groupOverlay.setAttribute(
229
- "height",
230
- (groupBounds.maxY - groupBounds.minY + totalPadding * 2).toString(),
231
- )
232
- groupOverlay.setAttribute("fill", "none")
233
- groupOverlay.setAttribute("stroke", group.color)
234
- groupOverlay.setAttribute("stroke-width", strokeWidth.toString())
235
- groupOverlay.setAttribute("stroke-dasharray", `${dashSize},${gapSize}`)
236
- groupOverlay.setAttribute("opacity", "0.8")
237
- groupOverlay.setAttribute("rx", "4")
238
- groupOverlay.setAttribute("ry", "4")
239
-
240
- const baseFontSize = Math.max(
241
- 6,
242
- Math.min(20, 14 / Math.max(scale, 0.2)),
243
- )
244
- const fontSizeReduction =
245
- group.depthLevel === 0 || group.depthLevel === 1
246
- ? 0
247
- : group.depthLevel * 0.2
248
- const fontSize = baseFontSize * (1 - fontSizeReduction)
249
-
250
- const labelPadding = Math.max(1, fontSize * 0.2)
251
- const labelText = group.name
252
-
253
- const tempText = document.createElementNS(
254
- "http://www.w3.org/2000/svg",
255
- "text",
256
- )
257
- tempText.setAttribute("font-size", fontSize.toString())
258
- tempText.setAttribute("font-family", "Arial, sans-serif")
259
- tempText.textContent = labelText
260
- svg.appendChild(tempText)
261
- const textBBox = tempText.getBBox()
262
- svg.removeChild(tempText)
263
-
264
- const labelWidth = textBBox.width + labelPadding * 2
265
- const labelHeight = fontSize + labelPadding * 2
266
- const labelX = groupBounds.minX - totalPadding
267
- const labelY = groupBounds.minY - totalPadding - labelHeight
268
-
269
- const labelBg = document.createElementNS(
270
- "http://www.w3.org/2000/svg",
271
- "rect",
272
- )
273
- labelBg.setAttribute("class", "schematic-group-overlay")
274
- labelBg.setAttribute("x", labelX.toString())
275
- labelBg.setAttribute("y", (labelY - labelHeight).toString())
276
- labelBg.setAttribute("width", labelWidth.toString())
277
- labelBg.setAttribute("height", labelHeight.toString())
278
- labelBg.setAttribute("fill", "transparent")
279
- labelBg.setAttribute("rx", "3")
280
- labelBg.setAttribute("ry", "3")
281
-
282
- const groupLabel = document.createElementNS(
283
- "http://www.w3.org/2000/svg",
284
- "text",
285
- )
286
- groupLabel.setAttribute("class", "schematic-group-overlay")
287
- groupLabel.setAttribute("x", (labelX + labelPadding).toString())
288
- groupLabel.setAttribute(
289
- "y",
290
- (labelY + labelHeight - labelPadding).toString(),
291
- )
292
- groupLabel.setAttribute("fill", group.color)
293
- groupLabel.setAttribute("font-size", fontSize.toString())
294
- groupLabel.setAttribute("font-family", "Arial, sans-serif")
295
- groupLabel.setAttribute(
296
- "font-weight",
297
- group.depthLevel === 0 ? "600" : "500",
298
- )
299
- groupLabel.setAttribute("stroke", group.color)
300
- groupLabel.setAttribute(
301
- "stroke-width",
302
- Math.max(0.2, fontSize * 0.02).toString(),
303
- )
304
- groupLabel.textContent = labelText
305
-
306
- svg.appendChild(groupOverlay)
307
- svg.appendChild(labelBg)
308
- svg.appendChild(groupLabel)
309
- })
310
- } catch (error) {
311
- console.error("Error creating group overlays:", error)
312
- }
326
+ }, 10) // Small delay to ensure SVG is ready
327
+
328
+ return () => clearTimeout(timeoutId)
313
329
  }, [svgDivRef, circuitJsonKey, showGroups])
314
330
  }
315
331
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-viewer",
3
- "version": "2.0.37",
3
+ "version": "2.0.38",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {