import { toRef , watch } from "vue" ; import { Edges , NodePositions , Nodes , Position } from "../common/types" ; import { OnClickHandler , OnDragHandler } from "../common/types" ; import { LayoutActivateParameters , LayoutHandler } from "./handler" ; import * as d3 from "d3-force" ; export interface ForceNodeDatum extends d3 . SimulationNodeDatum { id : string ; } export interface ForceEdgeDatum { source : string ; target : string ; } type d3Type = typeof d3 ; type CreateSimulationFunction = ( d3 : d3Type , nodes : ForceNodeDatum [ ] , edges : ForceEdgeDatum [ ] ) => d3 . Simulation < ForceNodeDatum , ForceEdgeDatum > ; export type ForceLayoutParameters = { positionFixedByDrag ? : boolean ; positionFixedByClickWithAltKey ? : boolean ; createSimulation ? : CreateSimulationFunction ; } ; export class ForceLayout implements LayoutHandler { private onDeactivate ? : ( ) => void ; constructor ( private options = { } ) { } activate ( parameters ) { const { layouts , nodes , edges , emitter , svgPanZoom } = parameters ; let { nodeLayouts , nodeLayoutMap } = this . buildNodeLayouts ( nodes , layouts , { x : 0 , y : 0 } ) ; const simulation = this . createSimulation ( nodeLayouts , this . forceLayoutEdges ( edges ) ) ; simulation . on ( "tick" , ( ) => { for ( const node of nodeLayouts ) { const layout = layouts ?. [ node . id ] ; if ( layout ) { layout . x = node . x ?? 0 ; layout . y = node . y ?? 0 ; } } } ) ; const restartSimulation = ( ) => { simulation . alpha ( 0.1 ) . restart ( ) ; } ; const onDrag = positions => { for ( const [ id , pos ] of Object . entries ( positions ) ) { const nodePos = nodeLayoutMap [ id ] ; nodePos . fx = pos . x ; nodePos . fy = pos . y ; } restartSimulation ( ) ; } ; const onDragEnd = positions => { for ( const [ id , pos ] of Object . entries ( positions ) ) { const layout = this . getNodeLayout ( layouts , id ) ; const nodePos = nodeLayoutMap ?. [ id ] ?? { x : 0 , y : 0 } ; if ( layout . value . fixed || this . options . positionFixedByDrag ) { nodePos . fx = pos . x ; nodePos . fy = pos . y ; layout . value . fixed = true ; } else { nodePos . x = pos . x ; nodePos . y = pos . y ; delete nodePos . fx ; delete nodePos . fy ; } } restartSimulation ( ) ; } ; const onClick = ( { node , event } ) => { if ( this . options . positionFixedByClickWithAltKey && event . altKey ) { const layout = this . getNodeLayout ( layouts , node ) ; let nodePos = nodeLayoutMap ?. [ node ] ; if ( ! nodePos ) { nodePos = { id : node , x : 0 , y : 0 } ; nodeLayoutMap [ node ] = nodePos ; } if ( layout . value . fixed ) { delete layout . value . fixed ; nodePos . x = nodePos . fx || nodePos . x ; nodePos . y = nodePos . fy || nodePos . y ; delete nodePos . fx ; delete nodePos . fy ; } else { layout . value . fixed = true ; nodePos . fx = nodePos . x ; nodePos . fy = nodePos . y ; } restartSimulation ( ) ; } } ; const stopNetworkWatch = watch ( ( ) => [ Object . keys ( nodes ) , edges ] , ( ) => { const area = svgPanZoom . getViewArea ( ) ; ( { nodeLayouts , nodeLayoutMap } = this . buildNodeLayouts ( nodes , layouts , area . center ) ) ; simulation . nodes ( nodeLayouts ) ; const forceEdges = simulation . force ( "edge" ) as d3 . ForceLink < ForceNodeDatum , ForceEdgeDatum > ; forceEdges . links ( this . forceLayoutEdges ( edges ) ) ; restartSimulation ( ) ; } , { deep : true } ) ; emitter . on ( "node:dragstart" , onDrag ) ; emitter . on ( "node:pointermove" , onDrag ) ; emitter . on ( "node:dragend" , onDragEnd ) ; emitter . on ( "node:click" , onClick ) ; this . onDeactivate = ( ) => { simulation . stop ( ) ; stopNetworkWatch ( ) ; emitter . off ( "node:dragstart" , onDrag ) ; emitter . off ( "node:pointermove" , onDrag ) ; emitter . off ( "node:dragend" , onDragEnd ) ; emitter . off ( "node:click" , onClick ) ; } ; } deactivate ( ) { if ( this . onDeactivate ) { this . onDeactivate ( ) ; } } private createSimulation ( nodes , edges ) { if ( this . options . createSimulation ) { return this . options . createSimulation ( d3 , nodes , edges ) ; } else { const forceLink = d3 . forceLink < ForceNodeDatum , ForceEdgeDatum > ( edges ) . id ( d => d . id ) ; return d3 . forceSimulation ( nodes ) . force ( "edge" , forceLink . distance ( 100 ) ) . force ( "charge" , d3 . forceManyBody ( ) ) . force ( "collide" , d3 . forceCollide ( 50 ) . strength ( 0.2 ) ) . force ( "center" , d3 . forceCenter ( ) . strength ( 0.05 ) ) . alphaMin ( 0.001 ) ; } } private buildNodeLayouts ( nodes , layouts , newPosition ) { const newNodes = Object . keys ( nodes ) . filter ( n => ! ( n in layouts ) ) ; for ( const nodeId of newNodes ) { layouts [ nodeId ] = { ... newPosition } ; } const nodeLayouts = this . forceNodeLayouts ( layouts ) ; const nodeLayoutMap = Object . fromEntries ( nodeLayouts . map ( n => [ n . id , n ] ) ) ; return { nodeLayouts , nodeLayoutMap } ; } private forceNodeLayouts ( layouts ) { return Object . entries ( layouts ) . map ( ( [ id , v ] ) => { return v . fixed ? { id , ... v , fx : v . x , fy : v . y } : { id , ... v } ; } ) ; } private forceLayoutEdges ( edges ) { return Object . values ( edges ) . map ( v => ( { source : v . source , target : v . target , } ) ) ; } private getNodeLayout ( layouts , node ) { const layout = toRef ( layouts , node ) ; if ( ! layout . value ) { layout . value = { x : 0 , y : 0 } ; } return layout ; } }