1187 lines
38 KiB
JavaScript
1187 lines
38 KiB
JavaScript
// js/chatbot/utils/drawio-layout-optimizer.js
|
||
|
||
/**
|
||
* Draw.io XML 布局优化工具
|
||
*
|
||
* 参考 smart-drawio-next 的优化思路,针对 draw.io XML 格式设计
|
||
* 主要优化:
|
||
* 0. Dagre 布局 - 使用标准 Sugiyama 算法进行层次化布局(推荐,显著减少交叉)
|
||
* 1. 网格对齐 - 确保所有坐标对齐到网格
|
||
* 2. 自动间距 - 避免节点重叠和拥挤,考虑连接关系智能排列
|
||
* 3. 避免穿透 - 检测连线是否穿过节点,调整节点位置避让
|
||
* 4. 智能连接 - 优化箭头的出入点位置,移除错误的 mxPoint 子元素
|
||
* 5. 连线理线 - 检测交叉并通过调整节点位置减少交叉
|
||
* 6. 样式统一 - 统一相同类型元素的样式
|
||
*
|
||
* @version 1.3.0 - 集成 Dagre.js 标准布局算法
|
||
* @date 2025-01-16
|
||
*/
|
||
|
||
/**
|
||
* 从 XML 字符串解析出 mxCell 节点
|
||
* @param {string} xmlString - draw.io XML 字符串
|
||
* @returns {Document} 解析后的 XML 文档对象
|
||
*/
|
||
function parseDrawioXml(xmlString) {
|
||
const parser = new DOMParser();
|
||
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
|
||
|
||
// 检查解析错误
|
||
const parserError = xmlDoc.querySelector('parsererror');
|
||
if (parserError) {
|
||
throw new Error('XML 解析失败: ' + parserError.textContent);
|
||
}
|
||
|
||
return xmlDoc;
|
||
}
|
||
|
||
/**
|
||
* 将 XML 文档序列化回字符串
|
||
* @param {Document} xmlDoc - XML 文档对象
|
||
* @returns {string} XML 字符串
|
||
*/
|
||
function serializeDrawioXml(xmlDoc) {
|
||
const serializer = new XMLSerializer();
|
||
return serializer.serializeToString(xmlDoc);
|
||
}
|
||
|
||
/**
|
||
* 获取节点的几何信息
|
||
* @param {Element} cell - mxCell 元素
|
||
* @returns {Object|null} {x, y, width, height} 或 null
|
||
*/
|
||
function getCellGeometry(cell) {
|
||
const geometry = cell.querySelector('mxGeometry');
|
||
if (!geometry) return null;
|
||
|
||
return {
|
||
x: parseFloat(geometry.getAttribute('x')) || 0,
|
||
y: parseFloat(geometry.getAttribute('y')) || 0,
|
||
width: parseFloat(geometry.getAttribute('width')) || 100,
|
||
height: parseFloat(geometry.getAttribute('height')) || 100
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 设置节点的几何信息
|
||
* @param {Element} cell - mxCell 元素
|
||
* @param {Object} geometry - {x, y, width, height}
|
||
*/
|
||
function setCellGeometry(cell, geometry) {
|
||
let geometryElem = cell.querySelector('mxGeometry');
|
||
if (!geometryElem) {
|
||
geometryElem = cell.ownerDocument.createElement('mxGeometry');
|
||
geometryElem.setAttribute('as', 'geometry');
|
||
cell.appendChild(geometryElem);
|
||
}
|
||
|
||
if (geometry.x !== undefined) geometryElem.setAttribute('x', geometry.x);
|
||
if (geometry.y !== undefined) geometryElem.setAttribute('y', geometry.y);
|
||
if (geometry.width !== undefined) geometryElem.setAttribute('width', geometry.width);
|
||
if (geometry.height !== undefined) geometryElem.setAttribute('height', geometry.height);
|
||
}
|
||
|
||
/**
|
||
* 网格对齐优化
|
||
* 确保所有坐标都对齐到网格(默认 10px)
|
||
*
|
||
* @param {Document} xmlDoc - XML 文档对象
|
||
* @param {number} gridSize - 网格大小,默认 10
|
||
*/
|
||
function optimizeGridAlignment(xmlDoc, gridSize = 10) {
|
||
const cells = xmlDoc.querySelectorAll('mxCell[vertex="1"]');
|
||
let optimizedCount = 0;
|
||
|
||
cells.forEach(cell => {
|
||
const geometry = getCellGeometry(cell);
|
||
if (!geometry) return;
|
||
|
||
// 对齐到网格
|
||
const alignedX = Math.round(geometry.x / gridSize) * gridSize;
|
||
const alignedY = Math.round(geometry.y / gridSize) * gridSize;
|
||
|
||
if (alignedX !== geometry.x || alignedY !== geometry.y) {
|
||
setCellGeometry(cell, {
|
||
x: alignedX,
|
||
y: alignedY,
|
||
width: geometry.width,
|
||
height: geometry.height
|
||
});
|
||
optimizedCount++;
|
||
}
|
||
});
|
||
|
||
console.log(`[DrawioOptimizer] 网格对齐: ${optimizedCount} 个节点已优化`);
|
||
return optimizedCount;
|
||
}
|
||
|
||
/**
|
||
* 检测节点重叠
|
||
* @param {Object} rect1 - {x, y, width, height}
|
||
* @param {Object} rect2 - {x, y, width, height}
|
||
* @param {number} minSpacing - 最小间距
|
||
* @returns {boolean} 是否重叠或过近
|
||
*/
|
||
function isOverlapping(rect1, rect2, minSpacing = 20) {
|
||
return !(
|
||
rect1.x + rect1.width + minSpacing < rect2.x ||
|
||
rect2.x + rect2.width + minSpacing < rect1.x ||
|
||
rect1.y + rect1.height + minSpacing < rect2.y ||
|
||
rect2.y + rect2.height + minSpacing < rect1.y
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 使用 Dagre 算法进行层次化布局(Sugiyama 算法)
|
||
* 这是图布局的标准算法,能显著减少连线交叉
|
||
*
|
||
* @param {Document} xmlDoc - XML 文档对象
|
||
* @param {Object} options - 布局选项
|
||
* @returns {number} 调整的节点数量
|
||
*/
|
||
function applyDagreLayout(xmlDoc, options = {}) {
|
||
// 检查 dagre 和 graphlib 是否可用(它们是两个独立的全局变量)
|
||
if (typeof window.dagre === 'undefined') {
|
||
console.warn('[DrawioOptimizer] ❌ Dagre 库未加载,跳过 Dagre 布局');
|
||
return 0;
|
||
}
|
||
if (typeof window.graphlib === 'undefined') {
|
||
console.warn('[DrawioOptimizer] ❌ Graphlib 库未加载,跳过 Dagre 布局');
|
||
return 0;
|
||
}
|
||
|
||
console.log('[DrawioOptimizer] 🎯 应用 Dagre 层次化布局算法 (LR 模式)...');
|
||
|
||
const defaultOptions = {
|
||
rankdir: 'LR', // 方向:LR (从左到右) - 层级横向展开,同层节点纵向排列
|
||
nodesep: 100, // 同层节点间距(LR模式下是垂直间距)- 增加以减少交叉
|
||
ranksep: 180, // 不同层间距(LR模式下是水平间距)- 增加以减少交叉
|
||
edgesep: 20, // 边之间的间距
|
||
ranker: 'network-simplex', // 使用 network-simplex 算法(最佳层分配)
|
||
marginx: 20, // 水平边距
|
||
marginy: 20 // 垂直边距
|
||
};
|
||
|
||
const opts = { ...defaultOptions, ...options };
|
||
|
||
try {
|
||
const allCells = Array.from(xmlDoc.querySelectorAll('mxCell[vertex="1"]'));
|
||
const allEdges = Array.from(xmlDoc.querySelectorAll('mxCell[edge="1"]'));
|
||
const cellMap = new Map();
|
||
|
||
// 构建 ID -> Cell 映射
|
||
xmlDoc.querySelectorAll('mxCell[id]').forEach(cell => {
|
||
cellMap.set(cell.getAttribute('id'), cell);
|
||
});
|
||
|
||
let adjustedCount = 0;
|
||
|
||
// 1. 识别 subgraph 容器(swimlane)和顶层节点
|
||
const subgraphContainers = [];
|
||
const topLevelCells = [];
|
||
const subgraphMembers = new Set(); // 记录 subgraph 内部节点
|
||
|
||
allCells.forEach(cell => {
|
||
const style = cell.getAttribute('style') || '';
|
||
const parent = cell.getAttribute('parent');
|
||
|
||
if (style.includes('swimlane')) {
|
||
// 这是一个 subgraph 容器
|
||
subgraphContainers.push(cell);
|
||
} else {
|
||
// 检查是否是 subgraph 内部节点
|
||
const parentCell = cellMap.get(parent);
|
||
if (parentCell && (parentCell.getAttribute('style') || '').includes('swimlane')) {
|
||
// 这是 subgraph 内部的节点
|
||
subgraphMembers.add(cell.getAttribute('id'));
|
||
} else {
|
||
// 这是顶层节点
|
||
topLevelCells.push(cell);
|
||
}
|
||
}
|
||
});
|
||
|
||
console.log(`[DrawioOptimizer] 📊 发现 ${subgraphContainers.length} 个子图, ${topLevelCells.length} 个顶层节点`);
|
||
|
||
// 2. 对每个 subgraph 内部单独进行 Dagre 布局
|
||
subgraphContainers.forEach(container => {
|
||
const containerId = container.getAttribute('id');
|
||
const containerGeo = getCellGeometry(container);
|
||
if (!containerGeo) return;
|
||
|
||
// 找出属于这个 subgraph 的所有节点
|
||
const members = allCells.filter(cell =>
|
||
cell.getAttribute('parent') === containerId
|
||
);
|
||
|
||
if (members.length === 0) return;
|
||
|
||
console.log(`[DrawioOptimizer] 🔹 子图 "${containerId}" 内部布局 (${members.length} 个节点)...`);
|
||
|
||
// 创建子图的 Dagre 图
|
||
const subG = new graphlib.Graph();
|
||
subG.setGraph({
|
||
...opts,
|
||
marginx: 20,
|
||
marginy: 30 // 顶部留空间给 swimlane 标题
|
||
});
|
||
subG.setDefaultEdgeLabel(() => ({}));
|
||
|
||
// 添加成员节点
|
||
members.forEach(cell => {
|
||
const id = cell.getAttribute('id');
|
||
const geo = getCellGeometry(cell);
|
||
if (!geo) return;
|
||
|
||
subG.setNode(id, {
|
||
width: geo.width,
|
||
height: geo.height,
|
||
originalGeo: geo,
|
||
cell: cell
|
||
});
|
||
});
|
||
|
||
// 添加子图内部的边
|
||
allEdges.forEach(edge => {
|
||
const source = edge.getAttribute('source');
|
||
const target = edge.getAttribute('target');
|
||
if (source && target && subG.hasNode(source) && subG.hasNode(target)) {
|
||
subG.setEdge(source, target);
|
||
}
|
||
});
|
||
|
||
// 执行子图布局
|
||
dagre.layout(subG);
|
||
|
||
// 应用布局结果(相对于 subgraph 容器的坐标)
|
||
// 先收集所有节点的原始坐标
|
||
const nodePositions = [];
|
||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||
|
||
subG.nodes().forEach(nodeId => {
|
||
const node = subG.node(nodeId);
|
||
if (!node) return;
|
||
|
||
const { cell, width, height } = node;
|
||
const rawX = Math.round(node.x - width / 2);
|
||
const rawY = Math.round(node.y - height / 2);
|
||
|
||
nodePositions.push({ cell, width, height, rawX, rawY });
|
||
|
||
minX = Math.min(minX, rawX);
|
||
minY = Math.min(minY, rawY);
|
||
maxX = Math.max(maxX, rawX + width);
|
||
maxY = Math.max(maxY, rawY + height);
|
||
});
|
||
|
||
console.log(`[DrawioOptimizer] 📍 原始节点范围: (${minX}, ${minY}) 到 (${maxX}, ${maxY})`);
|
||
|
||
// 计算需要的偏移量,确保节点在容器内正确位置
|
||
// swimlane 标题高度是 30px,左边距至少 20px
|
||
const targetMinX = 20; // 左边距
|
||
const targetMinY = 40; // 标题栏30px + 顶部边距10px
|
||
|
||
// 只在节点坐标异常时才应用偏移量
|
||
// 如果节点已经在合理位置(正数且接近目标),保持原位
|
||
let offsetX = 0;
|
||
let offsetY = 0;
|
||
|
||
// 只在节点出现负坐标或严重偏移时才修正
|
||
if (minX < 0) {
|
||
offsetX = targetMinX - minX; // 修正负坐标
|
||
} else if (minX < 10) {
|
||
offsetX = targetMinX - minX; // 太靠近边缘,加点边距
|
||
}
|
||
|
||
if (minY < 0) {
|
||
offsetY = targetMinY - minY; // 修正负坐标
|
||
} else if (minY < 30) {
|
||
offsetY = targetMinY - minY; // 太靠近标题栏,加点边距
|
||
}
|
||
|
||
console.log(`[DrawioOptimizer] 📐 应用偏移量: (${offsetX}, ${offsetY}) ${offsetX === 0 && offsetY === 0 ? '(无需调整)' : ''}`);
|
||
|
||
// 应用偏移后的坐标
|
||
let finalMinX = Infinity, finalMinY = Infinity, finalMaxX = -Infinity, finalMaxY = -Infinity;
|
||
|
||
nodePositions.forEach(({ cell, width, height, rawX, rawY }) => {
|
||
const finalX = rawX + offsetX;
|
||
const finalY = rawY + offsetY;
|
||
|
||
finalMinX = Math.min(finalMinX, finalX);
|
||
finalMinY = Math.min(finalMinY, finalY);
|
||
finalMaxX = Math.max(finalMaxX, finalX + width);
|
||
finalMaxY = Math.max(finalMaxY, finalY + height);
|
||
|
||
setCellGeometry(cell, {
|
||
x: finalX,
|
||
y: finalY,
|
||
width: width,
|
||
height: height
|
||
});
|
||
|
||
adjustedCount++;
|
||
});
|
||
|
||
console.log(`[DrawioOptimizer] ✅ 最终节点范围: (${finalMinX}, ${finalMinY}) 到 (${finalMaxX}, ${finalMaxY})`);
|
||
|
||
// 调整 subgraph 容器大小以包含所有成员
|
||
if (finalMinX !== Infinity && finalMinY !== Infinity) {
|
||
// 容器需要的尺寸 = 节点最大范围 + 底部/右侧边距
|
||
const requiredWidth = Math.ceil(finalMaxX + 20); // 右侧留20px边距
|
||
const requiredHeight = Math.ceil(finalMaxY + 20); // 底部留20px边距
|
||
|
||
setCellGeometry(container, {
|
||
x: containerGeo.x,
|
||
y: containerGeo.y,
|
||
width: Math.max(requiredWidth, 300), // 最小300px
|
||
height: Math.max(requiredHeight, 200) // 最小200px
|
||
});
|
||
|
||
console.log(`[DrawioOptimizer] 📏 容器调整: ${requiredWidth}x${requiredHeight}`);
|
||
}
|
||
});
|
||
|
||
// 3. 对顶层节点 + subgraph 容器进行全局 Dagre 布局
|
||
if (topLevelCells.length > 0 || subgraphContainers.length > 0) {
|
||
console.log(`[DrawioOptimizer] 🌍 全局布局 (${topLevelCells.length + subgraphContainers.length} 个顶层元素)...`);
|
||
|
||
const g = new graphlib.Graph();
|
||
g.setGraph(opts);
|
||
g.setDefaultEdgeLabel(() => ({}));
|
||
|
||
// 添加顶层节点和 subgraph 容器
|
||
[...topLevelCells, ...subgraphContainers].forEach(cell => {
|
||
const id = cell.getAttribute('id');
|
||
const geo = getCellGeometry(cell);
|
||
if (!geo) return;
|
||
|
||
g.setNode(id, {
|
||
width: geo.width,
|
||
height: geo.height,
|
||
originalGeo: geo,
|
||
cell: cell
|
||
});
|
||
});
|
||
|
||
// 添加顶层的边(不包括 subgraph 内部的边)
|
||
allEdges.forEach(edge => {
|
||
const source = edge.getAttribute('source');
|
||
const target = edge.getAttribute('target');
|
||
|
||
// 只添加至少有一端是顶层节点的边
|
||
if (source && target && g.hasNode(source) && g.hasNode(target)) {
|
||
g.setEdge(source, target);
|
||
}
|
||
});
|
||
|
||
// 执行全局布局
|
||
dagre.layout(g);
|
||
|
||
// 应用全局布局结果
|
||
g.nodes().forEach(nodeId => {
|
||
const node = g.node(nodeId);
|
||
if (!node) return;
|
||
|
||
const { cell, width, height } = node;
|
||
const newX = Math.round(node.x - width / 2);
|
||
const newY = Math.round(node.y - height / 2);
|
||
|
||
const geo = getCellGeometry(cell);
|
||
if (!geo) return;
|
||
|
||
// 直接设置容器位置
|
||
// 注意:subgraph 容器内的节点坐标是相对的,容器移动时会自动跟随,不需要单独调整
|
||
setCellGeometry(cell, {
|
||
x: newX,
|
||
y: newY,
|
||
width: width,
|
||
height: height
|
||
});
|
||
|
||
adjustedCount++;
|
||
});
|
||
}
|
||
|
||
console.log(`[DrawioOptimizer] ✅ Dagre 布局完成: ${adjustedCount} 个节点已优化`);
|
||
return adjustedCount;
|
||
|
||
} catch (error) {
|
||
console.error('[DrawioOptimizer] ❌ Dagre 布局失败:', error);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 自动间距优化(增强版 - 考虑连线关系)
|
||
* 检测并修复节点重叠问题,同时尽量减少连线交叉
|
||
*
|
||
* @param {Document} xmlDoc - XML 文档对象
|
||
* @param {number} minSpacing - 最小间距,默认 30px
|
||
*/
|
||
function optimizeSpacing(xmlDoc, minSpacing = 30) {
|
||
const cells = Array.from(xmlDoc.querySelectorAll('mxCell[vertex="1"]'));
|
||
const geometries = cells.map(cell => ({
|
||
cell,
|
||
id: cell.getAttribute('id'),
|
||
...getCellGeometry(cell)
|
||
})).filter(g => g.x !== undefined);
|
||
|
||
// 构建连接关系图
|
||
const edges = xmlDoc.querySelectorAll('mxCell[edge="1"]');
|
||
const connections = new Map(); // nodeId -> [targetIds]
|
||
|
||
edges.forEach(edge => {
|
||
const sourceId = edge.getAttribute('source');
|
||
const targetId = edge.getAttribute('target');
|
||
if (sourceId && targetId) {
|
||
if (!connections.has(sourceId)) {
|
||
connections.set(sourceId, []);
|
||
}
|
||
connections.get(sourceId).push(targetId);
|
||
}
|
||
});
|
||
|
||
let adjustedCount = 0;
|
||
|
||
// 按 Y 坐标分层
|
||
const layers = new Map();
|
||
geometries.forEach(g => {
|
||
const layerY = Math.round(g.y / 50) * 50; // 按 50px 分层
|
||
if (!layers.has(layerY)) {
|
||
layers.set(layerY, []);
|
||
}
|
||
layers.get(layerY).push(g);
|
||
});
|
||
|
||
// 对每一层进行排序优化,减少交叉
|
||
layers.forEach((nodesInLayer, layerY) => {
|
||
if (nodesInLayer.length <= 1) return;
|
||
|
||
// 按连接关系排序:如果节点有共同的目标,应该相邻放置
|
||
nodesInLayer.sort((a, b) => {
|
||
const aTargets = connections.get(a.id) || [];
|
||
const bTargets = connections.get(b.id) || [];
|
||
|
||
// 如果有共同目标,按第一个目标的 X 坐标排序
|
||
if (aTargets.length > 0 && bTargets.length > 0) {
|
||
// 简化:按第一个目标的 ID 字母序排序
|
||
return aTargets[0].localeCompare(bTargets[0]);
|
||
}
|
||
|
||
// 否则按当前 X 坐标排序
|
||
return a.x - b.x;
|
||
});
|
||
|
||
// 重新分配 X 坐标,保持间距
|
||
let currentX = nodesInLayer[0].x;
|
||
nodesInLayer.forEach((g, index) => {
|
||
if (index > 0) {
|
||
currentX += nodesInLayer[index - 1].width + minSpacing;
|
||
}
|
||
|
||
if (Math.abs(g.x - currentX) > 5) {
|
||
setCellGeometry(g.cell, {
|
||
x: currentX,
|
||
y: g.y,
|
||
width: g.width,
|
||
height: g.height
|
||
});
|
||
adjustedCount++;
|
||
}
|
||
|
||
g.x = currentX;
|
||
});
|
||
});
|
||
|
||
// 传统的重叠检测(跨层)
|
||
for (let i = 0; i < geometries.length; i++) {
|
||
for (let j = i + 1; j < geometries.length; j++) {
|
||
const g1 = geometries[i];
|
||
const g2 = geometries[j];
|
||
|
||
// 如果在不同层,跳过(已经在上面处理了)
|
||
const layer1 = Math.round(g1.y / 50) * 50;
|
||
const layer2 = Math.round(g2.y / 50) * 50;
|
||
if (layer1 === layer2) continue;
|
||
|
||
if (isOverlapping(g1, g2, minSpacing)) {
|
||
// 横向推开第二个节点
|
||
const newX = g1.x + g1.width + minSpacing;
|
||
setCellGeometry(g2.cell, {
|
||
x: newX,
|
||
y: g2.y,
|
||
width: g2.width,
|
||
height: g2.height
|
||
});
|
||
g2.x = newX; // 更新缓存
|
||
adjustedCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(`[DrawioOptimizer] 间距优化: ${adjustedCount} 个节点已调整`);
|
||
return adjustedCount;
|
||
}
|
||
|
||
/**
|
||
* 计算两个节点的最佳连接边缘
|
||
* 根据相对位置判断应该从哪条边连接
|
||
*
|
||
* 对于 LR 布局(从左到右):强制从左右边连接,不使用上下边
|
||
*
|
||
* @param {Object} sourceGeometry - 源节点几何信息
|
||
* @param {Object} targetGeometry - 目标节点几何信息
|
||
* @param {string} layoutDirection - 布局方向:'LR' 或 'TB',默认 'LR'
|
||
* @returns {Object} {exitX, exitY, entryX, entryY} - 归一化坐标 (0-1)
|
||
*/
|
||
function calculateOptimalConnection(sourceGeometry, targetGeometry, layoutDirection = 'LR') {
|
||
if (!sourceGeometry || !targetGeometry) {
|
||
return { exitX: 0.5, exitY: 0.5, entryX: 0.5, entryY: 0.5 };
|
||
}
|
||
|
||
// 计算中心点
|
||
const sourceCenterX = sourceGeometry.x + sourceGeometry.width / 2;
|
||
const sourceCenterY = sourceGeometry.y + sourceGeometry.height / 2;
|
||
const targetCenterX = targetGeometry.x + targetGeometry.width / 2;
|
||
const targetCenterY = targetGeometry.y + targetGeometry.height / 2;
|
||
|
||
// 计算相对位置
|
||
const dx = targetCenterX - sourceCenterX;
|
||
const dy = targetCenterY - sourceCenterY;
|
||
|
||
let exitX = 0.5, exitY = 0.5, entryX = 0.5, entryY = 0.5;
|
||
|
||
if (layoutDirection === 'LR') {
|
||
// LR 布局:强制使用左右边连接
|
||
if (dx > 0) {
|
||
// 目标在右侧 - 标准流向
|
||
exitX = 1; exitY = 0.5; // 从右边出发
|
||
entryX = 0; entryY = 0.5; // 从左边进入
|
||
} else {
|
||
// 目标在左侧 - 反向连接
|
||
exitX = 0; exitY = 0.5; // 从左边出发
|
||
entryX = 1; entryY = 0.5; // 从右边进入
|
||
}
|
||
} else {
|
||
// TB 布局:根据相对位置自动选择
|
||
const isHorizontal = Math.abs(dx) > Math.abs(dy);
|
||
|
||
if (isHorizontal) {
|
||
if (dx > 0) {
|
||
exitX = 1; exitY = 0.5;
|
||
entryX = 0; entryY = 0.5;
|
||
} else {
|
||
exitX = 0; exitY = 0.5;
|
||
entryX = 1; entryY = 0.5;
|
||
}
|
||
} else {
|
||
if (dy > 0) {
|
||
exitX = 0.5; exitY = 1;
|
||
entryX = 0.5; entryY = 0;
|
||
} else {
|
||
exitX = 0.5; exitY = 0;
|
||
entryX = 0.5; entryY = 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
return { exitX, exitY, entryX, entryY };
|
||
}
|
||
|
||
/**
|
||
* 检测两条连线是否交叉
|
||
* @param {Object} edge1 - {source, target, sourceGeo, targetGeo}
|
||
* @param {Object} edge2 - {source, target, sourceGeo, targetGeo}
|
||
* @returns {boolean} 是否交叉
|
||
*/
|
||
function detectEdgeCrossing(edge1, edge2) {
|
||
// 简化:检测线段的包围盒是否重叠
|
||
const box1 = {
|
||
minX: Math.min(edge1.sourceGeo.x, edge1.targetGeo.x),
|
||
maxX: Math.max(edge1.sourceGeo.x + edge1.sourceGeo.width, edge1.targetGeo.x + edge1.targetGeo.width),
|
||
minY: Math.min(edge1.sourceGeo.y, edge1.targetGeo.y),
|
||
maxY: Math.max(edge1.sourceGeo.y + edge1.sourceGeo.height, edge1.targetGeo.y + edge1.targetGeo.height)
|
||
};
|
||
|
||
const box2 = {
|
||
minX: Math.min(edge2.sourceGeo.x, edge2.targetGeo.x),
|
||
maxX: Math.max(edge2.sourceGeo.x + edge2.sourceGeo.width, edge2.targetGeo.x + edge2.targetGeo.width),
|
||
minY: Math.min(edge2.sourceGeo.y, edge2.targetGeo.y),
|
||
maxY: Math.max(edge2.sourceGeo.y + edge2.sourceGeo.height, edge2.targetGeo.y + edge2.targetGeo.height)
|
||
};
|
||
|
||
// 包围盒重叠检测
|
||
const overlapping = !(box1.maxX < box2.minX || box2.maxX < box1.minX ||
|
||
box1.maxY < box2.minY || box2.maxY < box1.minY);
|
||
|
||
if (!overlapping) return false;
|
||
|
||
// 进一步检测:如果是垂直布局(上下关系),检查是否交叉连接
|
||
const edge1IsVertical = Math.abs(edge1.targetGeo.y - edge1.sourceGeo.y) > 50;
|
||
const edge2IsVertical = Math.abs(edge2.targetGeo.y - edge2.sourceGeo.y) > 50;
|
||
|
||
if (edge1IsVertical && edge2IsVertical) {
|
||
// 检查交叉连接模式:A→C 和 B→D,如果 A 在 B 右侧但 C 在 D 左侧
|
||
const edge1SourceCenter = edge1.sourceGeo.x + edge1.sourceGeo.width / 2;
|
||
const edge1TargetCenter = edge1.targetGeo.x + edge1.targetGeo.width / 2;
|
||
const edge2SourceCenter = edge2.sourceGeo.x + edge2.sourceGeo.width / 2;
|
||
const edge2TargetCenter = edge2.targetGeo.x + edge2.targetGeo.width / 2;
|
||
|
||
// 交叉模式检测
|
||
if ((edge1SourceCenter > edge2SourceCenter && edge1TargetCenter < edge2TargetCenter) ||
|
||
(edge1SourceCenter < edge2SourceCenter && edge1TargetCenter > edge2TargetCenter)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 避免连线穿过节点的优化
|
||
* 检测连线路径是否穿过其他节点,并调整节点位置来避让
|
||
*
|
||
* @param {Document} xmlDoc - XML 文档对象
|
||
* @returns {number} 调整的节点数量
|
||
*/
|
||
function optimizeEdgeNodeAvoidance(xmlDoc) {
|
||
console.log('[DrawioOptimizer] 🎯 检测连线-节点冲突...');
|
||
|
||
const edges = xmlDoc.querySelectorAll('mxCell[edge="1"]');
|
||
const vertices = Array.from(xmlDoc.querySelectorAll('mxCell[vertex="1"]'));
|
||
const cellMap = new Map();
|
||
|
||
// 构建 ID -> Cell 的映射
|
||
xmlDoc.querySelectorAll('mxCell[id]').forEach(cell => {
|
||
cellMap.set(cell.getAttribute('id'), cell);
|
||
});
|
||
|
||
// 收集所有节点的几何信息
|
||
const nodeGeometries = vertices.map(cell => ({
|
||
cell,
|
||
id: cell.getAttribute('id'),
|
||
...getCellGeometry(cell)
|
||
})).filter(g => g.x !== undefined);
|
||
|
||
let adjustedCount = 0;
|
||
|
||
// 检测每条连线
|
||
edges.forEach(edge => {
|
||
const sourceId = edge.getAttribute('source');
|
||
const targetId = edge.getAttribute('target');
|
||
if (!sourceId || !targetId) return;
|
||
|
||
const sourceCell = cellMap.get(sourceId);
|
||
const targetCell = cellMap.get(targetId);
|
||
if (!sourceCell || !targetCell) return;
|
||
|
||
const sourceGeo = getCellGeometry(sourceCell);
|
||
const targetGeo = getCellGeometry(targetCell);
|
||
if (!sourceGeo || !targetGeo) return;
|
||
|
||
// 计算连线的包围盒(简化的正交路径)
|
||
// 正交路径:source中心 → 垂直移动 → 水平移动 → 垂直移动 → target中心
|
||
const sourceCenterX = sourceGeo.x + sourceGeo.width / 2;
|
||
const sourceCenterY = sourceGeo.y + sourceGeo.height / 2;
|
||
const targetCenterX = targetGeo.x + targetGeo.width / 2;
|
||
const targetCenterY = targetGeo.y + targetGeo.height / 2;
|
||
|
||
// 连线的包围盒(留10px余量)
|
||
const edgeBox = {
|
||
minX: Math.min(sourceCenterX, targetCenterX) - 10,
|
||
maxX: Math.max(sourceCenterX, targetCenterX) + 10,
|
||
minY: Math.min(sourceCenterY, targetCenterY) - 10,
|
||
maxY: Math.max(sourceCenterY, targetCenterY) + 10
|
||
};
|
||
|
||
// 检测是否有其他节点在连线路径上
|
||
nodeGeometries.forEach(node => {
|
||
// 跳过连线的起点和终点
|
||
if (node.id === sourceId || node.id === targetId) return;
|
||
|
||
// 检测节点是否在连线的包围盒内
|
||
const nodeBox = {
|
||
minX: node.x,
|
||
maxX: node.x + node.width,
|
||
minY: node.y,
|
||
maxY: node.y + node.height
|
||
};
|
||
|
||
// 包围盒相交检测
|
||
const isIntersecting = !(
|
||
nodeBox.maxX < edgeBox.minX ||
|
||
nodeBox.minX > edgeBox.maxX ||
|
||
nodeBox.maxY < edgeBox.minY ||
|
||
nodeBox.minY > edgeBox.minY
|
||
);
|
||
|
||
if (isIntersecting) {
|
||
// 检测节点是否在连线的"中间区域"(不是起点或终点附近)
|
||
const isInMiddleRegion =
|
||
nodeBox.minX > Math.min(sourceGeo.x + sourceGeo.width, targetGeo.x + targetGeo.width) &&
|
||
nodeBox.maxX < Math.max(sourceGeo.x, targetGeo.x);
|
||
|
||
if (isInMiddleRegion) {
|
||
// 需要调整节点位置
|
||
// 策略:将节点向左或向右移动,偏离连线路径
|
||
const edgeCenterX = (sourceCenterX + targetCenterX) / 2;
|
||
const nodeCenterX = node.x + node.width / 2;
|
||
|
||
// 计算移动方向(远离连线中心)
|
||
let newX;
|
||
if (nodeCenterX < edgeCenterX) {
|
||
// 节点在连线左侧,继续向左移
|
||
newX = edgeBox.minX - node.width - 30;
|
||
} else {
|
||
// 节点在连线右侧,继续向右移
|
||
newX = edgeBox.maxX + 30;
|
||
}
|
||
|
||
// 确保新位置不是负数
|
||
newX = Math.max(0, newX);
|
||
|
||
// 更新节点位置
|
||
setCellGeometry(node.cell, {
|
||
x: newX,
|
||
y: node.y,
|
||
width: node.width,
|
||
height: node.height
|
||
});
|
||
|
||
node.x = newX; // 更新缓存
|
||
adjustedCount++;
|
||
|
||
console.log(`[DrawioOptimizer] 调整节点 ${node.id},避让连线 ${sourceId}->${targetId}`);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
console.log(`[DrawioOptimizer] ✅ 连线-节点避让: ${adjustedCount} 个节点已调整`);
|
||
return adjustedCount;
|
||
}
|
||
|
||
/**
|
||
* 智能连接优化(增强版 - 包含理线功能)
|
||
* 自动设置连接线的出入点,并尝试减少交叉
|
||
*
|
||
* @param {Document} xmlDoc - XML 文档对象
|
||
* @param {string} layoutDirection - 布局方向:'LR' 或 'TB',默认 'LR'
|
||
*/
|
||
function optimizeConnections(xmlDoc, layoutDirection = 'LR') {
|
||
const edges = xmlDoc.querySelectorAll('mxCell[edge="1"]');
|
||
const cellMap = new Map();
|
||
|
||
// 构建 ID -> Cell 的映射
|
||
xmlDoc.querySelectorAll('mxCell[id]').forEach(cell => {
|
||
cellMap.set(cell.getAttribute('id'), cell);
|
||
});
|
||
|
||
// 收集所有连线信息
|
||
const edgeInfos = [];
|
||
edges.forEach(edge => {
|
||
const sourceId = edge.getAttribute('source');
|
||
const targetId = edge.getAttribute('target');
|
||
|
||
if (!sourceId || !targetId) return;
|
||
|
||
const sourceCell = cellMap.get(sourceId);
|
||
const targetCell = cellMap.get(targetId);
|
||
|
||
if (!sourceCell || !targetCell) return;
|
||
|
||
const sourceGeo = getCellGeometry(sourceCell);
|
||
const targetGeo = getCellGeometry(targetCell);
|
||
|
||
if (!sourceGeo || !targetGeo) return;
|
||
|
||
edgeInfos.push({
|
||
edge,
|
||
sourceId,
|
||
targetId,
|
||
sourceCell,
|
||
targetCell,
|
||
sourceGeo,
|
||
targetGeo
|
||
});
|
||
});
|
||
|
||
// 检测交叉并记录
|
||
let crossingCount = 0;
|
||
const crossingPairs = [];
|
||
|
||
for (let i = 0; i < edgeInfos.length; i++) {
|
||
for (let j = i + 1; j < edgeInfos.length; j++) {
|
||
if (detectEdgeCrossing(edgeInfos[i], edgeInfos[j])) {
|
||
crossingCount++;
|
||
crossingPairs.push([i, j]);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (crossingCount > 0) {
|
||
console.log(`[DrawioOptimizer] 检测到 ${crossingCount} 处连线交叉,尝试优化...`);
|
||
}
|
||
|
||
let optimizedCount = 0;
|
||
|
||
// 为每条连线设置最佳连接点
|
||
edgeInfos.forEach(info => {
|
||
const { edge, sourceGeo, targetGeo } = info;
|
||
|
||
// 计算最佳连接点(传入布局方向)
|
||
const connection = calculateOptimalConnection(sourceGeo, targetGeo, layoutDirection);
|
||
|
||
// 获取或创建 mxGeometry(必须是自闭合标签,不能有子元素)
|
||
let geometry = edge.querySelector('mxGeometry');
|
||
if (!geometry) {
|
||
geometry = xmlDoc.createElement('mxGeometry');
|
||
geometry.setAttribute('relative', '1');
|
||
geometry.setAttribute('as', 'geometry');
|
||
edge.appendChild(geometry);
|
||
}
|
||
|
||
// ❌ 删除任何 mxPoint 子元素(这会导致 "Could not add object mxGeometry" 错误)
|
||
const mxPoints = geometry.querySelectorAll('mxPoint');
|
||
mxPoints.forEach(point => point.remove());
|
||
|
||
// ✅ 连接点信息应该只放在 style 属性中,不要创建 mxPoint 子元素
|
||
// 更新样式:添加 exitX/exitY/entryX/entryY
|
||
const style = edge.getAttribute('style') || '';
|
||
const styleMap = new Map();
|
||
style.split(';').forEach(pair => {
|
||
const [key, value] = pair.split('=');
|
||
if (key) styleMap.set(key.trim(), value || '');
|
||
});
|
||
|
||
styleMap.set('exitX', connection.exitX);
|
||
styleMap.set('exitY', connection.exitY);
|
||
styleMap.set('entryX', connection.entryX);
|
||
styleMap.set('entryY', connection.entryY);
|
||
|
||
// 添加正交路由样式(美观且减少交叉)
|
||
if (!styleMap.has('edgeStyle')) {
|
||
styleMap.set('edgeStyle', 'orthogonalEdgeStyle');
|
||
}
|
||
if (!styleMap.has('rounded')) {
|
||
styleMap.set('rounded', '0');
|
||
}
|
||
|
||
const newStyle = Array.from(styleMap.entries())
|
||
.map(([k, v]) => v ? `${k}=${v}` : k)
|
||
.join(';');
|
||
|
||
edge.setAttribute('style', newStyle);
|
||
optimizedCount++;
|
||
});
|
||
|
||
console.log(`[DrawioOptimizer] 连接优化: ${optimizedCount} 条连接线已优化`);
|
||
return optimizedCount;
|
||
}
|
||
|
||
/**
|
||
* 样式统一优化
|
||
* 为相同类型的节点应用一致的样式
|
||
*
|
||
* @param {Document} xmlDoc - XML 文档对象
|
||
*/
|
||
function optimizeStyles(xmlDoc) {
|
||
const cells = xmlDoc.querySelectorAll('mxCell[vertex="1"]');
|
||
|
||
// 按节点类型分组(根据 style 中的 shape 或默认形状)
|
||
const typeGroups = new Map();
|
||
|
||
cells.forEach(cell => {
|
||
const style = cell.getAttribute('style') || '';
|
||
let type = 'default';
|
||
|
||
// 提取形状类型
|
||
const shapeMatch = style.match(/shape=([^;]+)/);
|
||
if (shapeMatch) {
|
||
type = shapeMatch[1];
|
||
} else if (style.includes('rounded=1')) {
|
||
type = 'rounded';
|
||
} else if (style.includes('ellipse')) {
|
||
type = 'ellipse';
|
||
}
|
||
|
||
if (!typeGroups.has(type)) {
|
||
typeGroups.set(type, []);
|
||
}
|
||
typeGroups.get(type).push(cell);
|
||
});
|
||
|
||
let optimizedCount = 0;
|
||
|
||
// 为每个类型组应用统一样式
|
||
typeGroups.forEach((cells, type) => {
|
||
if (cells.length < 2) return; // 少于2个节点,无需统一
|
||
|
||
// 收集第一个节点的样式作为基准
|
||
const referenceStyle = cells[0].getAttribute('style') || '';
|
||
const styleMap = new Map();
|
||
referenceStyle.split(';').forEach(pair => {
|
||
const [key, value] = pair.split('=');
|
||
if (key) styleMap.set(key.trim(), value || '');
|
||
});
|
||
|
||
// 确保有基本样式
|
||
if (!styleMap.has('fillColor')) {
|
||
// 根据类型设置默认颜色
|
||
const colors = {
|
||
'default': '#dae8fc',
|
||
'rounded': '#d5e8d4',
|
||
'ellipse': '#ffe6cc',
|
||
'swimlane': '#f5f5f5'
|
||
};
|
||
styleMap.set('fillColor', colors[type] || '#ffffff');
|
||
}
|
||
|
||
if (!styleMap.has('strokeColor')) {
|
||
styleMap.set('strokeColor', '#6c8ebf');
|
||
}
|
||
|
||
// 应用到所有同类型节点
|
||
const unifiedStyle = Array.from(styleMap.entries())
|
||
.map(([k, v]) => v ? `${k}=${v}` : k)
|
||
.join(';');
|
||
|
||
cells.forEach((cell, index) => {
|
||
if (index === 0) return; // 跳过参考节点
|
||
cell.setAttribute('style', unifiedStyle);
|
||
optimizedCount++;
|
||
});
|
||
});
|
||
|
||
console.log(`[DrawioOptimizer] 样式统一: ${optimizedCount} 个节点已优化`);
|
||
return optimizedCount;
|
||
}
|
||
|
||
/**
|
||
* 主优化函数
|
||
* 依次执行所有优化步骤
|
||
*
|
||
* @param {string} xmlString - 原始 draw.io XML 字符串
|
||
* @param {Object} options - 优化选项
|
||
* @param {boolean} options.gridAlignment - 是否网格对齐,默认 true
|
||
* @param {boolean} options.spacing - 是否间距优化,默认 true
|
||
* @param {boolean} options.connections - 是否连接优化,默认 true
|
||
* @param {boolean} options.styles - 是否样式统一,默认 false
|
||
* @returns {string} 优化后的 XML 字符串
|
||
*/
|
||
function optimizeDrawioLayout(xmlString, options = {}) {
|
||
const defaultOptions = {
|
||
dagreLayout: true, // 使用 Dagre 算法进行层次化布局(新增,默认开启)
|
||
gridAlignment: true,
|
||
spacing: true,
|
||
connections: true,
|
||
styles: false // 默认关闭,避免覆盖用户自定义样式
|
||
};
|
||
|
||
const opts = { ...defaultOptions, ...options };
|
||
|
||
try {
|
||
console.log('[DrawioOptimizer] 开始优化布局...');
|
||
|
||
// 解析 XML
|
||
const xmlDoc = parseDrawioXml(xmlString);
|
||
|
||
let totalOptimized = 0;
|
||
|
||
// 0. Dagre 层次化布局(优先级最高,使用标准 Sugiyama 算法)
|
||
if (opts.dagreLayout) {
|
||
totalOptimized += applyDagreLayout(xmlDoc, {
|
||
rankdir: 'LR', // 从左到右布局,层级横向展开
|
||
nodesep: 100, // 同层节点垂直间距 - 增加以减少交叉
|
||
ranksep: 180, // 不同层水平间距 - 增加以减少交叉
|
||
edgesep: 20, // 边之间的间距
|
||
ranker: 'network-simplex' // 最佳层分配算法
|
||
});
|
||
}
|
||
|
||
// 1. 网格对齐
|
||
if (opts.gridAlignment) {
|
||
totalOptimized += optimizeGridAlignment(xmlDoc, 10);
|
||
}
|
||
|
||
// 2. 间距优化(如果没有使用 dagre,则进行间距优化)
|
||
if (opts.spacing && !opts.dagreLayout) {
|
||
totalOptimized += optimizeSpacing(xmlDoc, 30);
|
||
}
|
||
|
||
// 3. 避免连线穿过节点
|
||
if (opts.spacing) {
|
||
totalOptimized += optimizeEdgeNodeAvoidance(xmlDoc);
|
||
}
|
||
|
||
// 4. 连接优化(传入布局方向)
|
||
if (opts.connections) {
|
||
totalOptimized += optimizeConnections(xmlDoc, 'LR');
|
||
}
|
||
|
||
// 5. 样式统一
|
||
if (opts.styles) {
|
||
totalOptimized += optimizeStyles(xmlDoc);
|
||
}
|
||
|
||
console.log(`[DrawioOptimizer] ✅ 优化完成,共优化 ${totalOptimized} 处`);
|
||
|
||
// 序列化回 XML
|
||
return serializeDrawioXml(xmlDoc);
|
||
|
||
} catch (error) {
|
||
console.error('[DrawioOptimizer] ❌ 优化失败:', error);
|
||
return xmlString; // 失败时返回原始 XML
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 对单个 diagram 进行优化(多页支持)
|
||
* @param {Element} diagram - diagram 元素
|
||
* @param {Object} opts - 优化选项
|
||
* @returns {number} 优化次数
|
||
*/
|
||
function optimizeDiagram(diagram, opts) {
|
||
const diagramName = diagram.getAttribute('name') || 'Unnamed';
|
||
|
||
// 创建临时文档,只包含当前 diagram
|
||
const tempDoc = document.implementation.createDocument(null, 'mxfile', null);
|
||
const tempDiagram = diagram.cloneNode(true);
|
||
tempDoc.documentElement.appendChild(tempDiagram);
|
||
|
||
let optimized = 0;
|
||
|
||
// 获取布局方向(默认 TB)
|
||
const layoutDir = opts.layoutDirection || 'TB';
|
||
|
||
// 应用所有优化(使用临时文档)
|
||
if (opts.dagreLayout) {
|
||
// 根据布局方向调整参数
|
||
const dagreOpts = layoutDir === 'TB' ? {
|
||
rankdir: 'TB', // 从上到下
|
||
nodesep: 80, // 同层节点横向间距
|
||
ranksep: 120, // 不同层纵向间距(更紧凑)
|
||
edgesep: 10,
|
||
ranker: 'network-simplex'
|
||
} : {
|
||
rankdir: 'LR', // 从左到右
|
||
nodesep: 100, // 同层节点纵向间距
|
||
ranksep: 180, // 不同层横向间距
|
||
edgesep: 20,
|
||
ranker: 'network-simplex'
|
||
};
|
||
|
||
optimized += applyDagreLayout(tempDoc, dagreOpts);
|
||
}
|
||
|
||
if (opts.gridAlignment) {
|
||
optimized += optimizeGridAlignment(tempDoc, 10);
|
||
}
|
||
|
||
if (opts.spacing && !opts.dagreLayout) {
|
||
optimized += optimizeSpacing(tempDoc, 30);
|
||
}
|
||
|
||
if (opts.spacing) {
|
||
optimized += optimizeEdgeNodeAvoidance(tempDoc);
|
||
}
|
||
|
||
if (opts.connections) {
|
||
optimized += optimizeConnections(tempDoc, layoutDir);
|
||
}
|
||
|
||
if (opts.styles) {
|
||
optimized += optimizeStyles(tempDoc);
|
||
}
|
||
|
||
// 将优化后的节点同步回原始 diagram
|
||
const optimizedDiagram = tempDoc.querySelector('diagram');
|
||
const originalCells = diagram.querySelectorAll('mxCell[id]');
|
||
const optimizedCells = optimizedDiagram.querySelectorAll('mxCell[id]');
|
||
|
||
const cellMap = new Map();
|
||
optimizedCells.forEach(cell => {
|
||
cellMap.set(cell.getAttribute('id'), cell);
|
||
});
|
||
|
||
originalCells.forEach(originalCell => {
|
||
const id = originalCell.getAttribute('id');
|
||
const optimizedCell = cellMap.get(id);
|
||
if (!optimizedCell) return;
|
||
|
||
// 同步几何信息和样式
|
||
const originalGeo = originalCell.querySelector('mxGeometry');
|
||
const optimizedGeo = optimizedCell.querySelector('mxGeometry');
|
||
|
||
if (originalGeo && optimizedGeo) {
|
||
// 同步所有属性
|
||
['x', 'y', 'width', 'height', 'relative', 'as', 'exitX', 'exitY', 'entryX', 'entryY'].forEach(attr => {
|
||
if (optimizedGeo.hasAttribute(attr)) {
|
||
originalGeo.setAttribute(attr, optimizedGeo.getAttribute(attr));
|
||
}
|
||
});
|
||
}
|
||
|
||
// 同步样式
|
||
if (optimizedCell.hasAttribute('style')) {
|
||
originalCell.setAttribute('style', optimizedCell.getAttribute('style'));
|
||
}
|
||
});
|
||
|
||
return optimized;
|
||
}
|
||
|
||
/**
|
||
* 主优化函数(支持多页图表)
|
||
* @param {string} xmlString - 原始 draw.io XML 字符串
|
||
* @param {Object} options - 优化选项
|
||
* @returns {string} 优化后的 XML 字符串
|
||
*/
|
||
function optimizeDrawioLayoutMultiPage(xmlString, options = {}) {
|
||
const defaultOptions = {
|
||
dagreLayout: true,
|
||
gridAlignment: true,
|
||
spacing: true,
|
||
connections: true,
|
||
styles: false
|
||
};
|
||
|
||
const opts = { ...defaultOptions, ...options };
|
||
|
||
try {
|
||
const xmlDoc = parseDrawioXml(xmlString);
|
||
const diagrams = Array.from(xmlDoc.querySelectorAll('diagram'));
|
||
|
||
if (diagrams.length === 0) {
|
||
console.warn('[DrawioOptimizer] ⚠️ 未找到 diagram 元素,使用旧版单页优化');
|
||
return optimizeDrawioLayout(xmlString, options);
|
||
}
|
||
|
||
console.log(`[DrawioOptimizer] 🎯 检测到 ${diagrams.length} 个页面,开始独立优化...`);
|
||
|
||
let totalOptimized = 0;
|
||
|
||
diagrams.forEach((diagram, index) => {
|
||
const name = diagram.getAttribute('name') || `Page ${index + 1}`;
|
||
console.log(`[DrawioOptimizer] 📄 优化页面 "${name}"...`);
|
||
const count = optimizeDiagram(diagram, opts);
|
||
console.log(`[DrawioOptimizer] ✅ 页面 "${name}" 完成,优化 ${count} 处`);
|
||
totalOptimized += count;
|
||
});
|
||
|
||
console.log(`[DrawioOptimizer] ✅ 全部完成,共优化 ${totalOptimized} 处`);
|
||
return serializeDrawioXml(xmlDoc);
|
||
|
||
} catch (error) {
|
||
console.error('[DrawioOptimizer] ❌ 多页优化失败,回退到单页模式:', error);
|
||
return optimizeDrawioLayout(xmlString, options);
|
||
}
|
||
}
|
||
|
||
// 导出到全局
|
||
window.DrawioLayoutOptimizer = {
|
||
optimizeDrawioLayout: optimizeDrawioLayoutMultiPage, // 使用新的多页版本
|
||
optimizeDrawioLayoutLegacy: optimizeDrawioLayout // 保留旧版本
|
||
};
|