Pavel Zhigalov 5 дней назад
Родитель
Сommit
04abf7b12d
3 измененных файлов с 113 добавлено и 158 удалено
  1. 1 1
      src/index.ts
  2. 0 2
      src/v1/optimizer/AlgorithmStep.ts
  3. 112 155
      src/v1/optimizer/siguiyama/CoordinateAssignmentStep.ts

+ 1 - 1
src/index.ts

@@ -37,7 +37,7 @@ function getSerializer() : Serializer<Graph<Node, Edge<Node>>, string> {
 	return new DefaultJsonSerializer();
 }
 
-const algorithm = new SiguiyamaAlgorithm().addStep(new CycleRemoveStep()).addStep(new LayerAssignmentStep()).addStep(new NodeOrderingStep()).addStep(new CoordinateAssignmentStep());
+const algorithm = new SiguiyamaAlgorithm().addStep(new CycleRemoveStep()).addStep(new LayerAssignmentStep()).addStep(new NodeOrderingStep()).addStep(new CoordinateAssignmentStep(20, 20));
 const optimizer = new BPMNOptimizer().setOptimizationAlgorithm(algorithm);
 
 const data = readFileSync("./data/graph.json").toString();

+ 0 - 2
src/v1/optimizer/AlgorithmStep.ts

@@ -8,12 +8,10 @@ export default abstract class AlgorithmStep<C extends AlgorithmContext> {
 	}
 
 	public onBeforeRun() : void {
-		console.log(`Before ${this._id} launch`);
 	}
 
 	public abstract run(context: C) : void;
 	
 	public onAfterRun() : void {
-		console.log(`After ${this._id} launch`);
 	}
 }

+ 112 - 155
src/v1/optimizer/siguiyama/CoordinateAssignmentStep.ts

@@ -1,21 +1,19 @@
-import AlgorithmStep from "../AlgorithmStep.js";
-import { SiguiyamaContext } from "./SiguiyamaContext.js";
 import Edge from "../../graph/Edge.js";
 import Graph from "../../graph/Graph.js";
+import Layer from "../../graph/layering/Layer.js";
 import Layering from "../../graph/layering/Layering.js";
 import Node from "../../graph/Node.js";
