Files
UnrealEngine/Engine/Extras/RoboMerge/v3/public/js/flow.graph.js
2025-05-18 13:04:45 +08:00

285 lines
12 KiB
JavaScript

// Copyright Epic Games, Inc. All Rights Reserved.
class Info {
constructor(id, tooltip, name, sources) {
this.id = id;
this.tooltip = tooltip;
this.name = name;
this.sources = sources;
this.forcedEdges = new Set();
this.defaultDests = new Set();
this.blockAssetDests = new Set();
this.importance = 0;
}
update(branch, getId) {
const edgeId = (alias) => branch.def.name + '->' + getId(alias);
// shuld really do the same for default and block assets
this.forcedEdges = new Set([...this.forcedEdges, ...branch.def.forceFlowTo.map(edgeId)]);
this.defaultDests = new Set([...this.defaultDests, ...branch.def.defaultFlow.map(edgeId)]);
this.blockAssetDests = new Set([...this.blockAssetDests, ...branch.def.blockAssetTargets.map(edgeId)]);
}
}
function decoratedAlias(bot, alias) {
return bot + ':' + alias;
}
function aliasForBranch(branch) {
return decoratedAlias(branch.bot, branch.def.upperName);
}
const EDGE_STYLES = {
roboshelf: [['color', 'purple'], ['arrowhead', 'diamond']],
forced: [],
gated: [['color', 'darkorange']],
defaultFlow: [['color', 'blue']],
onRequest: [['color', 'darkgray'], ['style', 'dashed']],
blockAssets: [['color', 'darkgray'], ['style', 'dashed'], ['arrowhead', 'odiamond']]
};
class Graph {
constructor(allBranches, options) {
this.allBranches = allBranches;
this.options = options;
this.nodeLabels = new Map();
this.links = [];
this.aliases = new Map();
this.allInfos = [];
this.linkOutward = new Set();
this.linkInward = new Set();
this.connectedNodes = new Set();
this.branchList = [];
}
singleBot(botName) {
this.branchList = this.allBranches.filter(b => b.bot === botName);
this.addNodesAndEdges(false);
const title = this.options.aliases.length > 0 ? botName + ' (' + this.options.aliases.join(", ") + ')': botName
return this.makeGraph(title, botName + ' integration paths');
}
allBots() {
this.branchList = this.options.botsToShow
? this.allBranches.filter(b => this.options.botsToShow.indexOf(b.bot) >= 0)
: this.allBranches;
this.findSharedNodes();
this.addNodesAndEdges(!this.options.noGroups);
let title = 'All bots';
if (this.options.botsToShow)
{
title = `Bots: ${this.options.botsToShow.join(',')}`;
}
return this.makeGraph(title, 'Flow including shared bots');
}
addNodesAndEdges(grouped) {
for (const branchStatus of this.branchList) {
const botTitle = grouped
? this.options.aliases && this.options.aliases.get(branchStatus.bot).length > 0
? branchStatus.bot + ' (' + this.options.aliases.get(branchStatus.bot).join(", ") + ')'
: branchStatus.bot
: null;
this.addBranch(branchStatus, grouped ? branchStatus.bot : null, botTitle);
}
for (const info of this.allInfos) {
for (const sourceBranch of info.sources) {
info.update(sourceBranch, this.getIdForBot(sourceBranch.bot));
}
}
for (const branchStatus of this.branchList) {
for (const flow of branchStatus.def.flowsTo) {
this.addEdge(branchStatus, flow);
}
}
}
makeGraph(title, tooltip) {
const lines = [
'digraph robomerge {',
'fontname="sans-serif"; labelloc=top; fontsize=16;',
'edge [penwidth=2]; nodesep=.7; ranksep=1.2;',
`label = "${title}";`,
`tooltip="${tooltip}";`,
`node [shape=box, style=filled, fontname="sans-serif", fillcolor=moccasin];`,
];
const nodeGroups = [];
for (const [groupName, v] of this.nodeLabels) {
const nodeInfo = [];
nodeGroups.push([groupName, nodeInfo]);
for (const info of v) {
const { id, tooltip, name, importance } = info;
let factor = (Math.min(importance, 10) - 1) / 9;
nodeInfo.push({
importance: factor,
marginX: (.2 * (1 - factor) + .4 * factor).toPrecision(1),
marginY: (.1 * (1 - factor) + .25 * factor).toPrecision(1),
fontSize: (14 * (1 - factor) + 20 * factor).toPrecision(2),
info: info
});
}
}
for (const [groupName, nodeInfo] of nodeGroups) {
if (groupName !== 'nogroup') {
lines.push(`subgraph cluster_${groupName} {
label="${nodeInfo[0].info.title}";
`);
}
for (const info of nodeInfo) {
if (this.options.hideDisconnected && !this.connectedNodes.has(info.info.id)) {
continue;
}
const attrs = [
['label', `"${info.info.name}"`],
['tooltip', `"${info.info.tooltip}"`],
['margin', `"${info.marginX},${info.marginY}"`],
['fontsize', info.fontSize],
];
if (info.importance > .5) {
attrs.push(['style', '"filled,bold"']);
}
if (info.info.graphNodeColor) {
attrs.push(['fillcolor', `"${info.info.graphNodeColor}"`]);
}
const attrStrs = attrs.map(([key, value]) => `${key}=${value}`);
lines.push(`${info.info.id} [${attrStrs.join(', ')}];`);
}
if (groupName !== 'nogroup') {
lines.push('}');
}
}
for (const link of this.links) {
// when there's forced/unforced pair, only the former is a constrait
// (this makes flow )
const combo = link.dst + '->' + link.src;
if (combo && this.linkOutward.has(combo) && this.linkInward.has(combo)) {
link.styles.push(['constraint', 'false']);
}
const styleStrs = link.styles.map(([key, value]) => `${key}=${value}`);
const suffix = styleStrs.length === 0 ? '' : ` [${styleStrs.join(', ')}]`;
lines.push(`${link.src} -> ${link.dst}${suffix} [tooltip="${link.tooltip}"];`);
}
lines.push('}');
return lines;
}
addBranch(branchStatus, group = null, title = null) {
const branch = branchStatus.def;
if (this.aliases.has(decoratedAlias(branchStatus.bot, branch.upperName)))
return;
let tooltip = `${branchStatus.bot} - ${branch.rootPath}`;
if (branch.aliases && branch.aliases.length !== 0) {
tooltip += ` (${branch.aliases.join(', ')})`;
}
const info = new Info(`_${branchStatus.bot}_${branch.upperName.replace(/[^\w]+/g, '_')}`, tooltip, branch.name, [branchStatus]);
info.title = title
this.allInfos.push(info);
if (branch.config.graphNodeColor) {
info.graphNodeColor = branch.config.graphNodeColor;
}
if (branch.upperName === 'MAIN') {
// special case branches named Main
info.importance = 10;
}
// note: branch.upperName is always in branch.aliases
for (const alias of branch.aliases) {
this.aliases.set(decoratedAlias(branchStatus.bot, alias), info);
}
setDefault(this.nodeLabels, group || 'nogroup', []).push(info);
}
// dstName
addEdge(srcBranchStatus, dst) {
const src = aliasForBranch(srcBranchStatus);
const srcInfo = this.aliases.get(src);
const dstInfo = this.aliases.get(decoratedAlias(srcBranchStatus.bot, dst));
++srcInfo.importance;
++dstInfo.importance;
const hasEdge = (edges, target) => edges.has(srcBranchStatus.def.name + '->' + target.id);
const isForced = hasEdge(srcInfo.forcedEdges, dstInfo);
if (!isForced && this.options.showOnlyForced) {
return;
}
const isGated = new Map(srcBranchStatus.def.edgeProperties).get(dst).lastGoodCLPath
this.connectedNodes.add(srcInfo.id);
this.connectedNodes.add(dstInfo.id);
const edgeStyle = isForced ? (isGated ? 'gated' : 'forced') :
hasEdge(srcInfo.defaultDests, dstInfo) ? 'defaultFlow' :
hasEdge(srcInfo.blockAssetDests, dstInfo) ? 'blockAssets' : 'onRequest';
const styles = [...EDGE_STYLES[edgeStyle]];
// prepare the tool tip of the edge to be as informative as possible
let suffix = '';
if(edgeStyle !== 'defaultFlow'){
suffix = `(${edgeStyle})`;
}
let tooltipcontent = `${srcInfo.name} -> ${dstInfo.name} ${suffix}\n`;
tooltipcontent += `\n${srcInfo.name}:\n${srcInfo.tooltip}\n`;
tooltipcontent += `\n${dstInfo.name}:\n${dstInfo.tooltip}\n`;
if (isGated)
{
tooltipcontent += `\nlast good CL path:\n${isGated}`
}
const link = { src: srcInfo.id, dst: dstInfo.id, styles, tooltip: tooltipcontent };
this.links.push(link);
if (isForced) {
this.linkOutward.add(link.src + '->' + link.dst);
}
else if (edgeStyle === 'blockAssets' || edgeStyle === 'onRequest') {
this.linkInward.add(link.dst + '->' + link.src);
}
}
findSharedNodes() {
const streamMap = new Map();
for (const branchStatus of this.branchList) {
let key = branchStatus.def.rootPath;
if(branchStatus.def.uniqueBranch)
{
key = branchStatus.def.name;
}
const [color, streams] = setDefault(streamMap, key, [null, []]);
streams.push(branchStatus);
const nodeColor = branchStatus.def.config.graphNodeColor;
if (!color && nodeColor) {
streamMap.set(key, [nodeColor, streams]);
}
}
// create a shared info for each set of nodes monitoring the same stream
for (const [k, v] of streamMap) {
const [color, streams] = v;
if (streams.length > 1) {
// create a tooltip with information from all the branches
let globalToolTip = '';
for (const branchStatus of streams) {
let tooltip = `${branchStatus.bot} - ${branchStatus.def.rootPath}`;
if (branchStatus.def.aliases && branchStatus.def.aliases.length !== 0) {
tooltip += ` (${branchStatus.def.aliases.join(', ')})`;
}
globalToolTip += tooltip + "\n";
}
globalToolTip = globalToolTip.trim();
const info = new Info(k.replace(/[^\w]+/g, '_'), globalToolTip, k, streams);
this.allInfos.push(info);
if (color) {
info.graphNodeColor = color;
}
for (const branchStatus of streams) {
for (const alias of branchStatus.def.aliases) {
this.aliases.set(decoratedAlias(branchStatus.bot, alias), info);
}
}
setDefault(this.nodeLabels, 'nogroup', []).push(info);
}
}
}
getIdForBot(bot) {
return (alias) => {
const info = this.aliases.get(decoratedAlias(bot, alias));
return info ? info.id : alias;
};
}
}