@tscircuit/schematic-viewer 2.0.28 → 2.0.30
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 +157 -72
- package/dist/index.js.map +1 -1
- package/examples/example10-groups-view-schematic-groups.fixture.tsx +49 -63
- package/examples/example11-automatic-grouping-view-schematic-groups.fixture.tsx +63 -53
- package/lib/components/SchematicViewer.tsx +1 -1
- package/lib/hooks/useSchematicGroupsOverlay.ts +207 -82
- package/package.json +1 -1
|
@@ -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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
comp.source_component_id,
|
|
131
|
+
const descendantGroups = getAllDescendantSourceGroups(
|
|
132
|
+
group.source_group_id,
|
|
96
133
|
)
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
131
|
-
|
|
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 +
|
|
226
|
+
(groupBounds.maxX - groupBounds.minX + totalPadding * 2).toString(),
|
|
135
227
|
)
|
|
136
228
|
groupOverlay.setAttribute(
|
|
137
229
|
"height",
|
|
138
|
-
(groupBounds.maxY - groupBounds.minY +
|
|
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",
|
|
143
|
-
groupOverlay.setAttribute("stroke-dasharray",
|
|
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
|
-
|
|
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", (
|
|
155
|
-
groupLabel.setAttribute(
|
|
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",
|
|
293
|
+
groupLabel.setAttribute("font-size", fontSize.toString())
|
|
158
294
|
groupLabel.setAttribute("font-family", "Arial, sans-serif")
|
|
159
|
-
groupLabel.setAttribute(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
groupLabel.
|
|
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)
|