Files
lan-manager/web/node_modules/@antv/layout/es/layout/comboCombined.js
openclaw 0a5f6a8047 Initial commit: Lan-manager project code
- Go backend (server/)
- Frontend (web/, server/static/)
- Database and deployment files
- Scripts and docs

Co-Authored-By: 狸花猫/Claude-Qwen3.6-Plus 🐾
2026-04-20 00:52:58 +08:00

370 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @fileOverview Combo force layout
* @author shiwu.wyy@antfin.com
*/
import { FORCE_LAYOUT_TYPE_MAP } from './constants';
import { Base } from './base';
import { isArray, isNumber, isFunction, traverseTreeUp, isObject, getLayoutBBox, } from '../util';
import { ConcentricLayout, GridLayout, GForceLayout, MDSLayout, } from '.';
/**
* combined two layouts (inner and outer) for graph with combos
*/
export class ComboCombinedLayout extends Base {
constructor(options) {
super();
/** 布局中心 */
this.center = [0, 0];
/** 内部计算参数 */
this.nodes = [];
this.edges = [];
this.combos = [];
this.comboEdges = [];
/** Combo 内部的 padding */
this.comboPadding = 10;
this.comboTrees = [];
this.updateCfg(options);
}
getDefaultCfg() {
return {};
}
/**
* 执行布局
*/
execute() {
const self = this;
const nodes = self.nodes;
const center = self.center;
if (!nodes || nodes.length === 0) {
if (self.onLayoutEnd)
self.onLayoutEnd();
return;
}
if (nodes.length === 1) {
nodes[0].x = center[0];
nodes[0].y = center[1];
if (self.onLayoutEnd)
self.onLayoutEnd();
return;
}
self.initVals();
// layout
self.run();
if (self.onLayoutEnd)
self.onLayoutEnd();
}
run() {
var _a;
const self = this;
const { nodes, edges, combos, comboEdges, center } = self;
const nodeMap = {};
nodes.forEach((node) => {
nodeMap[node.id] = node;
});
const comboMap = {};
combos.forEach((combo) => {
comboMap[combo.id] = combo;
});
const innerGraphs = self.getInnerGraphs(nodeMap);
// 每个 innerGraph 作为一个节点,带有大小,参与 force 计算
const outerNodeIds = [];
const outerNodes = [];
const nodeAncestorIdMap = {};
let allHaveNoPosition = true;
this.comboTrees.forEach((cTree) => {
const innerNode = innerGraphs[cTree.id];
if (!innerNode) {
return;
}
// 代表 combo 的节点
const oNode = Object.assign(Object.assign({}, cTree), { x: innerNode.x || comboMap[cTree.id].x, y: innerNode.y || comboMap[cTree.id].y, fx: innerNode.fx || comboMap[cTree.id].fx, fy: innerNode.fy || comboMap[cTree.id].fy, mass: innerNode.mass || comboMap[cTree.id].mass, size: innerNode.size });
outerNodes.push(oNode);
if (!isNaN(oNode.x) &&
oNode.x !== 0 &&
!isNaN(oNode.y) &&
oNode.y !== 0) {
allHaveNoPosition = false;
}
else {
oNode.x = Math.random() * 100;
oNode.y = Math.random() * 100;
}
outerNodeIds.push(cTree.id);
traverseTreeUp(cTree, (child) => {
if (child.id !== cTree.id)
nodeAncestorIdMap[child.id] = cTree.id;
return true;
});
});
nodes.forEach((node) => {
if (node.comboId && comboMap[node.comboId])
return;
// 代表节点的节点
const oNode = Object.assign({}, node);
outerNodes.push(oNode);
if (!isNaN(oNode.x) &&
oNode.x !== 0 &&
!isNaN(oNode.y) &&
oNode.y !== 0) {
allHaveNoPosition = false;
}
else {
oNode.x = Math.random() * 100;
oNode.y = Math.random() * 100;
}
outerNodeIds.push(node.id);
});
const outerEdges = [];
edges.concat(comboEdges).forEach((edge) => {
const sourceAncestorId = nodeAncestorIdMap[edge.source] || edge.source;
const targetAncestorId = nodeAncestorIdMap[edge.target] || edge.target;
// 若两个点的祖先都在力导图的节点中,且是不同的节点,创建一条链接两个祖先的边到力导图的边中
if (sourceAncestorId !== targetAncestorId &&
outerNodeIds.includes(sourceAncestorId) &&
outerNodeIds.includes(targetAncestorId)) {
outerEdges.push({
source: sourceAncestorId,
target: targetAncestorId,
});
}
});
// 若有需要最外层的 combo 或节点,则对最外层执行力导向
if (outerNodes === null || outerNodes === void 0 ? void 0 : outerNodes.length) {
if (outerNodes.length === 1) {
outerNodes[0].x = center[0];
outerNodes[0].y = center[1];
}
else {
const outerData = {
nodes: outerNodes,
edges: outerEdges,
};
// 需要使用一个同步的布局
// @ts-ignore
const outerLayout = this.outerLayout ||
new GForceLayout({
gravity: 1,
factor: 4,
linkDistance: (edge, source, target) => {
var _a, _b;
const nodeSize = ((((_a = source.size) === null || _a === void 0 ? void 0 : _a[0]) || 30) + (((_b = target.size) === null || _b === void 0 ? void 0 : _b[0]) || 30)) / 2;
return Math.min(nodeSize * 1.5, 700);
},
});
const outerLayoutType = (_a = outerLayout.getType) === null || _a === void 0 ? void 0 : _a.call(outerLayout);
outerLayout.updateCfg({
center,
kg: 5,
preventOverlap: true,
animate: false,
});
// 若所有 outerNodes 都没有位置,且 outerLayout 是力导家族的布局,则先执行 preset mds 或 grid
if (allHaveNoPosition && FORCE_LAYOUT_TYPE_MAP[outerLayoutType]) {
const outerLayoutPreset = outerNodes.length < 100 ? new MDSLayout() : new GridLayout();
outerLayoutPreset.layout(outerData);
}
outerLayout.layout(outerData);
}
// 根据外部布局结果,平移 innerGraphs 中的节点(第一层)
outerNodes.forEach((outerNode) => {
const innerGraph = innerGraphs[outerNode.id];
if (!innerGraph) {
const node = nodeMap[outerNode.id];
if (node) {
node.x = outerNode.x;
node.y = outerNode.y;
}
return;
}
innerGraph.visited = true;
innerGraph.x = outerNode.x;
innerGraph.y = outerNode.y;
innerGraph.nodes.forEach((node) => {
node.x += outerNode.x;
node.y += outerNode.y;
});
});
}
// 至上而下遍历树处理下面各层节点位置
const innerGraphIds = Object.keys(innerGraphs);
for (let i = innerGraphIds.length - 1; i >= 0; i--) {
const id = innerGraphIds[i];
const innerGraph = innerGraphs[id];
if (!innerGraph)
continue;
innerGraph.nodes.forEach((node) => {
if (!innerGraph.visited) {
node.x += innerGraph.x || 0;
node.y += innerGraph.y || 0;
}
if (nodeMap[node.id]) {
nodeMap[node.id].x = node.x;
nodeMap[node.id].y = node.y;
}
});
if (comboMap[id]) {
comboMap[id].x = innerGraph.x;
comboMap[id].y = innerGraph.y;
}
}
return { nodes, edges, combos, comboEdges };
}
getInnerGraphs(nodeMap) {
const self = this;
const { comboTrees, nodeSize, edges, comboPadding, spacing } = self;
const innerGraphs = {};
// @ts-ignore
const innerGraphLayout = this.innerLayout ||
new ConcentricLayout({ type: 'concentric', sortBy: 'id' });
innerGraphLayout.center = [0, 0];
innerGraphLayout.preventOverlap = true;
innerGraphLayout.nodeSpacing = spacing;
(comboTrees || []).forEach((ctree) => {
traverseTreeUp(ctree, (treeNode) => {
var _a;
// @ts-ignore
let padding = (comboPadding === null || comboPadding === void 0 ? void 0 : comboPadding(treeNode)) || 10; // 返回的最大值
if (isArray(padding))
padding = Math.max(...padding);
if (!((_a = treeNode.children) === null || _a === void 0 ? void 0 : _a.length)) {
// 空 combo
if (treeNode.itemType === 'combo') {
const treeNodeSize = padding
? [padding * 2, padding * 2]
: [30, 30];
innerGraphs[treeNode.id] = {
id: treeNode.id,
nodes: [],
size: treeNodeSize,
};
}
}
else {
// 非空 combo
const innerGraphNodes = treeNode.children.map((child) => {
if (child.itemType === 'combo')
return innerGraphs[child.id];
const oriNode = nodeMap[child.id] || {};
return Object.assign(Object.assign({}, oriNode), child);
});
const innerGraphNodeIds = innerGraphNodes.map((node) => node.id);
const innerGraphData = {
nodes: innerGraphNodes,
edges: edges.filter((edge) => innerGraphNodeIds.includes(edge.source) &&
innerGraphNodeIds.includes(edge.target)),
};
let minNodeSize = Infinity;
innerGraphNodes.forEach((node) => {
var _a;
// @ts-ignore
if (!node.size)
node.size = ((_a = innerGraphs[node.id]) === null || _a === void 0 ? void 0 : _a.size) ||
(nodeSize === null || nodeSize === void 0 ? void 0 : nodeSize(node)) || [30, 30];
if (isNumber(node.size))
node.size = [node.size, node.size];
if (minNodeSize > node.size[0])
minNodeSize = node.size[0];
if (minNodeSize > node.size[1])
minNodeSize = node.size[1];
});
// 根据节点数量、spacing调整布局参数
innerGraphLayout.layout(innerGraphData);
const { minX, minY, maxX, maxY } = getLayoutBBox(innerGraphNodes);
// move the innerGraph to [0, 0],for later controled by parent layout
const center = { x: (maxX + minX) / 2, y: (maxY + minY) / 2 };
innerGraphData.nodes.forEach((node) => {
node.x -= center.x;
node.y -= center.y;
});
const innerGraphWidth = Math.max(maxX - minX, minNodeSize) + padding * 2;
const innerGraphHeight = Math.max(maxY - minY, minNodeSize) + padding * 2;
innerGraphs[treeNode.id] = {
id: treeNode.id,
nodes: innerGraphNodes,
size: [innerGraphWidth, innerGraphHeight],
};
}
return true;
});
});
return innerGraphs;
}
initVals() {
const self = this;
const nodeSize = self.nodeSize;
const spacing = self.spacing;
let nodeSizeFunc;
let spacingFunc;
// nodeSpacing to function
if (isNumber(spacing)) {
spacingFunc = () => spacing;
}
else if (isFunction(spacing)) {
spacingFunc = spacing;
}
else {
spacingFunc = () => 0;
}
this.spacing = spacingFunc;
// nodeSize to function
if (!nodeSize) {
nodeSizeFunc = (d) => {
const spacing = spacingFunc(d);
if (d.size) {
if (isArray(d.size)) {
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
return (res + spacing) / 2;
}
if (isObject(d.size)) {
const res = d.size.width > d.size.height ? d.size.width : d.size.height;
return (res + spacing) / 2;
}
return (d.size + spacing) / 2;
}
return 10 + spacing / 2;
};
}
else if (isFunction(nodeSize)) {
nodeSizeFunc = (d) => {
const size = nodeSize(d);
const spacing = spacingFunc(d);
if (isArray(d.size)) {
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
return (res + spacing) / 2;
}
return ((size || 10) + spacing) / 2;
};
}
else if (isArray(nodeSize)) {
const larger = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
const radius = larger / 2;
nodeSizeFunc = (d) => radius + spacingFunc(d) / 2;
}
else {
// number type
const radius = nodeSize / 2;
nodeSizeFunc = (d) => radius + spacingFunc(d) / 2;
}
this.nodeSize = nodeSizeFunc;
// comboPadding to function
const comboPadding = self.comboPadding;
let comboPaddingFunc;
if (isNumber(comboPadding)) {
comboPaddingFunc = () => comboPadding;
}
else if (isArray(comboPadding)) {
comboPaddingFunc = () => Math.max.apply(null, comboPadding);
}
else if (isFunction(comboPadding)) {
comboPaddingFunc = comboPadding;
}
else {
// null type
comboPaddingFunc = () => 0;
}
this.comboPadding = comboPaddingFunc;
}
getType() {
return 'comboCombined';
}
}
//# sourceMappingURL=comboCombined.js.map