@tldiagram/core-ui 1.92.0 → 1.94.0
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/api/client.d.ts +13 -1
- package/dist/components/ElementNode.d.ts +14 -1
- package/dist/components/ZUI/ZUICanvas.d.ts +1 -0
- package/dist/config/runtime-vscode.d.ts +1 -0
- package/dist/config/runtime.d.ts +1 -0
- package/dist/index.js +10875 -9550
- package/dist/pages/InfiniteZoom.d.ts +5 -2
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +10 -3
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.test.d.ts +1 -0
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +27 -24
- package/dist/pages/ViewsGrid.d.ts +9 -1
- package/dist/shims/empty-node-module.d.ts +2 -0
- package/dist/store/useStore.d.ts +80 -0
- package/dist/store/useStore.test.d.ts +1 -0
- package/package.json +10 -7
- package/src/api/client.ts +39 -1
- package/src/components/ElementNode.tsx +21 -59
- package/src/components/ElementPanel.tsx +2 -3
- package/src/components/LayoutSection.tsx +95 -104
- package/src/components/ViewGridNode.tsx +1 -4
- package/src/components/ZUI/ZUICanvas.tsx +138 -1
- package/src/components/ZUI/renderer.ts +166 -66
- package/src/components/ZUI/useZUIInteraction.ts +235 -81
- package/src/config/runtime-vscode.ts +6 -0
- package/src/config/runtime.ts +4 -0
- package/src/main.tsx +26 -14
- package/src/pages/InfiniteZoom.tsx +14 -5
- package/src/pages/ViewEditor/context.tsx +14 -3
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.test.ts +30 -0
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +294 -146
- package/src/pages/ViewEditor/hooks/useViewData.ts +459 -256
- package/src/pages/ViewEditor/index.tsx +67 -70
- package/src/pages/Views.tsx +552 -83
- package/src/pages/ViewsGrid.tsx +26 -337
- package/src/shims/empty-node-module.ts +1 -0
- package/src/store/useStore.test.ts +285 -0
- package/src/store/useStore.ts +327 -0
|
@@ -37,6 +37,7 @@ export interface ScreenRect {
|
|
|
37
37
|
bottom: number
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const frameLabelRects: ScreenRect[] = []
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Returns a world-space font size that, when multiplied by zoom,
|
|
@@ -62,19 +63,61 @@ const TYPE_COLOR_400: Record<string, string> = {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
/** Border color: type .400 at 50% alpha - bold branded tint */
|
|
66
|
+
const typeBorderColorCache = new Map<string, string>()
|
|
65
67
|
function typeBorderColor(type: string, alpha = 0.5): string {
|
|
68
|
+
const cacheKey = `${type}:${alpha}`
|
|
69
|
+
const cached = typeBorderColorCache.get(cacheKey)
|
|
70
|
+
if (cached) return cached
|
|
71
|
+
|
|
66
72
|
const color = TYPE_COLOR_400[type]
|
|
67
73
|
const hex = typeof color === 'string' ? color : '#a0aec0'
|
|
68
74
|
const r = parseInt(hex.slice(1, 3), 16)
|
|
69
75
|
const g = parseInt(hex.slice(3, 5), 16)
|
|
70
76
|
const b = parseInt(hex.slice(5, 7), 16)
|
|
71
|
-
|
|
77
|
+
const rgba = `rgba(${r},${g},${b},${alpha})`
|
|
78
|
+
typeBorderColorCache.set(cacheKey, rgba)
|
|
79
|
+
return rgba
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface RendererThemeVars {
|
|
83
|
+
canvasBg: string
|
|
84
|
+
nodeBg: string
|
|
85
|
+
accent: string
|
|
86
|
+
labelBg: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const themeFallbacks: RendererThemeVars = {
|
|
90
|
+
canvasBg: '#0d121e',
|
|
91
|
+
nodeBg: '#2d3748',
|
|
92
|
+
accent: '#63b3ed',
|
|
93
|
+
labelBg: '#171923',
|
|
72
94
|
}
|
|
73
95
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
let cachedThemeVars: RendererThemeVars = themeFallbacks
|
|
97
|
+
let themeObserverStarted = false
|
|
98
|
+
|
|
99
|
+
function refreshThemeVars(): void {
|
|
100
|
+
if (typeof document === 'undefined') return
|
|
101
|
+
const styles = getComputedStyle(document.documentElement)
|
|
102
|
+
cachedThemeVars = {
|
|
103
|
+
canvasBg: styles.getPropertyValue('--bg-main').trim() || themeFallbacks.canvasBg,
|
|
104
|
+
nodeBg: styles.getPropertyValue('--bg-element').trim() || themeFallbacks.nodeBg,
|
|
105
|
+
accent: styles.getPropertyValue('--accent').trim() || themeFallbacks.accent,
|
|
106
|
+
labelBg: styles.getPropertyValue('--chakra-colors-gray-900').trim() || themeFallbacks.labelBg,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getThemeVars(): RendererThemeVars {
|
|
111
|
+
if (!themeObserverStarted && typeof document !== 'undefined') {
|
|
112
|
+
themeObserverStarted = true
|
|
113
|
+
refreshThemeVars()
|
|
114
|
+
const update = () => refreshThemeVars()
|
|
115
|
+
const mo = new MutationObserver(update)
|
|
116
|
+
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] })
|
|
117
|
+
window.matchMedia?.('(prefers-color-scheme: dark)').addEventListener?.('change', update)
|
|
118
|
+
window.matchMedia?.('(prefers-color-scheme: light)').addEventListener?.('change', update)
|
|
119
|
+
}
|
|
120
|
+
return cachedThemeVars
|
|
78
121
|
}
|
|
79
122
|
|
|
80
123
|
// ── Geometry helpers ───────────────────────────────────────────────
|
|
@@ -133,14 +176,6 @@ function rectsOverlap(a: ScreenRect, b: ScreenRect): boolean {
|
|
|
133
176
|
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top
|
|
134
177
|
}
|
|
135
178
|
|
|
136
|
-
function worldToScreen(matrix: DOMMatrix, x: number, y: number) {
|
|
137
|
-
return new DOMPoint(x, y).matrixTransform(matrix)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function screenToWorld(matrix: DOMMatrix, x: number, y: number) {
|
|
141
|
-
return new DOMPoint(x, y).matrixTransform(matrix.inverse())
|
|
142
|
-
}
|
|
143
|
-
|
|
144
179
|
export function pickEdgeLabelPosition(
|
|
145
180
|
matrix: DOMMatrix,
|
|
146
181
|
midX: number,
|
|
@@ -151,7 +186,8 @@ export function pickEdgeLabelPosition(
|
|
|
151
186
|
dy: number,
|
|
152
187
|
occupiedLabelRects: ScreenRect[],
|
|
153
188
|
) {
|
|
154
|
-
const
|
|
189
|
+
const screenMidX = matrix.a * midX + matrix.c * midY + matrix.e
|
|
190
|
+
const screenMidY = matrix.b * midX + matrix.d * midY + matrix.f
|
|
155
191
|
const screenTextW = Math.max(1, textW * matrix.a)
|
|
156
192
|
const screenTextH = Math.max(1, textH * matrix.d)
|
|
157
193
|
const gap = 6
|
|
@@ -161,21 +197,38 @@ export function pickEdgeLabelPosition(
|
|
|
161
197
|
const normalY = dx / length
|
|
162
198
|
const tangentX = dx / length
|
|
163
199
|
const tangentY = dy / length
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < 9; i++) {
|
|
202
|
+
let offsetX = 0
|
|
203
|
+
let offsetY = 0
|
|
204
|
+
if (i === 1) {
|
|
205
|
+
offsetX = normalX * step
|
|
206
|
+
offsetY = normalY * step
|
|
207
|
+
} else if (i === 2) {
|
|
208
|
+
offsetX = -normalX * step
|
|
209
|
+
offsetY = -normalY * step
|
|
210
|
+
} else if (i === 3) {
|
|
211
|
+
offsetX = normalX * step * 2
|
|
212
|
+
offsetY = normalY * step * 2
|
|
213
|
+
} else if (i === 4) {
|
|
214
|
+
offsetX = -normalX * step * 2
|
|
215
|
+
offsetY = -normalY * step * 2
|
|
216
|
+
} else if (i === 5) {
|
|
217
|
+
offsetX = tangentX * step
|
|
218
|
+
offsetY = tangentY * step
|
|
219
|
+
} else if (i === 6) {
|
|
220
|
+
offsetX = -tangentX * step
|
|
221
|
+
offsetY = -tangentY * step
|
|
222
|
+
} else if (i === 7) {
|
|
223
|
+
offsetX = tangentX * step + normalX * step
|
|
224
|
+
offsetY = tangentY * step + normalY * step
|
|
225
|
+
} else if (i === 8) {
|
|
226
|
+
offsetX = -tangentX * step - normalX * step
|
|
227
|
+
offsetY = -tangentY * step - normalY * step
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const centerX = screenMidX + offsetX
|
|
231
|
+
const centerY = screenMidY + offsetY
|
|
179
232
|
const rect: ScreenRect = {
|
|
180
233
|
left: centerX - screenTextW / 2 - gap,
|
|
181
234
|
top: centerY - screenTextH / 2 - gap / 2,
|
|
@@ -184,15 +237,21 @@ export function pickEdgeLabelPosition(
|
|
|
184
237
|
}
|
|
185
238
|
if (occupiedLabelRects.some((existing) => rectsOverlap(rect, existing))) continue
|
|
186
239
|
occupiedLabelRects.push(rect)
|
|
187
|
-
const
|
|
188
|
-
return { x:
|
|
240
|
+
const det = matrix.a * matrix.d - matrix.b * matrix.c
|
|
241
|
+
if (det === 0) return { x: midX, y: midY }
|
|
242
|
+
const translatedX = centerX - matrix.e
|
|
243
|
+
const translatedY = centerY - matrix.f
|
|
244
|
+
return {
|
|
245
|
+
x: (matrix.d * translatedX - matrix.c * translatedY) / det,
|
|
246
|
+
y: (-matrix.b * translatedX + matrix.a * translatedY) / det,
|
|
247
|
+
}
|
|
189
248
|
}
|
|
190
249
|
|
|
191
250
|
const fallbackRect: ScreenRect = {
|
|
192
|
-
left:
|
|
193
|
-
top:
|
|
194
|
-
right:
|
|
195
|
-
bottom:
|
|
251
|
+
left: screenMidX - screenTextW / 2 - gap,
|
|
252
|
+
top: screenMidY - screenTextH / 2 - gap / 2,
|
|
253
|
+
right: screenMidX + screenTextW / 2 + gap,
|
|
254
|
+
bottom: screenMidY + screenTextH / 2 + gap / 2,
|
|
196
255
|
}
|
|
197
256
|
occupiedLabelRects.push(fallbackRect)
|
|
198
257
|
return { x: midX, y: midY }
|
|
@@ -301,9 +360,15 @@ function parseHex(hex: string): { r: number; g: number; b: number } {
|
|
|
301
360
|
}
|
|
302
361
|
|
|
303
362
|
/** Derive a portal tint color from the accent: same hue, very low alpha. */
|
|
363
|
+
const portalTintColorCache = new Map<string, string>()
|
|
304
364
|
function portalTintColor(accent: string, alpha: number): string {
|
|
365
|
+
const cacheKey = `${accent}:${alpha}`
|
|
366
|
+
const cached = portalTintColorCache.get(cacheKey)
|
|
367
|
+
if (cached) return cached
|
|
305
368
|
const { r, g, b } = parseHex(accent)
|
|
306
|
-
|
|
369
|
+
const rgba = `rgba(${r},${g},${b},${alpha})`
|
|
370
|
+
portalTintColorCache.set(cacheKey, rgba)
|
|
371
|
+
return rgba
|
|
307
372
|
}
|
|
308
373
|
|
|
309
374
|
/** Draw a squiggly line from (x1, y1) to (x2, y2). */
|
|
@@ -719,24 +784,38 @@ function drawNode(
|
|
|
719
784
|
|
|
720
785
|
// ── Edge drawing ───────────────────────────────────────────────────
|
|
721
786
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
thresholds: { start: number; end: number },
|
|
728
|
-
accent: string,
|
|
729
|
-
labelBg: string,
|
|
730
|
-
occupiedLabelRects: ScreenRect[],
|
|
731
|
-
): void {
|
|
732
|
-
if (alpha < 0.05) return
|
|
733
|
-
const nodeMap = new Map(nodes.map((n) => [n.id, n]))
|
|
734
|
-
const handleUsage: Record<string, { edgeKey: string; type: 'source' | 'target'; otherNodeCoord: number }[]> = {}
|
|
787
|
+
interface HandleUsage {
|
|
788
|
+
edgeKey: string
|
|
789
|
+
type: 'source' | 'target'
|
|
790
|
+
otherNodeCoord: number
|
|
791
|
+
}
|
|
735
792
|
|
|
736
|
-
|
|
737
|
-
|
|
793
|
+
interface DrawEdgesLayoutMetadata {
|
|
794
|
+
nodeMap: Map<string, LayoutNode>
|
|
795
|
+
handleUsage: Record<string, HandleUsage[]>
|
|
796
|
+
handleUsageIndex: Record<string, number>
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const drawEdgesMetadataCache = new WeakMap<LayoutNode[], DrawEdgesLayoutMetadata>()
|
|
800
|
+
const emptyHandleUsage: HandleUsage[] = []
|
|
801
|
+
|
|
802
|
+
function getDrawEdgesLayoutMetadata(nodes: LayoutNode[]): DrawEdgesLayoutMetadata {
|
|
803
|
+
const cached = drawEdgesMetadataCache.get(nodes)
|
|
804
|
+
if (cached) return cached
|
|
805
|
+
|
|
806
|
+
const nodeMap = new Map<string, LayoutNode>()
|
|
807
|
+
const handleUsage: Record<string, HandleUsage[]> = {}
|
|
808
|
+
const handleUsageIndex: Record<string, number> = {}
|
|
809
|
+
|
|
810
|
+
for (const node of nodes) {
|
|
811
|
+
nodeMap.set(node.id, node)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
for (const node of nodes) {
|
|
815
|
+
for (let edgeIndex = 0; edgeIndex < node.edgesOut.length; edgeIndex++) {
|
|
816
|
+
const edge = node.edgesOut[edgeIndex]
|
|
738
817
|
const target = nodeMap.get(edge.targetId)
|
|
739
|
-
if (!target)
|
|
818
|
+
if (!target) continue
|
|
740
819
|
|
|
741
820
|
const edgeKey = `${node.id}:${edgeIndex}`
|
|
742
821
|
const sourceSide = getLogicalHandleId(edge.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE) ?? DEFAULT_SOURCE_HANDLE_SIDE
|
|
@@ -761,12 +840,34 @@ function drawEdges(
|
|
|
761
840
|
? node.worldY + node.worldH / 2
|
|
762
841
|
: node.worldX + node.worldW / 2,
|
|
763
842
|
})
|
|
764
|
-
}
|
|
765
|
-
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
766
845
|
|
|
767
|
-
Object.
|
|
846
|
+
for (const [usageKey, usages] of Object.entries(handleUsage)) {
|
|
768
847
|
usages.sort((a, b) => a.otherNodeCoord - b.otherNodeCoord)
|
|
769
|
-
|
|
848
|
+
for (let i = 0; i < usages.length; i++) {
|
|
849
|
+
const usage = usages[i]
|
|
850
|
+
handleUsageIndex[`${usageKey}:${usage.edgeKey}:${usage.type}`] = i
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const metadata = { nodeMap, handleUsage, handleUsageIndex }
|
|
855
|
+
drawEdgesMetadataCache.set(nodes, metadata)
|
|
856
|
+
return metadata
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function drawEdges(
|
|
860
|
+
ctx: CanvasRenderingContext2D,
|
|
861
|
+
nodes: LayoutNode[],
|
|
862
|
+
alpha: number,
|
|
863
|
+
zoom: number,
|
|
864
|
+
thresholds: { start: number; end: number },
|
|
865
|
+
accent: string,
|
|
866
|
+
labelBg: string,
|
|
867
|
+
occupiedLabelRects: ScreenRect[],
|
|
868
|
+
): void {
|
|
869
|
+
if (alpha < 0.05) return
|
|
870
|
+
const { nodeMap, handleUsage, handleUsageIndex } = getDrawEdgesLayoutMetadata(nodes)
|
|
770
871
|
|
|
771
872
|
for (const node of nodes) {
|
|
772
873
|
for (const [edgeIndex, edge] of node.edgesOut.entries()) {
|
|
@@ -807,10 +908,12 @@ function drawEdges(
|
|
|
807
908
|
const edgeKey = `${node.id}:${edgeIndex}`
|
|
808
909
|
const sourceSide = getLogicalHandleId(edge.sourceHandle, DEFAULT_SOURCE_HANDLE_SIDE) ?? DEFAULT_SOURCE_HANDLE_SIDE
|
|
809
910
|
const targetSide = getLogicalHandleId(edge.targetHandle, DEFAULT_TARGET_HANDLE_SIDE) ?? DEFAULT_TARGET_HANDLE_SIDE
|
|
810
|
-
const
|
|
811
|
-
const
|
|
812
|
-
const
|
|
813
|
-
const
|
|
911
|
+
const srcKey = `${node.id}-${sourceSide}`
|
|
912
|
+
const tgtKey = `${target.id}-${targetSide}`
|
|
913
|
+
const srcGroup = handleUsage[srcKey] ?? emptyHandleUsage
|
|
914
|
+
const tgtGroup = handleUsage[tgtKey] ?? emptyHandleUsage
|
|
915
|
+
const sourceGroupIndex = handleUsageIndex[`${srcKey}:${edgeKey}:source`] ?? -1
|
|
916
|
+
const targetGroupIndex = handleUsageIndex[`${tgtKey}:${edgeKey}:target`] ?? -1
|
|
814
917
|
|
|
815
918
|
const sH = getHandlePos(
|
|
816
919
|
effXSource,
|
|
@@ -1095,11 +1198,7 @@ export function renderFrame(
|
|
|
1095
1198
|
canvasW: number,
|
|
1096
1199
|
canvasH: number,
|
|
1097
1200
|
): ScreenRect[] {
|
|
1098
|
-
|
|
1099
|
-
const canvasBg = readCSSVar('--bg-main', '#0d121e')
|
|
1100
|
-
const nodeBg = readCSSVar('--bg-element', '#2d3748')
|
|
1101
|
-
const accent = readCSSVar('--accent', '#63b3ed')
|
|
1102
|
-
const labelBg = readCSSVar('--chakra-colors-gray-900', '#171923')
|
|
1201
|
+
const { canvasBg, nodeBg, accent, labelBg } = getThemeVars()
|
|
1103
1202
|
|
|
1104
1203
|
ctx.clearRect(0, 0, canvasW, canvasH)
|
|
1105
1204
|
|
|
@@ -1114,7 +1213,8 @@ export function renderFrame(
|
|
|
1114
1213
|
ctx.scale(view.zoom, view.zoom)
|
|
1115
1214
|
|
|
1116
1215
|
const thresholds = getExpandThresholds(canvasW)
|
|
1117
|
-
const occupiedLabelRects
|
|
1216
|
+
const occupiedLabelRects = frameLabelRects
|
|
1217
|
+
occupiedLabelRects.length = 0
|
|
1118
1218
|
|
|
1119
1219
|
for (const group of groups) {
|
|
1120
1220
|
if (!isVisible(group.worldX, group.worldY, group.worldW, group.worldH, view, canvasW, canvasH)) {
|