import { computed , ComputedRef , reactive , Ref , toRef , unref , UnwrapRef } from "vue" ; import { watch , watchEffect , WatchStopHandle } from "vue" ; import { inject , InjectionKey , provide } from "vue" ; import { nonNull , Reactive } from "../common/common" ; import { Config , Configs , EdgeConfig , MarkerStyle , NodeConfig } from "../common/configs" ; import { ShapeStyle , NodeLabelStyle , StrokeStyle } from "../common/configs" ; import { Edge , Edges , Layouts , Node , Nodes } from "../common/types" ; import { LinePosition , NodePositions , Position } from "../common/types" ; import { EdgeGroupStates } from "../common/edge-group" ; import { clearMarker , makeMarker } from "./marker" ; import * as EdgeGroup from "../common/edge-group" ; import * as v2d from "../common/2d" ; import * as V from "../common/vector" ; export type { EdgeGroupStates } ; interface NodeStateDatum { shape : Ref < ShapeStyle > ; staticShape : Ref < ShapeStyle > ; label : Ref < NodeLabelStyle > ; labelText : Ref < string > ; selected : boolean ; hovered : boolean ; draggable : Ref < boolean > ; selectable : Ref < boolean | number > ; } export type NodeState = UnwrapRef < NodeStateDatum > ; export type NodeStates = Record < string , NodeState > ; interface Line { stroke : StrokeStyle ; normalWidth : number ; source : MarkerStyle ; target : MarkerStyle ; } export interface Curve { center : V . Vector ; theta : number ; circle : { center : V . Vector ; radius : number ; } ; control : Position [ ] ; } interface EdgeStateDatum { line : Ref < Line > ; selectable : Ref < boolean | number > ; selected : boolean ; hovered : boolean ; origin : LinePosition ; labelPosition : LinePosition ; position : LinePosition ; curve ? : Curve ; sourceMarkerId ? : string ; targetMarkerId ? : string ; stopWatchHandle : WatchStopHandle ; } interface SummarizedEdgeStateDatum { stroke : Ref < StrokeStyle > ; } export type EdgeState = UnwrapRef < EdgeStateDatum > ; export type EdgeStates = Record < string , EdgeState > ; export type SummarizedEdgeState = UnwrapRef < SummarizedEdgeStateDatum > ; export type SummarizedEdgeStates = Record < string , SummarizedEdgeState > ; interface States { nodeStates : NodeStates ; edgeStates : EdgeStates ; edgeGroupStates : EdgeGroupStates ; summarizedEdgeStates : SummarizedEdgeStates ; layouts : Layouts ; } export type ReadonlyStates = Readonly < States > ; const statesKey = Symbol ( "states" ) as InjectionKey < States > ; const NONE_MARKER = { type : "none" , width : 0 , height : 0 , margin : 0 , units : "strokeWidth" , color : null , } ; export function provideStates ( nodes , edges , selectedNodes , selectedEdges , hoveredNodes , hoveredEdges , configs , layouts , scale ) { const nodeStates = reactive ( { } ) ; const edgeStates = reactive ( { } ) ; const summarizedEdgeStates = reactive ( { } ) ; Object . keys ( nodes ) . forEach ( id => { createNodeState ( nodeStates , nodes , id , selectedNodes . has ( id ) , false , configs . node ) ; } ) ; watch ( ( ) => [ ... selectedNodes ] , ( nodes , prev ) => { const append = nodes . filter ( n => ! prev . includes ( n ) ) ; const removed = prev . filter ( n => ! nodes . includes ( n ) ) ; append . forEach ( id => { const state = nodeStates [ id ] ; if ( state && ! state . selected ) state . selected = true ; } ) ; removed . forEach ( id => { const state = nodeStates [ id ] ; if ( state && state . selected ) state . selected = false ; } ) ; } ) ; watch ( ( ) => [ ... hoveredNodes ] , ( nodes , prev ) => { const append = nodes . filter ( n => ! prev . includes ( n ) ) ; const removed = prev . filter ( n => ! nodes . includes ( n ) ) ; append . forEach ( id => { const state = nodeStates [ id ] ; if ( state && ! state . hovered ) state . hovered = true ; } ) ; removed . forEach ( id => { const state = nodeStates [ id ] ; if ( state && state . hovered ) state . hovered = false ; } ) ; } ) ; watch ( ( ) => new Set ( Object . keys ( nodes ) ) , ( idSet , prev ) => { for ( const nodeId of idSet ) { if ( prev . has ( nodeId ) ) continue ; createNodeState ( nodeStates , nodes , nodeId , false , false , configs . node ) ; } const positions = layouts . nodes ; for ( const nodeId of prev ) { if ( idSet . has ( nodeId ) ) continue ; delete positions [ nodeId ] ; selectedNodes . delete ( nodeId ) ; hoveredNodes . delete ( nodeId ) ; delete nodeStates [ nodeId ] ; } } ) ; const edgeGroupStates = EdgeGroup . makeEdgeGroupStates ( nodes , edges , configs ) ; Object . keys ( edges ) . forEach ( id => { createEdgeState ( edgeStates , edgeGroupStates , nodeStates , edges , id , selectedEdges . has ( id ) , configs . edge , layouts . nodes , scale ) ; } ) ; watch ( edgeGroupStates . edgeGroups , _ => createSummarizedEdgeStates ( summarizedEdgeStates , edgeGroupStates , configs ) , { immediate : true } ) ; watch ( ( ) => [ ... selectedEdges ] , ( nodes , prev ) => { const append = nodes . filter ( n => ! prev . includes ( n ) ) ; const removed = prev . filter ( n => ! nodes . includes ( n ) ) ; append . forEach ( id => { const state = edgeStates [ id ] ; if ( state && ! state . selected ) state . selected = true ; } ) ; removed . forEach ( id => { const state = edgeStates [ id ] ; if ( state && state . selected ) state . selected = false ; } ) ; } ) ; watch ( ( ) => [ ... hoveredEdges ] , ( nodes , prev ) => { const append = nodes . filter ( n => ! prev . includes ( n ) ) ; const removed = prev . filter ( n => ! nodes . includes ( n ) ) ; append . forEach ( id => { const state = edgeStates [ id ] ; if ( state && ! state . hovered ) { state . hovered = true ; } } ) ; removed . forEach ( id => { const state = edgeStates [ id ] ; if ( state && state . hovered ) { state . hovered = false ; } } ) ; } ) ; watch ( ( ) => new Set ( Object . keys ( edges ) ) , ( idSet , prev ) => { for ( const edgeId of idSet ) { if ( prev . has ( edgeId ) ) continue ; createEdgeState ( edgeStates , edgeGroupStates , nodeStates , edges , edgeId , false , configs . edge , layouts . nodes , scale ) ; } for ( const edgeId of prev ) { if ( idSet . has ( edgeId ) ) continue ; selectedEdges . delete ( edgeId ) ; hoveredEdges . delete ( edgeId ) ; edgeStates [ edgeId ] . stopWatchHandle ( ) ; delete edgeStates [ edgeId ] ; } } ) ; const states = { nodeStates , edgeStates , edgeGroupStates , summarizedEdgeStates , layouts } ; provide ( statesKey , states ) ; return states ; } export function useStates ( ) { return nonNull ( inject ( statesKey ) , "states" ) as ReadonlyStates ; } function getNodeShape ( node , selected , hovered , config ) { if ( hovered && config . hover ) { return Config . values ( config . hover , node ) ; } else { return getNodeStaticShape ( node , selected , config ) ; } } function getNodeStaticShape ( node , selected , config ) { if ( selected && config . selected ) { return Config . values ( config . selected , node ) ; } else { return Config . values ( config . normal , node ) ; } } function getEdgeStroke ( edge , selected , hovered , config ) { if ( selected ) { return Config . values ( config . selected , edge ) ; } else if ( hovered && config . hover ) { return Config . values ( config . hover , edge ) ; } else { return Config . values ( config . normal , edge ) ; } } function createNodeState ( states , nodes , id , selected , hovered , config ) { states [ id ] = { selected , hovered } as any ; const state = states [ id ] as any as NodeStateDatum ; state . shape = computed ( ( ) => { if ( ! nodes [ id ] ) return unref ( state . shape ) ; return getNodeShape ( nodes [ id ] , state . selected , state . hovered , config ) ; } ) ; state . staticShape = computed ( ( ) => { if ( ! nodes [ id ] ) return unref ( state . staticShape ) ; return getNodeStaticShape ( nodes [ id ] , state . selected , config ) ; } ) ; state . label = computed ( ( ) => { if ( ! nodes [ id ] ) return unref ( state . label ) ; return Config . values ( config . label , nodes [ id ] ) ; } ) ; state . labelText = computed ( ( ) => { if ( config . label . text instanceof Function ) { return unref ( state . label ) . text ; } else { if ( ! nodes [ id ] ) return unref ( state . labelText ) ; return nodes [ id ] ?. [ unref ( state . label ) . text ] ?? "" ; } } ) ; state . draggable = computed ( ( ) => { if ( ! nodes [ id ] ) return unref ( state . draggable ) ; return Config . value ( config . draggable , nodes [ id ] ) ; } ) ; state . selectable = computed ( ( ) => { if ( ! nodes [ id ] ) return unref ( state . selectable ) ; return Config . value ( config . selectable , nodes [ id ] ) ; } ) ; } function toEdgeMarker ( marker ) { if ( marker . type === "none" ) { return NONE_MARKER ; } else { return marker ; } } function createEdgeState ( states , groupStates , nodeStates , edges , id , selected , config , layouts , scale ) { const edge = edges [ id ] ; if ( ! edge ) return ; states [ id ] = { line : undefined as any , selectable : false , selected , hovered : false , curve : undefined , origin : { x1 : 0 , y1 : 0 , x2 : 0 , y2 : 0 } , labelPosition : { x1 : 0 , y1 : 0 , x2 : 0 , y2 : 0 } , position : { x1 : 0 , y1 : 0 , x2 : 0 , y2 : 0 } , stopWatchHandle : ( ) => { } , } ; const state = states [ id ] as any as EdgeStateDatum ; const line = computed < Line > ( ( ) => { const edge = edges [ id ] ; const stroke = getEdgeStroke ( edge , state . selected , state . hovered , config ) ; const normalWidth = Config . value ( config . normal . width , edge ) ; const source = toEdgeMarker ( Config . values ( config . marker . source , [ edge , stroke ] ) ) ; const target = toEdgeMarker ( Config . values ( config . marker . target , [ edge , stroke ] ) ) ; return { stroke , normalWidth , source , target } ; } ) ; state . line = line ; state . selectable = computed ( ( ) => Config . value ( config . selectable , edges [ id ] ) ) ; const edgeLayoutPoint = toRef ( groupStates . edgeLayoutPoints , id ) ; const isEdgeSummarized = toRef ( groupStates . summarizedEdges , id ) ; const stopCalcHandle = watchEffect ( ( ) => { const edge = edges [ id ] ; if ( ! edge ) return ; const source = layouts [ edge ?. source ] ; const target = layouts [ edge ?. target ] ; const sourceShape = nodeStates [ edge ?. source ] ?. staticShape ; const targetShape = nodeStates [ edge ?. target ] ?. staticShape ; if ( ! source || ! target || ! sourceShape || ! targetShape ) { return ; } const shiftedPosition = EdgeGroup . calculateEdgeShiftedPosition ( edgeLayoutPoint . value , isEdgeSummarized . value , source , target , scale . value , config . keepOrder ) ; const [ sourceShapeMargin , targetShapeMargin ] = v2d . calculateDistancesFromCenterOfNodeToEndOfNode ( source , target , sourceShape , targetShape ) ; const s = scale . value ; state . labelPosition = v2d . applyMarginToLine ( shiftedPosition , sourceShapeMargin * s , targetShapeMargin * s ) ; let sourceMargin = 0 ; let targetMargin = 0 ; const l = line . value ; if ( l . source . type !== "none" ) { const marker = l . source ; sourceMargin = marker . margin + marker . width ; if ( marker . units === "strokeWidth" ) { sourceMargin *= l . normalWidth ; } } if ( l . target . type !== "none" ) { const marker = l . target ; targetMargin = marker . margin + marker . width ; if ( marker . units === "strokeWidth" ) { targetMargin *= l . normalWidth ; } } if ( config . margin === null || config . margin === undefined ) { if ( l . source . type !== "none" || l . target . type !== "none" ) { sourceMargin += sourceShapeMargin ; targetMargin += targetShapeMargin ; } } else { sourceMargin += config . margin + sourceShapeMargin ; targetMargin += config . margin + targetShapeMargin ; } const type = config . type ; if ( type === "straight" ) { state . origin = shiftedPosition ; state . curve = undefined ; if ( sourceMargin === 0 && targetMargin === 0 ) { state . position = state . origin ; } else { state . position = v2d . applyMarginToLine ( state . origin , sourceMargin * s , targetMargin * s ) ; } } else { state . origin = v2d . positionsToLinePosition ( source , target ) ; const shift = edgeLayoutPoint . value . groupWidth / 2 - edgeLayoutPoint . value . pointInGroup ; const [ position , curve ] = calculateCurvePositionAndState ( state . origin , shiftedPosition , shift , sourceMargin * s , targetMargin * s ) ; state . position = position ; state . curve = curve ; } } ) ; const stopUpdateMarkerHandle = watchEffect ( ( ) => { if ( ! edges [ id ] ) return ; state . sourceMarkerId = makeMarker ( line . value . source , true , state . sourceMarkerId , line . value . stroke . color ) ; state . targetMarkerId = makeMarker ( line . value . target , false , state . targetMarkerId , line . value . stroke . color ) ; } ) ; states [ id ] . stopWatchHandle = ( ) => { stopCalcHandle ( ) ; stopUpdateMarkerHandle ( ) ; clearMarker ( state . sourceMarkerId ) ; clearMarker ( state . targetMarkerId ) ; } ; } function calculateCurvePositionAndState ( originPosition , shiftedPosition , shift , sourceMargin , targetMargin ) { const origin = V . fromLinePosition ( originPosition ) ; const shifted = V . fromLinePosition ( shiftedPosition ) ; const shiftedCenter = V . getCenterOfLinePosition ( shiftedPosition ) ; const [ center , radius ] = V . calculateCircleCenterAndRadiusBy3Points ( origin . source , origin . target , shiftedCenter ) ; let position ; let curve = undefined ; if ( shift === 0 ) { if ( sourceMargin === 0 && targetMargin === 0 ) { position = originPosition ; } else { position = v2d . applyMarginToLine ( originPosition , sourceMargin , targetMargin ) ; } return [ position , curve ] ; } const centerToTop = V . fromVectors ( center , shiftedCenter ) ; const theta0 = V . calculateRelativeAngleRadian ( V . fromVectors ( center , origin . source ) , centerToTop ) ; if ( sourceMargin === 0 && targetMargin === 0 ) { position = originPosition ; } else { let sourceMoveRad = sourceMargin / radius ; let targetMoveRad = targetMargin / radius ; if ( theta0 > 0 ) { sourceMoveRad *= - 1 ; targetMoveRad *= - 1 ; } position = v2d . positionsToLinePosition ( v2d . moveOnCircumference ( origin . source , center , sourceMoveRad ) , v2d . moveOnCircumference ( origin . target , center , - targetMoveRad ) ) ; let theta1 = V . calculateRelativeAngleRadian ( V . fromVectors ( center , origin . source ) , V . fromVectors ( center , origin . target ) ) ; let theta2 = V . calculateRelativeAngleRadian ( V . fromPositions ( center , { x : position . x1 , y : position . y1 } ) , V . fromPositions ( center , { x : position . x2 , y : position . y2 } ) ) ; if ( theta0 * theta1 < 0 ) { theta1 = v2d . reverseAngleRadian ( theta1 ) ; if ( theta0 * theta2 < 0 ) { theta2 = v2d . reverseAngleRadian ( theta2 ) ; } } if ( theta1 * theta2 < 0 ) { const c = shiftedCenter . clone ( ) . add ( shifted . v . normalize ( ) . multiplyScalar ( 0.5 ) ) ; position = v2d . positionsToLinePosition ( shiftedCenter , c ) ; return [ position , curve ] ; } } const [ p1 , p2 ] = V . toVectorsFromLinePosition ( position ) ; const control = v2d . calculateBezierCurveControlPoint ( p1 , center , p2 , theta0 ) . map ( p => p . toObject ( ) ) ; curve = { center : shiftedCenter , theta : theta0 , circle : { center , radius } , control , } ; return [ position , curve ] ; } function createSummarizedEdgeStates ( summarizedEdgeStates , edgeGroupStates , configs ) { const groups = edgeGroupStates . edgeGroups ; Object . entries ( groups ) . filter ( ( [ id , group ] ) => group . summarize && ! ( id in summarizedEdgeStates ) ) . forEach ( ( [ id , group ] ) => { const state = { stroke : undefined as any } ; state . stroke = computed < StrokeStyle > ( ( ) => Config . values ( configs . edge . summarized . stroke , group . edges ) ) ; summarizedEdgeStates [ id ] = state ; } ) ; Object . keys ( summarizedEdgeStates ) . forEach ( id => { if ( ! edgeGroupStates . edgeGroups [ id ] ?. summarize ) { delete summarizedEdgeStates [ id ] ; } } ) ; }