Pavel Zhigalov 5 days ago
parent
commit
2e07ead8a2

+ 15 - 5
src/index.ts

@@ -1,11 +1,21 @@
 import { readFileSync } from "node:fs";
-import JointJsonDeserializer from "./v1/io/deserialize/json/JointJsonDeserializer.js";
+import DZWFDeserializer from "./v1/io/deserialize/dzwf/DZWFDeserializer.js";
 import CycleRemoveStep from "./v1/optimizer/siguiyama/CycleRemoveStep.js";
 import LayerAssignmentStep from "./v1/optimizer/siguiyama/LayerAssignmentStep.js";
 import SiguiyamaAlgorithm from "./v1/optimizer/siguiyama/SiguiyamaAlgorithm.js";
+import NodeOrderingStep from "./v1/optimizer/siguiyama/NodeOrderingStep.js";
+import CoordinateAssignmentStep from "./v1/optimizer/siguiyama/CoordinateAssignmentStep.js";
+import DZWFJsonSerializer from "./v1/io/serialize/dzwf/JointJsonSerializer.js";
 
-const deserializer = new JointJsonDeserializer();
-const graph = deserializer.deserialize(String(readFileSync("./data/graph.json")));
-const optimizer = new SiguiyamaAlgorithm().addStep(new CycleRemoveStep()).addStep(new LayerAssignmentStep());
+const deserializer = new DZWFDeserializer();
+const { graph, dzwfData } = deserializer.deserialize(String(readFileSync("./data/bpmn.json")));
+const optimizer = new SiguiyamaAlgorithm()
+	.addStep(new CycleRemoveStep())
+	.addStep(new LayerAssignmentStep())
+	.addStep(new NodeOrderingStep(20))
+	.addStep(new CoordinateAssignmentStep());
 
-optimizer.run(graph);
+optimizer.run(graph);
+
+const serializer = new DZWFJsonSerializer(dzwfData);
+serializer.serialize(graph);

+ 11 - 0
src/v1/errors/optimizer/NodeOrderingStepError.ts

@@ -0,0 +1,11 @@
+import OptimizerError from "./OptimizerError.js";
+
+/**
+ * Common class for all errors that occured in node ordering algorithm step
+ */
+export default class NodeOrderingStepError extends OptimizerError {
+	public constructor(message: string) {
+		super(message);
+		this.name = NodeOrderingStepError.name;
+	}
+}

+ 5 - 2
src/v1/graph/Node.ts

