From 9eb67275d7d0d6c70df2ffdfd12822abc30e1633 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Mon, 31 Aug 2026 21:03:22 +0300 Subject: [PATCH] rewritten using elkjs --- package-lock.json | 47 +++------------- package.json | 3 +- src/components/GameCanvas.tsx | 16 ++++-- src/components/NodeCard.tsx | 2 +- src/ui/graph.ts | 103 ++++++++++++---------------------- 5 files changed, 57 insertions(+), 114 deletions(-) diff --git a/package-lock.json b/package-lock.json index 171ac5b..11d493a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,7 @@ "name": "resource-graph-sim", "version": "0.1.0", "dependencies": { - "ngraph.forcelayout": "^3.3.1", - "ngraph.graph": "^20.1.2", + "elkjs": "^0.12.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -1326,6 +1325,12 @@ "dev": true, "license": "ISC" }, + "node_modules/elkjs": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.12.0.tgz", + "integrity": "sha512-YZcKynxVxYoKIOEpywEPwCFdg+BTbxQRNf3pbwdDCvc8O3kQD8bmIwSxKU1eOTVc4Xo+VG9Te+575mlfvOrhEQ==", + "license": "EPL-2.0 OR GPL-3.0-or-later" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1480,44 +1485,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/ngraph.events": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz", - "integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==", - "license": "BSD-3-Clause" - }, - "node_modules/ngraph.forcelayout": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz", - "integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==", - "license": "BSD-3-Clause", - "dependencies": { - "ngraph.events": "^1.0.0", - "ngraph.merge": "^1.0.0", - "ngraph.random": "^1.0.0" - } - }, - "node_modules/ngraph.graph": { - "version": "20.1.2", - "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.1.2.tgz", - "integrity": "sha512-W/G3GBR3Y5UxMLHTUCPP9v+pbtpzwuAEIqP5oZV+9IwgxAIEZwh+Foc60iPc1idlnK7Zxu0p3puxAyNmDvBd0Q==", - "license": "BSD-3-Clause", - "dependencies": { - "ngraph.events": "^1.4.0" - } - }, - "node_modules/ngraph.merge": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz", - "integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==", - "license": "MIT" - }, - "node_modules/ngraph.random": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz", - "integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==", - "license": "BSD-3-Clause" - }, "node_modules/node-releases": { "version": "2.0.54", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", diff --git a/package.json b/package.json index bc7dc24..cfe696d 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "ngraph.forcelayout": "^3.3.1", - "ngraph.graph": "^20.1.2", + "elkjs": "^0.12.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/src/components/GameCanvas.tsx b/src/components/GameCanvas.tsx index eb1ccdd..309f7e1 100644 --- a/src/components/GameCanvas.tsx +++ b/src/components/GameCanvas.tsx @@ -2,7 +2,7 @@ import type { Selection } from '../App'; import NodeCard from './NodeCard'; import { Game } from '../types'; import { build2dNodes, Node2D } from '../ui/graph'; -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { NodeLine } from './NodeLine'; interface Props { @@ -33,10 +33,16 @@ export const GameCanvas = function ({ return () => observer.disconnect(); }, []); - const nodes2d = useMemo( - () => build2dNodes(game.nodes, { width: size.width, height: size.height, padding: 40 }), - [game.nodes, size.width, size.height], - ); + const [nodes2d, setNodes2d] = useState([]); + + useEffect(() => { + let cancelled = false; + build2dNodes(game.nodes, { width: size.width, height: size.height, padding: 40 }) + .then((result) => { + if (!cancelled) setNodes2d(result); + }); + return () => { cancelled = true; }; + }, [game.nodes, size.width, size.height]); useLayoutEffect(() => { const worldEl = canvasRef.current; diff --git a/src/components/NodeCard.tsx b/src/components/NodeCard.tsx index 2c381c7..4c829aa 100644 --- a/src/components/NodeCard.tsx +++ b/src/components/NodeCard.tsx @@ -41,7 +41,7 @@ const NodeCard = forwardRef(function NodeCard({ node2d, s }
{ - info.links.map((link) =>
{link.target}/{link.resource}
) + info.links.map((link) =>
{link.target}/{link.resource}
) }
diff --git a/src/ui/graph.ts b/src/ui/graph.ts index d1815b9..ba232fb 100644 --- a/src/ui/graph.ts +++ b/src/ui/graph.ts @@ -1,8 +1,10 @@ -import createGraph, { Graph } from "ngraph.graph"; -import createLayout from "ngraph.forcelayout"; +import ELK, { ElkNode } from "elkjs/lib/elk.bundled.js"; import { Node } from "../types"; -const ITERATION_COUNT = 10; +const elk = new ELK(); + +const NODE_WIDTH = 100; +const NODE_HEIGHT = 90; export type Node2D = { id: Node['id']; @@ -10,70 +12,39 @@ export type Node2D = { node: Node; } -export function build2dNodes(nodes: Node[], area: { width: number, height: number, padding: number }): Node2D[] { - const graph = createGraph(); - for (const node of nodes) { - graph.addNode(node.behaviour.id, { node }); - } - for (const node of nodes) { - for (const link of node.behaviour.getDisplayInfo().links) - graph.addLink(node.behaviour.id, link.target); - } - return buildGraph2dNodes(graph, area); -} +export async function build2dNodes(nodes: Node[], area: { width: number, height: number, padding: number }): Promise { + const nodesById = new Map(nodes.map((node) => [node.behaviour.id, node])); -function buildGraph2dNodes(graph: Graph, area: { width: number, height: number, padding: number }): Node2D[] { - const layout = createLayout( - graph, - { - springLength: 300, - // springTransform: (link, spring) => { - // const from = graph.getNode(link.fromId).data.radius; - // const to = graph.getNode(link.toId).data.radius; - // spring.length = from + to + 10; // отступ 10px - // }, + const graph: ElkNode = { + id: "root", + layoutOptions: { + "elk.algorithm": "layered", + "elk.direction": "RIGHT", + "elk.spacing.nodeNode": "60", + "elk.layered.spacing.nodeNodeBetweenLayers": "100", }, - ); + children: nodes.map((node) => ({ + id: node.behaviour.id, + width: NODE_WIDTH, + height: NODE_HEIGHT, + })), + edges: nodes.flatMap((node) => + node.behaviour.getDisplayInfo().links.map((link, idx) => ({ + id: `${node.behaviour.id}->${link.target}.${idx}`, + sources: [node.behaviour.id], + targets: [link.target], + })) + ), + }; - function clampToBounds() { - const minX = area.padding; - const maxX = area.width - area.padding; - const minY = area.padding; - const maxY = area.height - area.padding; - layout.forEachBody((body) => { - if (body.pos.x < minX) { body.pos.x = minX; body.velocity.x = 0; } - if (body.pos.x > maxX) { body.pos.x = maxX; body.velocity.x = 0; } - if (body.pos.y < minY) { body.pos.y = minY; body.velocity.y = 0; } - if (body.pos.y > maxY) { body.pos.y = maxY; body.velocity.y = 0; } - }); - } + const layout = await elk.layout(graph); - for (var i = 0; i < ITERATION_COUNT; ++i) { - layout.step(); - // clampToBounds(); - } - - // const rect = layout.getGraphRect(); - // const W = area.width, H = 600, PADDING = 40; - - // const scaleX = (area.width - area.padding * 2) / (rect.x2 - rect.x1); - // const scaleY = (area.height - area.padding * 2) / (rect.y2 - rect.y1); - // const scale = Math.min(scaleX, scaleY); // сохраняем пропорции - - // function toCanvas(pos: { x: number; y: number }) { - // return { - // x: area.padding + (pos.x - rect.x1) * scale, - // y: area.padding + (pos.y - rect.y1) * scale, - // }; - // } - - const nodes: Node2D[] = []; - layout.forEachBody((body, nodeId) => { - nodes.push({ - id: nodeId.toString(), - position: { x: body.pos.x + area.width / 2, y: body.pos.y + area.height / 2 }, - node: graph.getNode(nodeId)?.data.node, - }); - }); - return nodes; -} \ No newline at end of file + return (layout.children ?? []).map((child) => ({ + id: child.id, + position: { + x: (child.x ?? 0) + area.padding, + y: (child.y ?? 0) + area.padding, + }, + node: nodesById.get(child.id)!, + })); +}