diff --git a/ui/package.json b/ui/package.json index 73daad63..c20b0e4e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -23,6 +23,7 @@ "ansi-to-react": "^6.1.6", "crypto-js": "^4.1.1", "d3-force": "^3.0.0", + "d3-quadtree": "^3.0.1", "formik": "^2.2.9", "graphql": "^16.6.0", "jwt-decode": "^3.1.2", diff --git a/ui/src/lib/store/canvasSlice.tsx b/ui/src/lib/store/canvasSlice.tsx index caf53c6e..33eed653 100644 --- a/ui/src/lib/store/canvasSlice.tsx +++ b/ui/src/lib/store/canvasSlice.tsx @@ -47,6 +47,7 @@ import { ReactFlowInstance, } from "reactflow"; import { node } from "prop-types"; +import { quadtree } from "d3-quadtree"; // TODO add node's data typing. type NodeData = { @@ -950,15 +951,6 @@ export const createCanvasSlice: StateCreator = ( */ autoForce: (scopeId) => { // 1. get all the nodes and edges in the scope - // 2. get - type NodeType = { - id: string; - x: number; - y: number; - r: number; - width: number; - height: number; - }; const nodes = get().nodes.filter( (node) => node.parentNode === (scopeId === "ROOT" ? undefined : scopeId) ); @@ -966,9 +958,8 @@ export const createCanvasSlice: StateCreator = ( const edges = get().edges; const tmpNodes: NodeType[] = nodes.map((node) => ({ id: node.id, - x: node.position.x + (node.width! + node.height!) / 2, - y: node.position.y + (node.width! + node.height!) / 2, - r: (node.width! + node.height!) / 2, + x: node.position.x + node.width! / 2, + y: node.position.y + node.height! / 2, width: node.width!, height: node.height!, })); @@ -999,18 +990,15 @@ export const createCanvasSlice: StateCreator = ( // .force("charge", forceManyBody().strength(-1000)) // .force("x", forceX()) // .force("y", forceY()) - .force( - "collide", - forceCollide().radius((d: any) => d.r) - ) + .force("collide", forceCollideRect()) // .force("link", d3.forceLink(edges).id(d => d.id)) // .force("charge", d3.forceManyBody()) // .force("center", forceCenter(0, 0)) .stop(); - simulation.tick(3000); + simulation.tick(1); tmpNodes.forEach((node) => { - node.x -= (node.width! + node.height!) / 2; - node.y -= (node.width! + node.height!) / 2; + node.x -= node.width! / 2; + node.y -= node.height! / 2; }); // The nodes will all have new positions now. I'll need to make the graph to be top-left, i.e., the leftmost is 20, the topmost is 20. // get the min x and y @@ -1019,8 +1007,9 @@ export const createCanvasSlice: StateCreator = ( let y1s = tmpNodes.map((node) => node.y); let miny = Math.min(...y1s); // calculate the offset, leave 50 padding for the scope. - const offsetx = 50 - minx; - const offsety = 50 - miny; + const padding = 50; + const offsetx = padding - minx; + const offsety = padding - miny; // move the nodes tmpNodes.forEach((node) => { node.x += offsetx; @@ -1052,12 +1041,12 @@ export const createCanvasSlice: StateCreator = ( const scope = nodesMap.get(scopeId); nodesMap.set(scopeId, { ...scope!, - width: maxx - minx + 100, - height: maxy - minx + 100, + width: maxx - minx + padding * 2, + height: maxy - minx + padding * 2, style: { ...scope!.style, - width: maxx - minx + 100, - height: maxy - minx + 100, + width: maxx - minx + padding * 2, + height: maxy - minx + padding * 2, }, }); } @@ -1081,6 +1070,68 @@ export const createCanvasSlice: StateCreator = ( }, }); +type NodeType = { + id: string; + x: number; + y: number; + width: number; + height: number; +}; + +function forceCollideRect() { + let nodes; + + function force(alpha) { + const padding = 5; + const quad = quadtree( + nodes, + (d: NodeType) => d.x, + (d) => d.y + ); + for (const d of nodes) { + quad.visit((q: any, x1, y1, x2, y2) => { + let updated = false; + if (q.data && q.data !== d) { + let x = d.x - q.data.x, + y = d.y - q.data.y, + xSpacing = padding + (q.data.width + d.width) / 2, + ySpacing = padding + (q.data.height + d.height) / 2, + absX = Math.abs(x), + absY = Math.abs(y), + l, + lx, + ly; + + if (absX < xSpacing && absY < ySpacing) { + l = Math.sqrt(x * x + y * y); + + lx = (absX - xSpacing) / l; + ly = (absY - ySpacing) / l; + + // the one that's barely within the bounds probably triggered the collision + if (Math.abs(lx) > Math.abs(ly)) { + lx = 0; + } else { + ly = 0; + } + d.x -= x *= lx; + d.y -= y *= ly; + q.data.x += x; + q.data.y += y; + + updated = true; + } + } + return updated; + }); + } + } + + force.initialize = (_) => (nodes = _); + + return force; +} + /** * Compute the rectangle of that is tightly fit to the children. * @param node @@ -1115,12 +1166,13 @@ function fitChildren( let maxy = Math.max(...y2s); let width = maxx - minx; let height = maxy - miny; + const padding = 50; return { // leave a 50 padding for the scope. - x: minx - 50, - y: miny - 50, - width: width + 100, - height: height + 100, + x: minx - padding, + y: miny - padding, + width: width + padding * 2, + height: height + padding * 2, }; } diff --git a/ui/src/lib/store/settingSlice.tsx b/ui/src/lib/store/settingSlice.tsx index 77fb236b..39c01494 100644 --- a/ui/src/lib/store/settingSlice.tsx +++ b/ui/src/lib/store/settingSlice.tsx @@ -45,7 +45,7 @@ export const createSettingSlice: StateCreator = ( }, autoRunLayout: localStorage.getItem("autoRunLayout") ? JSON.parse(localStorage.getItem("autoRunLayout")!) - : false, + : true, setAutoRunLayout: (b: boolean) => { set({ autoRunLayout: b }); // also write to local storage diff --git a/ui/yarn.lock b/ui/yarn.lock index 62140ff6..af8f5fc5 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -5928,7 +5928,7 @@ d3-force@^3.0.0: dependencies: d3-color "1 - 3" -"d3-quadtree@1 - 3": +"d3-quadtree@1 - 3", d3-quadtree@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==