-import Layer from "../../graph/layering/Layer.js";
+import AlgorithmStep from "../AlgorithmStep.js";
+import { SiguiyamaContext } from "./SiguiyamaContext.js";
 
 export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaContext> {
-	private readonly _iterationsCount: number;
-	private readonly _verticalSpacing: number;
-	private readonly _horizontalSpacing: number;
+	private readonly _layerGap: number;
+	private readonly _nodeGap: number;
 
-	public constructor(iterationsCount: number = 6, verticalSpacing: number = 50, horizontalSpacing: number = 50) {
+	public constructor(layerGap: number, nodeGap: number) {
 		super(CoordinateAssignmentStep.name);
-		this._iterationsCount = iterationsCount;
-		this._verticalSpacing = verticalSpacing;
-		this._horizontalSpacing = horizontalSpacing;
+		this._layerGap = layerGap;
+		this._nodeGap = nodeGap;
 	}
 
 	public run(context: SiguiyamaContext): void {
@@ -26,187 +24,146 @@ export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaCon
 		if(!layering)
 			throw new Error("Layering of graph was not found!");
 
-		// Step 1: Assign Y coordinates
-		this.assignYCoordinates(graph, layering);
-
-		// Step 2: Initial X placement
-		this.assignInitialXCoordinates(graph, layering);
+		const reversedLayers = layering.getLayers().reverse();
 
-		// Steps 3-5: Iterative alignment with constraint resolution
-		for(let i = 0; i < this._iterationsCount; i++) {
-			this.topDownPass(graph, layering);
-			this.bottomUpPass(graph, layering);
-		}
-
-		// Step 7: Optional centering
-		this.centerLayout(graph);
+		console.log(graph.getNodes());
+		this.assignXCoordinates(graph, reversedLayers);
+		this.assignYCoordinates(graph, layering, reversedLayers);
+		console.log(graph.getNodes());
 	}
 
-	private assignYCoordinates(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
-		const layers = layering.getLayers();
-		const layerHeights = new Map<number, number>();
-		const layerOffsets = new Map<number, number>();
+	/**
+	 * Назначение X-координаты для всех вершин 
+	 * @param graph Граф, содержащий информацию о вершинах и рёбрах
+	 * @param layers Массив из слоев, по которым распределены вершины
+	 */
+	private assignXCoordinates(graph: Graph<Node, Edge<Node>>, layers: Layer<Node>[]) : void {
+		let currentX = 0;
 
-		// Compute max height per layer
 		for(const layer of layers) {
-			let maxHeight = 0;
+			const layerWidth = Math.max(...layer.nodes.map((nodeId) => graph.getNode(nodeId)?.width ?? 0));
+		
 			for(const nodeId of layer.nodes) {
 				const node = graph.getNode(nodeId);
-				if(node && node.height > maxHeight)
-					maxHeight = node.height;
-			}
-			layerHeights.set(layer.index, maxHeight);
-		}
-
-		// Compute vertical offsets
-		let currentOffset = 0;
-		for(const layer of layers) {
-			layerOffsets.set(layer.index, currentOffset);
-			const height = layerHeights.get(layer.index) || 0;
-			currentOffset += height + this._verticalSpacing;
-		}
+				if(!node)
+					continue;
 
-		// Assign Y coordinates
-		for(const layer of layers) {
-			const offset = layerOffsets.get(layer.index) || 0;
-			for(const nodeId of layer.nodes) {
-				const node = graph.getNode(nodeId);
-				if(node) {
-					node.y = offset + node.height / 2;
-				}
+				node.x = currentX + (layerWidth - node.width) / 2;
 			}
-		}
-	}
-
-	private assignInitialXCoordinates(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
-		const layers = layering.getLayers();
 
-		for(const layer of layers) {
-			let x = 0;
-			for(const nodeId of layer.nodes) {
-				const node = graph.getNode(nodeId);
-				if(node) {
-					node.x = x + node.width / 2;
-					x += node.width + this._horizontalSpacing;
-				}
-			}
+			currentX += layerWidth + this._layerGap;
 		}
 	}
 
-	private topDownPass(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
-		const layers = layering.getLayers();
-		const targetXMap = new Map<string, number>();
+	/**
+	 * Назначение Y-координаты для всех вершин 
+	 * @param graph Граф, содержащий информацию о вершинах и рёбрах
+	 * @param layering Укладка графа
+	 * @param layers Массив из слоев, по которым распределены вершины
+	 */
+	private assignYCoordinates(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, layers: Layer<Node>[]) : void {
+		if(layers.length === 0)
+			return;
 
-		// Compute target positions based on incoming neighbors
-		for(let i = 1; i < layers.length; i++) {
-			const layer = layers[i]!;
-			for(const nodeId of layer.nodes) {
-				const node = graph.getNode(nodeId);
-				if(!node) continue;
-
-				const incomingNodes = graph.getNodeInputs(nodeId);
-				if(incomingNodes.length > 0) {
-					const avgX = incomingNodes.reduce((sum, neighbor) => sum + neighbor.x, 0) / incomingNodes.length;
-					targetXMap.set(nodeId, avgX);
-				}
-			}
-		}
+		this.distributeLayerEvenly(graph, layers[0]!);
 
-		// Apply positions with constraints
 		for(let i = 1; i < layers.length; i++) {
-			this.applyPositionsWithConstraints(graph, layers[i]!, targetXMap);
-		}
-	}
-
-	private bottomUpPass(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
-		const layers = layering.getLayers();
-		const targetXMap = new Map<string, number>();
-
-		// Compute target positions based on outgoing neighbors
-		for(let i = layers.length - 2; i >= 0; i--) {
 			const layer = layers[i]!;
-			for(const nodeId of layer.nodes) {
-				const node = graph.getNode(nodeId);
-				if(!node) continue;
-
-				const outgoingNodes = graph.getNodeOutputs(nodeId);
-				if(outgoingNodes.length > 0) {
-					const avgX = outgoingNodes.reduce((sum, neighbor) => sum + neighbor.x, 0) / outgoingNodes.length;
-					targetXMap.set(nodeId, avgX);
-				}
-			}
+			this.assignYByBarycenter(graph, layer);
+			this.resolveNodeOverlaps(graph, layer);
 		}
+	}
 
-		// Apply positions with constraints
-		for(let i = layers.length - 2; i >= 0; i--) {
-			this.applyPositionsWithConstraints(graph, layers[i]!, targetXMap);
+	/**
+	 * Равномерное распределение вершин внутри слоя
+	 * @param graph Граф, содержащий информацию о вершинах и рёбрах
+	 * @param layer Слой, вершины которого нужно распределить равномерно
+	 */
+	private distributeLayerEvenly(graph: Graph<Node, Edge<Node>>, layer: Layer<Node>) : void {
+		let currentY = 0.0;
+
+		for(const nodeId of layer.nodes) {
+			const node = graph.getNode(nodeId);
+			if(!node)
+				continue;
+
+			node.y = currentY;
+			currentY += node.height + this._nodeGap;
 		}
 	}
 
-	private applyPositionsWithConstraints(graph: Graph<Node, Edge<Node>>, layer: Layer<Node>, targetXMap: Map<string, number>): void {
-		const nodeIds = layer.nodes;
-
-		// Step 5.1: Apply target X positions
-		for(const nodeId of nodeIds) {
-			if(targetXMap.has(nodeId)) {
-				const node = graph.getNode(nodeId);
-				if(node) {
-					node.x = targetXMap.get(nodeId)!;
-				}
+	/**
+	 * Назначение Y-координаты для вершин внутрия слоя с выравниванием по barycenter соседних вершин
+	 * @param graph Граф, содержащий информацию о вершинах и рёбрах
+	 * @param layer Слой, вершины которого нужно распределить и выровнять
+	 */
+	private assignYByBarycenter(graph: Graph<Node, Edge<Node>>, layer: Layer<Node>) : void {
+		const assignments: { nodeId: Node["id"], y: number }[] = [];
+
+		for(const nodeId of layer.nodes) {
+			const node = graph.getNode(nodeId);
+			if(!node)
+				continue;
+
+			const predecessors = graph.getNodeInputs(nodeId);
+			if(predecessors.length === 0) {
+				assignments.push({ nodeId, y: node.y ?? 0 });
+				continue;
 			}
-		}
-
-		// Step 5.2: Resolve overlaps (left to right)
-		for(let i = 0; i < nodeIds.length - 1; i++) {
-			const leftNode = graph.getNode(nodeIds[i]!);
-			const rightNode = graph.getNode(nodeIds[i + 1]!);
 
-			if(!leftNode || !rightNode) continue;
-
-			const minDistance = leftNode.width / 2 + this._horizontalSpacing + rightNode.width / 2;
-
-			if(rightNode.x < leftNode.x + minDistance) {
-				rightNode.x = leftNode.x + minDistance;
-			}
+			const averageY = predecessors.reduce((sum, pred) => sum + (pred.y ?? 0) + (pred.height ?? 0) / 2, 0) / predecessors.length;
+			assignments.push({ nodeId, y: averageY - node.height / 2});
 		}
 
-		// Step 5.3: Stabilize (right to left)
-		for(let i = nodeIds.length - 1; i > 0; i--) {
-			const leftNode = graph.getNode(nodeIds[i - 1]!);
-			const rightNode = graph.getNode(nodeIds[i]!);
+		assignments.sort((a, b) => a.y - b.y);
 
-			if(!leftNode || !rightNode) continue;
+		for(const { nodeId, y } of assignments) {
+			const node = graph.getNode(nodeId);
+			if(!node)
+				continue;
 
-			const minDistance = leftNode.width / 2 + this._horizontalSpacing + rightNode.width / 2;
+			node.y = y;
+		}
+	}
 
-			if(leftNode.x > rightNode.x - minDistance) {
-				leftNode.x = rightNode.x - minDistance;
-			}
+	/**
+	 * Разрешение конфликтов, связанных с перекрытием вершин друг другом
+	 * @param graph Граф, содержащий информацию о вершинах и рёбрах
+	 * @param layer Слой, в котором необходимо разрешить конфликты перекрытия вершин
+	 */
+	private resolveNodeOverlaps(graph: Graph<Node, Edge<Node>>, layer: Layer<Node>) : void {
+		const nodes = layer.nodes.map((nodeId) => graph.getNode(nodeId)).filter((node) => node !== null).sort((a, b) => (a.y ?? 0) - (b.y ?? 0));
+
+		for(let i = 1; i < nodes.length; i++) {
+			const previous = nodes[i - 1], current = nodes[i];
+			const minY = (previous?.y ?? 0) + (previous?.height ?? 0) + this._nodeGap;
+			
+			if(current && (current.y ?? 0) < minY)
+				current.y = minY;
 		}
+
+		this.centerNodesVertically(nodes);
 	}
 
-	private centerLayout(graph: Graph<Node, Edge<Node>>): void {
-		const nodes = graph.getNodes();
 
-		if(nodes.length === 0) return;
+	/**
+	 * Центрирование вершин в слое по вертикали
+	 * @param nodes Вершины, которые надо отцентровать
+	 */
+	private centerNodesVertically(nodes: Node[]) : void {
+		if(!nodes || nodes.length == 0)
+			return;
 
-		// Find bounding box
-		let minX = Infinity;
-		let maxX = -Infinity;
+		const totalHeight = nodes.reduce((sum, node) => sum + (node.height ?? 0), 0) + this._nodeGap * (nodes.length - 1);
 
-		for(const node of nodes) {
-			const left = node.x - node.width / 2;
-			const right = node.x + node.width / 2;
-			minX = Math.min(minX, left);
-			maxX = Math.max(maxX, right);
-		}
+		const firstY = nodes[0]?.y ?? 0;
+		const offset = firstY - totalHeight / 2;
 
-		// Calculate center offset
-		const center = (minX + maxX) / 2;
+		const eps = 1.0;
+		if(Math.abs(offset) < eps)
+			return;
 
-		// Center all nodes
-		for(const node of nodes) {
-			node.x -= center;
-		}
+		for(const node of nodes)
+			node.y = (node.y ?? 0) - offset;
 	}
 }