From 6f80ca89f9932695eb34171bce931c2da7d805b4 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 15 Dec 2024 23:52:41 -0500 Subject: [PATCH] graph layout --- demo/graph layout.html | 51 ++++++++++++++ package.json | 6 +- src/folk-graph.ts | 131 +++++++++++++++++++++++++++++++++++ src/standalone/folk-graph.ts | 5 ++ 4 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 demo/graph layout.html create mode 100644 src/folk-graph.ts create mode 100644 src/standalone/folk-graph.ts diff --git a/demo/graph layout.html b/demo/graph layout.html new file mode 100644 index 0000000..eee8b77 --- /dev/null +++ b/demo/graph layout.html @@ -0,0 +1,51 @@ + + + + + + Physics + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json index 76fada5..00d2b54 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "@lit/reactive-element": "^2.0.4", "leaflet": "^1.9.4", "perfect-arrows": "^0.3.7", - "perfect-freehand": "^1.2.2" + "perfect-freehand": "^1.2.2", + "vite-plugin-wasm": "^3.3.0", + "webcola": "^3.4.0" }, "devDependencies": { "@types/leaflet": "^1.9.14", @@ -26,4 +28,4 @@ "typescript": "^5.7.2", "vite": "^6.0.3" } -} +} \ No newline at end of file diff --git a/src/folk-graph.ts b/src/folk-graph.ts new file mode 100644 index 0000000..347b40f --- /dev/null +++ b/src/folk-graph.ts @@ -0,0 +1,131 @@ +import { DOMRectTransform } from './common/DOMRectTransform.ts'; +import { FolkBaseSet } from './folk-base-set.ts'; +import { PropertyValues } from '@lit/reactive-element'; +import { Layout } from 'webcola'; +import { FolkShape } from './folk-shape.ts'; +import { FolkArrow } from './folk-arrow.ts'; + +type ColaNode = { + id: FolkShape; + x: number; + y: number; + width: number; + height: number; + rotation: number; +}; + +type ColaLink = { + source: FolkShape; + target: FolkShape; +}; + +export class FolkGraph extends FolkBaseSet { + static override tagName = 'folk-graph'; + + private graphSim: Layout; + private animationFrameId?: number; + private colaNodes: Map = new Map(); + private colaLinks: Map = new Map(); + + constructor() { + super(); + this.graphSim = new Layout(); + } + + connectedCallback() { + super.connectedCallback(); + this.startSimulation(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId); + } + this.colaNodes.clear(); + this.colaLinks.clear(); + } + + override update(changedProperties: PropertyValues) { + super.update(changedProperties); + this.updateGraph(); + } + + private updateGraph() { + // Clear existing nodes and links + this.colaNodes.clear(); + this.colaLinks.clear(); + + // Create nodes for shapes + for (const element of this.sourceElements) { + if (!(element instanceof FolkShape)) continue; + const rect = this.sourcesMap.get(element); + if (!(rect instanceof DOMRectTransform)) continue; + + const node: ColaNode = { + id: element, + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + width: rect.width, + height: rect.height, + rotation: rect.rotation, + }; + this.colaNodes.set(element, node); + } + + // Create links from arrows + const arrows = Array.from(this.sourceElements).filter( + (element): element is FolkArrow => element instanceof FolkArrow + ); + + for (const arrow of arrows) { + const source = arrow.sourceElement as FolkShape; + const target = arrow.targetElement as FolkShape; + if (!source || !target) continue; + if (!this.colaNodes.has(source) || !this.colaNodes.has(target)) continue; + + const link: ColaLink = { + source, + target, + }; + this.colaLinks.set(arrow, link); + } + + const nodes = [...this.colaNodes.values()]; + const nodeIdToIndex = new Map(nodes.map((n, i) => [n.id, i])); + + const links = Array.from(this.colaLinks.values()) + .map((l) => { + const source = nodeIdToIndex.get(l.source); + const target = nodeIdToIndex.get(l.target); + return source !== undefined && target !== undefined ? { source, target } : null; + }) + .filter((l): l is { source: number; target: number } => l !== null); + + this.graphSim.nodes(nodes).links(links).linkDistance(250).avoidOverlaps(true).handleDisconnected(true); + } + + private startSimulation() { + const step = () => { + this.graphSim.start(1, 0, 0, 0, true, false); + + for (const node of this.graphSim.nodes() as ColaNode[]) { + const shape = node.id; + const rect = this.sourcesMap.get(shape); + if (!(rect instanceof DOMRectTransform)) continue; + + if (shape !== document.activeElement) { + shape.x = node.x - rect.width / 2; + shape.y = node.y - rect.height / 2; + } else { + node.x = rect.x + rect.width / 2; + node.y = rect.y + rect.height / 2; + } + } + + this.animationFrameId = requestAnimationFrame(step); + }; + + this.animationFrameId = requestAnimationFrame(step); + } +} diff --git a/src/standalone/folk-graph.ts b/src/standalone/folk-graph.ts new file mode 100644 index 0000000..915170f --- /dev/null +++ b/src/standalone/folk-graph.ts @@ -0,0 +1,5 @@ +import { FolkGraph } from '../folk-graph'; + +FolkGraph.define(); + +export { FolkGraph };