@@ -6,13 +6,15 @@ export default class Node {
 	public y: number;
 	public width: number;
 	public height: number;
+	public type: string;
 
-	public constructor(x: number, y: number, width: number, height: number, id?: string) {
+	public constructor(x: number, y: number, width: number, height: number, type: string, id?: string) {
 		this.id = id || crypto.randomUUID();
 		this.x = x;
 		this.y = y;
 		this.width = width;
 		this.height = height;
+		this.type = type;
 	}
 }
 
@@ -21,5 +23,6 @@ export const NodeSchema = object({
 	x: number(),
 	y: number(),
 	width: number(),
-	height: number()
+	height: number(),
+	type: string()
 })

+ 5 - 0
src/v1/graph/layering/Layer.ts

@@ -28,6 +28,11 @@ export default class Layer<TNode extends Node> {
 		return [...this._nodes];
 	}
 
+	public set nodes(nodes: TNode["id"][]) {
+		this._nodes.length = 0;
+		this._nodes.push(...nodes);
+	}
+
 	public get width(): number {
 		return this._nodes.length;
 	}

+ 13 - 0
src/v1/graph/layering/Layering.ts

@@ -45,6 +45,19 @@ export default class Layering<TNode extends Node, TEdge extends Edge<TNode>> {
 		return this;
 	}
 
+	public getPositionOf(nodeId: TNode["id"]) : number | null {
+		const layerIndex = this.getLayerOf(nodeId);
+		if(layerIndex === null)
+			return null;
+
+		const layer = this._layers.get(layerIndex);
+		if(!layer)
+			return null;
+
+		const position = layer.nodes.indexOf(nodeId);
+		return position !== -1 ? position : null;
+	}
+
 	public isEdgeTight(edge: TEdge) : boolean {
 		const { from, to } = edge;
 		const fromLayerIndex = this.getLayerOf(from), toLayerIndex = this.getLayerOf(to);

+ 35 - 0
src/v1/io/deserialize/dzwf/DZWFDeserializer.ts

@@ -0,0 +1,35 @@
+import { InferOutput, parse } from "valibot";
+import DeserializerError from "../../../errors/DeserializerError.js";
+import Edge from "../../../graph/Edge.js";
+import Graph, { GraphSchema } from "../../../graph/Graph.js";
+import Node from "../../../graph/Node.js";
+import { JsonDeserializer } from "../json/JsonDeserializer.js";
+import { DZWFData } from "../../dzwf/DZWFData.js";
+
+export default class DZWFDeserializer implements JsonDeserializer<{ graph: Graph<Node, Edge<Node>>, dzwfData: DZWFData & unknown }> {
+	deserialize(input: string) : { graph: Graph<Node, Edge<Node>>, dzwfData: DZWFData & unknown }  {
+		const parsed = JSON.parse(input) as DZWFData;
+
+		if(!parsed)
+			throw new DeserializerError("Result of parsing graph structure not found");
+
+		let graph: InferOutput<typeof GraphSchema>;
+		try {
+			const nodes: InferOutput<typeof GraphSchema>["nodes"] = parsed.elements.map((element) => {
+				const { diagram: { position: { x, y } }, type, id } = element;
+				return { x, y, type, id, width: 0, height: 0 }
+			})
+			const edges: InferOutput<typeof GraphSchema>["edges"] = parsed.links.map((link) => {
+				const { sourceId, targetId, id } = link;
+				return { id, from: sourceId, to: targetId };
+			})
+
+			graph = parse(GraphSchema, { nodes, edges });
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		} catch (error: any) {
+			throw new DeserializerError(error.message)
+		}
+
+		return { graph: new Graph(graph.nodes, graph.edges), dzwfData: parsed };
+	}
+}

+ 0 - 25
src/v1/io/deserialize/json/JointJsonDeserializer.ts

@@ -1,25 +0,0 @@
-import { InferOutput, parse } from "valibot";
-import DeserializerError from "../../../errors/DeserializerError.js";
-import Edge from "../../../graph/Edge.js";
-import Graph, { GraphSchema } from "../../../graph/Graph.js";
-import Node from "../../../graph/Node.js";
-import { JsonDeserializer } from "./JsonDeserializer.js";
-
-export default class JointJsonDeserializer implements JsonDeserializer<Graph<Node, Edge<Node>>> {
-	deserialize(input: string) : Graph<Node, Edge<Node>> {
-		const parsed = JSON.parse(input);
-
-		if(!parsed)
-			throw new DeserializerError("Result of parsing graph structure not found");
-
-		let graph: InferOutput<typeof GraphSchema>;
-		try {
-			graph = parse(GraphSchema, parsed);
-		// eslint-disable-next-line @typescript-eslint/no-explicit-any
-		} catch (error: any) {
-			throw new DeserializerError(error.message)
-		}
-
-		return new Graph(graph.nodes, graph.edges);
-	}
-}

+ 19 - 0
src/v1/io/dzwf/DZWFData.ts

@@ -0,0 +1,19 @@
+export type DZWFData = { 
+	elements: { 
+		diagram: { 
+			position: { x: number, y: number }
+		},
+		outs: unknown[],
+		ins: unknown[],
+		variables: unknown[],
+		stages: unknown[],
+		name: string,
+		type: string,
+		id: string
+	}[], 
+	links: {
+		sourceId: string,
+		targetId: string,
+		id: string
+	}[] 
+};

+ 30 - 0
src/v1/io/serialize/dzwf/JointJsonSerializer.ts

@@ -0,0 +1,30 @@
+import { writeFileSync } from "node:fs";
+import Edge from "../../../graph/Edge.js";
+import Graph from "../../../graph/Graph.js";
+import Node from "../../../graph/Node.js";
+import Serializer from "../Serializer.js";
+import { DZWFData } from "../../dzwf/DZWFData.js";
+
+export default class DZWFJsonSerializer implements Serializer<Graph<Node, Edge<Node>>, void> {
+	private readonly _dzwfData: DZWFData & unknown;
+	private readonly _dzwfFileName: string;
+	
+	public constructor(dzwfData: DZWFData & unknown, dzwfFileName: string = "result.dzwf") {
+		this._dzwfData = dzwfData;
+		this._dzwfFileName = dzwfFileName;
+	}
+
+	serialize(data: Graph<Node, Edge<Node>>): void {
+		this._dzwfData.elements.forEach((element) => {
+			const { id } = element;
+
+			const node = data.getNode(id);
+			if(!node)
+				return;
+
+			element.diagram.position = { x: node.x, y: node.y };
+		});
+
+		writeFileSync(this._dzwfFileName, JSON.stringify(this._dzwfData));
+	}
+}

+ 0 - 21
src/v1/io/serialize/json/GraphJsonSerializer.ts

@@ -1,21 +0,0 @@
-import Edge from "../../../graph/Edge.js";
-import Graph from "../../../graph/Graph.js";
-import Node from "../../../graph/Node.js";
-import { JsonSerializer } from "./JsonSerializer.js";
-
-type NodeJson = { id: string, x: number, y: number };
-type EdgeJson = { id: string, from: NodeJson["id"], to: NodeJson["id"] };
-type GraphJson = { nodes: NodeJson[], edges: EdgeJson[] };
-
-export default class GraphJsonSerializer implements JsonSerializer<Graph<Node, Edge<Node>>> {
-	serialize(data: Graph<Node, Edge<Node>>): string {
-		const graphJson: GraphJson = {
-			nodes: data.getNodes().map((node) => { return { id: node.id, x: node.x, y: node.y } }),
-			edges: data.getEdges().map((edge) => {
-				return { id: edge.id, from: edge.from, to: edge.to };
-			})
-		}
-
-		return JSON.stringify(graphJson);
-	}
-}

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

@@ -1,5 +1,19 @@
 import { AlgorithmContext } from "./AlgorithmContext.js";
 
-export default interface AlgorithmStep<C extends AlgorithmContext> {
-	run(context: C): void
+export default abstract class AlgorithmStep<C extends AlgorithmContext> {
+	private readonly _id: string;
+
+	protected constructor(id: string) {
+		this._id = id;
+	}
+
+	public onBeforeRun() : void {
+		console.log(`Before ${this._id} launch`);
+	}
+
+	public abstract run(context: C) : void;
+	
+	public onAfterRun() : void {
+		console.log(`After ${this._id} launch`);
+	}
 }

+ 212 - 0
src/v1/optimizer/siguiyama/CoordinateAssignmentStep.ts

@@ -0,0 +1,212 @@
+import AlgorithmStep from "../AlgorithmStep.js";
+import { SiguiyamaContext } from "./SiguiyamaContext.js";
+import Edge from "../../graph/Edge.js";
+import Graph from "../../graph/Graph.js";
+import Layering from "../../graph/layering/Layering.js";
+import Node from "../../graph/Node.js";
+import Layer from "../../graph/layering/Layer.js";
+
+export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaContext> {
+	private readonly _iterationsCount: number;
+	private readonly _verticalSpacing: number;
+	private readonly _horizontalSpacing: number;
+
+	public constructor(iterationsCount: number = 6, verticalSpacing: number = 50, horizontalSpacing: number = 50) {
+		super(CoordinateAssignmentStep.name);
+		this._iterationsCount = iterationsCount;
+		this._verticalSpacing = verticalSpacing;
+		this._horizontalSpacing = horizontalSpacing;
+	}
+
+	public run(context: SiguiyamaContext): void {
+		const { graph, layering } = context;
+
+		if(!graph)
+			throw new Error("Source graph was not found!");
+		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);
+
+		// 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);
+	}
+
+	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>();
+
+		// Compute max height per layer
+		for(const layer of layers) {
+			let maxHeight = 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;
+		}
+
+		// 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;
+				}
+			}
+		}
+	}
+
+	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;
+				}
+			}
+		}
+	}
+
+	private topDownPass(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 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);
+				}
+			}
+		}
+
+		// 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);
+				}
+			}
+		}
+
+		// Apply positions with constraints
+		for(let i = layers.length - 2; i >= 0; i--) {
+			this.applyPositionsWithConstraints(graph, layers[i]!, targetXMap);
+		}
+	}
+
+	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)!;
+				}
+			}
+		}
+
+		// 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;
+			}
+		}
+
+		// 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]!);
+
+			if(!leftNode || !rightNode) continue;
+
+			const minDistance = leftNode.width / 2 + this._horizontalSpacing + rightNode.width / 2;
+
+			if(leftNode.x > rightNode.x - minDistance) {
+				leftNode.x = rightNode.x - minDistance;
+			}
+		}
+	}
+
+	private centerLayout(graph: Graph<Node, Edge<Node>>): void {
+		const nodes = graph.getNodes();
+
+		if(nodes.length === 0) return;
+
+		// Find bounding box
+		let minX = Infinity;
+		let maxX = -Infinity;
+
+		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);
+		}
+
+		// Calculate center offset
+		const center = (minX + maxX) / 2;
+
+		// Center all nodes
+		for(const node of nodes) {
+			node.x -= center;
+		}
+	}
+}

