@tldiagram/core-ui 2.0.1 → 2.0.2
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 +1 -0
- package/dist/api/client.test.d.ts +1 -0
- package/dist/components/ZUI/proxy.d.ts +13 -0
- package/dist/components/ZUI/proxy.test.d.ts +1 -0
- package/dist/crossBranch/resolve.d.ts +2 -0
- package/dist/index.js +4396 -4251
- package/dist/store/useStore.d.ts +1 -0
- package/dist/types/vscode-messages.d.ts +12 -0
- package/package.json +3 -3
- package/src/api/client.test.ts +17 -0
- package/src/api/client.ts +9 -3
- package/src/components/LayoutSection.tsx +12 -12
- package/src/components/ViewExplorer/TagManager/TagItem.tsx +2 -0
- package/src/components/ZUI/ZUICanvas.tsx +8 -1
- package/src/components/ZUI/focus.test.ts +20 -0
- package/src/components/ZUI/proxy.test.ts +101 -0
- package/src/components/ZUI/proxy.ts +113 -14
- package/src/components/ZUI/useZUIInteraction.ts +148 -25
- package/src/crossBranch/resolve.ts +13 -4
- package/src/pages/ViewEditor/index.tsx +3 -3
- package/src/store/useStore.test.ts +11 -0
- package/src/store/useStore.ts +15 -0
- package/src/types/vscode-messages.ts +16 -0
- package/src/utils/elementIcon.test.ts +6 -0
package/dist/store/useStore.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export declare function updatePlacedElementPosition(elements: PlacedElement[], e
|
|
|
68
68
|
export declare function removePlacedElement(elements: PlacedElement[], elementId: number): PlacedElement[];
|
|
69
69
|
export declare function placedElementToLibraryElement(element: PlacedElement): LibraryElement;
|
|
70
70
|
export declare function buildElementLibraryItems(allElements: LibraryElement[], viewElements: PlacedElement[]): LibraryElement[];
|
|
71
|
+
export declare function resolveElementForUpdate(elementId: number, selectedElement: LibraryElement | null, allElements: LibraryElement[], viewElements: PlacedElement[]): LibraryElement | null;
|
|
71
72
|
export declare function mergeSavedElementIntoPlacements(elements: PlacedElement[], saved: LibraryElement): PlacedElement[];
|
|
72
73
|
export declare function upsertConnectorInList(connectors: Connector[], connector: Connector): Connector[];
|
|
73
74
|
export declare function removeConnectorFromList(connectors: Connector[], connectorId: number): Connector[];
|
|
@@ -58,3 +58,15 @@ export type WebviewToExtensionMessage = {
|
|
|
58
58
|
filePath: string;
|
|
59
59
|
startLine: number;
|
|
60
60
|
};
|
|
61
|
+
export interface WatchEventDetail {
|
|
62
|
+
type: string;
|
|
63
|
+
repository_id?: number;
|
|
64
|
+
message?: string;
|
|
65
|
+
at: string;
|
|
66
|
+
data?: any;
|
|
67
|
+
}
|
|
68
|
+
export interface SyncStatusPayload {
|
|
69
|
+
localChanges: number;
|
|
70
|
+
needsPush: boolean;
|
|
71
|
+
needsPull: boolean;
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldiagram/core-ui",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"test": "vitest run"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@buf/tldiagramcom_diagram.bufbuild_es": "^2.12.0-
|
|
39
|
+
"@buf/tldiagramcom_diagram.bufbuild_es": "^2.12.0-20260510134954-1dd1981cf3f6.1",
|
|
40
40
|
"@bufbuild/protobuf": "^2.11.0",
|
|
41
41
|
"esbuild": "^0.25.12",
|
|
42
42
|
"zustand": "^5.0.12"
|
|
@@ -126,8 +126,8 @@
|
|
|
126
126
|
"@connectrpc/connect-web": "^2.1.1",
|
|
127
127
|
"@emotion/react": "^11.14.0",
|
|
128
128
|
"@emotion/styled": "^11.14.0",
|
|
129
|
-
"@tanstack/react-query": "^5.100.1",
|
|
130
129
|
"@eslint/js": "^9.0.0",
|
|
130
|
+
"@tanstack/react-query": "^5.100.1",
|
|
131
131
|
"@types/dagre": "^0.7.54",
|
|
132
132
|
"@types/react": "^18.3.12",
|
|
133
133
|
"@types/react-dom": "^18.3.1",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { normalizeConnectorRouteStyle } from './client'
|
|
3
|
+
|
|
4
|
+
describe('normalizeConnectorRouteStyle', () => {
|
|
5
|
+
it('keeps valid route styles', () => {
|
|
6
|
+
expect(normalizeConnectorRouteStyle('bezier')).toBe('bezier')
|
|
7
|
+
expect(normalizeConnectorRouteStyle('straight')).toBe('straight')
|
|
8
|
+
expect(normalizeConnectorRouteStyle('step')).toBe('step')
|
|
9
|
+
expect(normalizeConnectorRouteStyle('smoothstep')).toBe('smoothstep')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('maps legacy line styles to bezier', () => {
|
|
13
|
+
expect(normalizeConnectorRouteStyle('solid')).toBe('bezier')
|
|
14
|
+
expect(normalizeConnectorRouteStyle('dashed')).toBe('bezier')
|
|
15
|
+
expect(normalizeConnectorRouteStyle(undefined)).toBe('bezier')
|
|
16
|
+
})
|
|
17
|
+
})
|
package/src/api/client.ts
CHANGED
|
@@ -59,6 +59,12 @@ import {
|
|
|
59
59
|
import { transport } from './transport'
|
|
60
60
|
import { apiUrl, fetchApiAsset } from '../config/runtime'
|
|
61
61
|
|
|
62
|
+
const CONNECTOR_ROUTE_STYLES = new Set(['bezier', 'straight', 'step', 'smoothstep'])
|
|
63
|
+
|
|
64
|
+
export function normalizeConnectorRouteStyle(style: unknown): string {
|
|
65
|
+
return typeof style === 'string' && CONNECTOR_ROUTE_STYLES.has(style) ? style : 'bezier'
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
async function responseError(res: Response, fallback: string): Promise<Error> {
|
|
63
69
|
const body = await res.json().catch(() => null) as { error?: string } | null
|
|
64
70
|
return new Error(body?.error || `${fallback}: ${res.statusText}`)
|
|
@@ -403,7 +409,7 @@ function protoConnector(e: Record<string, unknown>): Connector {
|
|
|
403
409
|
description: (e.description ?? null) as string | null,
|
|
404
410
|
relationship: (e.relationship ?? null) as string | null,
|
|
405
411
|
direction: String(e.direction ?? 'forward'),
|
|
406
|
-
style:
|
|
412
|
+
style: normalizeConnectorRouteStyle(e.style),
|
|
407
413
|
url: (e.url ?? null) as string | null,
|
|
408
414
|
source_handle: (e.source_handle ?? null) as string | null,
|
|
409
415
|
target_handle: (e.target_handle ?? null) as string | null,
|
|
@@ -936,7 +942,7 @@ export const api = {
|
|
|
936
942
|
description: data.description ?? undefined,
|
|
937
943
|
relationship: data.relationship ?? undefined,
|
|
938
944
|
direction: data.direction ?? undefined,
|
|
939
|
-
style: data.style
|
|
945
|
+
style: normalizeConnectorRouteStyle(data.style),
|
|
940
946
|
url: data.url ?? undefined,
|
|
941
947
|
sourceHandle: data.source_handle ?? undefined,
|
|
942
948
|
targetHandle: data.target_handle ?? undefined,
|
|
@@ -970,7 +976,7 @@ export const api = {
|
|
|
970
976
|
description: data.description ?? undefined,
|
|
971
977
|
relationship: data.relationship ?? undefined,
|
|
972
978
|
direction: data.direction ?? undefined,
|
|
973
|
-
style: data.style
|
|
979
|
+
style: data.style === undefined ? undefined : normalizeConnectorRouteStyle(data.style),
|
|
974
980
|
url: data.url ?? undefined,
|
|
975
981
|
sourceHandle: data.source_handle ?? undefined,
|
|
976
982
|
targetHandle: data.target_handle ?? undefined,
|
|
@@ -66,7 +66,7 @@ export default function LayoutSection({ view, canEdit, onUnsupportedMutation }:
|
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
const [forceConfig, setForceConfig] = useState<ForceConfig>({
|
|
69
|
-
linkDistance:
|
|
69
|
+
linkDistance: 300,
|
|
70
70
|
chargeStrength: -150,
|
|
71
71
|
collideRadius: 130,
|
|
72
72
|
iterations: 300,
|
|
@@ -576,17 +576,17 @@ export default function LayoutSection({ view, canEdit, onUnsupportedMutation }:
|
|
|
576
576
|
</Button>
|
|
577
577
|
{/* Apply button */}
|
|
578
578
|
<VStack spacing={2} w="full">
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
579
|
+
<Button
|
|
580
|
+
size="sm"
|
|
581
|
+
w="full"
|
|
582
|
+
variant="outline"
|
|
583
|
+
colorScheme="blue"
|
|
584
|
+
onClick={adjustConnectorsConfirm.onOpen}
|
|
585
|
+
isLoading={collisionRunning}
|
|
586
|
+
isDisabled={!canEdit || !view}
|
|
587
|
+
loadingText="Removing Connector Collisions..."
|
|
588
|
+
fontWeight="bold"
|
|
589
|
+
fontSize="xs"
|
|
590
590
|
letterSpacing="0.05em"
|
|
591
591
|
textTransform="uppercase"
|
|
592
592
|
h="32px"
|
|
@@ -104,6 +104,8 @@ export const TagItem: React.FC<Props> = ({
|
|
|
104
104
|
onDragOver={handleDragOver}
|
|
105
105
|
onDragLeave={handleDragLeave}
|
|
106
106
|
onDrop={handleDrop}
|
|
107
|
+
draggable
|
|
108
|
+
onDragStart={handleDragStart}
|
|
107
109
|
cursor="grab"
|
|
108
110
|
_active={{ cursor: 'grabbing' }}
|
|
109
111
|
role="group"
|
|
@@ -46,6 +46,8 @@ import {
|
|
|
46
46
|
type VisibleNodeAnchor,
|
|
47
47
|
} from './proxy'
|
|
48
48
|
|
|
49
|
+
const MAX_PROXY_HOVER_VIEW_LINKS = 5
|
|
50
|
+
|
|
49
51
|
export interface ZUICanvasHandle {
|
|
50
52
|
fitView(): void
|
|
51
53
|
focusDiagram(viewId: number): boolean
|
|
@@ -1053,7 +1055,7 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
1053
1055
|
</VStack>
|
|
1054
1056
|
<Divider borderColor="whiteAlpha.200" />
|
|
1055
1057
|
<VStack align="stretch" spacing={2} width="full">
|
|
1056
|
-
{hoveredItem.data.details.ownerViewIds.map((ownerViewId, index) => (
|
|
1058
|
+
{hoveredItem.data.details.ownerViewIds.slice(0, MAX_PROXY_HOVER_VIEW_LINKS).map((ownerViewId, index) => (
|
|
1057
1059
|
<Button
|
|
1058
1060
|
key={`${ownerViewId}-${index}`}
|
|
1059
1061
|
as={RouterLink}
|
|
@@ -1069,6 +1071,11 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
1069
1071
|
{hoveredItem.data.details!.ownerViewNames[index] ?? `Open View ${ownerViewId}`}
|
|
1070
1072
|
</Button>
|
|
1071
1073
|
))}
|
|
1074
|
+
{hoveredItem.data.details.ownerViewIds.length > MAX_PROXY_HOVER_VIEW_LINKS && (
|
|
1075
|
+
<Text fontSize="xs" color="gray.500" textAlign="center">
|
|
1076
|
+
+{hoveredItem.data.details.ownerViewIds.length - MAX_PROXY_HOVER_VIEW_LINKS} more view{hoveredItem.data.details.ownerViewIds.length - MAX_PROXY_HOVER_VIEW_LINKS === 1 ? '' : 's'}
|
|
1077
|
+
</Text>
|
|
1078
|
+
)}
|
|
1072
1079
|
</VStack>
|
|
1073
1080
|
<Divider borderColor="whiteAlpha.200" />
|
|
1074
1081
|
<HStack width="full" spacing={2}>
|
|
@@ -284,6 +284,26 @@ function expectScreenRectVisible(
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
describe('ZUI focus targets', () => {
|
|
287
|
+
it('derives ZUI node icons from assigned catalog technology connectors', () => {
|
|
288
|
+
const data: ExploreData = {
|
|
289
|
+
tree: [treeNode(1, 'Root', null, null)],
|
|
290
|
+
navigations: [],
|
|
291
|
+
views: {
|
|
292
|
+
1: {
|
|
293
|
+
placements: [{
|
|
294
|
+
...placed(1, 1, 0, 0),
|
|
295
|
+
technology_connectors: [{ type: 'catalog', slug: 'golang', label: 'Go', is_primary_icon: true }],
|
|
296
|
+
}],
|
|
297
|
+
connectors: [],
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const layout = computeLayout(data)
|
|
303
|
+
|
|
304
|
+
expect(layout.groups[0]?.nodes[0]?.logoUrl).toBe('/icons/golang.png')
|
|
305
|
+
})
|
|
306
|
+
|
|
287
307
|
it('rebases a high-zoom camera to a small centered render transform', () => {
|
|
288
308
|
const rebase = getCameraRebase(
|
|
289
309
|
{ x: -147_317_059.10654327, y: -184_315_493.52577353, zoom: 906_732.1382976775 },
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { collectVisibleNodeAnchors, getProxyBezierBadgeGeometry, type VisibleNodeAnchor } from './proxy'
|
|
3
|
+
import { DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA } from '../../crossBranch/settings'
|
|
4
|
+
import type { LayoutNode } from './types'
|
|
5
|
+
|
|
6
|
+
function node(id: string, elementId: number, children: LayoutNode[] = []): LayoutNode {
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
elementId,
|
|
10
|
+
diagramId: elementId,
|
|
11
|
+
worldX: 0,
|
|
12
|
+
worldY: 0,
|
|
13
|
+
worldW: 100,
|
|
14
|
+
worldH: 100,
|
|
15
|
+
label: id,
|
|
16
|
+
type: 'service',
|
|
17
|
+
logoUrl: null,
|
|
18
|
+
description: null,
|
|
19
|
+
technology: null,
|
|
20
|
+
tags: [],
|
|
21
|
+
ancestorElementIds: [],
|
|
22
|
+
pathElementIds: [elementId],
|
|
23
|
+
children,
|
|
24
|
+
childScale: 1,
|
|
25
|
+
childOffsetX: 0,
|
|
26
|
+
childOffsetY: 0,
|
|
27
|
+
edgesOut: [],
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('collectVisibleNodeAnchors', () => {
|
|
32
|
+
it('keeps fallback anchors for ancestors when only deeper descendants are self drawn', () => {
|
|
33
|
+
const grandchild = node('grandchild', 3)
|
|
34
|
+
const child = node('child', 2, [grandchild])
|
|
35
|
+
const parent = node('parent', 1, [child])
|
|
36
|
+
|
|
37
|
+
const anchors = collectVisibleNodeAnchors(
|
|
38
|
+
[{ nodes: [parent] }],
|
|
39
|
+
{ x: 0, y: 0, zoom: 5 },
|
|
40
|
+
1000,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
expect(anchors.visibleAnchors.get(3)?.renderAlpha).toBe(1)
|
|
44
|
+
expect(anchors.visibleAnchors.get(2)?.renderAlpha).toBeGreaterThan(0)
|
|
45
|
+
expect(anchors.visibleAnchors.get(1)?.renderAlpha).toBeGreaterThan(0)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('keeps fully expanded ancestor fallback anchors eligible for connectors', () => {
|
|
49
|
+
const grandchild = node('grandchild', 3)
|
|
50
|
+
const child = node('child', 2, [grandchild])
|
|
51
|
+
const parent = node('parent', 1, [child])
|
|
52
|
+
|
|
53
|
+
const anchors = collectVisibleNodeAnchors(
|
|
54
|
+
[{ nodes: [parent] }],
|
|
55
|
+
{ x: 0, y: 0, zoom: 5 },
|
|
56
|
+
1000,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
expect(anchors.visibleAnchors.get(1)?.renderAlpha).toBeGreaterThanOrEqual(DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA)
|
|
60
|
+
expect(anchors.visibleAnchors.get(2)?.renderAlpha).toBeGreaterThanOrEqual(DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('keeps ancestor anchors eligible while the body fades into the dashed border', () => {
|
|
64
|
+
const child = node('child', 2)
|
|
65
|
+
const parent = node('parent', 1, [child])
|
|
66
|
+
|
|
67
|
+
const anchors = collectVisibleNodeAnchors(
|
|
68
|
+
[{ nodes: [parent] }],
|
|
69
|
+
{ x: 0, y: 0, zoom: 3.7 },
|
|
70
|
+
1000,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
expect(anchors.visibleAnchors.get(1)?.renderAlpha).toBeGreaterThanOrEqual(DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
function anchor(partial: Partial<VisibleNodeAnchor>): VisibleNodeAnchor {
|
|
78
|
+
return {
|
|
79
|
+
nodeId: partial.nodeId ?? 'node',
|
|
80
|
+
elementId: partial.elementId ?? 1,
|
|
81
|
+
label: partial.label ?? 'node',
|
|
82
|
+
worldX: partial.worldX ?? 0,
|
|
83
|
+
worldY: partial.worldY ?? 0,
|
|
84
|
+
worldW: partial.worldW ?? 100,
|
|
85
|
+
worldH: partial.worldH ?? 100,
|
|
86
|
+
pathDepth: partial.pathDepth ?? 1,
|
|
87
|
+
renderAlpha: partial.renderAlpha ?? 1,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('getProxyBezierBadgeGeometry', () => {
|
|
92
|
+
it('positions badges on the bezier curve instead of the straight connector chord', () => {
|
|
93
|
+
const source = anchor({ worldX: 0, worldY: 0, worldW: 200, worldH: 100 })
|
|
94
|
+
const target = anchor({ elementId: 2, worldX: 300, worldY: 180, worldW: 80, worldH: 100 })
|
|
95
|
+
|
|
96
|
+
const geometry = getProxyBezierBadgeGeometry(source, target)
|
|
97
|
+
|
|
98
|
+
expect(geometry.midX).not.toBe((200 + 300) / 2)
|
|
99
|
+
expect(Math.hypot(geometry.tangentX, geometry.tangentY)).toBeGreaterThan(0)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
@@ -10,6 +10,12 @@ import type { LayoutNode, ZUIViewState, HoveredItem } from './types'
|
|
|
10
10
|
import { getExpandThresholds, pickEdgeLabelPosition, type ScreenRect } from './renderer'
|
|
11
11
|
import type { CrossBranchContextSettings } from '../../crossBranch/types'
|
|
12
12
|
import { DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA } from '../../crossBranch/settings'
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_SOURCE_HANDLE_SIDE,
|
|
15
|
+
DEFAULT_TARGET_HANDLE_SIDE,
|
|
16
|
+
getHandleFlowPosition,
|
|
17
|
+
type LogicalHandleSide,
|
|
18
|
+
} from '../../utils/edgeDistribution'
|
|
13
19
|
|
|
14
20
|
export interface VisibleNodeAnchor {
|
|
15
21
|
nodeId: string
|
|
@@ -35,6 +41,10 @@ function transitionT(screenW: number, start: number, end: number): number {
|
|
|
35
41
|
return clamp((screenW - start) / (end - start), 0, 1)
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
function zoomableNodeConnectorAlpha(inheritedAlpha: number, bodyAlpha: number): number {
|
|
45
|
+
return Math.max(bodyAlpha, Math.min(inheritedAlpha, DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA))
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
function visualRectForNode(
|
|
39
49
|
absX: number,
|
|
40
50
|
absY: number,
|
|
@@ -90,8 +100,10 @@ function collectVisibleAnchorForNode(
|
|
|
90
100
|
parentAbsScale: number,
|
|
91
101
|
parentChildOffsetX: number,
|
|
92
102
|
parentChildOffsetY: number,
|
|
93
|
-
) {
|
|
94
|
-
if (hiddenTags.size > 0 && node.tags.some((tag) => hiddenTags.has(tag)))
|
|
103
|
+
): { selfDrawn: boolean; descendantDrawn: boolean } {
|
|
104
|
+
if (hiddenTags.size > 0 && node.tags.some((tag) => hiddenTags.has(tag))) {
|
|
105
|
+
return { selfDrawn: false, descendantDrawn: false }
|
|
106
|
+
}
|
|
95
107
|
|
|
96
108
|
const absX = parentAbsX + (node.worldX - parentChildOffsetX) * parentAbsScale
|
|
97
109
|
const absY = parentAbsY + (node.worldY - parentChildOffsetY) * parentAbsScale
|
|
@@ -99,7 +111,7 @@ function collectVisibleAnchorForNode(
|
|
|
99
111
|
const absW = node.worldW * absScale
|
|
100
112
|
const absH = node.worldH * absScale
|
|
101
113
|
const screenW = absW * view.zoom
|
|
102
|
-
if (screenW < 2) return { selfDrawn: false }
|
|
114
|
+
if (screenW < 2) return { selfDrawn: false, descendantDrawn: false }
|
|
103
115
|
|
|
104
116
|
const hasChildren = node.children && node.children.length > 0
|
|
105
117
|
const t = hasChildren ? transitionT(screenW, thresholds.start, thresholds.end) : 0
|
|
@@ -118,11 +130,11 @@ function collectVisibleAnchorForNode(
|
|
|
118
130
|
worldW: visualRect.worldW,
|
|
119
131
|
worldH: visualRect.worldH,
|
|
120
132
|
pathDepth: node.pathElementIds.length,
|
|
121
|
-
renderAlpha: hasChildren ? parentAlpha : inheritedAlpha,
|
|
133
|
+
renderAlpha: hasChildren ? zoomableNodeConnectorAlpha(inheritedAlpha, parentAlpha) : inheritedAlpha,
|
|
122
134
|
})
|
|
123
135
|
}
|
|
124
136
|
|
|
125
|
-
let
|
|
137
|
+
let hasVisibleDescendant = false
|
|
126
138
|
if (hasChildren && t > 0.05) {
|
|
127
139
|
for (const child of node.children) {
|
|
128
140
|
const childResult = collectVisibleAnchorForNode(
|
|
@@ -139,11 +151,11 @@ function collectVisibleAnchorForNode(
|
|
|
139
151
|
node.childOffsetX,
|
|
140
152
|
node.childOffsetY,
|
|
141
153
|
)
|
|
142
|
-
|
|
154
|
+
hasVisibleDescendant = hasVisibleDescendant || childResult.selfDrawn || childResult.descendantDrawn
|
|
143
155
|
}
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
if (!selfDrawn &&
|
|
158
|
+
if (!selfDrawn && hasVisibleDescendant) {
|
|
147
159
|
registerVisibleAnchor(node, visibleAnchors, byNodeId, {
|
|
148
160
|
nodeId: node.id,
|
|
149
161
|
elementId: node.elementId,
|
|
@@ -153,11 +165,11 @@ function collectVisibleAnchorForNode(
|
|
|
153
165
|
worldW: visualRect.worldW,
|
|
154
166
|
worldH: visualRect.worldH,
|
|
155
167
|
pathDepth: node.pathElementIds.length,
|
|
156
|
-
renderAlpha:
|
|
168
|
+
renderAlpha: zoomableNodeConnectorAlpha(inheritedAlpha, inheritedAlpha * 0.28),
|
|
157
169
|
})
|
|
158
170
|
}
|
|
159
171
|
|
|
160
|
-
return { selfDrawn }
|
|
172
|
+
return { selfDrawn, descendantDrawn: selfDrawn || hasVisibleDescendant }
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
function collectVisibleAnchorsInNodes(
|
|
@@ -287,6 +299,84 @@ function getDirectAnchorPoints(source: VisibleNodeAnchor, target: VisibleNodeAnc
|
|
|
287
299
|
return { sourcePoint, targetPoint }
|
|
288
300
|
}
|
|
289
301
|
|
|
302
|
+
type AnchorSide = 'top' | 'bottom' | 'left' | 'right'
|
|
303
|
+
|
|
304
|
+
interface AnchorPoint {
|
|
305
|
+
x: number
|
|
306
|
+
y: number
|
|
307
|
+
side: AnchorSide
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
interface ProxyBezierBadgeGeometry {
|
|
311
|
+
midX: number
|
|
312
|
+
midY: number
|
|
313
|
+
tangentX: number
|
|
314
|
+
tangentY: number
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function bezierControlPoint(
|
|
318
|
+
point: AnchorPoint,
|
|
319
|
+
stem: number,
|
|
320
|
+
): AnchorPoint {
|
|
321
|
+
switch (point.side) {
|
|
322
|
+
case 'left':
|
|
323
|
+
return { x: point.x - stem, y: point.y, side: point.side }
|
|
324
|
+
case 'right':
|
|
325
|
+
return { x: point.x + stem, y: point.y, side: point.side }
|
|
326
|
+
case 'top':
|
|
327
|
+
return { x: point.x, y: point.y - stem, side: point.side }
|
|
328
|
+
case 'bottom':
|
|
329
|
+
return { x: point.x, y: point.y + stem, side: point.side }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function getProxyBezierBadgeGeometry(
|
|
334
|
+
source: VisibleNodeAnchor,
|
|
335
|
+
target: VisibleNodeAnchor,
|
|
336
|
+
options: {
|
|
337
|
+
sourceHandle?: string | null
|
|
338
|
+
targetHandle?: string | null
|
|
339
|
+
sourceFallback?: LogicalHandleSide
|
|
340
|
+
targetFallback?: LogicalHandleSide
|
|
341
|
+
} = {},
|
|
342
|
+
): ProxyBezierBadgeGeometry {
|
|
343
|
+
const sourcePoint = getHandleFlowPosition(
|
|
344
|
+
source.worldX,
|
|
345
|
+
source.worldY,
|
|
346
|
+
source.worldW,
|
|
347
|
+
source.worldH,
|
|
348
|
+
options.sourceHandle,
|
|
349
|
+
options.sourceFallback ?? DEFAULT_SOURCE_HANDLE_SIDE,
|
|
350
|
+
)
|
|
351
|
+
const targetPoint = getHandleFlowPosition(
|
|
352
|
+
target.worldX,
|
|
353
|
+
target.worldY,
|
|
354
|
+
target.worldW,
|
|
355
|
+
target.worldH,
|
|
356
|
+
options.targetHandle,
|
|
357
|
+
options.targetFallback ?? DEFAULT_TARGET_HANDLE_SIDE,
|
|
358
|
+
)
|
|
359
|
+
const dx = Math.abs(targetPoint.x - sourcePoint.x)
|
|
360
|
+
const dy = Math.abs(targetPoint.y - sourcePoint.y)
|
|
361
|
+
const sourceStem = Math.max(
|
|
362
|
+
(sourcePoint.side === 'left' || sourcePoint.side === 'right' ? dx : dy) * 0.5,
|
|
363
|
+
(sourcePoint.side === 'left' || sourcePoint.side === 'right' ? source.worldW : source.worldH) * 0.5,
|
|
364
|
+
)
|
|
365
|
+
const targetStem = Math.max(
|
|
366
|
+
(targetPoint.side === 'left' || targetPoint.side === 'right' ? dx : dy) * 0.5,
|
|
367
|
+
(targetPoint.side === 'left' || targetPoint.side === 'right' ? target.worldW : target.worldH) * 0.5,
|
|
368
|
+
)
|
|
369
|
+
const cp1 = bezierControlPoint(sourcePoint, sourceStem)
|
|
370
|
+
const cp2 = bezierControlPoint(targetPoint, targetStem)
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
midX: 0.125 * sourcePoint.x + 0.375 * cp1.x + 0.375 * cp2.x + 0.125 * targetPoint.x,
|
|
374
|
+
midY: 0.125 * sourcePoint.y + 0.375 * cp1.y + 0.375 * cp2.y + 0.125 * targetPoint.y,
|
|
375
|
+
tangentX: -0.75 * sourcePoint.x - 0.75 * cp1.x + 0.75 * cp2.x + 0.75 * targetPoint.x,
|
|
376
|
+
tangentY: -0.75 * sourcePoint.y - 0.75 * cp1.y + 0.75 * cp2.y + 0.75 * targetPoint.y,
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
290
380
|
function getDevicePixelRatio(): number {
|
|
291
381
|
return typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
|
|
292
382
|
}
|
|
@@ -536,9 +626,18 @@ export function drawVisibleDirectProxyBadges(
|
|
|
536
626
|
const alpha = Math.min(source.renderAlpha, target.renderAlpha)
|
|
537
627
|
if (alpha < 0.01) continue
|
|
538
628
|
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
629
|
+
const sourceFallback = badge.details.connectors[0]?.connector.source_element_id === badge.sourceAnchorElementId
|
|
630
|
+
? DEFAULT_SOURCE_HANDLE_SIDE
|
|
631
|
+
: DEFAULT_TARGET_HANDLE_SIDE
|
|
632
|
+
const targetFallback = badge.details.connectors[0]?.connector.target_element_id === badge.targetAnchorElementId
|
|
633
|
+
? DEFAULT_TARGET_HANDLE_SIDE
|
|
634
|
+
: DEFAULT_SOURCE_HANDLE_SIDE
|
|
635
|
+
const { midX, midY, tangentX, tangentY } = getProxyBezierBadgeGeometry(source, target, {
|
|
636
|
+
sourceHandle: badge.sourceHandle,
|
|
637
|
+
targetHandle: badge.targetHandle,
|
|
638
|
+
sourceFallback,
|
|
639
|
+
targetFallback,
|
|
640
|
+
})
|
|
542
641
|
const label = `+${badge.count}`
|
|
543
642
|
|
|
544
643
|
ctx.save()
|
|
@@ -550,8 +649,8 @@ export function drawVisibleDirectProxyBadges(
|
|
|
550
649
|
midY,
|
|
551
650
|
badgeMetrics.worldW,
|
|
552
651
|
badgeMetrics.worldH,
|
|
553
|
-
|
|
554
|
-
|
|
652
|
+
tangentX,
|
|
653
|
+
tangentY,
|
|
555
654
|
occupiedLabelRects,
|
|
556
655
|
)
|
|
557
656
|
drawFixedScreenProxyBadge(
|