@tscircuit/schematic-viewer 2.0.27 → 2.0.29

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.
@@ -2,12 +2,31 @@ import { useEffect } from "react"
2
2
  import { su } from "@tscircuit/soup-util"
3
3
  import type { CircuitJson } from "circuit-json"
4
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
+
5
25
  export const useSchematicGroupsOverlay = (
6
- svgDivRef: React.RefObject<HTMLDivElement | null>,
7
- circuitJson: CircuitJson,
8
- circuitJsonKey: string,
9
- showGroups: boolean,
26
+ options: UseSchematicGroupsOverlayOptions,
10
27
  ) => {
28
+ const { svgDivRef, circuitJson, circuitJsonKey, showGroups } = options
29
+
11
30
  useEffect(() => {
12
31
  if (
13
32
  !svgDivRef.current ||
@@ -15,7 +34,6 @@ export const useSchematicGroupsOverlay = (
15
34
  !circuitJson ||
16
35
  circuitJson.length === 0
17
36
  ) {
18
- // Remove any existing group overlays
19
37
  if (svgDivRef.current) {
20
38
  const existingOverlays = svgDivRef.current.querySelectorAll(
21
39
  ".schematic-group-overlay",
@@ -30,29 +48,69 @@ export const useSchematicGroupsOverlay = (
30
48
  return
31
49
  }
32
50
 
33
- // Remove existing overlays first
34
51
  const existingOverlays = svg.querySelectorAll(".schematic-group-overlay")
35
52
  existingOverlays.forEach((overlay) => overlay.remove())
36
53
 
37
54
  try {
38
- // Get explicit groups first
39
- const sourceGroups = su(circuitJson).source_group?.list() || []
55
+ const sourceGroups =
56
+ su(circuitJson)
57
+ .source_group?.list()
58
+ .filter((x) => !!!x.is_subcircuit) || []
40
59
  const schematicComponents =
41
60
  su(circuitJson).schematic_component?.list() || []
42
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
87
+ }
88
+
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
95
+ }
96
+ return 1 + getGroupDepthLevel(groupWithParent.parent_source_group_id)
97
+ }
98
+
99
+ const hasMeaningfulGroups =
100
+ sourceGroups.length > 0 &&
101
+ sourceGroups.some((group) => group.name && group.name.trim() !== "")
102
+
43
103
  let groupsToRender: Array<{
44
104
  id: string
45
105
  name: string
46
106
  components: any[]
47
107
  color: string
108
+ depthLevel: number
109
+ hasChildren: boolean
110
+ sourceGroupId?: string
48
111
  }> = []
49
112
 
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
113
  if (hasMeaningfulGroups) {
55
- // Use explicit groups
56
114
  const groupMap = new Map<string, any[]>()
57
115
 
58
116
  for (const comp of schematicComponents) {
@@ -67,97 +125,186 @@ export const useSchematicGroupsOverlay = (
67
125
  }
68
126
  }
69
127
 
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[]>()
128
+ sourceGroups.forEach((group, index) => {
129
+ let groupComponents = groupMap.get(group.source_group_id) || []
86
130
 
87
- for (const comp of schematicComponents) {
88
- const sourceComp = su(circuitJson).source_component.get(
89
- comp.source_component_id,
131
+ const descendantGroups = getAllDescendantSourceGroups(
132
+ group.source_group_id,
90
133
  )
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)
134
+ for (const descendantGroupId of descendantGroups) {
135
+ const descendantComponents = groupMap.get(descendantGroupId) || []
136
+ groupComponents = [...groupComponents, ...descendantComponents]
97
137
  }
98
- }
99
138
 
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
- )
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
+ }
155
+ })
108
156
  }
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
109
189
 
110
- // Render group overlays
111
- groupsToRender.forEach((group, groupIndex) => {
190
+ groupsToRender.sort((a, b) => a.depthLevel - b.depthLevel)
191
+
192
+ groupsToRender.forEach((group) => {
112
193
  if (group.components.length === 0) return
113
194
 
114
- // Calculate bounding box for the group
115
195
  const groupBounds = calculateGroupBounds(group.components, svg)
116
196
  if (!groupBounds) return
117
197
 
118
- // Create group overlay rectangle
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
+
119
211
  const groupOverlay = document.createElementNS(
120
212
  "http://www.w3.org/2000/svg",
121
213
  "rect",
122
214
  )
123
215
  groupOverlay.setAttribute("class", "schematic-group-overlay")
124
- groupOverlay.setAttribute("x", (groupBounds.minX - 25).toString())
125
- groupOverlay.setAttribute("y", (groupBounds.minY - 25).toString())
216
+ groupOverlay.setAttribute(
217
+ "x",
218
+ (groupBounds.minX - totalPadding).toString(),
219
+ )
220
+ groupOverlay.setAttribute(
221
+ "y",
222
+ (groupBounds.minY - totalPadding).toString(),
223
+ )
126
224
  groupOverlay.setAttribute(
127
225
  "width",
128
- (groupBounds.maxX - groupBounds.minX + 50).toString(),
226
+ (groupBounds.maxX - groupBounds.minX + totalPadding * 2).toString(),
129
227
  )
130
228
  groupOverlay.setAttribute(
131
229
  "height",
132
- (groupBounds.maxY - groupBounds.minY + 50).toString(),
230
+ (groupBounds.maxY - groupBounds.minY + totalPadding * 2).toString(),
133
231
  )
134
232
  groupOverlay.setAttribute("fill", "none")
135
233
  groupOverlay.setAttribute("stroke", group.color)
136
- groupOverlay.setAttribute("stroke-width", "3")
137
- groupOverlay.setAttribute("stroke-dasharray", "8,4")
234
+ groupOverlay.setAttribute("stroke-width", strokeWidth.toString())
235
+ groupOverlay.setAttribute("stroke-dasharray", `${dashSize},${gapSize}`)
138
236
  groupOverlay.setAttribute("opacity", "0.8")
139
237
  groupOverlay.setAttribute("rx", "4")
140
238
  groupOverlay.setAttribute("ry", "4")
141
239
 
142
- // Create group label
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
+
143
282
  const groupLabel = document.createElementNS(
144
283
  "http://www.w3.org/2000/svg",
145
284
  "text",
146
285
  )
147
286
  groupLabel.setAttribute("class", "schematic-group-overlay")
148
- groupLabel.setAttribute("x", (groupBounds.minX - 10).toString())
149
- groupLabel.setAttribute("y", (groupBounds.minY - 8).toString())
287
+ groupLabel.setAttribute("x", (labelX + labelPadding).toString())
288
+ groupLabel.setAttribute(
289
+ "y",
290
+ (labelY + labelHeight - labelPadding).toString(),
291
+ )
150
292
  groupLabel.setAttribute("fill", group.color)
151
- groupLabel.setAttribute("font-size", "14")
293
+ groupLabel.setAttribute("font-size", fontSize.toString())
152
294
  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
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
158
305
 
159
- // Add overlays to the SVG (use appendChild to ensure they're on top)
160
306
  svg.appendChild(groupOverlay)
307
+ svg.appendChild(labelBg)
161
308
  svg.appendChild(groupLabel)
162
309
  })
163
310
  } catch (error) {
@@ -166,20 +313,6 @@ export const useSchematicGroupsOverlay = (
166
313
  }, [svgDivRef, circuitJsonKey, showGroups])
167
314
  }
168
315
 
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
316
  function calculateGroupBounds(components: any[], svg: SVGElement) {
184
317
  let minX = Infinity,
185
318
  minY = Infinity,
@@ -187,18 +320,16 @@ function calculateGroupBounds(components: any[], svg: SVGElement) {
187
320
  maxY = -Infinity
188
321
 
189
322
  for (const component of components) {
190
- // Look for the component group element (based on circuit-to-svg documentation)
191
323
  let componentElement = svg.querySelector(
192
324
  `g[data-schematic-component-id="${component.schematic_component_id}"]`,
193
325
  )
194
-
326
+
195
327
  if (!componentElement) {
196
- // Fallback to any element with the data attribute
197
328
  componentElement = svg.querySelector(
198
329
  `[data-schematic-component-id="${component.schematic_component_id}"]`,
199
330
  )
200
331
  }
201
-
332
+
202
333
  if (componentElement) {
203
334
  const bbox = (componentElement as SVGGraphicsElement).getBBox()
204
335
  minX = Math.min(minX, bbox.x)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-viewer",
3
- "version": "2.0.27",
3
+ "version": "2.0.29",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {