@tscircuit/schematic-viewer 2.0.28 → 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.
@@ -9,11 +9,24 @@ interface UseSchematicGroupsOverlayOptions {
9
9
  showGroups: boolean
10
10
  }
11
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
+
12
25
  export const useSchematicGroupsOverlay = (
13
26
  options: UseSchematicGroupsOverlayOptions,
14
27
  ) => {
15
28
  const { svgDivRef, circuitJson, circuitJsonKey, showGroups } = options
16
-
29
+
17
30
  useEffect(() => {
18
31
  if (
19
32
  !svgDivRef.current ||
@@ -21,7 +34,6 @@ export const useSchematicGroupsOverlay = (
21
34
  !circuitJson ||
22
35
  circuitJson.length === 0
23
36
  ) {
24
- // Remove any existing group overlays
25
37
  if (svgDivRef.current) {
26
38
  const existingOverlays = svgDivRef.current.querySelectorAll(
27
39
  ".schematic-group-overlay",
@@ -36,29 +48,69 @@ export const useSchematicGroupsOverlay = (
36
48
  return
37
49
  }
38
50
 
39
- // Remove existing overlays first
40
51
  const existingOverlays = svg.querySelectorAll(".schematic-group-overlay")
41
52
  existingOverlays.forEach((overlay) => overlay.remove())
42
53
 
43
54
  try {
44
- // Get explicit groups first
45
- const sourceGroups = su(circuitJson).source_group?.list() || []
55
+ const sourceGroups =
56
+ su(circuitJson)
57
+ .source_group?.list()
58
+ .filter((x) => !!!x.is_subcircuit) || []
46
59
  const schematicComponents =
47
60
  su(circuitJson).schematic_component?.list() || []
48
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
+
49
103
  let groupsToRender: Array<{
50
104
  id: string
51
105
  name: string
52
106
  components: any[]
53
107
  color: string
108
+ depthLevel: number
109
+ hasChildren: boolean
110
+ sourceGroupId?: string
54
111
  }> = []
55
112
 
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
113
  if (hasMeaningfulGroups) {
61
- // Use explicit groups
62
114
  const groupMap = new Map<string, any[]>()
63
115
 
64
116
  for (const comp of schematicComponents) {
@@ -73,97 +125,186 @@ export const useSchematicGroupsOverlay = (
73
125
  }
74
126
  }
75
127
 
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[]>()
128
+ sourceGroups.forEach((group, index) => {
129
+ let groupComponents = groupMap.get(group.source_group_id) || []
92
130
 
93
- for (const comp of schematicComponents) {
94
- const sourceComp = su(circuitJson).source_component.get(
95
- comp.source_component_id,
131
+ const descendantGroups = getAllDescendantSourceGroups(
132
+ group.source_group_id,
96
133
  )
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)
134
+ for (const descendantGroupId of descendantGroups) {
135
+ const descendantComponents = groupMap.get(descendantGroupId) || []
136
+ groupComponents = [...groupComponents, ...descendantComponents]
103
137
  }
104
- }
105
138
 
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
- )
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
+ })
114
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
189
+
190
+ groupsToRender.sort((a, b) => a.depthLevel - b.depthLevel)
115
191
 
116
- // Render group overlays
117
- groupsToRender.forEach((group, groupIndex) => {
192
+ groupsToRender.forEach((group) => {
118
193
  if (group.components.length === 0) return
119
194
 
120
- // Calculate bounding box for the group
121
195
  const groupBounds = calculateGroupBounds(group.components, svg)
122
196
  if (!groupBounds) return
123
197
 
124
- // 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
+
125
211
  const groupOverlay = document.createElementNS(
126
212
  "http://www.w3.org/2000/svg",
127
213
  "rect",
128
214
  )
129
215
  groupOverlay.setAttribute("class", "schematic-group-overlay")
130
- groupOverlay.setAttribute("x", (groupBounds.minX - 25).toString())
131
- 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
+ )
132
224
  groupOverlay.setAttribute(
133
225
  "width",
134
- (groupBounds.maxX - groupBounds.minX + 50).toString(),
226
+ (groupBounds.maxX - groupBounds.minX + totalPadding * 2).toString(),
135
227
  )
136
228
  groupOverlay.setAttribute(
137
229
  "height",
138
- (groupBounds.maxY - groupBounds.minY + 50).toString(),
230
+ (groupBounds.maxY - groupBounds.minY + totalPadding * 2).toString(),
139
231
  )
140
232
  groupOverlay.setAttribute("fill", "none")
141
233
  groupOverlay.setAttribute("stroke", group.color)
142
- groupOverlay.setAttribute("stroke-width", "3")
143
- groupOverlay.setAttribute("stroke-dasharray", "8,4")
234
+ groupOverlay.setAttribute("stroke-width", strokeWidth.toString())
235
+ groupOverlay.setAttribute("stroke-dasharray", `${dashSize},${gapSize}`)
144
236
  groupOverlay.setAttribute("opacity", "0.8")
145
237
  groupOverlay.setAttribute("rx", "4")
146
238
  groupOverlay.setAttribute("ry", "4")
147
239
 
148
- // 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
+
149
282
  const groupLabel = document.createElementNS(
150
283
  "http://www.w3.org/2000/svg",
151
284
  "text",
152
285
  )
153
286
  groupLabel.setAttribute("class", "schematic-group-overlay")
154
- groupLabel.setAttribute("x", (groupBounds.minX - 10).toString())
155
- 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
+ )
156
292
  groupLabel.setAttribute("fill", group.color)
157
- groupLabel.setAttribute("font-size", "14")
293
+ groupLabel.setAttribute("font-size", fontSize.toString())
158
294
  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
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
164
305
 
165
- // Add overlays to the SVG (use appendChild to ensure they're on top)
166
306
  svg.appendChild(groupOverlay)
307
+ svg.appendChild(labelBg)
167
308
  svg.appendChild(groupLabel)
168
309
  })
169
310
  } catch (error) {
@@ -172,20 +313,6 @@ export const useSchematicGroupsOverlay = (
172
313
  }, [svgDivRef, circuitJsonKey, showGroups])
173
314
  }
174
315
 
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
316
  function calculateGroupBounds(components: any[], svg: SVGElement) {
190
317
  let minX = Infinity,
191
318
  minY = Infinity,
@@ -193,18 +320,16 @@ function calculateGroupBounds(components: any[], svg: SVGElement) {
193
320
  maxY = -Infinity
194
321
 
195
322
  for (const component of components) {
196
- // Look for the component group element (based on circuit-to-svg documentation)
197
323
  let componentElement = svg.querySelector(
198
324
  `g[data-schematic-component-id="${component.schematic_component_id}"]`,
199
325
  )
200
-
326
+
201
327
  if (!componentElement) {
202
- // Fallback to any element with the data attribute
203
328
  componentElement = svg.querySelector(
204
329
  `[data-schematic-component-id="${component.schematic_component_id}"]`,
205
330
  )
206
331
  }
207
-
332
+
208
333
  if (componentElement) {
209
334
  const bbox = (componentElement as SVGGraphicsElement).getBBox()
210
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.28",
3
+ "version": "2.0.29",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {