Bläddra i källkod

coordinate assignment v00

Pavel Zhigalov 5 dagar sedan
förälder
incheckning
a27515519a

+ 3 - 3
src/index.ts

@@ -1,8 +1,8 @@
 import { readFileSync, writeFileSync } from "node:fs";
 import BPMNError from "./v1/errors/BPMNError.js";
-import Edge from "./v1/graph/Edge.js";
+import Edge from "./v1/graph/edge/Edge.js";
 import Graph from "./v1/graph/Graph.js";
-import Node from "./v1/graph/Node.js";
+import Node from "./v1/graph/node/Node.js";
 import Deserializer from "./v1/io/deserialize/Deserializer.js";
 import DefaultJsonDeserializer from "./v1/io/deserialize/json/DefaultJsonDeserializer.js";
 import Serializer from "./v1/io/serialize/Serializer.js";
@@ -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(100, 50));
 const optimizer = new BPMNOptimizer().setOptimizationAlgorithm(algorithm);
 
 const data = readFileSync("./data/graph.json").toString();

+ 2 - 2
src/v1/graph/Graph.ts

@@ -1,6 +1,6 @@
 import { array, forward, object, partialCheck, pipe } from "valibot";
-import Edge, { EdgeSchema } from "./Edge.js";
-import Node, { NodeSchema } from "./Node.js";
+import Edge, { EdgeSchema } from "./edge/Edge.js";
+import Node, { NodeSchema } from "./node/Node.js";
 import GraphError from "../errors/graph/GraphError.js";
 
 type GraphCache<TNode extends Node> = Partial<{

+ 3 - 3
src/v1/graph/Edge.ts → src/v1/graph/edge/Edge.ts

@@ -1,10 +1,10 @@
 import { object, string } from "valibot";
-import Node, { NodeSchema } from "./Node.js";
+import Node, { NodeSchema } from "../node/Node.js";
 
 export default class Edge<TNode extends Node> {
 	public readonly id: string;
-	public from: TNode["id"];
-	public to: TNode["id"];
+	public readonly from: TNode["id"];
+	public readonly to: TNode["id"];
 	
 	public constructor(from: TNode["id"], to: TNode["id"], id?: string) {
 		this.from = from;

+ 1 - 1
src/v1/graph/layering/Layer.ts

@@ -1,4 +1,4 @@
-import Node from "../Node.js";
+import Node from "../node/Node.js";
 
 export default class Layer<TNode extends Node> {
 	public readonly index: number;

+ 2 - 2
src/v1/graph/layering/Layering.ts

@@ -1,6 +1,6 @@
 import LayeringError from "../../errors/graph/layering/LayeringError.js";
-import Edge from "../Edge.js";
-import Node from "../Node.js";
+import Edge from "../edge/Edge.js";
+import Node from "../node/Node.js";
 import Layer from "./Layer.js";
 
 export default class Layering<TNode extends Node, TEdge extends Edge<TNode>> {

+ 10 - 0
src/v1/graph/node/DummyNode.ts

@@ -0,0 +1,10 @@
+import Node from "./Node.js";
+
+/**
+ * Мнимая вершина графа, имеющая нулевые размеры
+ */
+export default class DummyNode extends Node {
+	public constructor(x: number, y: number, id?: string) {
+		super(x, y, 0, 0, "dummy-node", id);
+	}
+}

+ 0 - 0
src/v1/graph/Node.ts → src/v1/graph/node/Node.ts


+ 4 - 4
src/v1/io/deserialize/dzwf/DZWFDeserializer.ts

@@ -1,8 +1,8 @@
 import { InferOutput, parse } from "valibot";
 import DeserializerError from "../../../errors/DeserializerError.js";
-import Edge from "../../../graph/Edge.js";
+import Edge from "../../../graph/edge/Edge.js";
 import Graph, { GraphSchema } from "../../../graph/Graph.js";
-import Node from "../../../graph/Node.js";
+import Node from "../../../graph/node/Node.js";
 import { JsonDeserializer } from "../json/JsonDeserializer.js";
 import { DZWFData } from "../../dzwf/DZWFData.js";
 
@@ -20,8 +20,8 @@ export default class DZWFDeserializer implements JsonDeserializer<{ graph: Graph
 				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 };
+				const { sourceId, targetId, id, diagram } = link;
+				return { id, from: sourceId, to: targetId, waypoints: diagram?.vertices ?? [] };
 			})
 
 			graph = parse(GraphSchema, { nodes, edges });

+ 2 - 2
src/v1/io/deserialize/json/DefaultJsonDeserializer.ts

@@ -1,8 +1,8 @@
 import { InferOutput, parse } from "valibot";
 import DeserializerError from "../../../errors/DeserializerError.js";
-import Edge from "../../../graph/Edge.js";
+import Edge from "../../../graph/edge/Edge.js";
 import Graph, { GraphSchema } from "../../../graph/Graph.js";
-import Node from "../../../graph/Node.js";
+import Node from "../../../graph/node/Node.js";
 import { JsonDeserializer } from "./JsonDeserializer.js";
 
 export default class DefaultJsonDeserializer implements JsonDeserializer<Graph<Node, Edge<Node>>> {

+ 4 - 1
src/v1/io/dzwf/DZWFData.ts

@@ -14,6 +14,9 @@ export type DZWFData = {
 	links: {
 		sourceId: string,
 		targetId: string,
-		id: string
+		id: string,
+		diagram?: {
+			vertices: { x: number, y: number }[]
+		}
 	}[] 
 };

+ 2 - 2
src/v1/io/serialize/dzwf/JointJsonSerializer.ts

@@ -1,7 +1,7 @@
 import { writeFileSync } from "node:fs";
-import Edge from "../../../graph/Edge.js";
+import Edge from "../../../graph/edge/Edge.js";
 import Graph from "../../../graph/Graph.js";
-import Node from "../../../graph/Node.js";
+import Node from "../../../graph/node/Node.js";
 import Serializer from "../Serializer.js";
 import { DZWFData } from "../../dzwf/DZWFData.js";
 

+ 2 - 2
src/v1/io/serialize/json/DefaultJsonSerializer.ts

@@ -1,6 +1,6 @@
-import Edge from "../../../graph/Edge.js";
+import Edge from "../../../graph/edge/Edge.js";
 import Graph from "../../../graph/Graph.js";
-import Node from "../../../graph/Node.js";
+import Node from "../../../graph/node/Node.js";
 import { JsonSerializer } from "./JsonSerializer.js";
 
 export default class DefaultJsonSerializer implements JsonSerializer<Graph<Node, Edge<Node>>> {

+ 2 - 2
src/v1/optimizer/AlgorithmContext.ts

@@ -1,6 +1,6 @@
-import Edge from "../graph/Edge.js"
+import Edge from "../graph/edge/Edge.js"
 import Graph from "../graph/Graph.js"
-import Node from "../graph/Node.js"
+import Node from "../graph/node/Node.js"
 
 export type AlgorithmContext = {
 	graph: Graph<Node, Edge<Node>>,

+ 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`);
 	}
 }

+ 50 - 171
src/v1/optimizer/siguiyama/CoordinateAssignmentStep.ts

@@ -1,212 +1,91 @@
-import AlgorithmStep from "../AlgorithmStep.js";
-import { SiguiyamaContext } from "./SiguiyamaContext.js";
-import Edge from "../../graph/Edge.js";
+import Edge from "../../graph/edge/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";
+import Node from "../../graph/node/Node.js";
+import AlgorithmStep from "../AlgorithmStep.js";
+import { FeedbackSet, 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 {
-		const { graph, layering } = context;
+		const { graph, layering, feedbackSet } = context;
 
 		if(!graph)
 			throw new Error("Source graph was not found!");
 		if(!layering)
 			throw new Error("Layering of graph was not found!");
+		if(!feedbackSet)
+			throw new Error("Feedback set was not found!");
 
-		// Step 1: Assign Y coordinates
-		this.assignYCoordinates(graph, layering);
+		this.reverseEdgesInFeedback(graph, feedbackSet);
 
-		// 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);
-		}
+		const reversedLayers = layering.getLayers().reverse();
 
-		// Step 7: Optional centering
-		this.centerLayout(graph);
+		this.assignXCoordinates(graph, reversedLayers);
+		this.assignYCoordinates(graph, reversedLayers);
 	}
 
-	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);
-		}
+	private reverseEdgesInFeedback(graph: Graph<Node,Edge<Node>>, feedbackSet: FeedbackSet) : void {
+		const edges = graph.getEdges();
 
-		// 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;
-		}
+		for(const edge of edges) {
+			if(!feedbackSet.includes(edge.id))
+				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;
-				}
-			}
+			const from = edge.to, to = edge.from;
+			graph.updateEdge(edge.id, { ...edge, from, to });
 		}
 	}
 
-	private assignInitialXCoordinates(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
-		const layers = layering.getLayers();
+	/**
+	 * Назначение X-координаты для всех вершин 
+	 * @param graph Граф, содержащий информацию о вершинах и рёбрах
+	 * @param layers Массив из слоев, по которым распределены вершины
+	 */
+	private assignXCoordinates(graph: Graph<Node, Edge<Node>>, layers: Layer<Node>[]) : void {
+		let currentX = 0;
 
 		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]!;
+			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) continue;
+				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);
-				}
+				node.x = currentX;
 			}
-		}
 
-		// Apply positions with constraints
-		for(let i = 1; i < layers.length; i++) {
-			this.applyPositionsWithConstraints(graph, layers[i]!, targetXMap);
+			currentX += layerWidth + this._layerGap;
 		}
 	}
 
-	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]!;
+	/**
+	 * Назначение Y-координаты для всех вершин 
+	 * @param graph Граф, содержащий информацию о вершинах и рёбрах
+	 * @param layering Укладка графа
+	 * @param layers Массив из слоев, по которым распределены вершины
+	 */
+	private assignYCoordinates(graph: Graph<Node, Edge<Node>>, layers: Layer<Node>[]) : void {
+		for(const layer of layers) {
+			let currentY = 0.0;
+			
 			for(const nodeId of layer.nodes) {
 				const node = graph.getNode(nodeId);
-				if(!node) continue;
+				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);
-				}
+				node.y = currentY;
+				currentY += node.height + this._nodeGap;
 			}
 		}
-
-		// 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;
-		}
 	}
 }

+ 3 - 3
src/v1/optimizer/siguiyama/CycleRemoveStep.ts

@@ -1,7 +1,7 @@
 import CycleRemoveStepError from "../../errors/optimizer/CycleRemoveStepError.js";
-import Edge from "../../graph/Edge.js";
+import Edge from "../../graph/edge/Edge.js";
 import Graph from "../../graph/Graph.js";
-import Node from "../../graph/Node.js";
+import Node from "../../graph/node/Node.js";
 import AlgorithmStep from "../AlgorithmStep.js";
 import { SiguiyamaContext } from "./SiguiyamaContext.js";
 
@@ -63,7 +63,7 @@ export default class CycleRemoveStep extends AlgorithmStep<SiguiyamaContext> {
 
 			if(fromIndex > toIndex) {
 				graph.updateEdge(edge.id, { ...edge, from: edge.to, to: edge.from });
-				feedbackSet.push(edge);
+				feedbackSet.push(edge.id);
 			}
 		}
 

+ 56 - 4
src/v1/optimizer/siguiyama/LayerAssignmentStep.ts

@@ -1,10 +1,11 @@
 import LayerAssignmentStepError from "../../errors/optimizer/LayerAssignmentStepError.js";
-import Edge from "../../graph/Edge.js";
+import DummyNode from "../../graph/node/DummyNode.js";
+import Edge from "../../graph/edge/Edge.js";
 import Graph from "../../graph/Graph.js";
 import Layering from "../../graph/layering/Layering.js";
-import Node from "../../graph/Node.js";
+import Node from "../../graph/node/Node.js";
 import AlgorithmStep from "../AlgorithmStep.js";
-import { SiguiyamaContext } from "./SiguiyamaContext.js";
+import { FeedbackSet, SiguiyamaContext } from "./SiguiyamaContext.js";
 
 /**
  * Network simplex algorithm
@@ -22,10 +23,13 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
 		if(!feedbackSet)
 			throw new LayerAssignmentStepError("Feedback set is undefined!");
 
+		console.log(graph)
 		const layering = this.longestPathAlgorithm(graph);
 
+		this.subdivideLongEdges(graph, layering, feedbackSet);
+		console.log(graph)
+
 		context.layering = layering;
-		console.log(layering.getLayers().map((l) => l.nodes));
 	}
 
 	private longestPathAlgorithm(dag: Graph<Node, Edge<Node>>) : NonNullable<SiguiyamaContext["layering"]> {
@@ -63,4 +67,52 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
 
 		return layering;
 	}
+
+	private subdivideLongEdges(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, feedbackSet: FeedbackSet) : void {
+		const edges = graph.getEdges();
+
+		for(const edge of edges) {
+			const edgeSpan = layering.getEdgeSpan(edge);
+
+
+			if(edgeSpan <= 1)
+				continue;
+
+			this.subdivideLongEdge(graph, layering, edge, feedbackSet, edgeSpan);
+		}
+	}
+
+	private subdivideLongEdge(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, edge: Edge<Node>, feedbackSet: FeedbackSet, span: number) : void {
+		if(span <= 1)
+			return;
+
+		const edgeReversedIndex = feedbackSet.findIndex((e) => e == edge.id);
+		const isReversed = edgeReversedIndex !== -1;
+
+		if(isReversed)
+			feedbackSet.splice(edgeReversedIndex, 1);
+
+		graph.removeEdge(edge.id);
+
+		const fromLayer = layering.getLayerOf(edge.from)!;
+		let previousNodeId = edge.from;
+
+		for(let i = 1; i < span; i++) {
+			const dummyId = `dummy-${edge.id}-${i}`;
+			const node = new DummyNode(0, 0, dummyId);
+			const dummyLayerIndex = fromLayer - i;
+
+			graph.addNode(node);
+			layering.assign(node, dummyLayerIndex);
+
+			const newEdge = new Edge(previousNodeId, node.id, `${edge.id}_segment_${i}`);
+
+			graph.addEdge(newEdge);
+
+			previousNodeId = node.id;
+		}
+
+		const lastEdge = new Edge(previousNodeId, edge.to, `${edge.id}_segment_${span}`);
+		graph.addEdge(lastEdge);
+	}
 }

+ 2 - 2
src/v1/optimizer/siguiyama/NodeOrderingStep.ts

@@ -1,8 +1,8 @@
 import NodeOrderingStepError from "../../errors/optimizer/NodeOrderingStepError.js";
-import Edge from "../../graph/Edge.js";
+import Edge from "../../graph/edge/Edge.js";
 import Graph from "../../graph/Graph.js";
 import Layering from "../../graph/layering/Layering.js";
-import Node from "../../graph/Node.js";
+import Node from "../../graph/node/Node.js";
 import AlgorithmStep from "../AlgorithmStep.js";
 import { SiguiyamaContext } from "./SiguiyamaContext.js";
 

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

@@ -1,6 +1,6 @@
-import Edge from "../../graph/Edge.js";
+import Edge from "../../graph/edge/Edge.js";
 import Graph from "../../graph/Graph.js";
-import Node from "../../graph/Node.js";
+import Node from "../../graph/node/Node.js";
 import Step from "../AlgorithmStep.js";
 import { SiguiyamaContext } from "./SiguiyamaContext.js";
 

+ 3 - 3
src/v1/optimizer/siguiyama/SiguiyamaContext.ts

@@ -1,9 +1,9 @@
-import Edge from "../../graph/Edge.js"
+import Edge from "../../graph/edge/Edge.js"
 import Layering from "../../graph/layering/Layering.js";
-import Node from "../../graph/Node.js"
+import Node from "../../graph/node/Node.js"
 import { AlgorithmContext } from "../AlgorithmContext.js"
 
-export type FeedbackSet = Edge<Node>[];
+export type FeedbackSet = Edge<Node>["id"][];
 
 export type SiguiyamaContext = AlgorithmContext & Partial<{
 	feedbackSet: FeedbackSet,