@tldiagram/core-ui 1.95.1 → 2.0.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 +184 -3
- package/dist/components/ConnectorPanel.d.ts +5 -1
- package/dist/components/CrossBranchControls.d.ts +4 -3
- package/dist/components/ElementNode.d.ts +5 -0
- package/dist/components/ElementPanel.d.ts +6 -1
- package/dist/components/LayoutSection.d.ts +2 -1
- package/dist/components/MergeDialog.d.ts +16 -0
- package/dist/components/NodeContainer.d.ts +2 -0
- package/dist/components/ProxyConnectorPanel.d.ts +4 -1
- package/dist/components/ViewExplorer/index.d.ts +1 -1
- package/dist/components/ViewFloatingMenu-vscode.d.ts +5 -0
- package/dist/components/ViewFloatingMenu.d.ts +8 -1
- package/dist/components/ViewGridNode.d.ts +3 -0
- package/dist/components/ViewPanel.d.ts +2 -1
- package/dist/components/WorkspacePanel.d.ts +2 -0
- package/dist/components/ZUI/ZUICanvas.d.ts +4 -0
- package/dist/components/ZUI/focus.d.ts +32 -0
- package/dist/components/ZUI/focus.test.d.ts +1 -0
- package/dist/components/ZUI/layout.d.ts +2 -2
- package/dist/components/ZUI/proxy.d.ts +20 -4
- package/dist/components/ZUI/renderer.d.ts +35 -1
- package/dist/components/ZUI/types.d.ts +6 -0
- package/dist/components/ZUI/useZUIInteraction.d.ts +1 -0
- package/dist/context/WorkspaceVersionContext.d.ts +49 -0
- package/dist/crossBranch/resolve.d.ts +39 -2
- package/dist/crossBranch/resolve.test.d.ts +1 -0
- package/dist/crossBranch/settings.d.ts +6 -1
- package/dist/crossBranch/types.d.ts +8 -0
- package/dist/hooks/useElementSearch.d.ts +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16529 -14030
- package/dist/pages/InfiniteZoom.d.ts +1 -0
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +6 -1
- package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +2 -0
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +4 -2
- package/dist/pages/ViewEditor/hooks/useViewEditHistory.d.ts +13 -0
- package/dist/pages/viewsJumpSearch.d.ts +22 -0
- package/dist/pages/viewsJumpSearch.test.d.ts +1 -0
- package/dist/store/useStore.d.ts +3 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/elementIcon.d.ts +2 -0
- package/dist/utils/elementIcon.test.d.ts +1 -0
- package/dist/utils/sourceEditor.d.ts +7 -0
- package/dist/utils/watchDiffSummary.d.ts +34 -0
- package/package.json +2 -2
- package/src/App.tsx +12 -8
- package/src/api/client.ts +488 -26
- package/src/components/CodePreviewPanel.tsx +90 -16
- package/src/components/ConnectorPanel.tsx +34 -3
- package/src/components/ContextNeighborElement.tsx +2 -5
- package/src/components/CrossBranchControls.tsx +46 -17
- package/src/components/ElementNode.tsx +98 -47
- package/src/components/ElementPanel.tsx +62 -25
- package/src/components/InlineElementAdder.tsx +8 -3
- package/src/components/LayoutSection.tsx +4 -1
- package/src/components/MergeDialog.tsx +269 -0
- package/src/components/NodeContainer.tsx +55 -17
- package/src/components/ProxyConnectorPanel.tsx +58 -16
- package/src/components/ViewBezierConnector.tsx +116 -21
- package/src/components/ViewExplorer/index.tsx +1 -1
- package/src/components/ViewFloatingMenu-vscode.tsx +5 -0
- package/src/components/ViewFloatingMenu.tsx +110 -1
- package/src/components/ViewGridNode.tsx +59 -8
- package/src/components/ViewPanel.tsx +3 -2
- package/src/components/WorkspacePanel.tsx +938 -0
- package/src/components/ZUI/ZUICanvas.tsx +216 -122
- package/src/components/ZUI/focus.test.ts +534 -0
- package/src/components/ZUI/focus.ts +293 -0
- package/src/components/ZUI/layout.ts +7 -11
- package/src/components/ZUI/proxy.ts +470 -114
- package/src/components/ZUI/renderer.ts +510 -134
- package/src/components/ZUI/types.ts +6 -0
- package/src/components/ZUI/useZUIInteraction.ts +66 -29
- package/src/context/WorkspaceVersionContext.tsx +126 -0
- package/src/crossBranch/resolve.test.ts +342 -0
- package/src/crossBranch/resolve.ts +368 -68
- package/src/crossBranch/settings.ts +49 -3
- package/src/crossBranch/types.ts +9 -0
- package/src/hooks/useElementSearch.ts +45 -0
- package/src/index.css +11 -0
- package/src/index.ts +7 -0
- package/src/pages/AppearanceSettings.tsx +24 -1
- package/src/pages/Dependencies.tsx +231 -65
- package/src/pages/InfiniteZoom.tsx +41 -19
- package/src/pages/Settings.tsx +1 -1
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +103 -24
- package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +102 -6
- package/src/pages/ViewEditor/hooks/useViewData.ts +42 -26
- package/src/pages/ViewEditor/hooks/useViewEditHistory.ts +62 -0
- package/src/pages/ViewEditor/index.tsx +549 -59
- package/src/pages/Views.tsx +112 -41
- package/src/pages/ViewsGrid.tsx +332 -113
- package/src/pages/viewsJumpSearch.test.ts +193 -0
- package/src/pages/viewsJumpSearch.ts +111 -0
- package/src/store/useStore.ts +58 -0
- package/src/types/index.ts +10 -0
- package/src/utils/elementIcon.test.ts +28 -0
- package/src/utils/elementIcon.ts +20 -0
- package/src/utils/sourceEditor.ts +46 -0
- package/src/utils/watchDiffSummary.ts +159 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
resolveZUIProxyConnectors,
|
|
3
|
+
type ZUIHiddenProxyBadge,
|
|
4
|
+
type ZUIViewportBounds,
|
|
5
|
+
type ZUIProxyResolution,
|
|
6
|
+
type ZUIResolvedConnector,
|
|
7
|
+
} from '../../crossBranch/resolve'
|
|
2
8
|
import type { WorkspaceGraphSnapshot } from '../../crossBranch/types'
|
|
3
9
|
import type { LayoutNode, ZUIViewState, HoveredItem } from './types'
|
|
4
10
|
import { getExpandThresholds, pickEdgeLabelPosition, type ScreenRect } from './renderer'
|
|
5
11
|
import type { CrossBranchContextSettings } from '../../crossBranch/types'
|
|
12
|
+
import { DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA } from '../../crossBranch/settings'
|
|
6
13
|
|
|
7
14
|
export interface VisibleNodeAnchor {
|
|
8
15
|
nodeId: string
|
|
@@ -20,12 +27,58 @@ function clamp(value: number, min: number, max: number) {
|
|
|
20
27
|
return value < min ? min : value > max ? max : value
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
function connectorAlpha(alpha: number): number {
|
|
31
|
+
return clamp(alpha * 1.1, 0.35, 0.95)
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
function transitionT(screenW: number, start: number, end: number): number {
|
|
24
35
|
return clamp((screenW - start) / (end - start), 0, 1)
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
function
|
|
28
|
-
|
|
38
|
+
function visualRectForNode(
|
|
39
|
+
absX: number,
|
|
40
|
+
absY: number,
|
|
41
|
+
absW: number,
|
|
42
|
+
absH: number,
|
|
43
|
+
hasChildren: boolean,
|
|
44
|
+
screenW: number,
|
|
45
|
+
thresholds: { start: number; end: number },
|
|
46
|
+
) {
|
|
47
|
+
if (!hasChildren && screenW > thresholds.end) {
|
|
48
|
+
const scale = thresholds.end / screenW
|
|
49
|
+
const visualW = absW * scale
|
|
50
|
+
const visualH = absH * scale
|
|
51
|
+
return {
|
|
52
|
+
worldX: absX + (absW - visualW) / 2,
|
|
53
|
+
worldY: absY + (absH - visualH) / 2,
|
|
54
|
+
worldW: visualW,
|
|
55
|
+
worldH: visualH,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
worldX: absX,
|
|
61
|
+
worldY: absY,
|
|
62
|
+
worldW: absW,
|
|
63
|
+
worldH: absH,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function registerVisibleAnchor(
|
|
68
|
+
node: LayoutNode,
|
|
69
|
+
visibleAnchors: Map<number, VisibleNodeAnchor>,
|
|
70
|
+
byNodeId: Map<string, VisibleNodeAnchor>,
|
|
71
|
+
anchor: VisibleNodeAnchor,
|
|
72
|
+
) {
|
|
73
|
+
const existing = visibleAnchors.get(node.elementId)
|
|
74
|
+
if (!existing || existing.pathDepth < anchor.pathDepth || existing.renderAlpha < anchor.renderAlpha) {
|
|
75
|
+
visibleAnchors.set(node.elementId, anchor)
|
|
76
|
+
}
|
|
77
|
+
byNodeId.set(node.id, anchor)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function collectVisibleAnchorForNode(
|
|
81
|
+
node: LayoutNode,
|
|
29
82
|
view: ZUIViewState,
|
|
30
83
|
thresholds: { start: number; end: number },
|
|
31
84
|
hiddenTags: Set<string>,
|
|
@@ -38,42 +91,42 @@ function collectVisibleAnchorsInNodes(
|
|
|
38
91
|
parentChildOffsetX: number,
|
|
39
92
|
parentChildOffsetY: number,
|
|
40
93
|
) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
byNodeId.set(node.id, anchor)
|
|
72
|
-
}
|
|
94
|
+
if (hiddenTags.size > 0 && node.tags.some((tag) => hiddenTags.has(tag))) return { selfDrawn: false }
|
|
95
|
+
|
|
96
|
+
const absX = parentAbsX + (node.worldX - parentChildOffsetX) * parentAbsScale
|
|
97
|
+
const absY = parentAbsY + (node.worldY - parentChildOffsetY) * parentAbsScale
|
|
98
|
+
const absScale = parentAbsScale
|
|
99
|
+
const absW = node.worldW * absScale
|
|
100
|
+
const absH = node.worldH * absScale
|
|
101
|
+
const screenW = absW * view.zoom
|
|
102
|
+
if (screenW < 2) return { selfDrawn: false }
|
|
103
|
+
|
|
104
|
+
const hasChildren = node.children && node.children.length > 0
|
|
105
|
+
const t = hasChildren ? transitionT(screenW, thresholds.start, thresholds.end) : 0
|
|
106
|
+
const parentAlpha = inheritedAlpha * (1 - t)
|
|
107
|
+
const childAlpha = inheritedAlpha * t
|
|
108
|
+
const selfDrawn = !hasChildren || t <= 0.95
|
|
109
|
+
const visualRect = visualRectForNode(absX, absY, absW, absH, hasChildren, screenW, thresholds)
|
|
110
|
+
|
|
111
|
+
if (selfDrawn) {
|
|
112
|
+
registerVisibleAnchor(node, visibleAnchors, byNodeId, {
|
|
113
|
+
nodeId: node.id,
|
|
114
|
+
elementId: node.elementId,
|
|
115
|
+
label: node.label,
|
|
116
|
+
worldX: visualRect.worldX,
|
|
117
|
+
worldY: visualRect.worldY,
|
|
118
|
+
worldW: visualRect.worldW,
|
|
119
|
+
worldH: visualRect.worldH,
|
|
120
|
+
pathDepth: node.pathElementIds.length,
|
|
121
|
+
renderAlpha: hasChildren ? parentAlpha : inheritedAlpha,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
73
124
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
125
|
+
let hasDirectChildDrawn = false
|
|
126
|
+
if (hasChildren && t > 0.05) {
|
|
127
|
+
for (const child of node.children) {
|
|
128
|
+
const childResult = collectVisibleAnchorForNode(
|
|
129
|
+
child,
|
|
77
130
|
view,
|
|
78
131
|
thresholds,
|
|
79
132
|
hiddenTags,
|
|
@@ -86,8 +139,57 @@ function collectVisibleAnchorsInNodes(
|
|
|
86
139
|
node.childOffsetX,
|
|
87
140
|
node.childOffsetY,
|
|
88
141
|
)
|
|
142
|
+
hasDirectChildDrawn = hasDirectChildDrawn || childResult.selfDrawn
|
|
89
143
|
}
|
|
90
144
|
}
|
|
145
|
+
|
|
146
|
+
if (!selfDrawn && hasDirectChildDrawn) {
|
|
147
|
+
registerVisibleAnchor(node, visibleAnchors, byNodeId, {
|
|
148
|
+
nodeId: node.id,
|
|
149
|
+
elementId: node.elementId,
|
|
150
|
+
label: node.label,
|
|
151
|
+
worldX: visualRect.worldX,
|
|
152
|
+
worldY: visualRect.worldY,
|
|
153
|
+
worldW: visualRect.worldW,
|
|
154
|
+
worldH: visualRect.worldH,
|
|
155
|
+
pathDepth: node.pathElementIds.length,
|
|
156
|
+
renderAlpha: Math.max(0.12, inheritedAlpha * 0.28),
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { selfDrawn }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function collectVisibleAnchorsInNodes(
|
|
164
|
+
nodes: LayoutNode[],
|
|
165
|
+
view: ZUIViewState,
|
|
166
|
+
thresholds: { start: number; end: number },
|
|
167
|
+
hiddenTags: Set<string>,
|
|
168
|
+
visibleAnchors: Map<number, VisibleNodeAnchor>,
|
|
169
|
+
byNodeId: Map<string, VisibleNodeAnchor>,
|
|
170
|
+
inheritedAlpha: number,
|
|
171
|
+
parentAbsX: number,
|
|
172
|
+
parentAbsY: number,
|
|
173
|
+
parentAbsScale: number,
|
|
174
|
+
parentChildOffsetX: number,
|
|
175
|
+
parentChildOffsetY: number,
|
|
176
|
+
) {
|
|
177
|
+
for (const node of nodes) {
|
|
178
|
+
collectVisibleAnchorForNode(
|
|
179
|
+
node,
|
|
180
|
+
view,
|
|
181
|
+
thresholds,
|
|
182
|
+
hiddenTags,
|
|
183
|
+
visibleAnchors,
|
|
184
|
+
byNodeId,
|
|
185
|
+
inheritedAlpha,
|
|
186
|
+
parentAbsX,
|
|
187
|
+
parentAbsY,
|
|
188
|
+
parentAbsScale,
|
|
189
|
+
parentChildOffsetX,
|
|
190
|
+
parentChildOffsetY,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
91
193
|
}
|
|
92
194
|
|
|
93
195
|
export function collectVisibleNodeAnchors(
|
|
@@ -121,13 +223,23 @@ export function collectVisibleNodeAnchors(
|
|
|
121
223
|
return { visibleAnchors, byNodeId }
|
|
122
224
|
}
|
|
123
225
|
|
|
124
|
-
function
|
|
226
|
+
function getAnchorCenter(anchor: VisibleNodeAnchor) {
|
|
227
|
+
return {
|
|
228
|
+
x: anchor.worldX + anchor.worldW / 2,
|
|
229
|
+
y: anchor.worldY + anchor.worldH / 2,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function containsPoint(anchor: VisibleNodeAnchor, x: number, y: number) {
|
|
234
|
+
return x >= anchor.worldX &&
|
|
235
|
+
x <= anchor.worldX + anchor.worldW &&
|
|
236
|
+
y >= anchor.worldY &&
|
|
237
|
+
y <= anchor.worldY + anchor.worldH
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getRectBoundaryPoint(anchor: VisibleNodeAnchor, dx: number, dy: number) {
|
|
125
241
|
const cx = anchor.worldX + anchor.worldW / 2
|
|
126
242
|
const cy = anchor.worldY + anchor.worldH / 2
|
|
127
|
-
const tx = towards.worldX + towards.worldW / 2
|
|
128
|
-
const ty = towards.worldY + towards.worldH / 2
|
|
129
|
-
const dx = tx - cx
|
|
130
|
-
const dy = ty - cy
|
|
131
243
|
const hw = anchor.worldW / 2
|
|
132
244
|
const hh = anchor.worldH / 2
|
|
133
245
|
|
|
@@ -148,15 +260,191 @@ function getDirectAnchorPoint(anchor: VisibleNodeAnchor, towards: VisibleNodeAnc
|
|
|
148
260
|
}
|
|
149
261
|
}
|
|
150
262
|
|
|
263
|
+
function getDirectAnchorPoint(anchor: VisibleNodeAnchor, towards: VisibleNodeAnchor) {
|
|
264
|
+
const anchorCenter = getAnchorCenter(anchor)
|
|
265
|
+
const towardsCenter = getAnchorCenter(towards)
|
|
266
|
+
|
|
267
|
+
// Nested anchors represent parent/child nodes. Aim the child endpoint away
|
|
268
|
+
// from the parent center so proxy lines attach to the nearer child edge.
|
|
269
|
+
if (containsPoint(towards, anchorCenter.x, anchorCenter.y)) {
|
|
270
|
+
return getRectBoundaryPoint(
|
|
271
|
+
anchor,
|
|
272
|
+
anchorCenter.x - towardsCenter.x,
|
|
273
|
+
anchorCenter.y - towardsCenter.y,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return getRectBoundaryPoint(
|
|
278
|
+
anchor,
|
|
279
|
+
towardsCenter.x - anchorCenter.x,
|
|
280
|
+
towardsCenter.y - anchorCenter.y,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function getDirectAnchorPoints(source: VisibleNodeAnchor, target: VisibleNodeAnchor) {
|
|
285
|
+
const sourcePoint = getDirectAnchorPoint(source, target)
|
|
286
|
+
const targetPoint = getDirectAnchorPoint(target, source)
|
|
287
|
+
return { sourcePoint, targetPoint }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function getDevicePixelRatio(): number {
|
|
291
|
+
return typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function roundRectPath(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
|
|
295
|
+
ctx.beginPath()
|
|
296
|
+
ctx.roundRect(x, y, w, h, r)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function drawFixedScreenProxyBadge(
|
|
300
|
+
ctx: CanvasRenderingContext2D,
|
|
301
|
+
label: string,
|
|
302
|
+
labelPos: { x: number; y: number },
|
|
303
|
+
badgeCssW: number,
|
|
304
|
+
badgeCssH: number,
|
|
305
|
+
labelBg: string,
|
|
306
|
+
strokeStyle: string,
|
|
307
|
+
lineDashCss: number[],
|
|
308
|
+
fontWeight = 600,
|
|
309
|
+
) {
|
|
310
|
+
const matrix = ctx.getTransform()
|
|
311
|
+
const dpr = getDevicePixelRatio()
|
|
312
|
+
const centerX = matrix.a * labelPos.x + matrix.c * labelPos.y + matrix.e
|
|
313
|
+
const centerY = matrix.b * labelPos.x + matrix.d * labelPos.y + matrix.f
|
|
314
|
+
const badgeW = badgeCssW * dpr
|
|
315
|
+
const badgeH = badgeCssH * dpr
|
|
316
|
+
const radius = badgeH / 2
|
|
317
|
+
|
|
318
|
+
ctx.save()
|
|
319
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
320
|
+
ctx.fillStyle = labelBg
|
|
321
|
+
roundRectPath(ctx, centerX - badgeW / 2, centerY - badgeH / 2, badgeW, badgeH, radius)
|
|
322
|
+
ctx.fill()
|
|
323
|
+
ctx.strokeStyle = strokeStyle
|
|
324
|
+
ctx.lineWidth = dpr
|
|
325
|
+
ctx.setLineDash(lineDashCss.map((value) => value * dpr))
|
|
326
|
+
ctx.stroke()
|
|
327
|
+
ctx.setLineDash([])
|
|
328
|
+
ctx.fillStyle = 'white'
|
|
329
|
+
ctx.font = `${fontWeight} ${11 * dpr}px Inter, system-ui, sans-serif`
|
|
330
|
+
ctx.textAlign = 'center'
|
|
331
|
+
ctx.textBaseline = 'middle'
|
|
332
|
+
ctx.fillText(label, centerX, centerY)
|
|
333
|
+
ctx.restore()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function measureProxyBadge(ctx: CanvasRenderingContext2D, label: string, zoom: number, fontWeight = 600) {
|
|
337
|
+
ctx.save()
|
|
338
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
339
|
+
ctx.font = `${fontWeight} 11px Inter, system-ui, sans-serif`
|
|
340
|
+
const textW = ctx.measureText(label).width
|
|
341
|
+
ctx.restore()
|
|
342
|
+
|
|
343
|
+
const badgeCssW = Math.max(24, textW + 14)
|
|
344
|
+
const badgeCssH = 24
|
|
345
|
+
return {
|
|
346
|
+
badgeCssW,
|
|
347
|
+
badgeCssH,
|
|
348
|
+
worldW: badgeCssW / zoom,
|
|
349
|
+
worldH: badgeCssH / zoom,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
interface IndexedProxyConnector {
|
|
354
|
+
connector: ZUIResolvedConnector
|
|
355
|
+
x1: number
|
|
356
|
+
y1: number
|
|
357
|
+
x2: number
|
|
358
|
+
y2: number
|
|
359
|
+
midX: number
|
|
360
|
+
midY: number
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export interface ProxyConnectorSpatialIndex {
|
|
364
|
+
cellSize: number
|
|
365
|
+
cells: Map<string, IndexedProxyConnector[]>
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const PROXY_CONNECTOR_INDEX_CELL_SIZE = 360
|
|
369
|
+
|
|
370
|
+
function proxyCellKey(cx: number, cy: number): string {
|
|
371
|
+
return `${cx},${cy}`
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function addProxyConnectorToSpatialIndex(index: ProxyConnectorSpatialIndex, connector: IndexedProxyConnector): void {
|
|
375
|
+
const minX = Math.min(connector.x1, connector.x2)
|
|
376
|
+
const maxX = Math.max(connector.x1, connector.x2)
|
|
377
|
+
const minY = Math.min(connector.y1, connector.y2)
|
|
378
|
+
const maxY = Math.max(connector.y1, connector.y2)
|
|
379
|
+
const startX = Math.floor(minX / index.cellSize)
|
|
380
|
+
const endX = Math.floor(maxX / index.cellSize)
|
|
381
|
+
const startY = Math.floor(minY / index.cellSize)
|
|
382
|
+
const endY = Math.floor(maxY / index.cellSize)
|
|
383
|
+
|
|
384
|
+
for (let cx = startX; cx <= endX; cx++) {
|
|
385
|
+
for (let cy = startY; cy <= endY; cy++) {
|
|
386
|
+
const key = proxyCellKey(cx, cy)
|
|
387
|
+
let bucket = index.cells.get(key)
|
|
388
|
+
if (!bucket) {
|
|
389
|
+
bucket = []
|
|
390
|
+
index.cells.set(key, bucket)
|
|
391
|
+
}
|
|
392
|
+
bucket.push(connector)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function buildProxyConnectorSpatialIndex(
|
|
398
|
+
connectors: ZUIResolvedConnector[],
|
|
399
|
+
visibleAnchorsByNodeId: Map<string, VisibleNodeAnchor>,
|
|
400
|
+
): ProxyConnectorSpatialIndex {
|
|
401
|
+
const index: ProxyConnectorSpatialIndex = {
|
|
402
|
+
cellSize: PROXY_CONNECTOR_INDEX_CELL_SIZE,
|
|
403
|
+
cells: new Map(),
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const connector of connectors) {
|
|
407
|
+
const source = visibleAnchorsByNodeId.get(connector.sourceNodeId)
|
|
408
|
+
const target = visibleAnchorsByNodeId.get(connector.targetNodeId)
|
|
409
|
+
if (!source || !target) continue
|
|
410
|
+
|
|
411
|
+
const { sourcePoint, targetPoint } = getDirectAnchorPoints(source, target)
|
|
412
|
+
addProxyConnectorToSpatialIndex(index, {
|
|
413
|
+
connector,
|
|
414
|
+
x1: sourcePoint.x,
|
|
415
|
+
y1: sourcePoint.y,
|
|
416
|
+
x2: targetPoint.x,
|
|
417
|
+
y2: targetPoint.y,
|
|
418
|
+
midX: (sourcePoint.x + targetPoint.x) / 2,
|
|
419
|
+
midY: (sourcePoint.y + targetPoint.y) / 2,
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return index
|
|
424
|
+
}
|
|
425
|
+
|
|
151
426
|
export function buildVisibleProxyConnectors(
|
|
152
427
|
snapshot: WorkspaceGraphSnapshot | null,
|
|
153
428
|
visibleAnchors: Map<number, VisibleNodeAnchor>,
|
|
154
429
|
settings: CrossBranchContextSettings,
|
|
155
|
-
|
|
430
|
+
viewport?: ZUIViewportBounds | null,
|
|
431
|
+
): ZUIProxyResolution {
|
|
432
|
+
const minAlpha = settings.minConnectorAnchorAlpha ?? DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA
|
|
433
|
+
const eligibleAnchors = Array.from(visibleAnchors.entries())
|
|
434
|
+
.filter(([, anchor]) => anchor.renderAlpha >= minAlpha)
|
|
435
|
+
const connectorAnchors = new Map(eligibleAnchors.map(([elementId, anchor]) => [elementId, anchor.nodeId]))
|
|
436
|
+
const anchorsByElementId = new Map(eligibleAnchors.map(([elementId, anchor]) => [elementId, {
|
|
437
|
+
nodeId: anchor.nodeId,
|
|
438
|
+
worldX: anchor.worldX,
|
|
439
|
+
worldY: anchor.worldY,
|
|
440
|
+
worldW: anchor.worldW,
|
|
441
|
+
worldH: anchor.worldH,
|
|
442
|
+
}]))
|
|
156
443
|
return resolveZUIProxyConnectors(
|
|
157
444
|
snapshot,
|
|
158
|
-
|
|
445
|
+
connectorAnchors,
|
|
159
446
|
settings,
|
|
447
|
+
{ viewport, anchorsByElementId },
|
|
160
448
|
)
|
|
161
449
|
}
|
|
162
450
|
|
|
@@ -166,8 +454,27 @@ export function drawVisibleProxyConnectors(
|
|
|
166
454
|
visibleAnchorsByNodeId: Map<string, VisibleNodeAnchor>,
|
|
167
455
|
zoom: number,
|
|
168
456
|
labelBg: string,
|
|
457
|
+
accent: string,
|
|
169
458
|
occupiedLabelRects: ScreenRect[],
|
|
170
459
|
) {
|
|
460
|
+
const connectorsByActualPair = new Map<string, ZUIResolvedConnector[]>()
|
|
461
|
+
for (const connector of connectors) {
|
|
462
|
+
const pairKey = `${Math.min(connector.sourceElementId, connector.targetElementId)}::${Math.max(connector.sourceElementId, connector.targetElementId)}`
|
|
463
|
+
const family = connectorsByActualPair.get(pairKey)
|
|
464
|
+
if (family) family.push(connector)
|
|
465
|
+
else connectorsByActualPair.set(pairKey, [connector])
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const provenanceKeys = new Set<string>()
|
|
469
|
+
for (const family of connectorsByActualPair.values()) {
|
|
470
|
+
if (family.length < 2) continue
|
|
471
|
+
const sorted = [...family].sort((left, right) => {
|
|
472
|
+
if (left.maxDepth !== right.maxDepth) return left.maxDepth - right.maxDepth
|
|
473
|
+
return (left.sourceDepth + left.targetDepth) - (right.sourceDepth + right.targetDepth)
|
|
474
|
+
})
|
|
475
|
+
for (const connector of sorted.slice(1)) provenanceKeys.add(connector.key)
|
|
476
|
+
}
|
|
477
|
+
|
|
171
478
|
for (const connector of connectors) {
|
|
172
479
|
const source = visibleAnchorsByNodeId.get(connector.sourceNodeId)
|
|
173
480
|
const target = visibleAnchorsByNodeId.get(connector.targetNodeId)
|
|
@@ -175,104 +482,153 @@ export function drawVisibleProxyConnectors(
|
|
|
175
482
|
const alpha = Math.min(source.renderAlpha, target.renderAlpha)
|
|
176
483
|
if (alpha < 0.01) continue
|
|
177
484
|
|
|
178
|
-
const sourcePoint =
|
|
179
|
-
const targetPoint = getDirectAnchorPoint(target, source)
|
|
485
|
+
const { sourcePoint, targetPoint } = getDirectAnchorPoints(source, target)
|
|
180
486
|
const midX = (sourcePoint.x + targetPoint.x) / 2
|
|
181
487
|
const midY = (sourcePoint.y + targetPoint.y) / 2
|
|
182
488
|
const label = String(connector.details.count)
|
|
183
489
|
|
|
184
490
|
ctx.save()
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
491
|
+
const isProvenanceStub = provenanceKeys.has(connector.key)
|
|
492
|
+
if (isProvenanceStub) {
|
|
493
|
+
ctx.restore()
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
ctx.globalAlpha = connectorAlpha(alpha) * 0.8
|
|
498
|
+
ctx.strokeStyle = accent
|
|
499
|
+
ctx.lineWidth = 1 / zoom
|
|
500
|
+
ctx.lineCap = 'round'
|
|
501
|
+
ctx.setLineDash([1 / zoom, 4 / zoom])
|
|
188
502
|
ctx.beginPath()
|
|
189
503
|
ctx.moveTo(sourcePoint.x, sourcePoint.y)
|
|
190
504
|
ctx.lineTo(targetPoint.x, targetPoint.y)
|
|
191
505
|
ctx.stroke()
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const textMetrics = ctx.measureText(label)
|
|
195
|
-
const textW = textMetrics.width
|
|
196
|
-
const textH = fontSize
|
|
506
|
+
ctx.setLineDash([])
|
|
507
|
+
const badge = measureProxyBadge(ctx, label, zoom)
|
|
197
508
|
const labelPos = pickEdgeLabelPosition(
|
|
198
509
|
ctx.getTransform(),
|
|
199
510
|
midX,
|
|
200
511
|
midY,
|
|
201
|
-
|
|
202
|
-
|
|
512
|
+
badge.worldW,
|
|
513
|
+
badge.worldH,
|
|
203
514
|
targetPoint.x - sourcePoint.x,
|
|
204
515
|
targetPoint.y - sourcePoint.y,
|
|
205
516
|
occupiedLabelRects,
|
|
206
517
|
)
|
|
207
|
-
|
|
208
|
-
const py = 4 / zoom
|
|
209
|
-
const badgeW = textW + px * 2
|
|
210
|
-
const badgeH = textH + py * 2
|
|
211
|
-
const badgeRadius = badgeH / 2
|
|
212
|
-
ctx.fillStyle = labelBg
|
|
213
|
-
ctx.beginPath()
|
|
214
|
-
ctx.roundRect(
|
|
215
|
-
labelPos.x - badgeW / 2,
|
|
216
|
-
labelPos.y - badgeH / 2,
|
|
217
|
-
badgeW,
|
|
218
|
-
badgeH,
|
|
219
|
-
badgeRadius,
|
|
220
|
-
)
|
|
221
|
-
ctx.fill()
|
|
222
|
-
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'
|
|
223
|
-
ctx.lineWidth = 1 / zoom
|
|
224
|
-
ctx.stroke()
|
|
225
|
-
ctx.fillStyle = 'white'
|
|
226
|
-
ctx.textAlign = 'center'
|
|
227
|
-
ctx.textBaseline = 'middle'
|
|
228
|
-
ctx.fillText(label, labelPos.x, labelPos.y)
|
|
518
|
+
drawFixedScreenProxyBadge(ctx, label, labelPos, badge.badgeCssW, badge.badgeCssH, labelBg, accent, [1, 4])
|
|
229
519
|
|
|
230
520
|
ctx.restore()
|
|
231
521
|
}
|
|
232
522
|
}
|
|
233
523
|
|
|
524
|
+
export function drawVisibleDirectProxyBadges(
|
|
525
|
+
ctx: CanvasRenderingContext2D,
|
|
526
|
+
badges: ZUIHiddenProxyBadge[],
|
|
527
|
+
visibleAnchorsByNodeId: Map<string, VisibleNodeAnchor>,
|
|
528
|
+
zoom: number,
|
|
529
|
+
labelBg: string,
|
|
530
|
+
occupiedLabelRects: ScreenRect[],
|
|
531
|
+
) {
|
|
532
|
+
for (const badge of badges) {
|
|
533
|
+
const source = visibleAnchorsByNodeId.get(badge.sourceNodeId)
|
|
534
|
+
const target = visibleAnchorsByNodeId.get(badge.targetNodeId)
|
|
535
|
+
if (!source || !target) continue
|
|
536
|
+
const alpha = Math.min(source.renderAlpha, target.renderAlpha)
|
|
537
|
+
if (alpha < 0.01) continue
|
|
538
|
+
|
|
539
|
+
const { sourcePoint, targetPoint } = getDirectAnchorPoints(source, target)
|
|
540
|
+
const midX = (sourcePoint.x + targetPoint.x) / 2
|
|
541
|
+
const midY = (sourcePoint.y + targetPoint.y) / 2
|
|
542
|
+
const label = `+${badge.count}`
|
|
543
|
+
|
|
544
|
+
ctx.save()
|
|
545
|
+
ctx.globalAlpha = alpha
|
|
546
|
+
const badgeMetrics = measureProxyBadge(ctx, label, zoom)
|
|
547
|
+
const labelPos = pickEdgeLabelPosition(
|
|
548
|
+
ctx.getTransform(),
|
|
549
|
+
midX,
|
|
550
|
+
midY,
|
|
551
|
+
badgeMetrics.worldW,
|
|
552
|
+
badgeMetrics.worldH,
|
|
553
|
+
targetPoint.x - sourcePoint.x,
|
|
554
|
+
targetPoint.y - sourcePoint.y,
|
|
555
|
+
occupiedLabelRects,
|
|
556
|
+
)
|
|
557
|
+
drawFixedScreenProxyBadge(
|
|
558
|
+
ctx,
|
|
559
|
+
label,
|
|
560
|
+
labelPos,
|
|
561
|
+
badgeMetrics.badgeCssW,
|
|
562
|
+
badgeMetrics.badgeCssH,
|
|
563
|
+
labelBg,
|
|
564
|
+
'rgba(255, 255, 255, 0.5)',
|
|
565
|
+
[4, 3],
|
|
566
|
+
)
|
|
567
|
+
ctx.restore()
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
234
571
|
export function findHoveredProxyConnector(
|
|
235
572
|
worldX: number,
|
|
236
573
|
worldY: number,
|
|
237
|
-
|
|
238
|
-
visibleAnchorsByNodeId: Map<string, VisibleNodeAnchor>,
|
|
574
|
+
index: ProxyConnectorSpatialIndex,
|
|
239
575
|
view: ZUIViewState,
|
|
240
576
|
): HoveredItem | null {
|
|
241
577
|
const threshold = 18 / view.zoom
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const dx = x2 - x1
|
|
251
|
-
const dy = y2 - y1
|
|
252
|
-
const l2 = dx * dx + dy * dy
|
|
253
|
-
if (l2 === 0) continue
|
|
254
|
-
let t = ((worldX - x1) * dx + (worldY - y1) * dy) / l2
|
|
255
|
-
t = Math.max(0, Math.min(1, t))
|
|
256
|
-
const nearestX = x1 + t * dx
|
|
257
|
-
const nearestY = y1 + t * dy
|
|
258
|
-
const dist = Math.sqrt((worldX - nearestX) ** 2 + (worldY - nearestY) ** 2)
|
|
259
|
-
if (dist > threshold) continue
|
|
578
|
+
const startX = Math.floor((worldX - threshold) / index.cellSize)
|
|
579
|
+
const endX = Math.floor((worldX + threshold) / index.cellSize)
|
|
580
|
+
const startY = Math.floor((worldY - threshold) / index.cellSize)
|
|
581
|
+
const endY = Math.floor((worldY + threshold) / index.cellSize)
|
|
582
|
+
const thresholdSquared = threshold * threshold
|
|
583
|
+
const seen = new Set<string>()
|
|
584
|
+
let bestConnector: IndexedProxyConnector | null = null
|
|
585
|
+
let bestDistSquared = thresholdSquared
|
|
260
586
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
587
|
+
for (let cx = startX; cx <= endX; cx++) {
|
|
588
|
+
for (let cy = startY; cy <= endY; cy++) {
|
|
589
|
+
const bucket = index.cells.get(proxyCellKey(cx, cy))
|
|
590
|
+
if (!bucket) continue
|
|
591
|
+
|
|
592
|
+
for (const indexed of bucket) {
|
|
593
|
+
const connector = indexed.connector
|
|
594
|
+
if (seen.has(connector.key)) continue
|
|
595
|
+
seen.add(connector.key)
|
|
596
|
+
const x1 = indexed.x1
|
|
597
|
+
const y1 = indexed.y1
|
|
598
|
+
const x2 = indexed.x2
|
|
599
|
+
const y2 = indexed.y2
|
|
600
|
+
const dx = x2 - x1
|
|
601
|
+
const dy = y2 - y1
|
|
602
|
+
const l2 = dx * dx + dy * dy
|
|
603
|
+
if (l2 === 0) continue
|
|
604
|
+
let t = ((worldX - x1) * dx + (worldY - y1) * dy) / l2
|
|
605
|
+
t = Math.max(0, Math.min(1, t))
|
|
606
|
+
const nearestX = x1 + t * dx
|
|
607
|
+
const nearestY = y1 + t * dy
|
|
608
|
+
const distSquared = (worldX - nearestX) ** 2 + (worldY - nearestY) ** 2
|
|
609
|
+
if (distSquared > bestDistSquared) continue
|
|
610
|
+
bestDistSquared = distSquared
|
|
611
|
+
bestConnector = indexed
|
|
612
|
+
}
|
|
275
613
|
}
|
|
276
614
|
}
|
|
277
|
-
|
|
615
|
+
|
|
616
|
+
if (!bestConnector) return null
|
|
617
|
+
|
|
618
|
+
const connector = bestConnector.connector
|
|
619
|
+
return {
|
|
620
|
+
type: 'edge',
|
|
621
|
+
data: {
|
|
622
|
+
sourceId: connector.details.sourceAnchorName,
|
|
623
|
+
targetId: connector.details.targetAnchorName,
|
|
624
|
+
label: connector.details.label || 'Cross-branch connector',
|
|
625
|
+
diagramId: connector.details.ownerViewIds[0] ?? 0,
|
|
626
|
+
sourceObjId: connector.sourceAnchorElementId,
|
|
627
|
+
targetObjId: connector.targetAnchorElementId,
|
|
628
|
+
isProxy: true,
|
|
629
|
+
details: connector.details,
|
|
630
|
+
},
|
|
631
|
+
absX: bestConnector.midX,
|
|
632
|
+
absY: bestConnector.midY,
|
|
633
|
+
}
|
|
278
634
|
}
|