@work-graph/cli 0.2.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/README.md +31 -0
- package/bin/work-graph.mjs +238 -0
- package/package.json +38 -0
- package/vendor/packages/design-tokens/generated/gripe-dark-default.css +67 -0
- package/vendor/packages/design-tokens/generated/marketplace-default.css +67 -0
- package/vendor/packages/design-tokens/generated/workgraph-dark.css +67 -0
- package/vendor/packages/workgraph-mcp/README.md +28 -0
- package/vendor/packages/workgraph-mcp/bin/workgraph-mcp.mjs +21 -0
- package/vendor/packages/workgraph-mcp/package.json +37 -0
- package/vendor/packages/workgraph-mcp/src/handlers.mjs +761 -0
- package/vendor/packages/workgraph-mcp/src/index.mjs +638 -0
- package/vendor/packages/workgraph-mcp/src/prompts.mjs +162 -0
- package/vendor/public/assets/workgraph-logo.svg +11 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Medium.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Regular.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Semibold.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/stylesheet.css +25 -0
- package/vendor/public/graph-canvas-lit-flow.css +154 -0
- package/vendor/public/graph-canvas-lit-flow.css.map +7 -0
- package/vendor/public/graph-canvas-lit-flow.js +8530 -0
- package/vendor/public/graph-canvas-lit-flow.js.map +7 -0
- package/vendor/src/agentBehaviorRulesAudit.mjs +168 -0
- package/vendor/src/agentBehaviorRulesBundle.mjs +144 -0
- package/vendor/src/agentRunApi.mjs +136 -0
- package/vendor/src/agentToolLoopGuard.mjs +88 -0
- package/vendor/src/agentWorkerClaudeProvider.mjs +288 -0
- package/vendor/src/agentWorkerCursorSdkProvider.mjs +156 -0
- package/vendor/src/agentWorkerLiveLoop.mjs +455 -0
- package/vendor/src/agentWorkerLocalCliProvider.mjs +217 -0
- package/vendor/src/agentWorkerLocalRunner.mjs +246 -0
- package/vendor/src/agentWorkerOpenAiProvider.mjs +459 -0
- package/vendor/src/analyticsPanelProjection.mjs +212 -0
- package/vendor/src/analyticsRecordStore.mjs +165 -0
- package/vendor/src/analyticsRecordWorkItems.mjs +104 -0
- package/vendor/src/architectureL1Canon.mjs +419 -0
- package/vendor/src/architectureLayout.mjs +229 -0
- package/vendor/src/architectureSnapshot.mjs +490 -0
- package/vendor/src/architectureViewsProjection.mjs +116 -0
- package/vendor/src/atomInspector.mjs +253 -0
- package/vendor/src/atomInspectorApi.mjs +130 -0
- package/vendor/src/auditGapMatrixRefresh.mjs +121 -0
- package/vendor/src/backlogSchemaLint.mjs +176 -0
- package/vendor/src/blockedOnebaseGoPreflightEval.mjs +100 -0
- package/vendor/src/bracketIrTraceSignal.mjs +93 -0
- package/vendor/src/bvcAtomParser.mjs +210 -0
- package/vendor/src/bvcDialectRegistry.mjs +86 -0
- package/vendor/src/bvcFileFormat.mjs +218 -0
- package/vendor/src/bvcFormatCli.mjs +55 -0
- package/vendor/src/bvcLintCli.mjs +48 -0
- package/vendor/src/bvcNewWritePolicy.mjs +70 -0
- package/vendor/src/charterPreflightPromoteGate.mjs +194 -0
- package/vendor/src/claimNoEligibleEval.mjs +205 -0
- package/vendor/src/closingAnalysisSuggest.mjs +59 -0
- package/vendor/src/codeGapAnalyzer.mjs +308 -0
- package/vendor/src/codeGapBacklogFeeder.mjs +82 -0
- package/vendor/src/codeGapDraftIntakeApi.mjs +307 -0
- package/vendor/src/codeGapOperatorProjection.mjs +60 -0
- package/vendor/src/codeSyntaxHighlight.mjs +123 -0
- package/vendor/src/codegenEvidence.mjs +187 -0
- package/vendor/src/compilerRoundTripCli.mjs +164 -0
- package/vendor/src/dagreGraphLayout.mjs +78 -0
- package/vendor/src/draftIntakePromotionRules.mjs +205 -0
- package/vendor/src/epicWorkScope.mjs +85 -0
- package/vendor/src/evalLiveLlmEnv.mjs +63 -0
- package/vendor/src/evidenceReadModel.mjs +167 -0
- package/vendor/src/gfsOverlayProjectPassport.mjs +235 -0
- package/vendor/src/globalStepPathToBvcReferences.mjs +196 -0
- package/vendor/src/goldenPath.mjs +69 -0
- package/vendor/src/graphCanvasLayout.mjs +464 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasMinimap.ts +261 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasSvgEdges.ts +259 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasTheme.css +152 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCardNode.ts +328 -0
- package/vendor/src/graphCanvasLitFlow/client/mountGraphCanvasLitFlow.ts +322 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeLabels.mjs +58 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeRouter.mjs +142 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasLayoutProfile.mjs +32 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasNodeMetrics.mjs +45 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasProjection.mjs +115 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasProjectionToFlow.mjs +133 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasTraversal.mjs +77 -0
- package/vendor/src/graphCanvasLitFlow/layoutIntentRoadmapWorkStack.mjs +73 -0
- package/vendor/src/graphCanvasLitFlow/resolveGraphCanvasOverlaps.mjs +77 -0
- package/vendor/src/graphRagContextSlice.mjs +461 -0
- package/vendor/src/gvmVerifyWorkerGate.mjs +95 -0
- package/vendor/src/homeSnapshotApi.mjs +131 -0
- package/vendor/src/homeSnapshotProjection.mjs +275 -0
- package/vendor/src/inboxEventStream.mjs +140 -0
- package/vendor/src/intentComposerApi.mjs +245 -0
- package/vendor/src/intentGraphGbcSliceBoundary.mjs +258 -0
- package/vendor/src/intentGraphProjection.mjs +208 -0
- package/vendor/src/intentHierarchy.mjs +241 -0
- package/vendor/src/intentNodeLint.mjs +107 -0
- package/vendor/src/intentNodeRuntime.mjs +185 -0
- package/vendor/src/intentRoadmapCanvas.mjs +393 -0
- package/vendor/src/intentRoadmapEpicProjection.mjs +122 -0
- package/vendor/src/intentRoadmapMermaid.mjs +165 -0
- package/vendor/src/intentRoadmapProjection.mjs +85 -0
- package/vendor/src/intentTreeLint.mjs +114 -0
- package/vendor/src/intentTreeMigration.mjs +150 -0
- package/vendor/src/intentTreeWorkItems.mjs +227 -0
- package/vendor/src/kanbanBoardProjection.mjs +58 -0
- package/vendor/src/languageAdapterRegistry.mjs +180 -0
- package/vendor/src/languageAdapters/goAdapter.mjs +62 -0
- package/vendor/src/languageAdapters/jsTsAdapter.mjs +60 -0
- package/vendor/src/languageAdapters/jsonYamlAdapter.mjs +103 -0
- package/vendor/src/languageAdapters/onebaseOsAdapter.mjs +55 -0
- package/vendor/src/languageAdapters/plaintextAdapter.mjs +36 -0
- package/vendor/src/languageAdapters/shared.mjs +68 -0
- package/vendor/src/languageAdapters/stepAdapter.mjs +81 -0
- package/vendor/src/lintPlanWorkAlignment.mjs +136 -0
- package/vendor/src/loopHintRepeatToolEval.mjs +153 -0
- package/vendor/src/lowcodeScaffoldCli.mjs +386 -0
- package/vendor/src/markdownDocumentRender.mjs +208 -0
- package/vendor/src/memoryPanelProjection.mjs +116 -0
- package/vendor/src/memoryRecordWriter.mjs +243 -0
- package/vendor/src/memoryWorkerSlice.mjs +238 -0
- package/vendor/src/migrateStepToBvc.mjs +133 -0
- package/vendor/src/missionControlServerHandlers.mjs +195 -0
- package/vendor/src/missionControlUiClient.mjs +278 -0
- package/vendor/src/onebaseCliCapabilityProbe.mjs +107 -0
- package/vendor/src/onebaseCliRunner.mjs +145 -0
- package/vendor/src/onebaseGrossProfitStaticVerify.mjs +98 -0
- package/vendor/src/onebaseParityEvidenceSync.mjs +88 -0
- package/vendor/src/onebasePvrgGraphNodes.mjs +257 -0
- package/vendor/src/onebaseRestEvidenceAdapter.mjs +216 -0
- package/vendor/src/onebaseVectorDslCodegenReadiness.mjs +137 -0
- package/vendor/src/onebaseWorkItemTemplate.mjs +154 -0
- package/vendor/src/onebaseWorkerTools.mjs +586 -0
- package/vendor/src/operatorShellProjection.mjs +102 -0
- package/vendor/src/pipelineProseRender.mjs +180 -0
- package/vendor/src/pipelineStageLint.mjs +118 -0
- package/vendor/src/promptRulesEditorApi.mjs +174 -0
- package/vendor/src/promptRulesProjection.mjs +134 -0
- package/vendor/src/pvrg/bladeAdapter.mjs +40 -0
- package/vendor/src/pvrgTaskScope.mjs +152 -0
- package/vendor/src/releaseGateMatrix.mjs +188 -0
- package/vendor/src/schematicView.mjs +305 -0
- package/vendor/src/seedAnalyticsRecord.mjs +217 -0
- package/vendor/src/semanticSearchBm25.mjs +103 -0
- package/vendor/src/semanticSearchExcerpts.mjs +68 -0
- package/vendor/src/semanticSearchTfidfVector.mjs +86 -0
- package/vendor/src/semanticSearchWorkflow.mjs +366 -0
- package/vendor/src/stepAtomFormatter.mjs +413 -0
- package/vendor/src/stepGraphSlice.mjs +318 -0
- package/vendor/src/ui/atoms/badge.mjs +40 -0
- package/vendor/src/ui/atoms/badgeClient.mjs +32 -0
- package/vendor/src/ui/atoms/button.mjs +114 -0
- package/vendor/src/ui/atoms/buttonClient.mjs +49 -0
- package/vendor/src/ui/atoms/icon.mjs +23 -0
- package/vendor/src/ui/atoms/input.mjs +38 -0
- package/vendor/src/ui/atoms/modal.mjs +44 -0
- package/vendor/src/ui/atoms/select.mjs +98 -0
- package/vendor/src/ui/backlogShellButtons.mjs +238 -0
- package/vendor/src/ui/htmlEscape.mjs +11 -0
- package/vendor/src/ui/molecules/rating.mjs +48 -0
- package/vendor/src/ui/molecules/tabs.mjs +70 -0
- package/vendor/src/ui/organisms/modal.mjs +1 -0
- package/vendor/src/ui/pages/uiKitPage.mjs +147 -0
- package/vendor/src/ui/workItemStatusTone.mjs +36 -0
- package/vendor/src/unifiedLinkageProjection.mjs +264 -0
- package/vendor/src/verificationLoop.mjs +206 -0
- package/vendor/src/workGraphBacklogPersist.mjs +234 -0
- package/vendor/src/workGraphBacklogUiServer.mjs +9192 -0
- package/vendor/src/workGraphBoundedTargetFileRead.mjs +178 -0
- package/vendor/src/workGraphCycleSlice.mjs +184 -0
- package/vendor/src/workGraphDaemonTick.mjs +307 -0
- package/vendor/src/workGraphDaemonWatch.mjs +157 -0
- package/vendor/src/workGraphEngineRoot.mjs +136 -0
- package/vendor/src/workGraphInstallLayout.mjs +65 -0
- package/vendor/src/workGraphLlmUsefulnessEval.mjs +611 -0
- package/vendor/src/workGraphPhasePromoteReadyQueue.mjs +159 -0
- package/vendor/src/workGraphProjectHost.mjs +149 -0
- package/vendor/src/workGraphProjectInit.mjs +392 -0
- package/vendor/src/workGraphPromoteReadyApi.mjs +115 -0
- package/vendor/src/workGraphRecoveryPolicy.mjs +124 -0
- package/vendor/src/workGraphRunnerQueueProjection.mjs +187 -0
- package/vendor/src/workGraphRuntime.mjs +1008 -0
- package/vendor/src/workGraphToolSurfaceAudit.mjs +372 -0
- package/vendor/src/workGraphToolTransportRuntime.mjs +195 -0
- package/vendor/src/workGraphWorkerProvider.mjs +600 -0
- package/vendor/src/workItemBvcQuality.mjs +262 -0
- package/vendor/src/workItemCreateAnalysis.mjs +157 -0
- package/vendor/src/workItemDecisionPipeline.mjs +278 -0
- package/vendor/src/workItemEpicCascade.mjs +176 -0
- package/vendor/src/workItemExecutionGate.mjs +78 -0
- package/vendor/src/workItemHierarchy.mjs +226 -0
- package/vendor/src/workItemProseLint.mjs +133 -0
- package/vendor/src/workItemTextRusify.mjs +794 -0
- package/vendor/src/workItemTraceEnvelope.mjs +158 -0
- package/vendor/src/workItemUiReferences.mjs +272 -0
- package/vendor/src/workflowEpicGrouping.mjs +67 -0
- package/vendor/src/workflowTreeProjection.mjs +53 -0
- package/vendor/src/workspaceRegistry.mjs +150 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Graph edge router (n8n-style bezier / vertical cubic).
|
|
3
|
+
* Used by SVG overlay; lit-flow flow-edge is not used for product views.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {{ x: number, width: number }} node
|
|
8
|
+
* @param {number} targetCenterX
|
|
9
|
+
*/
|
|
10
|
+
function anchorX(node, targetCenterX) {
|
|
11
|
+
const ratio = (targetCenterX - node.x) / Math.max(node.width, 1);
|
|
12
|
+
return node.x + node.width * Math.max(0.15, Math.min(0.85, ratio));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{
|
|
17
|
+
* fromNode: { x: number, y: number, width: number, height: number },
|
|
18
|
+
* toNode: { x: number, y: number, width: number, height: number },
|
|
19
|
+
* rejected?: boolean,
|
|
20
|
+
* label?: string,
|
|
21
|
+
* }} edge
|
|
22
|
+
* @param {'LR' | 'TB' | string} [layoutDirection]
|
|
23
|
+
*/
|
|
24
|
+
export function buildGraphCanvasEdgeGeometry(edge, layoutDirection = 'LR') {
|
|
25
|
+
const from = edge.fromNode;
|
|
26
|
+
const to = edge.toNode;
|
|
27
|
+
const fromCy = from.y + from.height / 2;
|
|
28
|
+
const toCy = to.y + to.height / 2;
|
|
29
|
+
const toCx = to.x + to.width / 2;
|
|
30
|
+
const fromCx = from.x + from.width / 2;
|
|
31
|
+
|
|
32
|
+
let startX;
|
|
33
|
+
let startY;
|
|
34
|
+
let endX;
|
|
35
|
+
let endY;
|
|
36
|
+
let orientation;
|
|
37
|
+
|
|
38
|
+
if (layoutDirection === 'LR') {
|
|
39
|
+
const goesRight = to.x > from.x + from.width - 8;
|
|
40
|
+
if (goesRight) {
|
|
41
|
+
orientation = 'horizontal';
|
|
42
|
+
startX = from.x + from.width;
|
|
43
|
+
startY = fromCy;
|
|
44
|
+
endX = to.x;
|
|
45
|
+
endY = toCy;
|
|
46
|
+
} else if (toCy > fromCy + 8) {
|
|
47
|
+
orientation = 'vertical';
|
|
48
|
+
startX = anchorX(from, toCx);
|
|
49
|
+
endX = anchorX(to, fromCx);
|
|
50
|
+
startY = from.y + from.height;
|
|
51
|
+
endY = to.y;
|
|
52
|
+
} else {
|
|
53
|
+
orientation = 'vertical-reverse';
|
|
54
|
+
startX = anchorX(from, toCx);
|
|
55
|
+
endX = anchorX(to, fromCx);
|
|
56
|
+
startY = from.y;
|
|
57
|
+
endY = to.y + to.height;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
const goesDown = toCy > fromCy + 8;
|
|
61
|
+
if (goesDown) {
|
|
62
|
+
orientation = 'vertical';
|
|
63
|
+
startX = anchorX(from, toCx);
|
|
64
|
+
endX = anchorX(to, fromCx);
|
|
65
|
+
startY = from.y + from.height;
|
|
66
|
+
endY = to.y;
|
|
67
|
+
} else if (to.x >= from.x + from.width - 8) {
|
|
68
|
+
orientation = 'horizontal';
|
|
69
|
+
startX = from.x + from.width;
|
|
70
|
+
startY = fromCy;
|
|
71
|
+
endX = to.x;
|
|
72
|
+
endY = toCy;
|
|
73
|
+
} else {
|
|
74
|
+
orientation = 'horizontal-reverse';
|
|
75
|
+
startX = from.x;
|
|
76
|
+
startY = fromCy;
|
|
77
|
+
endX = to.x + to.width;
|
|
78
|
+
endY = toCy;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const dy = endY - startY;
|
|
83
|
+
const dx = endX - startX;
|
|
84
|
+
const bend = Math.max(28, Math.min(72, (Math.abs(dx) + Math.abs(dy)) * 0.22));
|
|
85
|
+
const d = orientation === 'vertical' || orientation === 'vertical-reverse'
|
|
86
|
+
? `M ${startX} ${startY} C ${startX} ${startY + Math.sign(dy || 1) * bend}, ${endX} ${endY - Math.sign(dy || 1) * bend}, ${endX} ${endY}`
|
|
87
|
+
: (() => {
|
|
88
|
+
const midX = startX + (endX - startX) / 2;
|
|
89
|
+
return `M ${startX} ${startY} C ${midX} ${startY}, ${midX} ${endY}, ${endX} ${endY}`;
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
const label = String(edge.label ?? '').trim();
|
|
93
|
+
const hideLabel = orientation === 'vertical' && label === 'подзадача';
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
d,
|
|
97
|
+
startX,
|
|
98
|
+
startY,
|
|
99
|
+
endX,
|
|
100
|
+
endY,
|
|
101
|
+
orientation,
|
|
102
|
+
rejected: edge.rejected === true,
|
|
103
|
+
upstream: edge.upstream === true,
|
|
104
|
+
label: hideLabel ? '' : label,
|
|
105
|
+
labelX: (startX + endX) / 2,
|
|
106
|
+
labelY: orientation === 'horizontal' || orientation === 'horizontal-reverse'
|
|
107
|
+
? Math.min(startY, endY) - 10
|
|
108
|
+
: startY + (endY - startY) / 2,
|
|
109
|
+
labelPlacement: orientation === 'horizontal' && to.x > from.x + from.width * 0.35
|
|
110
|
+
? 'start'
|
|
111
|
+
: 'center',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {{
|
|
117
|
+
* layoutDirection?: string,
|
|
118
|
+
* nodes?: Array<{ id: string, x?: number, y?: number, width?: number, height?: number }>,
|
|
119
|
+
* edges?: Array<{ id?: string, from: string, to: string, label?: string, rejected?: boolean, upstream?: boolean }>,
|
|
120
|
+
* }} projection
|
|
121
|
+
*/
|
|
122
|
+
export function buildGraphCanvasEdgeRoutes(projection) {
|
|
123
|
+
const layoutDirection = projection?.layoutDirection ?? 'LR';
|
|
124
|
+
const nodeById = new Map((projection?.nodes ?? []).map((node) => [node.id, node]));
|
|
125
|
+
const routes = [];
|
|
126
|
+
|
|
127
|
+
for (const edge of projection?.edges ?? []) {
|
|
128
|
+
const fromNode = nodeById.get(edge.from);
|
|
129
|
+
const toNode = nodeById.get(edge.to);
|
|
130
|
+
if (!fromNode || !toNode) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
routes.push({
|
|
134
|
+
id: edge.id ?? `${edge.from}-${edge.to}`,
|
|
135
|
+
from: edge.from,
|
|
136
|
+
to: edge.to,
|
|
137
|
+
...buildGraphCanvasEdgeGeometry({ ...edge, fromNode, toNode }, layoutDirection),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return routes;
|
|
142
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dagre spacing inspired by n8n / Vue Flow layout recipes:
|
|
3
|
+
* generous ranksep, nodesep >= rendered card height gap, edgesep for parallel edges.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {import('../dagreGraphLayout.mjs').DagreLayoutOptions} */
|
|
7
|
+
export const N8N_INSPIRED_DAGRE_LR = {
|
|
8
|
+
rankdir: 'LR',
|
|
9
|
+
ranksep: 128,
|
|
10
|
+
nodesep: 64,
|
|
11
|
+
edgesep: 32,
|
|
12
|
+
marginx: 36,
|
|
13
|
+
marginy: 36,
|
|
14
|
+
ranker: 'network-simplex',
|
|
15
|
+
align: 'UL',
|
|
16
|
+
resolveOverlaps: true,
|
|
17
|
+
overlapGap: 32,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** @type {import('../dagreGraphLayout.mjs').DagreLayoutOptions} */
|
|
21
|
+
export const N8N_INSPIRED_DAGRE_TB = {
|
|
22
|
+
rankdir: 'TB',
|
|
23
|
+
ranksep: 96,
|
|
24
|
+
nodesep: 56,
|
|
25
|
+
edgesep: 28,
|
|
26
|
+
marginx: 32,
|
|
27
|
+
marginy: 32,
|
|
28
|
+
ranker: 'network-simplex',
|
|
29
|
+
align: 'UL',
|
|
30
|
+
resolveOverlaps: true,
|
|
31
|
+
overlapGap: 28,
|
|
32
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Card width aligned with n8n-style node proportions (~240px). */
|
|
2
|
+
export const GRAPH_CARD_WIDTH = 240;
|
|
3
|
+
|
|
4
|
+
/** Minimum card height including layer pill + one title line. */
|
|
5
|
+
export const GRAPH_CARD_MIN_HEIGHT = 96;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Estimate rendered graph card height (must stay in sync with graphCardNode.ts).
|
|
9
|
+
*
|
|
10
|
+
* @param {{ title?: string, summary?: string, status?: string, layer?: boolean }} input
|
|
11
|
+
*/
|
|
12
|
+
export function estimateGraphCardHeight(input = {}) {
|
|
13
|
+
const title = String(input.title ?? '');
|
|
14
|
+
const summary = String(input.summary ?? '');
|
|
15
|
+
const hasStatus = String(input.status ?? '').trim() !== '';
|
|
16
|
+
const hasLayer = input.layer !== false;
|
|
17
|
+
|
|
18
|
+
const titleLines = Math.max(1, Math.ceil(title.length / 30));
|
|
19
|
+
const summaryLines = summary.trim() ? Math.max(1, Math.ceil(summary.length / 36)) : 0;
|
|
20
|
+
|
|
21
|
+
let height = 21;
|
|
22
|
+
if (hasLayer) {
|
|
23
|
+
height += 30;
|
|
24
|
+
}
|
|
25
|
+
height += titleLines * 18;
|
|
26
|
+
if (summaryLines) {
|
|
27
|
+
height += 6 + summaryLines * 16;
|
|
28
|
+
}
|
|
29
|
+
if (hasStatus) {
|
|
30
|
+
height += 31;
|
|
31
|
+
}
|
|
32
|
+
height += 12;
|
|
33
|
+
|
|
34
|
+
return Math.max(GRAPH_CARD_MIN_HEIGHT, height);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {{ title?: string, summary?: string, status?: string, layer?: boolean, width?: number }} input
|
|
39
|
+
*/
|
|
40
|
+
export function measureGraphCardNode(input = {}) {
|
|
41
|
+
return {
|
|
42
|
+
width: input.width ?? GRAPH_CARD_WIDTH,
|
|
43
|
+
height: estimateGraphCardHeight(input),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export const GRAPH_CANVAS_LIT_FLOW_PROJECTION_SCHEMA = 'workgraph.graph-canvas-lit-flow-projection.v1';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {object} canvas intent roadmap canvas model
|
|
5
|
+
* @param {{ viewId?: string }} [options]
|
|
6
|
+
*/
|
|
7
|
+
export function buildGraphCanvasProjectionFromIntentCanvas(canvas, options = {}) {
|
|
8
|
+
const nodes = (canvas?.nodes ?? []).map((node) => ({
|
|
9
|
+
id: node.id,
|
|
10
|
+
kind: node.kind ?? 'unknown',
|
|
11
|
+
title: node.title ?? node.id,
|
|
12
|
+
layer: node.layer ?? '',
|
|
13
|
+
x: node.x ?? 0,
|
|
14
|
+
y: node.y ?? 0,
|
|
15
|
+
width: node.width ?? 220,
|
|
16
|
+
height: node.height ?? 78,
|
|
17
|
+
selected: node.selected === true,
|
|
18
|
+
rejected: node.kind === 'intent_option' && node.selected !== true,
|
|
19
|
+
status: node.status ?? '',
|
|
20
|
+
taskId: node.kind === 'work_item' || node.kind === 'work_epic' ? (node.workId ?? node.id) : undefined,
|
|
21
|
+
intentNodeId: String(node.kind ?? '').startsWith('intent_') ? node.id : undefined,
|
|
22
|
+
doneChildCount: node.doneChildCount,
|
|
23
|
+
childCount: node.childCount,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const edges = (canvas?.edges ?? []).map((edge, index) => ({
|
|
27
|
+
id: edge.id ?? `${edge.from}-${edge.to}-${index}`,
|
|
28
|
+
from: edge.from,
|
|
29
|
+
to: edge.to,
|
|
30
|
+
label: edge.label ?? '',
|
|
31
|
+
rejected: edge.rejected === true,
|
|
32
|
+
upstream: false,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
schema: GRAPH_CANVAS_LIT_FLOW_PROJECTION_SCHEMA,
|
|
37
|
+
layoutDirection: canvas?.layoutDirection ?? 'LR',
|
|
38
|
+
viewId: options.viewId ?? 'intent-roadmap',
|
|
39
|
+
nodes,
|
|
40
|
+
edges,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {object} layout architecture layout from buildArchitectureLayout
|
|
46
|
+
* @param {{ viewId?: string }} [options]
|
|
47
|
+
*/
|
|
48
|
+
export function buildGraphCanvasProjectionFromArchitectureLayout(layout, options = {}) {
|
|
49
|
+
const nodes = (layout?.nodes ?? []).map((node) => ({
|
|
50
|
+
id: node.block?.id ?? node.id,
|
|
51
|
+
kind: 'architecture_block',
|
|
52
|
+
title: node.block?.title ?? node.block?.id ?? node.id,
|
|
53
|
+
layer: node.block?.layer ?? '',
|
|
54
|
+
summary: node.block?.summary ?? '',
|
|
55
|
+
x: node.x ?? 0,
|
|
56
|
+
y: node.y ?? 0,
|
|
57
|
+
width: node.width ?? 220,
|
|
58
|
+
height: node.height ?? 78,
|
|
59
|
+
focused: node.focused === true,
|
|
60
|
+
blockId: node.block?.id ?? node.id,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const edges = (layout?.edges ?? []).map((edge, index) => ({
|
|
64
|
+
id: edge.id ?? `${edge.from}-${edge.to}-${index}`,
|
|
65
|
+
from: edge.from,
|
|
66
|
+
to: edge.to,
|
|
67
|
+
label: edge.label ?? edge.type ?? '',
|
|
68
|
+
rejected: false,
|
|
69
|
+
upstream: edge.upstream === true || edge.type === 'maps_to',
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
schema: GRAPH_CANVAS_LIT_FLOW_PROJECTION_SCHEMA,
|
|
74
|
+
layoutDirection: 'LR',
|
|
75
|
+
viewId: options.viewId ?? 'architecture',
|
|
76
|
+
nodes,
|
|
77
|
+
edges,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {object} model schematic view model
|
|
83
|
+
* @param {{ viewId?: string }} [options]
|
|
84
|
+
*/
|
|
85
|
+
export function buildGraphCanvasProjectionFromSchematicModel(model, options = {}) {
|
|
86
|
+
const nodes = (model?.nodes ?? []).map((node) => ({
|
|
87
|
+
id: node.id,
|
|
88
|
+
kind: 'schematic_block',
|
|
89
|
+
title: node.title ?? node.id,
|
|
90
|
+
layer: node.layer ?? '',
|
|
91
|
+
summary: node.summary ?? '',
|
|
92
|
+
x: node.x ?? 0,
|
|
93
|
+
y: node.y ?? 0,
|
|
94
|
+
width: node.width ?? 220,
|
|
95
|
+
height: node.height ?? 78,
|
|
96
|
+
schematicId: node.id,
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const edges = (model?.edges ?? []).map((edge, index) => ({
|
|
100
|
+
id: edge.id ?? `${edge.from}-${edge.to}-${index}`,
|
|
101
|
+
from: edge.from,
|
|
102
|
+
to: edge.to,
|
|
103
|
+
label: edge.label ?? edge.type ?? '',
|
|
104
|
+
rejected: false,
|
|
105
|
+
upstream: edge.upstream === true,
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
schema: GRAPH_CANVAS_LIT_FLOW_PROJECTION_SCHEMA,
|
|
110
|
+
layoutDirection: 'LR',
|
|
111
|
+
viewId: options.viewId ?? 'schematic',
|
|
112
|
+
nodes,
|
|
113
|
+
edges,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildGraphCanvasEdgeLabelHtml,
|
|
3
|
+
buildGraphCanvasEdgeStrokeStyle,
|
|
4
|
+
} from './graphCanvasEdgeLabels.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {object} edge
|
|
8
|
+
* @param {Array<{ id: string, x?: number, y?: number, width?: number, height?: number }>} layoutNodes
|
|
9
|
+
*/
|
|
10
|
+
function buildFlowEdgeHandles(edge, layoutNodes) {
|
|
11
|
+
const from = layoutNodes.find((node) => node.id === edge.from);
|
|
12
|
+
const to = layoutNodes.find((node) => node.id === edge.to);
|
|
13
|
+
if (!from || !to) {
|
|
14
|
+
return { sourceHandle: 'source', targetHandle: 'target', edgeType: 'default' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const dx = (to.x ?? 0) - (from.x ?? 0);
|
|
18
|
+
const dy = (to.y ?? 0) - (from.y ?? 0);
|
|
19
|
+
const horizontal = dx > ((from.width ?? 0) * 0.35);
|
|
20
|
+
const verticalDown = !horizontal && dy > 12;
|
|
21
|
+
|
|
22
|
+
if (verticalDown) {
|
|
23
|
+
return {
|
|
24
|
+
sourceHandle: 'source-bottom',
|
|
25
|
+
targetHandle: 'target-top',
|
|
26
|
+
edgeType: 'smoothstep',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (horizontal) {
|
|
31
|
+
return {
|
|
32
|
+
sourceHandle: 'source',
|
|
33
|
+
targetHandle: 'target',
|
|
34
|
+
edgeType: 'default',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { sourceHandle: 'source', targetHandle: 'target', edgeType: 'default' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {object} edge
|
|
43
|
+
* @param {Array<{ id: string, x?: number, y?: number, width?: number, height?: number }>} layoutNodes
|
|
44
|
+
* @param {'dark' | 'light'} theme
|
|
45
|
+
*/
|
|
46
|
+
function buildFlowEdgeLabelFields(edge, layoutNodes, theme) {
|
|
47
|
+
const label = String(edge.label ?? '').trim();
|
|
48
|
+
if (label === '') {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const from = layoutNodes.find((node) => node.id === edge.from);
|
|
53
|
+
const to = layoutNodes.find((node) => node.id === edge.to);
|
|
54
|
+
const html = buildGraphCanvasEdgeLabelHtml(label, theme, { rejected: edge.rejected === true });
|
|
55
|
+
const handles = buildFlowEdgeHandles(edge, layoutNodes);
|
|
56
|
+
|
|
57
|
+
if (handles.edgeType === 'smoothstep' && label === 'подзадача') {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (handles.edgeType === 'default' && (to?.x ?? 0) > (from?.x ?? 0) + ((from?.width ?? 0) * 0.35)) {
|
|
62
|
+
return { startLabel: label, startLabelHtml: html };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {import('./graphCanvasProjection.mjs').GRAPH_CANVAS_LIT_FLOW_PROJECTION_SCHEMA extends string ? object : never} projection
|
|
70
|
+
* @param {{ theme?: 'dark' | 'light' }} [options]
|
|
71
|
+
*/
|
|
72
|
+
export function graphCanvasProjectionToFlow(projection, options = {}) {
|
|
73
|
+
const theme = options.theme === 'light' ? 'light' : 'dark';
|
|
74
|
+
const layoutNodes = projection?.nodes ?? [];
|
|
75
|
+
const nodes = (projection?.nodes ?? []).map((node) => ({
|
|
76
|
+
id: node.id,
|
|
77
|
+
type: 'graph-card',
|
|
78
|
+
position: { x: node.x, y: node.y },
|
|
79
|
+
data: {
|
|
80
|
+
kind: node.kind,
|
|
81
|
+
title: node.title,
|
|
82
|
+
layer: node.layer ?? '',
|
|
83
|
+
summary: node.summary ?? '',
|
|
84
|
+
status: node.status ?? '',
|
|
85
|
+
selected: node.selected === true,
|
|
86
|
+
rejected: node.rejected === true,
|
|
87
|
+
focused: node.focused === true,
|
|
88
|
+
taskId: node.taskId ?? '',
|
|
89
|
+
intentNodeId: node.intentNodeId ?? '',
|
|
90
|
+
blockId: node.blockId ?? '',
|
|
91
|
+
schematicId: node.schematicId ?? '',
|
|
92
|
+
doneChildCount: node.doneChildCount ?? 0,
|
|
93
|
+
childCount: node.childCount ?? 0,
|
|
94
|
+
},
|
|
95
|
+
width: node.width,
|
|
96
|
+
height: node.height,
|
|
97
|
+
draggable: false,
|
|
98
|
+
selectable: true,
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
const edges = (projection?.edges ?? []).map((edge) => {
|
|
102
|
+
const rejected = edge.rejected === true;
|
|
103
|
+
const handles = buildFlowEdgeHandles(edge, layoutNodes);
|
|
104
|
+
return {
|
|
105
|
+
id: edge.id,
|
|
106
|
+
source: edge.from,
|
|
107
|
+
target: edge.to,
|
|
108
|
+
sourceHandle: handles.sourceHandle,
|
|
109
|
+
targetHandle: handles.targetHandle,
|
|
110
|
+
label: '',
|
|
111
|
+
type: handles.edgeType,
|
|
112
|
+
animated: false,
|
|
113
|
+
selectable: false,
|
|
114
|
+
markerEnd: {
|
|
115
|
+
type: 'ArrowClosed',
|
|
116
|
+
width: 14,
|
|
117
|
+
height: 14,
|
|
118
|
+
color: buildGraphCanvasEdgeStrokeStyle(edge, theme).stroke,
|
|
119
|
+
},
|
|
120
|
+
data: {
|
|
121
|
+
rejected,
|
|
122
|
+
upstream: edge.upstream === true,
|
|
123
|
+
...buildFlowEdgeLabelFields(edge, layoutNodes, theme),
|
|
124
|
+
},
|
|
125
|
+
style: {
|
|
126
|
+
...buildGraphCanvasEdgeStrokeStyle(edge, theme),
|
|
127
|
+
strokeWidth: rejected ? 1.75 : 2.25,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return { nodes, edges };
|
|
133
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function buildEdgeMaps(edges) {
|
|
2
|
+
/** @type {Map<string, Set<string>>} */
|
|
3
|
+
const outgoing = new Map();
|
|
4
|
+
/** @type {Map<string, Set<string>>} */
|
|
5
|
+
const incoming = new Map();
|
|
6
|
+
|
|
7
|
+
for (const edge of edges ?? []) {
|
|
8
|
+
if (!edge?.from || !edge?.to) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (!outgoing.has(edge.from)) {
|
|
12
|
+
outgoing.set(edge.from, new Set());
|
|
13
|
+
}
|
|
14
|
+
if (!incoming.has(edge.to)) {
|
|
15
|
+
incoming.set(edge.to, new Set());
|
|
16
|
+
}
|
|
17
|
+
outgoing.get(edge.from).add(edge.to);
|
|
18
|
+
incoming.get(edge.to).add(edge.from);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { outgoing, incoming };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getOutgoingNodeIds(nodeId, edges) {
|
|
25
|
+
const { outgoing } = buildEdgeMaps(edges);
|
|
26
|
+
return [...(outgoing.get(nodeId) ?? [])];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getIncomingNodeIds(nodeId, edges) {
|
|
30
|
+
const { incoming } = buildEdgeMaps(edges);
|
|
31
|
+
return [...(incoming.get(nodeId) ?? [])];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getUpstreamNodeIds(nodeId, edges, visited = new Set()) {
|
|
35
|
+
if (visited.has(nodeId)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
visited.add(nodeId);
|
|
39
|
+
const direct = getIncomingNodeIds(nodeId, edges);
|
|
40
|
+
const nested = direct.flatMap((id) => getUpstreamNodeIds(id, edges, visited));
|
|
41
|
+
return [...new Set([...direct, ...nested])];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getDownstreamNodeIds(nodeId, edges, visited = new Set()) {
|
|
45
|
+
if (visited.has(nodeId)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
visited.add(nodeId);
|
|
49
|
+
const direct = getOutgoingNodeIds(nodeId, edges);
|
|
50
|
+
const nested = direct.flatMap((id) => getDownstreamNodeIds(id, edges, visited));
|
|
51
|
+
return [...new Set([...direct, ...nested])];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function sortNodeIdsByVerticalPosition(nodeIds, nodes) {
|
|
55
|
+
const byId = new Map((nodes ?? []).map((node) => [node.id, node]));
|
|
56
|
+
return [...nodeIds].sort((left, right) => {
|
|
57
|
+
const leftNode = byId.get(left);
|
|
58
|
+
const rightNode = byId.get(right);
|
|
59
|
+
const dy = (leftNode?.y ?? 0) - (rightNode?.y ?? 0);
|
|
60
|
+
if (dy !== 0) {
|
|
61
|
+
return dy;
|
|
62
|
+
}
|
|
63
|
+
return String(left).localeCompare(String(right), 'en');
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getSiblingNodeIds(nodeId, edges, nodes) {
|
|
68
|
+
const parents = getIncomingNodeIds(nodeId, edges);
|
|
69
|
+
if (parents.length === 0) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const siblings = parents.flatMap((parentId) => getOutgoingNodeIds(parentId, edges));
|
|
73
|
+
return sortNodeIdsByVerticalPosition(
|
|
74
|
+
siblings.filter((id) => id !== nodeId),
|
|
75
|
+
nodes,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent roadmap hybrid layout (n8n-style):
|
|
3
|
+
* - intent spine (question → … → decision) via dagre LR
|
|
4
|
+
* - work tree as a vertical stack column to the right of decision
|
|
5
|
+
*
|
|
6
|
+
* @param {Array<{ id: string, kind: string, width: number, height: number, [key: string]: unknown }>} workNodes
|
|
7
|
+
* @param {Array<{ from: string, to: string }>} workEdges
|
|
8
|
+
* @param {{ id: string, x: number, y: number, width: number, height: number }} anchorNode
|
|
9
|
+
* @param {{ ranksep?: number, gap?: number }} [options]
|
|
10
|
+
*/
|
|
11
|
+
export function layoutIntentRoadmapWorkStack(workNodes, workEdges, anchorNode, options = {}) {
|
|
12
|
+
if (!workNodes.length) {
|
|
13
|
+
return new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ranksep = options.ranksep ?? 128;
|
|
17
|
+
const gap = options.gap ?? 36;
|
|
18
|
+
const byId = new Map(workNodes.map((node) => [node.id, node]));
|
|
19
|
+
const order = [];
|
|
20
|
+
|
|
21
|
+
/** @type {Set<string>} */
|
|
22
|
+
const visited = new Set();
|
|
23
|
+
const roots = workNodes.filter((node) => !workEdges.some((edge) => edge.to === node.id));
|
|
24
|
+
|
|
25
|
+
function walk(nodeId) {
|
|
26
|
+
if (visited.has(nodeId) || !byId.has(nodeId)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
visited.add(nodeId);
|
|
30
|
+
order.push(nodeId);
|
|
31
|
+
for (const edge of workEdges.filter((candidate) => candidate.from === nodeId)) {
|
|
32
|
+
walk(edge.to);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const root of roots) {
|
|
37
|
+
walk(root.id);
|
|
38
|
+
}
|
|
39
|
+
for (const node of workNodes) {
|
|
40
|
+
walk(node.id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const columnX = anchorNode.x + anchorNode.width + ranksep;
|
|
44
|
+
/** @type {Map<string, object>} */
|
|
45
|
+
const placed = new Map();
|
|
46
|
+
|
|
47
|
+
let y = anchorNode.y + anchorNode.height / 2 - (byId.get(order[0])?.height ?? 0) / 2;
|
|
48
|
+
|
|
49
|
+
for (const nodeId of order) {
|
|
50
|
+
const node = byId.get(nodeId);
|
|
51
|
+
if (!node) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
placed.set(nodeId, {
|
|
55
|
+
...node,
|
|
56
|
+
x: columnX,
|
|
57
|
+
y,
|
|
58
|
+
});
|
|
59
|
+
y += node.height + gap;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return placed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {Array<{ kind?: string }>} nodes
|
|
67
|
+
*/
|
|
68
|
+
export function isIntentRoadmapIntentKind(kind) {
|
|
69
|
+
return kind === 'intent_question'
|
|
70
|
+
|| kind === 'intent_analysis'
|
|
71
|
+
|| kind === 'intent_option'
|
|
72
|
+
|| kind === 'intent_decision';
|
|
73
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-layout pass: n8n stores manual positions; we auto-fix dagre underestimates
|
|
3
|
+
* when several nodes share the same rank (vertical stacks in LR layout).
|
|
4
|
+
*
|
|
5
|
+
* @param {Array<{ id: string, x: number, y: number, width: number, height: number }>} nodes
|
|
6
|
+
* @param {{ gap?: number, rankTolerance?: number, layoutDirection?: 'LR' | 'TB' | 'RL' | 'BT' }} [options]
|
|
7
|
+
*/
|
|
8
|
+
export function resolveGraphCanvasOverlaps(nodes, options = {}) {
|
|
9
|
+
if (!nodes.length) {
|
|
10
|
+
return nodes;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const gap = options.gap ?? 28;
|
|
14
|
+
const rankTolerance = options.rankTolerance ?? 48;
|
|
15
|
+
const horizontal = options.layoutDirection === 'LR' || options.layoutDirection === 'RL';
|
|
16
|
+
const rankKey = horizontal
|
|
17
|
+
? (node) => Math.round(node.x / rankTolerance) * rankTolerance
|
|
18
|
+
: (node) => Math.round(node.y / rankTolerance) * rankTolerance;
|
|
19
|
+
const crossKey = horizontal
|
|
20
|
+
? (node) => node.y
|
|
21
|
+
: (node) => node.x;
|
|
22
|
+
|
|
23
|
+
/** @type {Map<number, Array<typeof nodes[number]>>} */
|
|
24
|
+
const lanes = new Map();
|
|
25
|
+
for (const node of nodes) {
|
|
26
|
+
const key = rankKey(node);
|
|
27
|
+
if (!lanes.has(key)) {
|
|
28
|
+
lanes.set(key, []);
|
|
29
|
+
}
|
|
30
|
+
lanes.get(key).push(node);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const laneNodes of lanes.values()) {
|
|
34
|
+
laneNodes.sort((left, right) => crossKey(left) - crossKey(right));
|
|
35
|
+
for (let index = 1; index < laneNodes.length; index += 1) {
|
|
36
|
+
const previous = laneNodes[index - 1];
|
|
37
|
+
const current = laneNodes[index];
|
|
38
|
+
const previousEnd = horizontal
|
|
39
|
+
? previous.y + previous.height
|
|
40
|
+
: previous.x + previous.width;
|
|
41
|
+
const currentStart = horizontal ? current.y : current.x;
|
|
42
|
+
const minStart = previousEnd + gap;
|
|
43
|
+
if (currentStart < minStart) {
|
|
44
|
+
const shift = minStart - currentStart;
|
|
45
|
+
for (let rest = index; rest < laneNodes.length; rest += 1) {
|
|
46
|
+
if (horizontal) {
|
|
47
|
+
laneNodes[rest].y += shift;
|
|
48
|
+
} else {
|
|
49
|
+
laneNodes[rest].x += shift;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return nodes;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {Array<{ x: number, y: number, width: number, height: number }>} nodes
|
|
61
|
+
*/
|
|
62
|
+
export function graphCanvasNodesOverlap(nodes) {
|
|
63
|
+
for (let leftIndex = 0; leftIndex < nodes.length; leftIndex += 1) {
|
|
64
|
+
for (let rightIndex = leftIndex + 1; rightIndex < nodes.length; rightIndex += 1) {
|
|
65
|
+
const left = nodes[leftIndex];
|
|
66
|
+
const right = nodes[rightIndex];
|
|
67
|
+
const separated = left.x + left.width <= right.x
|
|
68
|
+
|| right.x + right.width <= left.x
|
|
69
|
+
|| left.y + left.height <= right.y
|
|
70
|
+
|| right.y + right.height <= left.y;
|
|
71
|
+
if (!separated) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|