import { Component , Vue } from 'vue-property-decorator' ; import Victor from 'victor' ; import { TweenLite } from 'gsap' ; import { line , curveLinearClosed , curveBundle } from 'd3-shape' ; import _ from 'lodash' ; import ns from '@/store/namespaces' ; import { InputPort , OutputPort } from '@/components/port' ; import { ARROW_SIZE_PX , ARROW_WING_SIZE_PX , LONG_ANIMATION_DURATION_S } from '@/common/constants' ; import ContextMenu from '@/components/context-menu/context-menu' ; /**
 * EdgeSave stores the ids of nodes and ports.
 * On deserialization we use this information to reconstruct the edge.
 */ export interface EdgeSave { sourceNodeId : string ; sourcePortId : string ; targetNodeId : string ; targetPortId : string ; } export const arrowPath = ( base , head ) : string => { const p = new Victor ( base . x , base . y ) ; const q = new Victor ( head . x , head . y ) ; if ( p . distance ( q ) <= ARROW_SIZE_PX * .1 ) { return '' ; } const pq = q . clone ( ) . subtract ( p ) . normalize ( ) ; const pqLeft = pq . clone ( ) . rotate ( Math . PI / 2 ) ; const pqRight = pq . clone ( ) . rotate ( - Math . PI / 2 ) ; const root = q . clone ( ) . subtract ( pq . clone ( ) . multiplyScalar ( ARROW_SIZE_PX ) ) ; const points = [ q . toArray ( ) , root . clone ( ) . add ( pqLeft . multiplyScalar ( ARROW_WING_SIZE_PX ) ) . toArray ( ) , root . clone ( ) . add ( pqRight . multiplyScalar ( ARROW_WING_SIZE_PX ) ) . toArray ( ) , ] ; return line ( ) . curve ( curveLinearClosed ) ( points as Array < [ number , number ] > ) as string ; } ; @ Component ( { components : { ContextMenu } } ) export default class Edge extends Vue { public source ! : OutputPort ; public target ! : InputPort ; @ ns . dataflow . Mutation ( 'removeEdge' ) private dataflowRemoveEdge ! : ( edge : Vue ) => void ; @ ns . interaction . Mutation ( 'mouseupOnEdge' ) private mouseupOnEdge ! : ( edge : Vue ) => void ; @ ns . interaction . State ( 'isNodeListDragging' ) private isNodeListDragging ! : boolean ; @ ns . interaction . State ( 'isNodeDragging' ) private isNodeDragging ! : boolean ; @ ns . interaction . Getter ( 'isDraggedNodeDroppable' ) private isDraggedNodeDroppable ! : boolean ; private x1 : number = 0 ; private y1 : number = 0 ; private x2 : number = 0 ; private y2 : number = 0 ; private edgePathStr : string = '' ; private isHovered = false ; get getCurvePath ( ) : string { const sx = this . x1 ; const sy = this . y1 ; const ex = this . x2 ; const ey = this . y2 ; const points = [ [ sx , sy ] ] ; const yDirDown = ey > sy ; if ( ex >= sx ) { let headWidth = Math . max ( 0 , ( ex - sx ) / 2 - ARROW_SIZE_PX ) ; const tailWidth = ex - sx - headWidth ; if ( tailWidth < ARROW_SIZE_PX && Math . abs ( ey - sy ) >= ARROW_SIZE_PX ) { headWidth = ex - sx ; points . push ( [ sx + headWidth , sy ] ) ; } else { points . push ( [ sx + headWidth , sy ] ) ; points . push ( [ sx + headWidth , ey ] ) ; } } else { let midy ; const boxSource = this . source . node . getBoundingBox ( ) ; const boxTarget = this . target . node . getBoundingBox ( ) ; const sourceYrange = [ boxSource . y , boxSource . y + boxSource . height ] ; const targetYrange = [ boxTarget . y , boxTarget . y + boxTarget . height ] ; if ( sourceYrange [ 0 ] <= targetYrange [ 1 ] && sourceYrange [ 1 ] >= targetYrange [ 0 ] ) { if ( ! yDirDown ) { midy = targetYrange [ 0 ] - 20 ; } else { midy = targetYrange [ 1 ] + 20 ; } } else { midy = ( Math . max ( sourceYrange [ 0 ] , targetYrange [ 0 ] ) + Math . min ( sourceYrange [ 1 ] , targetYrange [ 1 ] ) ) / 2 ; } points . push ( [ sx , midy ] ) ; points . push ( [ ex , midy ] ) ; } const lastPoint = [ ex , ey ] ; points . push ( lastPoint ) ; const edgePath = line ( ) . curve ( curveBundle . beta ( 1 ) ) ( points ) as string ; this . edgePathStr = edgePath ; return edgePath ; } get getArrowPath ( ) : string { const points = this . getArrowPoints ( this . edgePathStr ) ; return arrowPath ( points . base , points . head ) ; } public serialize ( ) { return { sourceNodeId : this . source . node . id , sourcePortId : this . source . id , targetNodeId : this . target . node . id , targetPortId : this . target . id } ; } public getEdgeSvgNode ( ) { return this . $refs . edge as Element ; } public updateCoordinates ( ) { const sourceCenter = this . source . getCenterCoordinates ( ) ; this . x1 = sourceCenter . x ; this . y1 = sourceCenter . y ; const targetCenter = this . target . getCenterCoordinates ( ) ; this . x2 = targetCenter . x ; this . y2 = targetCenter . y ; } private contextMenuRemove ( ) { this . dataflowRemoveEdge ( this ) ; } private mounted ( ) { this . updateCoordinates ( ) ; this . appear ( ) ; } private appear ( ) { TweenLite . from ( this . $refs . edge , LONG_ANIMATION_DURATION_S , { opacity : 0 } ) ; } private getArrowPoints ( edgePath ) { const segments = edgePath . match ( /[CLM]([\d.-]+,?)+/g ) as string [ ] ; let distance = 0 ; const xyCoords = segments . join ( '' ) . split ( /[CLM,]/g ) . map ( val => + val ) . slice ( 1 ) ; const xCoords = xyCoords . filter ( ( e , index ) => index % 2 === 0 ) ; const yCoords = xyCoords . filter ( ( e , index ) => index % 2 === 1 ) ; if ( ( _ . last ( segments ) as string ) [ 0 ] === 'L' ) { const n = xCoords . length ; const p1 = new Victor ( xCoords [ n - 2 ] , yCoords [ n - 2 ] ) ; const p2 = new Victor ( xCoords [ n - 1 ] , yCoords [ n - 1 ] ) ; distance += p1 . distance ( p2 ) ; if ( distance >= ARROW_SIZE_PX ) { return { base : p1 , head : p2 } ; } segments . pop ( ) ; xCoords . pop ( ) ; yCoords . pop ( ) ; } xCoords . splice ( 0 , xCoords . length - 4 ) ; yCoords . splice ( 0 , yCoords . length - 4 ) ; const bezierCurve = ( t , val ) => { const ct = 1 - t ; return ct * ct * ct * val [ 0 ] + t * t * t * val [ 3 ] + 3 * ct * t * ( ct * val [ 1 ] + t * val [ 2 ] ) ; } ; let lastPoint = new Victor ( _ . last ( xCoords ) as number , _ . last ( yCoords ) as number ) ; let p ; const step = .01 ; for ( p = 1 - step ; p >= 0 && distance < ARROW_SIZE_PX ; p -= step ) { const point = new Victor ( bezierCurve ( p , xCoords ) , bezierCurve ( p , yCoords ) ) ; distance += point . distance ( lastPoint ) ; lastPoint = point ; } const base = { x : bezierCurve ( p , xCoords ) , y : bezierCurve ( p , yCoords ) } ; const baseVector = new Victor ( base . x , base . y ) ; const head = new Victor ( _ . last ( xCoords ) as number , _ . last ( yCoords ) as number ) . subtract ( baseVector ) . normalize ( ) . multiplyScalar ( ARROW_SIZE_PX ) . add ( baseVector ) ; return { base , head } ; } private onMouseup ( ) { this . mouseupOnEdge ( this ) ; } private onMouseover ( ) { if ( this . isNodeDragging && ! this . isDraggedNodeDroppable ) { return ; } this . isHovered = true ; } private onMouseout ( ) { this . isHovered = false ; } }