+ 6 - 2
src/v1/optimizer/siguiyama/CycleRemoveStep.ts

@@ -8,8 +8,12 @@ import { SiguiyamaContext } from "./SiguiyamaContext.js";
 /**
  * Greedy Cycle Removal Algoirthm
  */
-export default class CycleRemoveStep implements AlgorithmStep<SiguiyamaContext> {
-	run(context: SiguiyamaContext): void {
+export default class CycleRemoveStep extends AlgorithmStep<SiguiyamaContext> {
+	public constructor() {
+		super(CycleRemoveStep.name)
+	}
+
+	public run(context: SiguiyamaContext): void {
 		const { graph } = context;
 		if(!graph)
 			throw new CycleRemoveStepError("Graph not found!");

+ 10 - 237
src/v1/optimizer/siguiyama/LayerAssignmentStep.ts

@@ -6,8 +6,15 @@ import Node from "../../graph/Node.js";
 import AlgorithmStep from "../AlgorithmStep.js";
 import { SiguiyamaContext } from "./SiguiyamaContext.js";
 
-export default class LayerAssignmentStep implements AlgorithmStep<SiguiyamaContext> {
-	run(context: SiguiyamaContext): void {
+/**
+ * Network simplex algorithm
+ */
+export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext> {
+	public constructor() {
+		super(LayerAssignmentStep.name);
+	}
+
+	public run(context: SiguiyamaContext): void {
 		const { graph, feedbackSet } = context;
 
 		if(!graph)
@@ -16,11 +23,9 @@ export default class LayerAssignmentStep implements AlgorithmStep<SiguiyamaConte
 			throw new LayerAssignmentStepError("Feedback set is undefined!");
 
 		const layering = this.longestPathAlgorithm(graph);
-		this.networkSimplexAlgorithm(graph, layering);
-
-		console.log(JSON.stringify(layering, null, 2))
 
 		context.layering = layering;
+		console.log(layering.getLayers().map((l) => l.nodes));
 	}
 
 	private longestPathAlgorithm(dag: Graph<Node, Edge<Node>>) : NonNullable<SiguiyamaContext["layering"]> {
@@ -58,236 +63,4 @@ export default class LayerAssignmentStep implements AlgorithmStep<SiguiyamaConte
 
 		return layering;
 	}
-
-	private networkSimplexAlgorithm(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : void {
-		const spanningTree = this.getFeasibleTree(dag, layering);
-
-		let leaveEdge = this.leaveEdge(dag, layering, spanningTree);
-		while(leaveEdge !== null) {
-			const enterEdge = this.enterEdge(dag, layering,	spanningTree, leaveEdge);
-			this.exchange(dag, layering, spanningTree, leaveEdge, enterEdge);
-			leaveEdge = this.leaveEdge(dag, layering, spanningTree);
-		}
-
-		layering.normalize();
-		this.balance(dag, layering);
-	}
-
-	private getFeasibleTree(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : Set<Edge<Node>["id"]> {
-		const spanningTree = new Set<Edge<Node>["id"]>();
-		const visitedNodes = new Set<Node["id"]>();
-		const nodes = dag.getNodes(), edges = dag.getEdges();
-
-		if(nodes.length === 0)
-			return spanningTree;
-
-		const queue: Node["id"][] = [nodes[0]!.id];
-		visitedNodes.add(nodes[0]!.id);
-
-		while(queue.length > 0) {
-			const nodeId = queue.shift()!;
-
-			for(const edge of edges) {
-				if(!layering.isEdgeTight(edge))
-					continue;
-
-				const { id: edgeId, from, to } = edge;
-
-				const isFromCurrent = from === nodeId && !visitedNodes.has(to);
-				const isToCurrent = to === nodeId && !visitedNodes.has(from);
-
-				if(!isFromCurrent && !isToCurrent)
-					continue;
-
-				const neighbourId = isFromCurrent ? edge.to : edge.from;
-				spanningTree.add(edgeId);
-				visitedNodes.add(neighbourId);
-				queue.push(neighbourId);
-			}
-		}
-
-		return spanningTree;
-	}
-
-	private leaveEdge(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>) : Edge<Node> | null {
-		const edges = dag.getEdges();
-
-		for(const edge of edges) {
-			if(!spanningTree.has(edge.id))
-				continue;
-
-			const cutValue = this.getCutValue(dag, layering, spanningTree, edge);
-			if(cutValue < 0)
-				return edge;
-		}
-
-		return null;
-	}
-
-	private getCutValue(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>, edge: Edge<Node>) : number {
-		const { head, tail } = this.splitTree(dag, spanningTree, edge);
-
-		let cutValue = 0;
-
-		for(const edge of dag.getEdges()) {
-			const { from, to } = edge;
-			const fromInTail = tail.has(from);
-			const toInTail = tail.has(to);
-			const fromInHead = head.has(from);
-			const toInHead = head.has(to);
-
-			if(fromInTail && toInHead) cutValue++; // tail -> head
-			if(fromInHead && toInTail) cutValue--; // head -> tail
-		}
-
-		return cutValue;
-	}
-
-	private splitTree(dag: Graph<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>, removedEdge: Edge<Node>) : { head: Set<Node["id"]>, tail: Set<Node["id"]> } {
-		const { from } = removedEdge;
-		
-		const treeEdges = dag.getEdges().filter((edge) => {
-			return spanningTree.has(edge.id) && edge.id !== removedEdge.id;
-		})
-
-		const tail = new Set<Node["id"]>();
-		const queue = [removedEdge.from];
-		tail.add(from);
-
-		while(queue.length > 0) {
-			const nodeId = queue.shift()!;
-			for(const edge of treeEdges) {
-				const { from, to } = edge;
-				if(from == nodeId && !tail.has(to)) {
-					tail.add(to);
-					queue.push(to);
-				}
-				if(to == nodeId && !tail.has(from)) {
-					tail.add(from);
-					queue.push(from);
-				}
-			}
-		}
-
-		const head = new Set<Node["id"]>(
-			dag.getNodes().map((node) => node.id).filter((id) => !tail.has(id))
-		);
-
-		return { head, tail };
-	}
-
-	private enterEdge(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>, leaveEdge: Edge<Node>) : Edge<Node> {
-		const { head, tail } = this.splitTree(dag, spanningTree, leaveEdge);
-
-		let minSpan = Infinity;
-		let bestEdge: Edge<Node> | null = null;
-
-		for(const edge of dag.getEdges()) {
-			if(spanningTree.has(edge.id))
-				continue;
-
-			const fromInHead = head.has(edge.from);
-			const toInTail = tail.has(edge.to);
-			const fromInTail = tail.has(edge.from);
-			const toInHead = head.has(edge.to);
-
-			// edges between components (any direction)
-			if((!fromInHead || !toInTail) && (!fromInTail || !toInHead))
-				continue;
-
-			const span = layering.getEdgeSpan(edge);
-			if(span >= minSpan)
-				continue;
-
-			minSpan = span;
-			bestEdge = edge;
-		}
-
-		if(!bestEdge)
-			throw new LayerAssignmentStepError("No enter edge found");
-
-		return bestEdge;
-	}
-
-	private exchange(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>, leaveEdge: Edge<Node>, enterEdge: Edge<Node>) : void {
-		spanningTree.delete(leaveEdge.id);
-		spanningTree.add(enterEdge.id);
-	}
-
-	private recomputeLayers(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>) : void {
-		const nodes = dag.getNodes();
-		const treeEdges = dag.getEdges().filter((edge) => spanningTree.has(edge.id));
-
-		const inDegree = new Map<Node["id"], number>();
-		nodes.forEach((node) => inDegree.set(node.id, 0));
-		treeEdges.forEach((edge) => inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1));
-
-		const source = nodes.find((node) => inDegree.get(node.id) === 0);
-		if(!source)
-			return;
-
-		layering.setLayerOf(source.id, 0);
-
-		const queue = [source.id];
-		const visited = new Set<Node["id"]>([source.id]);
-
-		while(queue.length > 0) {
-			const nodeId = queue.shift()!;
-			const currentLayer = layering.getLayerOf(nodeId) ?? 0;
-		
-			for(const edge of treeEdges) {
-				const { from, to } = edge;
-				if(from === nodeId && !visited.has(to)) {
-					layering.setLayerOf(to, currentLayer + 1);
-					visited.add(to);
-					queue.push(to);
-				}
-				if(to === nodeId && !visited.has(from)) {
-					layering.setLayerOf(from, currentLayer - 1);
-					visited.add(from);
-					queue.push(from);
-				}
-			}
-		}
-	}
-
-	private balance(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : void {
-		for(const { id } of dag.getNodes()) {
-			const inDegree = dag.getNodeInputs(id).length;
-			const outDegree = dag.getNodeOutputs(id).length;
-
-			if(inDegree !== outDegree)
-				continue;
-
-			const currentLayer = layering.getLayerOf(id);
-			if(currentLayer == null)
-				continue;
-
-			const predecessorLayers = dag.getNodeInputs(id).map((node) => layering.getLayerOf(node.id)).filter((layer) => layer !== null);
-			if(predecessorLayers.length === 0)
-				continue;
-
-			const successorLayers = dag.getNodeOutputs(id).map((node) => layering.getLayerOf(node.id)).filter((layer) => layer !== null);
-			if(successorLayers.length === 0)
-				continue;
-
-			const minFeasible = Math.max(...predecessorLayers) + 1;
-			const maxFeasible = Math.min(...successorLayers) - 1;
-
-			let bestLayer = currentLayer;
-			let minWidth = layering.getLayers().find((layer) => layer.index === currentLayer)?.width ?? Infinity;
-
-			for(let i = minFeasible; i <= maxFeasible; i++) {
-				const width = layering.getLayers().find((layer) => layer.index === currentLayer)?.width ?? Infinity;
-				if(width >= minWidth)
-					continue;
-
-				minWidth = width;
-				bestLayer = i;
-			}
-
-			if(bestLayer !== currentLayer)
-				layering.setLayerOf(id, bestLayer);
-		}
-	}
 }

+ 122 - 0
src/v1/optimizer/siguiyama/NodeOrderingStep.ts

@@ -0,0 +1,122 @@
+import NodeOrderingStepError from "../../errors/optimizer/NodeOrderingStepError.js";
+import Edge from "../../graph/Edge.js";
+import Graph from "../../graph/Graph.js";
+import Layering from "../../graph/layering/Layering.js";
+import Node from "../../graph/Node.js";
+import AlgorithmStep from "../AlgorithmStep.js";
+import { SiguiyamaContext } from "./SiguiyamaContext.js";
+
+export default class NodeOrderingStep extends AlgorithmStep<SiguiyamaContext> {
+	private readonly _iterationsCount: number;
+
+	public constructor(iterationsCount: number = 24) {
+		super(NodeOrderingStep.name);
+		this._iterationsCount = iterationsCount;
+	}
+
+	public run(context: SiguiyamaContext): void {
+		const { graph, layering } = context;
+
+		if(!graph)
+			throw new NodeOrderingStepError("Source graph was not found!")
+		if(!layering)
+			throw new NodeOrderingStepError("Layering of graph was not found!");
+
+		this.barycenter(graph, layering);
+		console.log(layering.getLayers().map((l) => l.nodes));
+	}
+
+	private barycenter(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : void {
+		let crossingsCount = Infinity;
+
+		for(let i = 0; i < this._iterationsCount; i++) {
+			this.sweepDown(graph, layering);
+			this.sweepUp(graph, layering);
+
+			const currentCrossingsCount = this.countTotalCrossings(graph, layering);
+
+			if(currentCrossingsCount > crossingsCount)
+				break;
+
+			crossingsCount = currentCrossingsCount;
+		}
+	}
+
+	private sweepDown(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : void {
+		const layers = layering.getLayers();
+
+		for(let i = 0; i < layers.length; i++) {
+			const currLayer = layers[i]!;
+			const newNodesOrder = this.applyBarycenter(graph, layering, currLayer.nodes, "down");
+			currLayer.nodes = newNodesOrder;
+		}
+	}
+
+	private sweepUp(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : void {
+		const layers = layering.getLayers();
+
+		for(let i = layers.length - 1; i > 0; i--) {
+			const currLayer = layers[i]!;
+			const newNodesOrder = this.applyBarycenter(graph, layering, currLayer.nodes, "up");
+			currLayer.nodes = newNodesOrder;
+		}
+	}
+
+	private applyBarycenter(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, nodeIds: Node["id"][], direction: "up" | "down") : Node["id"][] {
+		const barycenterValues = nodeIds.map((nodeId) => {
+			const node = graph.getNode(nodeId);
+			if(!node)
+				return { nodeId, value: Infinity };
+
+			const neighbors = direction === "down" ? graph.getNodeInputs(nodeId) : graph.getNodeOutputs(nodeId);
+			if(neighbors.length === 0) {
+				const currentPosition = layering.getPositionOf(nodeId);
+				return { nodeId, value: currentPosition ?? Infinity };
+			}
+
+			const posistionSum = neighbors.reduce((sum, neighbor) => {
+				const position = layering.getPositionOf(neighbor.id);
+				return sum + (position ?? 0);
+			}, 0);
+
+			return { nodeId, value: posistionSum / neighbors.length };
+		});
+
+		return barycenterValues.sort((a, b) => a.value - b.value).map((entry) => entry.nodeId);
+	}
+
+	private countTotalCrossings(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : number {
+		const layers = layering.getLayers();
+
+		let totalCrossings = 0;
+
+		for(let i = 0; i < layers.length - 1; i++)
+			totalCrossings += this.countCrossingsBetweenLayers(graph, layering, i, i + 1);
+
+		return totalCrossings;
+	}
+
+	private countCrossingsBetweenLayers(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, upperLayerIndex: number, lowerLayerIndex: number) : number {
+		const edges = graph.getEdges().filter(({ from, to }) => {
+			const fromLayer = layering.getLayerOf(from);
+			const toLayer = layering.getLayerOf(to);
+			return fromLayer === upperLayerIndex && toLayer === lowerLayerIndex;
+		});
+		
+		let crossings = 0;
+
+		for(let i = 0; i < edges.length; i++) {
+			for(let j = i + 1; j < edges.length; j++) {
+				const posU = layering.getPositionOf(edges[i]!.from) ?? 0;
+				const posV = layering.getPositionOf(edges[i]!.to) ?? 0;
+				const posS = layering.getPositionOf(edges[j]!.from) ?? 0;
+				const posT = layering.getPositionOf(edges[j]!.to) ?? 0;
+
+				if((posU < posS && posV > posT) || (posU > posS && posV < posT))
+					crossings++;
+			}
+		}
+
+		return crossings;
+	}
+}

+ 2 - 0
src/v1/optimizer/siguiyama/SiguiyamaAlgorithm.ts

@@ -16,7 +16,9 @@ export default class SiguiyamaAlgorithm {
 		const context: SiguiyamaContext = { graph, config: {} };
 
 		this._steps.forEach((step) => {
+			step.onBeforeRun();
 			step.run(context);
+			step.onAfterRun();
 		})
 
 		return context;