import { Element } from './elements' ; export interface Vector { readonly x : number ; readonly y : number ; } export const Vector = { equals ( a , b ) { return a . x === b . x && a . y === b . y ; } , length ( { x , y } ) { return Math . sqrt ( x * x + y * y ) ; } , normalize ( { x , y } ) { if ( x === 0 && y === 0 ) { return { x , y } ; } const inverseLength = 1 / Math . sqrt ( x * x + y * y ) ; return { x : x * inverseLength , y : y * inverseLength } ; } , dot ( { x : x1 , y : y1 } , { x : x2 , y : y2 } ) { return x1 * x2 + y1 * y2 ; } , cross2D ( { x : x1 , y : y1 } , { x : x2 , y : y2 } ) { return x1 * y2 - y1 * x2 ; } , } ; export interface Size { readonly width : number ; readonly height : number ; } export interface Rect { readonly x : number ; readonly y : number ; readonly width : number ; readonly height : number ; } export const Rect = { center ( { x , y , width , height } ) { return { x : x + width / 2 , y : y + height / 2 } ; } , } ; export function boundsOf ( element ) { const { x , y } = element . position ; const { width , height } = element . size ; return { x , y , width , height } ; } function intersectRayFromRectangleCenter ( sourceRect , rayTarget ) { const isTargetInsideRect = sourceRect . width === 0 || sourceRect . height === 0 || ( rayTarget . x > sourceRect . x && rayTarget . x < sourceRect . x + sourceRect . width && rayTarget . y > sourceRect . y && rayTarget . y < sourceRect . y + sourceRect . height ) ; const halfWidth = sourceRect . width / 2 ; const halfHeight = sourceRect . height / 2 ; const center = { x : sourceRect . x + halfWidth , y : sourceRect . y + halfHeight , } ; if ( isTargetInsideRect ) { return center ; } const direction = Vector . normalize ( { x : rayTarget . x - center . x , y : rayTarget . y - center . y , } ) ; const rightDirection = { x : Math . abs ( direction . x ) , y : direction . y } ; const isHorizontal = Vector . cross2D ( { x : halfWidth , y : - halfHeight } , rightDirection ) > 0 && Vector . cross2D ( { x : halfWidth , y : halfHeight } , rightDirection ) < 0 ; if ( isHorizontal ) { return { x : center . x + halfWidth * Math . sign ( direction . x ) , y : center . y + ( halfWidth * direction . y ) / Math . abs ( direction . x ) , } ; } else { return { x : center . x + ( halfHeight * direction . x ) / Math . abs ( direction . y ) , y : center . y + halfHeight * Math . sign ( direction . y ) , } ; } } export function isPolylineEqual ( left , right ) { if ( left === right ) { return true ; } if ( left . length !== right . length ) { return false ; } for ( let i = 0 ; i < left . length ; i ++ ) { const a = left [ i ] ; const b = right [ i ] ; if ( ! ( a . x === b . x && a . y === b . y ) ) { return false ; } } return true ; } export function computePolyline ( source , target , vertices ) { const sourceRect = boundsOf ( source ) ; const targetRect = boundsOf ( target ) ; const startPoint = intersectRayFromRectangleCenter ( sourceRect , vertices . length > 0 ? vertices [ 0 ] : Rect . center ( targetRect ) ) ; const endPoint = intersectRayFromRectangleCenter ( targetRect , vertices . length > 0 ? vertices [ vertices . length - 1 ] : Rect . center ( sourceRect ) ) ; return [ startPoint , ... vertices , endPoint ] ; } export function computePolylineLength ( polyline ) { let previous ; return polyline . reduce ( ( acc , point ) => { const segmentLength = previous ? Vector . length ( { x : point . x - previous . x , y : point . y - previous . y } ) : 0 ; previous = point ; return acc + segmentLength ; } , 0 ) ; } export function getPointAlongPolyline ( polyline , offset ) { if ( polyline . length === 0 ) { throw new Error ( 'Cannot compute a point for empty polyline' ) ; } if ( offset < 0 ) { return polyline [ 0 ] ; } let currentOffset = 0 ; for ( let i = 1 ; i < polyline . length ; i ++ ) { const previous = polyline [ i - 1 ] ; const point = polyline [ i ] ; const segment = { x : point . x - previous . x , y : point . y - previous . y } ; const segmentLength = Vector . length ( segment ) ; const newOffset = currentOffset + segmentLength ; if ( offset < newOffset ) { const leftover = ( offset - currentOffset ) / segmentLength ; return { x : previous . x + leftover * segment . x , y : previous . y + leftover * segment . y , } ; } else { currentOffset = newOffset ; } } return polyline [ polyline . length - 1 ] ; } export function findNearestSegmentIndex ( polyline , location ) { let minDistance = Infinity ; let foundIndex = 0 ; for ( let i = 0 ; i < polyline . length - 1 ; i ++ ) { const pivot = polyline [ i ] ; const next = polyline [ i + 1 ] ; const target = { x : location . x - pivot . x , y : location . y - pivot . y } ; const segment = { x : next . x - pivot . x , y : next . y - pivot . y } ; const segmentLength = Vector . length ( segment ) ; const projectionToSegment = Vector . dot ( target , segment ) / segmentLength ; if ( projectionToSegment < 0 || projectionToSegment > segmentLength ) { continue ; } const distanceToSegment = Math . abs ( Vector . cross2D ( target , segment ) ) / segmentLength ; if ( distanceToSegment < minDistance ) { minDistance = distanceToSegment ; foundIndex = i ; } } return foundIndex ; } export function findElementAtPoint ( elements , point ) { for ( let i = elements . length - 1 ; i >= 0 ; i -- ) { const element = elements [ i ] ; const { x , y , width , height } = boundsOf ( element ) ; if ( element . temporary ) { continue ; } if ( point . x >= x && point . x <= x + width && point . y >= y && point . y <= y + height ) { return element ; } } return undefined ; } export function computeGrouping ( elements ) { const grouping = new Map < string , Element [ ] > ( ) ; for ( const element of elements ) { const group = element . group ; if ( typeof group === 'string' ) { let children = grouping . get ( group ) ; if ( ! children ) { children = [ ] ; grouping . set ( group , children ) ; } children . push ( element ) ; } } return grouping ; }