diff --git a/examples/graph/dagre-layout.bat b/examples/graph/dagre-layout.bat new file mode 100644 index 0000000000..8799589efd --- /dev/null +++ b/examples/graph/dagre-layout.bat @@ -0,0 +1,5 @@ +@echo off +set main=./examples/graph/dagre-layout.js +cd .. +cd .. +npm run dev \ No newline at end of file diff --git a/examples/graph/dagre-layout.js b/examples/graph/dagre-layout.js new file mode 100644 index 0000000000..7e3bc76a64 --- /dev/null +++ b/examples/graph/dagre-layout.js @@ -0,0 +1,85 @@ +import phaser from 'phaser/src/phaser.js'; +import GraphPlugin from '../../plugins/graph-plugin.js'; + +class Demo extends Phaser.Scene { + constructor() { + super({ + key: 'examples' + }) + } + + preload() { } + + create() { + var nodeA = CreateNode(this, 0xFFFF00); + var nodeB = CreateNode(this); + var nodeC = CreateNode(this); + var nodeD = CreateNode(this); + var edgeAB = CreateEdge(this); + var edgeAC = CreateEdge(this); + var edgeBD = CreateEdge(this); + var edgeCD = CreateEdge(this); + + var graph = this.rexGraph.add.graph() + .addNodes([nodeA, nodeB, nodeC, nodeD], { padding: 3 }) + .addEdge(edgeAB, nodeA, nodeB) + .addEdge(edgeAC, nodeA, nodeC) + .addEdge(edgeBD, nodeB, nodeD) + .addEdge(edgeCD, nodeC, nodeD) + + graph.on('layout.edge', function (edgeGameObject, path) { + var startPoint = path[0]; + var endPoint = path[path.length - 1]; + edgeGameObject + .setPosition(startPoint.x, startPoint.y) + .setTo(0, 0, endPoint.x - startPoint.x, endPoint.y - startPoint.y) + }); + + + graph.once('layout.complete', function () { + console.log('layout.complete') + }) + + this.rexGraph.DagreLayout(graph, { rankdir: 'LR' }) + + console.log('done') + + } + + update() { + } +} + +var CreateNode = function (scene, color) { + if (color === undefined) { + color = 0x888888; + } + return scene.add.rectangle(0, 0, 100, 100).setStrokeStyle(3, color) +} + +var CreateEdge = function (scene) { + return scene.add.line(0, 0, 0, 0, 0, 0, 0xff0000).setLineWidth(2).setOrigin(0) +} + +var config = { + type: Phaser.AUTO, + parent: 'phaser-example', + width: 800, + height: 600, + scale: { + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + }, + scene: Demo, + plugins: { + scene: [ + { + key: 'rexGraph', + plugin: GraphPlugin, + mapping: 'rexGraph' + } + ] + } +}; + +var game = new Phaser.Game(config); \ No newline at end of file diff --git a/examples/graph/elk-layout.js b/examples/graph/elk-layout.js index 160ad1ec3d..a7b3d3d655 100644 --- a/examples/graph/elk-layout.js +++ b/examples/graph/elk-layout.js @@ -35,14 +35,18 @@ class Demo extends Phaser.Scene { .setTo(0, 0, endPoint.x - startPoint.x, endPoint.y - startPoint.y) }); + + graph.once('layout.complete', function () { + console.log('layout.complete') + }) + this.rexGraph.ELKLayout(graph, { layoutOptions: { // 'elk.direction': 'DOWN' } }) - .once('layout.complete', function () { - console.log('layout.complete') - }) + + console.log('done') } diff --git a/package-lock.json b/package-lock.json index c6f95568ec..405aef6d5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.80.9", "license": "MIT", "dependencies": { + "dagre": "^0.8.5", "eventemitter3": "^3.1.2", "graphology": "^0.25.4", "i18next": "^22.5.1", @@ -4120,6 +4121,16 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -5999,6 +6010,15 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/graphology": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.25.4.tgz", @@ -7567,8 +7587,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.debounce": { "version": "4.0.8", diff --git a/package.json b/package.json index c1b982a646..0bb224624e 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "dependencies": { "eventemitter3": "^3.1.2", "graphology": "^0.25.4", + "dagre": "^0.8.5", "i18next": "^22.5.1", "i18next-http-backend": "^2.5.2", "js-yaml": "^4.1.0", diff --git a/plugins/graph-components.js b/plugins/graph-components.js index e97f630a90..b3adcdbf8d 100644 --- a/plugins/graph-components.js +++ b/plugins/graph-components.js @@ -1,7 +1,9 @@ import Graph from './graph/graph/Graph.js'; import ELKLayout from './graph/layout/elkjs/Layout.js'; +import DagreLayout from './graph/layout/dagre/Layout.js'; export { Graph, - ELKLayout + ELKLayout, + DagreLayout } \ No newline at end of file diff --git a/plugins/graph-plugin.js b/plugins/graph-plugin.js index ca46714173..5882db3598 100644 --- a/plugins/graph-plugin.js +++ b/plugins/graph-plugin.js @@ -2,6 +2,7 @@ import ObjectFactory from './graph/ObjectFactory.js'; import GraphFactory from './graph/graph/Factory.js'; import ELKLayout from './graph/layout/elkjs/Layout.js'; +import DagreLayout from './graph/layout/dagre/Layout.js'; class GraphPlugin extends Phaser.Plugins.ScenePlugin { constructor(scene, pluginManager) { @@ -26,7 +27,12 @@ class GraphPlugin extends Phaser.Plugins.ScenePlugin { ELKLayout(graph, config) { ELKLayout(graph, config); - return graph + return graph; + } + + DagreLayout(graph, config) { + DagreLayout(graph, config); + return graph; } } diff --git a/plugins/graph/layout/dagre/BuildGraphData.js b/plugins/graph/layout/dagre/BuildGraphData.js new file mode 100644 index 0000000000..605dcddb3a --- /dev/null +++ b/plugins/graph/layout/dagre/BuildGraphData.js @@ -0,0 +1,51 @@ +import dagre from 'dagre'; +import UIDToObj from '../../graphitem/UIDToObj.js'; +import GetBoundsConfig from '../../../utils/bounds/GetBoundsConfig.js'; + +var BuildGraphData = function (graph, config) { + var graphData = new dagre.graphlib.Graph(); + graphData.setGraph(config); + graphData.setDefaultEdgeLabel(function () { }); + + var nodeGameObjectMap = {}; + graph.graph.forEachNode(function (uid, attributes) { + var nodeGameObject = UIDToObj(uid); + if (!nodeGameObject) { + return; + } + + var padding = GetBoundsConfig(attributes.padding); + var width = nodeGameObject.displayWidth + padding.left + padding.right; + var height = nodeGameObject.displayHeight + padding.top + padding.bottom; + + graphData.setNode(uid, { + gameObject: nodeGameObject, padding: padding, + width: width, height: height, + }) + + nodeGameObjectMap[uid] = nodeGameObject; + }) + + graph.graph.forEachEdge(function (uid, attributes, sourceUID, targetUID) { + var sourceGameObject = nodeGameObjectMap[sourceUID]; + var targetGameObject = nodeGameObjectMap[targetUID]; + + if (!sourceGameObject || !targetGameObject) { + return; + } + var edgeGameObject = UIDToObj(uid); + if (!edgeGameObject) { + return; + } + + graphData.setEdge(sourceUID, targetUID, { + gameObject: edgeGameObject, + sourceGameObject: sourceGameObject, + targetGameObject: targetGameObject, + }) + }) + + return graphData; +} + +export default BuildGraphData; \ No newline at end of file diff --git a/plugins/graph/layout/dagre/GetPath.js b/plugins/graph/layout/dagre/GetPath.js new file mode 100644 index 0000000000..44aed510e2 --- /dev/null +++ b/plugins/graph/layout/dagre/GetPath.js @@ -0,0 +1,5 @@ +var GetPath = function (edgeData) { + return edgeData.points; +} + +export default GetPath; \ No newline at end of file diff --git a/plugins/graph/layout/dagre/Layout.js b/plugins/graph/layout/dagre/Layout.js new file mode 100644 index 0000000000..58753dcba9 --- /dev/null +++ b/plugins/graph/layout/dagre/Layout.js @@ -0,0 +1,20 @@ +import LayoutBase from '../utils/Layout.js'; +import BuildGraphData from './BuildGraphData.js'; +import RunLayout from './RunLayout.js'; +import PlaceGameObjects from './PlaceGameObjects.js'; + +var callbacks = { + buildGraphData: BuildGraphData, + isAsyncRunLayout: false, + runLayout: RunLayout, + placeGameObjects: PlaceGameObjects, +} + +var Layout = async function (graph, config) { + if (config === undefined) { + config = {}; + } + await LayoutBase(callbacks, graph, config); +} + +export default Layout; \ No newline at end of file diff --git a/plugins/graph/layout/dagre/PlaceGameObjects.js b/plugins/graph/layout/dagre/PlaceGameObjects.js new file mode 100644 index 0000000000..e6b82613ba --- /dev/null +++ b/plugins/graph/layout/dagre/PlaceGameObjects.js @@ -0,0 +1,26 @@ +import AlignIn from '../../../utils/actions/AlignIn.js'; +import GetPath from './GetPath.js'; + +const ALIGN_CENTER = Phaser.Display.Align.CENTER; + +var PlaceGameObjects = function (graph, graphData, config) { + graphData.nodes().forEach(function (nodeKey) { + var nodeData = graphData.node(nodeKey); + var gameObject = nodeData.gameObject; + var padding = nodeData.padding; + var x = nodeData.x - (nodeData.width / 2) + padding.left; // nodeData.x is centerX + var y = nodeData.y - (nodeData.height / 2) + padding.top; // nodeData.y is centerY + var width = nodeData.width - padding.left - padding.right; + var height = nodeData.height - padding.top - padding.bottom; + AlignIn(gameObject, x, y, width, height, ALIGN_CENTER); + graph.emit('layout.node', nodeData.gameObject); + }); + + graphData.edges().forEach(function (edgeKey) { + var edgeData = graphData.edge(edgeKey); + var path = GetPath(edgeData); + graph.emit('layout.edge', edgeData.gameObject, path, edgeData.sourceGameObject, edgeData.targetGameObject); + }); +} + +export default PlaceGameObjects; \ No newline at end of file diff --git a/plugins/graph/layout/dagre/RunLayout.js b/plugins/graph/layout/dagre/RunLayout.js new file mode 100644 index 0000000000..7263dd987f --- /dev/null +++ b/plugins/graph/layout/dagre/RunLayout.js @@ -0,0 +1,7 @@ +import dagre from 'dagre'; + +var RunLayout = async function (graphData, config) { + await dagre.layout(graphData); +} + +export default RunLayout; diff --git a/plugins/graph/layout/elkjs/Layout.js b/plugins/graph/layout/elkjs/Layout.js index a2cdcc1a10..3e323735bd 100644 --- a/plugins/graph/layout/elkjs/Layout.js +++ b/plugins/graph/layout/elkjs/Layout.js @@ -1,29 +1,20 @@ -import ELK from '../../../utils/elkjs/elk.bundled.js'; +import LayoutBase from '../utils/Layout.js'; import BuildGraphData from './BuildGraphData.js'; +import RunLayout from './RunLayout.js'; import PlaceGameObjects from './PlaceGameObjects.js'; +var callbacks = { + buildGraphData: BuildGraphData, + isAsyncRunLayout: true, + runLayout: RunLayout, + placeGameObjects: PlaceGameObjects, +} + var Layout = async function (graph, config) { if (config === undefined) { config = {}; } - - graph.emit('layout.start', graph); - - var graphData = BuildGraphData(graph, config); - - graph.emit('layout.prelayout', graph); - - var elk = new ELK(); - graphData = await elk.layout(graphData, { - layoutOptions: config.layoutOptions, - - }); - - graph.emit('layout.postlayout', graph); - - PlaceGameObjects(graph, graphData, config); - - graph.emit('layout.complete', graph); + await LayoutBase(callbacks, graph, config); } export default Layout; \ No newline at end of file diff --git a/plugins/graph/layout/elkjs/RunLayout.js b/plugins/graph/layout/elkjs/RunLayout.js new file mode 100644 index 0000000000..c2bb3218bb --- /dev/null +++ b/plugins/graph/layout/elkjs/RunLayout.js @@ -0,0 +1,10 @@ +import ELK from '../../../utils/elkjs/elk.bundled.js'; + +var RunLayout = async function (graphData, config) { + var elk = new ELK(); + graphData = await elk.layout(graphData, { + layoutOptions: config.layoutOptions, + }); +} + +export default RunLayout; \ No newline at end of file diff --git a/plugins/graph/layout/utils/Layout.js b/plugins/graph/layout/utils/Layout.js new file mode 100644 index 0000000000..caab45cab2 --- /dev/null +++ b/plugins/graph/layout/utils/Layout.js @@ -0,0 +1,25 @@ +var Layout = async function (callbacks, graph, config) { + if (config === undefined) { + config = {}; + } + + graph.emit('layout.start', graph); + + var graphData = callbacks.buildGraphData(graph, config); + + graph.emit('layout.prelayout', graph); + + if (callbacks.isAsyncRunLayout) { + await callbacks.runLayout(graphData, config); + } else { + callbacks.runLayout(graphData, config); + } + + graph.emit('layout.postlayout', graph); + + callbacks.placeGameObjects(graph, graphData, config); + + graph.emit('layout.complete', graph); +} + +export default Layout; \ No newline at end of file