icestormikk 4 weeks ago
parent
commit
cddbb32bb2

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
 	"name": "bpmn-optimizer",
-	"version": "0.0.0",
+	"version": "0.1.0",
 	"lockfileVersion": 3,
 	"requires": true,
 	"packages": {
 		"": {
 			"name": "bpmn-optimizer",
-			"version": "0.0.0",
+			"version": "0.1.0",
 			"license": "ISC",
 			"dependencies": {
 				"valibot": "^1.2.0"

+ 7 - 5
package.json

@@ -1,6 +1,6 @@
 {
 	"name": "bpmn-optimizer",
-	"version": "0.0.1",
+	"version": "0.1.0",
 	"description": "Optimizer for BPMN diagrams",
 	"keywords": [
 		"bpmn",
@@ -13,9 +13,11 @@
 	"author": "icestormikk",
 	"type": "module",
 	"main": "dist/index.js",
-    "types": "dist/index.d.ts",
-    "files": ["dist"],
-    "scripts": {
+	"types": "dist/index.d.ts",
+	"files": [
+		"dist"
+	],
+	"scripts": {
 		"build": "tsc",
 		"start": "node dist/index.js",
 		"dev": "npm run build && node dist/index.js",
@@ -35,4 +37,4 @@
 	"dependencies": {
 		"valibot": "^1.2.0"
 	}
-}
+}

+ 21 - 0
src/index.ts

@@ -4,6 +4,15 @@ import Node from "./graph/node/Node.js";
 import Edge from "./graph/edge/Edge.js";
 import BPMNError from "./errors/BPMNError.js";
 import {SiguiyamaContext} from "./optimizer/siguiyama/SiguiyamaContext.js";
+import DzwfJsonDeserializer from "./io/deserialize/json/DzwfJsonDeserializer.js";
+import {readFileSync, writeFileSync} from "node:fs";
+import CycleRemoveStep from "./optimizer/steps/CycleRemoveStep.js";
+import LayerAssignmentStep from "./optimizer/steps/LayerAssignmentStep.js";
+import NodeOrderingStep from "./optimizer/steps/NodeOrderingStep.js";
+import CoordinateAssignmentStep from "./optimizer/steps/CoordinateAssignmentStep.js";
+import EdgeRoutingStep from "./optimizer/steps/EdgeRoutingStep.js";
+import CleanupStep from "./optimizer/steps/CleanupStep.js";
+import DzwfJsonSerializer from "./io/serialize/json/DzwfJsonSerializer.js";
 
 export class BPMNOptimizer {
 	private _optimizationAlgorithm?: SiguiyamaAlgorithm;
@@ -19,3 +28,15 @@ export class BPMNOptimizer {
 		return this._optimizationAlgorithm.run(graph);
 	}
 }
+
+const deserializer = new DzwfJsonDeserializer();
+const data = deserializer.deserialize(readFileSync("./data/bpmn-graph.json", "utf8"));
+
+const algorithm = new SiguiyamaAlgorithm().addStep(new CycleRemoveStep()).addStep(new LayerAssignmentStep()).addStep(new NodeOrderingStep()).addStep(new CoordinateAssignmentStep()).addStep(new EdgeRoutingStep()).addStep(new CleanupStep());
+
+const optimizer = new BPMNOptimizer().setOptimizationAlgorithm(algorithm);
+
+optimizer.run(data.graph);
+
+const serializer = new DzwfJsonSerializer();
+writeFileSync("./data/document.dzwf", serializer.serialize(data));

+ 5 - 3
src/io/deserialize/json/DzwfJsonDeserializer.ts

@@ -13,14 +13,16 @@ export default class DzwfJsonDeserializer implements JsonDeserializer<{ dzwfData
 			return new Node(diagram.position.x, diagram.position.y, diagram.size.width, diagram.size.height, type, id);
 		})
 
-		const edges = json.links.map(({ sourceId, targetId, id }) => {
+		const edges = json.links.map(({ sourceId, targetId, id, diagram }) => {
 			const source = nodes.find((n) => n.getId() === sourceId);
 			const target = nodes.find((n) => n.getId() === targetId);
 
 			if(!source || !target)
-				throw new DeserializerError(`Can't find source or target node for edge ${id}`)
+				throw new DeserializerError(`Can't find source or target node for edge with ${id} id`)
 
-			return new Edge(source, target);
+			const waypoints= diagram?.vertices || [];
+
+			return new Edge(source, target, waypoints, id);
 		})
 
 		return { dzwfData: json, graph: new Graph(nodes, edges) };

+ 8 - 68
src/io/serialize/json/DzwfJsonSerializer.ts

@@ -3,7 +3,7 @@ import Node from "../../../graph/node/Node.js";
 import Edge from "../../../graph/edge/Edge.js";
 import Graph from "../../../graph/Graph.js";
 import SerializerError from "../../../errors/SerializerError.js";
-import {DzwfData, DzwfLink} from "../../dzwf/DzwfData.js";
+import {DzwfData} from "../../dzwf/DzwfData.js";
 
 export default class DzwfJsonSerializer implements JsonSerializer<{ dzwfData: DzwfData, graph: Graph<Node, Edge<Node>> }> {
 	serialize(data: { dzwfData: DzwfData, graph: Graph<Node, Edge<Node>> }): string {
@@ -18,77 +18,17 @@ export default class DzwfJsonSerializer implements JsonSerializer<{ dzwfData: Dz
 			element.diagram.position.y = node.getY();
 		}
 
-		dzwfData.links = this.restoreLinks(graph.getEdges());
+		for(const link of dzwfData.links) {
+			const edge = graph.getEdge(link.id);
+			if(!edge)
+				throw new SerializerError(`Can't find edge with id ${link.id} in result graph`);
 
-		return JSON.stringify(dzwfData, null, 4);
-	}
-
-	private restoreLinks(edges: Edge<Node>[]): DzwfLink[] {
-		const outgoing = new Map<string, Edge<Node>[]>();
-		const links: DzwfLink[] = [];
-
-		for(const edge of edges) {
-			const fromId = edge.getFrom().getId();
-			const bucket = outgoing.get(fromId);
-			if(bucket)
-				bucket.push(edge);
-			else
-				outgoing.set(fromId, [edge]);
-		}
-
-		for(const edge of edges) {
-			const from = edge.getFrom();
-			if(this.isDummyNode(from))
+			if(!link.diagram)
 				continue;
 
-			const waypoints: { x: number, y: number }[] = [];
-			const visitedDummyIds = new Set<string>();
-			let target = edge.getTo();
-
-			while(this.isDummyNode(target)) {
-				if(visitedDummyIds.has(target.getId()))
-					throw new SerializerError(`Cycle was detected while restoring edge ${edge.getId()}`);
-				visitedDummyIds.add(target.getId());
-
-				waypoints.push(this.toCenter(target));
-
-				const nextEdges = outgoing.get(target.getId()) || [];
-				if(nextEdges.length !== 1)
-					throw new SerializerError(
-						`Can't restore edge ${edge.getId()}: dummy node ${target.getId()} must have exactly one outgoing edge, got ${nextEdges.length}`
-					);
-
-				target = nextEdges[0]!.getTo();
-			}
-
-			links.push({
-				id: this.resolveLinkId(edge),
-				sourceId: from.getId(),
-				targetId: target.getId(),
-				diagram: { vertices: waypoints }
-			});
+			link.diagram.vertices = edge.getWaypoints();
 		}
 
-		return links;
-	}
-
-	private isDummyNode(node: Node): boolean {
-		return node.getType() === "dummy-node";
-	}
-
-	private toCenter(node: Node): { x: number, y: number } {
-		return {
-			x: node.getX() + node.getWidth() / 2,
-			y: node.getY() + node.getHeight() / 2
-		};
-	}
-
-	private resolveLinkId(edge: Edge<Node>): string {
-		const firstTarget = edge.getTo();
-		if(!this.isDummyNode(firstTarget))
-			return edge.getId();
-
-		const candidate = firstTarget as Node & Partial<{ getOriginalEdgeId: () => string }>;
-		return typeof candidate.getOriginalEdgeId === "function" ? candidate.getOriginalEdgeId() : edge.getId();
+		return JSON.stringify(dzwfData, null, 4);
 	}
 }

+ 23 - 50
src/optimizer/steps/CleanupStep.ts

@@ -4,38 +4,37 @@ import Graph from "../../graph/Graph.js";
 import Node from "../../graph/node/Node.js";
 import Edge from "../../graph/edge/Edge.js";
 import DummyNode from "../../graph/node/DummyNode.js";
+import CleanupStepError from "../../errors/optimizer/CleanupStepError.js";
 
 /**
  * Шаг восстановления исходного графа, удаления лишних конструкций, возникших в процессе работы алгоритма Сигуямы
  */
 export default class CleanupStep extends AlgorithmStep<SiguiyamaContext> {
+	public constructor() {
+		super(CleanupStep.name);
+	}
+
 	public run(context: SiguiyamaContext): void {
 		const { graph, feedbackSet, edgeSubdivisions } = context;
 
 		if(!graph)
-			throw new Error("Graph is null or undefined");
+			throw new CleanupStepError("Graph is null or undefined");
 		if(!feedbackSet)
-			throw new Error("Feedback set is null or undefined");
+			throw new CleanupStepError("Feedback set is null or undefined");
 		if(!edgeSubdivisions)
-			throw new Error("Edge Subdivisions information is null or undefined");
+			throw new CleanupStepError("Edge Subdivisions information is null or undefined");
 
 		this.restoreEdges(graph, edgeSubdivisions);
-		this.applyFeedbackSet(graph, feedbackSet, edgeSubdivisions);
+		this.applyFeedbackSet(feedbackSet);
 	}
 
 	private restoreEdges(graph: Graph<Node, Edge<Node>>, edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>>) : void {
 		if(edgeSubdivisions.size === 0)
 			return;
 
-		const childEdges = new Set<Edge<Node>>();
-		for(const subdivision of edgeSubdivisions.values()) {
-			childEdges.add(subdivision.left);
-			childEdges.add(subdivision.right);
-		}
-
 		const roots: Edge<Node>[] = [];
 		for(const edge of edgeSubdivisions.keys())
-			if(!childEdges.has(edge))
+			if(edge instanceof Edge)
 				roots.push(edge);
 
 		const collectSegments = (edge: Edge<Node>): Edge<Node>[] => {
@@ -46,26 +45,25 @@ export default class CleanupStep extends AlgorithmStep<SiguiyamaContext> {
 			return [...collectSegments(subdivision.left), ...collectSegments(subdivision.right)];
 		};
 
-		const dummyNodeIds = new Set<string>();
-
 		for(const root of roots) {
 			const segments = collectSegments(root);
 			const waypoints: Array<{ x: number, y: number }> = [];
 
 			for(let i = 0; i < segments.length - 1; i++) {
-				const joinNode = segments[i]!.getTo();
-				if(joinNode instanceof DummyNode) {
-					waypoints.push({ x: joinNode.getX(), y: joinNode.getY() });
-					dummyNodeIds.add(joinNode.getId());
-				}
+				const node = segments[i]!.getTo();
+				if(node instanceof DummyNode)
+					waypoints.push({ x: node.getX(), y: node.getY() });
+					// dummyNodeIds.add(joinNode.getId());
 			}
 
 			const firstNode = segments[0]?.getFrom();
-			const lastNode = segments[segments.length - 1]?.getTo();
 			if(firstNode)
 				root.setFrom(firstNode);
+
+			const lastNode = segments[segments.length - 1]?.getTo();
 			if(lastNode)
 				root.setTo(lastNode);
+
 			root.setWaypoints(waypoints);
 
 			for(const segment of segments)
@@ -75,43 +73,18 @@ export default class CleanupStep extends AlgorithmStep<SiguiyamaContext> {
 				graph.addEdge(root);
 		}
 
-		for(const node of graph.getNodes()) {
+		for(const node of graph.getNodes())
 			if(node instanceof DummyNode)
-				dummyNodeIds.add(node.getId());
-		}
-
-		for(const dummyNodeId of dummyNodeIds)
-			graph.removeNode(dummyNodeId);
+				graph.removeEdge(node.getId());
 	}
 
-	private applyFeedbackSet(graph: Graph<Node, Edge<Node>>, feedbackSet: FeedbackSet, edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>>) : void {
+	private applyFeedbackSet(feedbackSet: FeedbackSet) : void {
 		if(feedbackSet.length === 0)
 			return;
 
-		const feedbackEdgeIds = new Set(feedbackSet.map((edge) => edge.getId()));
-		const reversedEdgeIds = new Set<string>();
-
-		const hasFeedbackDescendant = (edge: Edge<Node>): boolean => {
-			if(feedbackEdgeIds.has(edge.getId()))
-				return true;
-
-			const subdivision = edgeSubdivisions.get(edge);
-			if(!subdivision)
-				return false;
-
-			return hasFeedbackDescendant(subdivision.left) || hasFeedbackDescendant(subdivision.right);
-		};
-
-		for(const edge of graph.getEdges()) {
-			if(reversedEdgeIds.has(edge.getId()))
-				continue;
-			if(!hasFeedbackDescendant(edge))
-				continue;
-
-			const from = edge.getFrom();
-			edge.setFrom(edge.getTo());
-			edge.setTo(from);
-			reversedEdgeIds.add(edge.getId());
+		for(const edge of feedbackSet) {
+			const from = edge.getFrom(), to = edge.getTo();
+			edge.setFrom(to).setTo(from);
 		}
 	}
 }

+ 4 - 11
src/optimizer/steps/LayerAssignmentStep.ts

@@ -4,7 +4,7 @@ import Graph from "../../graph/Graph.js";
 import Layering from "../../graph/layering/Layering.js";
 import Node from "../../graph/node/Node.js";
 import AlgorithmStep from "../AlgorithmStep.js";
-import {EdgeSubdivisions, FeedbackSet, SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
+import {EdgeSubdivisions, SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
 import DummyNode from "../../graph/node/DummyNode.js";
 import DummyEdge from "../../graph/edge/DummyEdge.js";
 
@@ -39,7 +39,7 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
 		const layering = this.longestPathAlgorithm(graph);
 		const edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>> = new Map();
 
-		this.divideLongEdges(graph, layering, feedbackSet, edgeSubdivisions);
+		this.divideLongEdges(graph, layering, edgeSubdivisions);
 
 		context.layering = layering;
 		context.edgeSubdivisions = edgeSubdivisions;
@@ -90,10 +90,9 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
      *
      * @param graph Граф, в который добавляются dummy-вершины и сегменты рёбер.
      * @param layering Текущее разбиение графа на слои, используемое для вычисления span и вставки dummy-вершин.
-     * @param feedbackSet Набор рёбер обратной связи
      * @param edgeSubdivisions Информация о разделении рёбер в графе
      */
-	private divideLongEdges(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, feedbackSet: FeedbackSet, edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>>) : void {
+	private divideLongEdges(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>>) : void {
 		const edges = graph.getEdges();
 
 		for(const edge of edges) {
@@ -101,7 +100,7 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
 			if(span < 2)
 				continue;
 
-			const from = edge.getFrom(), to = edge.getTo();
+			const from = edge.getFrom();
 			const fromLayer = layering.getNodeLayerIndex(from);
 			let currentEdge: Edge<Node> = edge;
 
@@ -119,12 +118,6 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
 
 				graph.addEdge(left).addEdge(right);
 
-				const feedbackIndex = feedbackSet.indexOf(currentEdge);
-				if(feedbackIndex >= 0) {
-					feedbackSet.splice(feedbackIndex, 1);
-					feedbackSet.push(left, right);
-				}
-
 				if(currentEdge !== edge)
 					graph.removeEdge(currentEdge.getId());