import * as svg from "./svg" ; import { IBounds , ICanvasItems , ISelectionSnapshot , IData , IGlob , IGlobPoints , INode , INodeSnapshot , INodeAdjacentHandleSnapshot , } from "./types" ; import { getNormalOnCurve , getClosestPointOnCurve } from "./bez" ; import * as vec from "./vec" ; import { Intersection , ShapeInfo } from "kld-intersections" ; import { getCommonBounds , getGlobBounds , getNodeBounds } from "./bounds-utils" ; export function getCircleTangentToPoint ( A , r0 , P , side ) { const B = vec . lrp ( A , P , 0.5 ) , r1 = vec . dist ( A , B ) , delta = vec . sub ( B , A ) , d = vec . len ( delta ) ; if ( ! ( d <= r0 + r1 && d >= Math . abs ( r0 - r1 ) ) ) { return ; } const a = ( r0 * r0 - r1 * r1 + d * d ) / ( 2.0 * d ) , n = 1 / d , p = vec . add ( A , vec . mul ( delta , a * n ) ) , h = Math . sqrt ( r0 * r0 - a * a ) , k = vec . mul ( vec . per ( delta ) , h * n ) ; return side === 0 ? vec . add ( p , k ) : vec . sub ( p , k ) ; } export function circleCircleIntersections ( a , b ) { const R = a [ 2 ] , r = b [ 2 ] ; let dx = b [ 0 ] - a [ 0 ] , dy = b [ 1 ] - a [ 1 ] ; const d = Math . sqrt ( dx * dx + dy * dy ) , x = ( d * d - r * r + R * R ) / ( 2 * d ) , y = Math . sqrt ( R * R - x * x ) ; dx /= d ; dy /= d ; return [ [ a [ 0 ] + dx * x - dy * y , a [ 1 ] + dy * x + dx * y ] , [ a [ 0 ] + dx * x + dy * y , a [ 1 ] + dy * x - dx * y ] , ] ; } export function getClosestPointOnCircle ( C , r , P , padding = 0 ) { const v = vec . sub ( C , P ) ; return vec . sub ( C , vec . mul ( vec . div ( v , vec . len ( v ) ) , r + padding ) ) ; } export function projectPoint ( p0 , a , d ) { return [ Math . cos ( a ) * d + p0 [ 0 ] , Math . sin ( a ) * d + p0 [ 1 ] ] ; } function shortAngleDist ( a0 , a1 ) { const max = Math . PI * 2 ; const da = ( a1 - a0 ) % max ; return ( ( 2 * da ) % max ) - da ; } export function lerpAngles ( a0 , a1 , t ) { return a0 + shortAngleDist ( a0 , a1 ) * t ; } export function getBezierCurveSegments ( points , tension = 0.4 ) { const len = points . length , cpoints = [ ... points ] ; if ( len < 2 ) { throw Error ( "Curve must have at least two points." ) ; } for ( let i = 1 ; i < len - 1 ; i ++ ) { const p0 = points [ i - 1 ] , p1 = points [ i ] , p2 = points [ i + 1 ] ; const pdx = p2 [ 0 ] - p0 [ 0 ] , pdy = p2 [ 1 ] - p0 [ 1 ] , pd = Math . hypot ( pdx , pdy ) , nx = pdx / pd , ny = pdy / pd , dp = Math . hypot ( p1 [ 0 ] - p0 [ 0 ] , p1 [ 1 ] - p0 [ 1 ] ) , dn = Math . hypot ( p1 [ 0 ] - p2 [ 0 ] , p1 [ 1 ] - p2 [ 1 ] ) ; cpoints [ i ] = [ p1 [ 0 ] - nx * dp * tension , p1 [ 1 ] - ny * dp * tension , p1 [ 0 ] + nx * dn * tension , p1 [ 1 ] + ny * dn * tension , nx , ny , ] ; } const d0 = Math . hypot ( points [ 0 ] [ 0 ] + cpoints [ 1 ] [ 0 ] ) ; cpoints [ 0 ] [ 2 ] = ( points [ 0 ] [ 0 ] + cpoints [ 1 ] [ 0 ] ) / 2 ; cpoints [ 0 ] [ 3 ] = ( points [ 0 ] [ 1 ] + cpoints [ 1 ] [ 1 ] ) / 2 ; cpoints [ 0 ] [ 4 ] = ( cpoints [ 1 ] [ 0 ] - points [ 0 ] [ 0 ] ) / d0 ; cpoints [ 0 ] [ 5 ] = ( cpoints [ 1 ] [ 1 ] - points [ 0 ] [ 1 ] ) / d0 ; const d1 = Math . hypot ( points [ len - 1 ] [ 1 ] + cpoints [ len - 1 ] [ 1 ] ) ; cpoints [ len - 1 ] [ 0 ] = ( points [ len - 1 ] [ 0 ] + cpoints [ len - 2 ] [ 2 ] ) / 2 ; cpoints [ len - 1 ] [ 1 ] = ( points [ len - 1 ] [ 1 ] + cpoints [ len - 2 ] [ 3 ] ) / 2 ; cpoints [ len - 1 ] [ 4 ] = ( cpoints [ len - 2 ] [ 2 ] - points [ len - 1 ] [ 0 ] ) / - d1 ; cpoints [ len - 1 ] [ 5 ] = ( cpoints [ len - 2 ] [ 3 ] - points [ len - 1 ] [ 1 ] ) / - d1 ; const results = [ ] ; for ( let i = 1 ; i < cpoints . length ; i ++ ) { results . push ( { start : points [ i - 1 ] . slice ( 0 , 2 ) , tangentStart : cpoints [ i - 1 ] . slice ( 2 , 4 ) , normalStart : cpoints [ i - 1 ] . slice ( 4 , 6 ) , pressureStart : 2 + ( ( i - 1 ) % 2 === 0 ? 1.5 : 0 ) , end : points [ i ] . slice ( 0 , 2 ) , tangentEnd : cpoints [ i ] . slice ( 0 , 2 ) , normalEnd : cpoints [ i ] . slice ( 4 , 6 ) , pressureEnd : 2 + ( i % 2 === 0 ? 1.5 : 0 ) , } ) ; } return results ; } export function cubicBezier ( tx , x1 , y1 , x2 , y2 ) { const x0 = 0 , y0 = 0 , x3 = 1 , y3 = 1 , A = x3 - 3 * x2 + 3 * x1 - x0 , B = 3 * x2 - 6 * x1 + 3 * x0 , C = 3 * x1 - 3 * x0 , D = x0 , E = y3 - 3 * y2 + 3 * y1 - y0 , F = 3 * y2 - 6 * y1 + 3 * y0 , G = 3 * y1 - 3 * y0 , H = y0 , iterations = 5 ; let i , slope , x , t = tx ; for ( i = 0 ; i < iterations ; i ++ ) { x = A * t * t * t + B * t * t + C * t + D ; slope = 1 / ( 3 * A * t * t + 2 * B * t + C ) ; t -= ( x - tx ) * slope ; t = t > 1 ? 1 : t < 0 ? 0 : t ; } return Math . abs ( E * t * t * t + F * t * t + G * t * H ) ; } export function copyToClipboard ( string ) { let textarea ; let result ; try { navigator . clipboard . writeText ( string ) ; } catch ( e ) { try { textarea = document . createElement ( "textarea" ) ; textarea . setAttribute ( "position" , "fixed" ) ; textarea . setAttribute ( "top" , "0" ) ; textarea . setAttribute ( "readonly" , "true" ) ; textarea . setAttribute ( "contenteditable" , "true" ) ; textarea . style . position = "fixed" ; textarea . value = string ; document . body . appendChild ( textarea ) ; textarea . focus ( ) ; textarea . select ( ) ; const range = document . createRange ( ) ; range . selectNodeContents ( textarea ) ; const sel = window . getSelection ( ) ; sel . removeAllRanges ( ) ; sel . addRange ( range ) ; textarea . setSelectionRange ( 0 , textarea . value . length ) ; result = document . execCommand ( "copy" ) ; } catch ( err ) { result = null ; } finally { document . body . removeChild ( textarea ) ; } } return ! ! result ; } param points param k returns export function getSpline ( pts , k = 0.5 ) { let p0 , [ p1 , p2 , p3 ] = pts ; const results = [ ] ; for ( let i = 1 , len = pts . length ; i < len ; i ++ ) { p0 = p1 ; p1 = p2 ; p2 = p3 ; p3 = pts [ i + 2 ] ? pts [ i + 2 ] : p2 ; results . push ( [ p1 [ 0 ] + ( ( p2 [ 0 ] - p0 [ 0 ] ) / 6 ) * k , p1 [ 1 ] + ( ( p2 [ 1 ] - p0 [ 1 ] ) / 6 ) * k , p2 [ 0 ] - ( ( p3 [ 0 ] - p1 [ 0 ] ) / 6 ) * k , p2 [ 1 ] - ( ( p3 [ 1 ] - p1 [ 1 ] ) / 6 ) * k , pts [ i ] [ 0 ] , pts [ i ] [ 1 ] , ] ) ; } return results ; } export function getCurvePoints ( pts , tension = 0.5 , isClosed = false , numOfSegments = 3 ) { const _pts = [ ... pts ] , len = pts . length , res = [ ] ; let t1x , t2x , t1y , t2y , c1 , c2 , c3 , c4 , st , st2 , st3 ; if ( isClosed ) { _pts . unshift ( _pts [ len - 1 ] ) ; _pts . push ( _pts [ 0 ] ) ; } else { _pts . unshift ( _pts [ 0 ] ) ; _pts . push ( _pts [ len - 1 ] ) ; } for ( let i = 1 ; i < _pts . length - 2 ; i ++ ) { for ( let t = 0 ; t <= numOfSegments ; t ++ ) { st = t / numOfSegments ; st2 = Math . pow ( st , 2 ) ; st3 = Math . pow ( st , 3 ) ; c1 = 2 * st3 - 3 * st2 + 1 ; c2 = - ( 2 * st3 ) + 3 * st2 ; c3 = st3 - 2 * st2 + st ; c4 = st3 - st2 ; t1x = ( _pts [ i + 1 ] [ 0 ] - _pts [ i - 1 ] [ 0 ] ) * tension ; t2x = ( _pts [ i + 2 ] [ 0 ] - _pts [ i ] [ 0 ] ) * tension ; t1y = ( _pts [ i + 1 ] [ 1 ] - _pts [ i - 1 ] [ 1 ] ) * tension ; t2y = ( _pts [ i + 2 ] [ 1 ] - _pts [ i ] [ 1 ] ) * tension ; res . push ( [ c1 * _pts [ i ] [ 0 ] + c2 * _pts [ i + 1 ] [ 0 ] + c3 * t1x + c4 * t2x , c1 * _pts [ i ] [ 1 ] + c2 * _pts [ i + 1 ] [ 1 ] + c3 * t1y + c4 * t2y , ] ) ; } } res . push ( pts [ pts . length - 1 ] ) ; return res ; } export function angleDelta ( a0 , a1 ) { return shortAngleDist ( a0 , a1 ) ; } param x param y param cx param cy param angle export function rotatePoint ( A , B , angle ) { const s = Math . sin ( angle ) ; const c = Math . cos ( angle ) ; const px = A [ 0 ] - B [ 0 ] ; const py = A [ 1 ] - B [ 1 ] ; const nx = px * c - py * s ; const ny = px * s + py * c ; return [ nx + B [ 0 ] , ny + B [ 1 ] ] ; } export function degreesToRadians ( d ) { return ( d * Math . PI ) / 180 ; } export function radiansToDegrees ( r ) { return ( r * 180 ) / Math . PI ; } export function getArcLength ( C , r , A , B ) { const sweep = getSweep ( C , A , B ) ; return r * ( 2 * Math . PI ) * ( sweep / ( 2 * Math . PI ) ) ; } export function getArcDashOffset ( C , r , A , B , step ) { const del0 = getSweep ( C , A , B ) ; const len0 = getArcLength ( C , r , A , B ) ; const off0 = del0 < 0 ? len0 : 2 * Math . PI * C [ 2 ] - len0 ; return - off0 / 2 + step ; } export function getEllipseDashOffset ( A , step ) { const c = 2 * Math . PI * A [ 2 ] ; return - c / 2 + - step ; } export function getSweep ( C , A , B ) { return angleDelta ( vec . angle ( C , A ) , vec . angle ( C , B ) ) ; } export function getGlobPath ( glob , start , end ) { if ( ! glob . points ) return "" ; const { point : C0 , radius : r0 } = start ; const { point : C1 , radius : r1 } = end ; const { E0 , E0p , F0 , F0p , E1 , E1p , F1 , F1p } = glob . points ; return [ svg . moveTo ( E0 ) , start . cap === "round" ? svg . arcTo ( C0 , r0 , E0 , E0p ) : svg . lineTo ( E0p ) , svg . bezierTo ( F0p , F1p , E1p ) , end . cap === "round" ? svg . arcTo ( C1 , r1 , E1p , E1 ) : svg . lineTo ( E1 ) , svg . bezierTo ( F1 , F0 , E0 ) , svg . closePath ( ) , ] . join ( " " ) ; } export function getGlobPoints ( glob , start , end ) { return getGlob ( start . point , start . radius , end . point , end . radius , glob . D , glob . Dp , glob . a , glob . b , glob . ap , glob . bp ) ; } export function getGlob ( C0 , r0 , C1 , r1 , D , Dp , a , b , ap , bp ) { let E0 = getCircleTangentToPoint ( C0 , r0 , D , 0 ) , E0p = getCircleTangentToPoint ( C0 , r0 , Dp , 1 ) , E1 = getCircleTangentToPoint ( C1 , r1 , D , 1 ) , E1p = getCircleTangentToPoint ( C1 , r1 , Dp , 0 ) ; if ( ! ( E0 || E0p ) ) { E0 = C0 ; E0p = C0 ; } else if ( ! E0 ) { E0 = E0p ; } else if ( ! E0p ) { E0p = E0 ; } if ( ! ( E1 || E1p ) ) { E1 = C1 ; E1p = C1 ; } else if ( ! E1 ) { E1 = E1p ; } else if ( ! E1p ) { E1p = E1 ; } const F0 = vec . round ( vec . lrp ( E0 , D , a ) ) , F1 = vec . round ( vec . lrp ( E1 , D , b ) ) , F0p = vec . round ( vec . lrp ( E0p , Dp , ap ) ) , F1p = vec . round ( vec . lrp ( E1p , Dp , bp ) ) ; let N0 = vec . tangent ( C0 , vec . lrp ( E0 , E0p , 0.5 ) ) , N0p = vec . mul ( N0 , - 1 ) , N1 = vec . tangent ( vec . lrp ( E1 , E1p , 0.5 ) , C1 ) , N1p = vec . mul ( N1 , - 1 ) ; if ( getSweep ( C0 , E0 , E0p ) > 0 ) { ; [ N0 , N0p ] = [ N0p , N0 ] ; } if ( getSweep ( C1 , E1 , E1p ) > 0 ) { ; [ N1 , N1p ] = [ N1p , N1 ] ; } return { C0 , r0 , C1 , r1 , E0 , E0p , E1 , E1p , F0 , F0p , F1 , F1p , N0 , N0p , N1 , N1p , D , Dp , D1 : projectPoint ( D , vec . angle ( D , E0 ) , vec . dist ( D , E0 ) * 2 ) , Dp1 : projectPoint ( Dp , vec . angle ( Dp , E0p ) , vec . dist ( Dp , E0p ) * 2 ) , D2 : projectPoint ( D , vec . angle ( D , E1 ) , vec . dist ( D , E1 ) * 2 ) , Dp2 : projectPoint ( Dp , vec . angle ( Dp , E1p ) , vec . dist ( Dp , E1p ) * 2 ) , } ; } export function deepCompareArrays < T > ( a , b ) { if ( a ?. length !== b ?. length ) return false ; return deepCompare ( a , b ) ; } export function deepCompare < T > ( a , b ) { return a === b || JSON . stringify ( a ) === JSON . stringify ( b ) ; } param x0 param y0 param r0 param x1 param y1 param r1 returns export function getOuterTangents ( C0 , r0 , C1 , r1 ) { const a0 = vec . angle ( C0 , C1 ) ; const d = vec . dist ( C0 , C1 ) ; if ( d < Math . abs ( r1 - r0 ) ) return ; const a1 = Math . acos ( ( r0 - r1 ) / d ) , t0 = a0 + a1 , t1 = a0 - a1 ; return [ [ C0 [ 0 ] + r0 * Math . cos ( t1 ) , C0 [ 1 ] + r0 * Math . sin ( t1 ) ] , [ C1 [ 0 ] + r1 * Math . cos ( t1 ) , C1 [ 1 ] + r1 * Math . sin ( t1 ) ] , [ C0 [ 0 ] + r0 * Math . cos ( t0 ) , C0 [ 1 ] + r0 * Math . sin ( t0 ) ] , [ C1 [ 0 ] + r1 * Math . cos ( t0 ) , C1 [ 1 ] + r1 * Math . sin ( t0 ) ] , ] ; } export function arrsIntersect < T , K > ( a , b , fn ? ) ; export function arrsIntersect < T > ( a , b ) ; export function arrsIntersect < T > ( a , b , fn ? ) { return a . some ( ( item ) => b . includes ( fn ? fn ( item ) : item ) ) ; } export function getTouchDisplay ( ) { return ( "ontouchstart" in window || navigator . maxTouchPoints > 0 || navigator . msMaxTouchPoints > 0 ) ; } const rounds = [ 1 , 10 , 100 , 1000 ] ; export function round ( n , p = 2 ) { return Math . floor ( n * rounds [ p ] ) / rounds [ p ] ; } param y1 param y2 param mu export function lerp ( y1 , y2 , mu ) { mu = clamp ( mu , 0 , 1 ) ; return y1 * ( 1 - mu ) + y2 * mu ; } param value param rangeA param rangeB param clamp export function modulate ( value , rangeA , rangeB , clamp = false ) { const [ fromLow , fromHigh ] = rangeA ; const [ v0 , v1 ] = rangeB ; const result = v0 + ( ( value - fromLow ) / ( fromHigh - fromLow ) ) * ( v1 - v0 ) ; return clamp ? v0 < v1 ? Math . max ( Math . min ( result , v1 ) , v0 ) : Math . max ( Math . min ( result , v0 ) , v1 ) : result ; } param n param min export function clamp ( n , min ) ; export function clamp ( n , min , max ) ; export function clamp ( n , min , max ? ) { return Math . max ( min , typeof max !== "undefined" ? Math . min ( n , max ) : n ) ; } export function computePointOnCurve ( t , points ) { if ( t === 0 ) { return points [ 0 ] ; } const order = points . length - 1 ; if ( t === 1 ) { return points [ order ] ; } const mt = 1 - t ; let p = points ; if ( order === 0 ) { return points [ 0 ] ; } if ( order === 1 ) { return [ mt * p [ 0 ] [ 0 ] + t * p [ 1 ] [ 0 ] , mt * p [ 0 ] [ 1 ] + t * p [ 1 ] [ 1 ] ] ; } if ( order < 4 ) { const mt2 = mt * mt , t2 = t * t ; let a , b , c , d = 0 ; if ( order === 2 ) { p = [ p [ 0 ] , p [ 1 ] , p [ 2 ] , [ 0 , 0 ] ] ; a = mt2 ; b = mt * t * 2 ; c = t2 ; } else if ( order === 3 ) { a = mt2 * mt ; b = mt2 * t * 3 ; c = mt * t2 * 3 ; d = t * t2 ; } return [ a * p [ 0 ] [ 0 ] + b * p [ 1 ] [ 0 ] + c * p [ 2 ] [ 0 ] + d * p [ 3 ] [ 0 ] , a * p [ 0 ] [ 1 ] + b * p [ 1 ] [ 1 ] + c * p [ 2 ] [ 1 ] + d * p [ 3 ] [ 1 ] , ] ; } } function distance2 ( p , point ) { const dx = p . x - point [ 0 ] , dy = p . y - point [ 1 ] ; return dx * dx + dy * dy ; } param pathNode param point returns export function getClosestPointOnPath ( pathNode , point ) { const pathLen = pathNode . getTotalLength ( ) ; let p = 8 , best , bestLen , bestDist = Infinity , bl , al ; for ( let scan , scanLen = 0 , scanDist ; scanLen <= pathLen ; scanLen += p ) { if ( ( scanDist = distance2 ( ( scan = pathNode . getPointAtLength ( scanLen ) ) , point ) ) < bestDist ) { ; ( best = scan ) , ( bestLen = scanLen ) , ( bestDist = scanDist ) ; } } p /= 2 ; while ( p > 0.5 ) { let before , after , bd , ad ; if ( ( bl = bestLen - p ) >= 0 && ( bd = distance2 ( ( before = pathNode . getPointAtLength ( bl ) ) , point ) ) < bestDist ) { ; ( best = before ) , ( bestLen = bl ) , ( bestDist = bd ) ; } else if ( ( al = bestLen + p ) <= pathLen && ( ad = distance2 ( ( after = pathNode . getPointAtLength ( al ) ) , point ) ) < bestDist ) { ; ( best = after ) , ( bestLen = al ) , ( bestDist = ad ) ; } else { p /= 2 ; } } return { point : [ best . x , best . y ] , distance : bestDist , length : ( bl + al ) / 2 , t : ( bl + al ) / 2 / pathLen , } ; } export function det ( a , b , c , d , e , f , g , h , i ) { return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g ; } param p0 param p1 param center returns export function circleFromThreePoints ( A , B , C ) { const a = det ( A [ 0 ] , A [ 1 ] , 1 , B [ 0 ] , B [ 1 ] , 1 , C [ 0 ] , C [ 1 ] , 1 ) ; const bx = - det ( A [ 0 ] * A [ 0 ] + A [ 1 ] * A [ 1 ] , A [ 1 ] , 1 , B [ 0 ] * B [ 0 ] + B [ 1 ] * B [ 1 ] , B [ 1 ] , 1 , C [ 0 ] * C [ 0 ] + C [ 1 ] * C [ 1 ] , C [ 1 ] , 1 ) ; const by = det ( A [ 0 ] * A [ 0 ] + A [ 1 ] * A [ 1 ] , A [ 0 ] , 1 , B [ 0 ] * B [ 0 ] + B [ 1 ] * B [ 1 ] , B [ 0 ] , 1 , C [ 0 ] * C [ 0 ] + C [ 1 ] * C [ 1 ] , C [ 0 ] , 1 ) ; const c = - det ( A [ 0 ] * A [ 0 ] + A [ 1 ] * A [ 1 ] , A [ 0 ] , A [ 1 ] , B [ 0 ] * B [ 0 ] + B [ 1 ] * B [ 1 ] , B [ 0 ] , B [ 1 ] , C [ 0 ] * C [ 0 ] + C [ 1 ] * C [ 1 ] , C [ 0 ] , C [ 1 ] ) ; return [ - bx / ( 2 * a ) , - by / ( 2 * a ) , Math . sqrt ( bx * bx + by * by - 4 * a * c ) / ( 2 * Math . abs ( a ) ) , ] ; } export function throttle < P extends any [ ] , T extends ( ... args : P ) => any > ( fn , wait , preventDefault ? ) { let inThrottle , lastFn , lastTime ; return function ( ... args ) { if ( preventDefault ) args [ 0 ] . preventDefault ( ) ; const context = this ; if ( ! inThrottle ) { fn . apply ( context , args ) ; lastTime = Date . now ( ) ; inThrottle = true ; } else { clearTimeout ( lastFn ) ; lastFn = setTimeout ( function ( ) { if ( Date . now ( ) - lastTime >= wait ) { fn . apply ( context , args ) ; lastTime = Date . now ( ) ; } } , Math . max ( wait - ( Date . now ( ) - lastTime ) , 0 ) ) ; } } ; } export function getSnapshots ( nodes , bounds ) { const acc = { } as Record < string , INodeSnapshot > ; for ( const node of nodes ) { const { radius , point : [ x , y ] , } = node ; acc [ node . id ] = { id : node . id , x : x , y : y , nx : ( x - bounds . minX ) / bounds . width , ny : ( y - bounds . minY ) / bounds . height , nmx : 1 - ( x - bounds . minX ) / bounds . width , nmy : 1 - ( y - bounds . minY ) / bounds . height , nw : radius / bounds . width , nh : radius / bounds . height , radius , } ; } return acc ; } export function getSnapglobs ( globs , bounds ) { return Object . fromEntries ( globs . map ( ( glob ) => { const { D : [ dx , dy ] , Dp : [ dpx , dpy ] , } = glob ; return [ glob . id , { D : { x : dx , y : dy , nx : ( dx - bounds . minX ) / bounds . width , ny : ( dy - bounds . minY ) / bounds . height , nmx : 1 - ( dx - bounds . minX ) / bounds . width , nmy : 1 - ( dy - bounds . minY ) / bounds . height , } , Dp : { x : dpx , y : dpy , nx : ( dpx - bounds . minX ) / bounds . width , ny : ( dpy - bounds . minY ) / bounds . height , nmx : 1 - ( dpx - bounds . minX ) / bounds . width , nmy : 1 - ( dpy - bounds . minY ) / bounds . height , } , a : glob . a , ap : glob . ap , b : glob . b , bp : glob . bp , } , ] ; } ) ) ; } export function getEdgeTransform ( initialNodes , initialGlobs , bounds , edge ) { if ( initialNodes . length === 0 && initialGlobs . length === 0 ) { throw Error ( "Must have at least one thing!" ) ; } if ( ! bounds ) { throw Error ( "Must have bounds!" ) ; } const snapshots = getSnapshots ( initialNodes , bounds ) ; const snapglobs = getSnapglobs ( initialGlobs , bounds ) ; let { minX : x0 , minY : y0 , maxX : x1 , maxY : y1 } = bounds ; let { maxX : mx , maxY : my , width : mw , height : mh } = bounds ; return function edgeResize ( point , nodes , globs , preserveRadii = false ) { const [ x , y ] = point ; if ( edge === 0 || edge === 2 ) { edge === 0 ? ( y0 = y ) : ( y1 = y ) ; my = y0 < y1 ? y0 : y1 ; mh = Math . abs ( y1 - y0 ) ; for ( const node of nodes ) { const { ny , nmy , nw , nh } = snapshots [ node . id ] ; node . point [ 1 ] = round ( my + ( y1 < y0 ? nmy : ny ) * mh ) ; if ( ! preserveRadii ) { node . radius = ( nw * mw + nh * mh ) / 2 ; } else { node . radius = snapshots [ node . id ] . radius ; } } for ( const glob of globs ) { const { D , Dp , a , b , ap , bp } = snapglobs [ glob . id ] ; if ( y1 < y0 ) { Object . assign ( glob , { D : [ Dp . x , my + Dp . nmy * mh ] , Dp : [ D . x , my + D . nmy * mh ] , a : ap , ap : a , b : bp , bp : b , } ) ; } else { Object . assign ( glob , { D : [ D . x , my + D . ny * mh ] , Dp : [ Dp . x , my + Dp . ny * mh ] , a , ap , b , bp , } ) ; } } } else { edge === 1 ? ( x1 = x ) : ( x0 = x ) ; mx = x0 < x1 ? x0 : x1 ; mw = Math . abs ( x1 - x0 ) ; for ( const node of nodes ) { const { nx , nmx , nw , nh } = snapshots [ node . id ] ; node . point [ 0 ] = round ( mx + ( x1 < x0 ? nmx : nx ) * mw ) ; if ( ! preserveRadii ) { node . radius = ( nw * mw + nh * mh ) / 2 ; } else { node . radius = snapshots [ node . id ] . radius ; } } for ( const glob of globs ) { const { D , Dp , a , b , ap , bp } = snapglobs [ glob . id ] ; if ( x1 < x0 ) { Object . assign ( glob , { D : [ mx + Dp . nmx * mw , Dp . y ] , Dp : [ mx + D . nmx * mw , D . y ] , a : ap , ap : a , b : bp , bp : b , } ) ; } else { Object . assign ( glob , { D : [ mx + D . nx * mw , D . y ] , Dp : [ mx + Dp . nx * mw , Dp . y ] , a , ap , b , bp , } ) ; } } } } ; } param boxes param corner example export function getCornerTransform ( initialNodes , initialGlobs , bounds , corner ) { if ( initialNodes . length === 0 && initialGlobs . length === 0 ) { throw Error ( "Must have at least one thing!" ) ; } if ( ! bounds ) { throw Error ( "Must have bounds!" ) ; } const snapshots = getSnapshots ( initialNodes , bounds ) ; const snapglobs = getSnapglobs ( initialGlobs , bounds ) ; let { minX : x0 , minY : y0 , maxX : x1 , maxY : y1 } = bounds ; let { maxX : mx , maxY : my , width : mw , height : mh } = bounds ; return function cornerTransform ( point , nodes , globs , preserveRadii = false ) { const [ x , y ] = point ; corner < 2 ? ( y0 = y ) : ( y1 = y ) ; my = y0 < y1 ? y0 : y1 ; mh = Math . abs ( y1 - y0 ) ; corner === 1 || corner === 2 ? ( x1 = x ) : ( x0 = x ) ; mx = x0 < x1 ? x0 : x1 ; mw = Math . abs ( x1 - x0 ) ; for ( const node of nodes ) { const { nx , nmx , ny , nmy , nw , nh } = snapshots [ node . id ] ; node . point = vec . round ( [ mx + ( x1 < x0 ? nmx : nx ) * mw , my + ( y1 < y0 ? nmy : ny ) * mh , ] ) ; if ( ! preserveRadii ) { node . radius = ( nw * mw + nh * mh ) / 2 ; } else { node . radius = snapshots [ node . id ] . radius ; } } for ( const glob of globs ) { const { D , Dp , a , ap , b , bp } = snapglobs [ glob . id ] ; Object . assign ( glob , { a : a , ap : ap , b : b , bp : bp , } ) ; if ( x1 < x0 && y1 < y0 ) { Object . assign ( glob , { D : [ mx + D . nmx * mw , my + D . nmy * mh ] , Dp : [ mx + Dp . nmx * mw , my + Dp . nmy * mh ] , a , ap , b , bp , } ) ; } else if ( x1 < x0 ) { Object . assign ( glob , { D : [ mx + Dp . nmx * mw , my + Dp . ny * mh ] , Dp : [ mx + D . nmx * mw , my + D . ny * mh ] , a : ap , ap : a , b : bp , bp : b , } ) ; } else if ( y1 < y0 ) { Object . assign ( glob , { D : [ mx + Dp . nx * mw , my + Dp . nmy * mh ] , Dp : [ mx + D . nx * mw , my + D . nmy * mh ] , a : ap , ap : a , b : bp , bp : b , } ) ; } else { Object . assign ( glob , { D : [ mx + D . nx * mw , my + D . ny * mh ] , Dp : [ mx + Dp . nx * mw , my + Dp . ny * mh ] , a , ap , b , bp , } ) ; } } } ; } export function getCornerRotater ( initialNodes , initialGlobs , point , bounds ) { const center = [ bounds . minX + bounds . width / 2 , bounds . minY + bounds . height / 2 , ] ; const snapshots = getSnapshots ( initialNodes , bounds ) ; const snapglobs = getSnapglobs ( initialGlobs , bounds ) ; const angle = vec . angle ( point , center ) ; return function cornerRotate ( point , nodes , globs ) { const delta = angleDelta ( angle , vec . angle ( point , center ) ) ; for ( const node of nodes ) { const snap = snapshots [ node . id ] ; node . point = rotatePoint ( [ snap . x , snap . y ] , center , delta ) ; } for ( const glob of globs ) { const snap = snapglobs [ glob . id ] ; glob . D = rotatePoint ( [ snap . D . x , snap . D . y ] , center , delta ) ; glob . Dp = rotatePoint ( [ snap . Dp . x , snap . Dp . y ] , center , delta ) ; } } ; } export type EdgeTransform = ReturnType < typeof getEdgeTransform > ; export type CornerTransform = ReturnType < typeof getCornerTransform > ; export type CornerRotater = ReturnType < typeof getCornerRotater > ; export function getGlobOutline ( { C0 , r0 , C1 , r1 , E0 , E1 , F0 , F1 , E0p , E1p , F0p , F1p } , startCap = "round" , endCap = "round" ) { return [ svg . moveTo ( E0 ) , startCap === "round" ? svg . arcTo ( C0 , r0 , E0 , E0p ) : svg . lineTo ( E0p ) , svg . bezierTo ( F0p , F1p , E1p ) , endCap === "round" ? svg . arcTo ( C1 , r1 , E1p , E1 ) : svg . lineTo ( E1 ) , svg . bezierTo ( F1 , F0 , E0 ) , svg . closePath ( ) , ] . join ( " " ) ; } export function pointInCircle ( A , C , r ) { return vec . dist ( A , C ) <= r ; } export function sign ( value ) { return value < 0 ? - 1 : 1 ; } export function getBoundsBoundsIntersection ( a , b ) { const dx = a . minX - b . minX ; const px = b . minX + b . width / 2 + a . minX + a . width / 2 - Math . abs ( dx ) ; if ( px <= 0 ) return null ; const dy = a . minY - b . minY ; const py = b . minY + b . height / 2 + a . minY + a . height / 2 - Math . abs ( dy ) ; if ( py <= 0 ) return null ; return true ; const hit = { } as { delta : number [ ] ; normal : number [ ] ; point : number [ ] ; } ; if ( px < py ) { const sx = sign ( dx ) ; hit . delta [ 0 ] = px * sx ; hit . normal [ 0 ] = sx ; hit . point [ 0 ] = a . minX + ( a . width / 2 ) * sx ; hit . point [ 1 ] = b . minY ; } else { const sy = sign ( dy ) ; hit . delta [ 1 ] = py * sy ; hit . normal [ 1 ] = sy ; hit . point [ 0 ] = b . minX ; hit . point [ 1 ] = a . minY + ( a . height / 2 ) * sy ; } } export function getCircleBoundsIntersection ( C , r , b ) { return Intersection . intersect ( ShapeInfo . rectangle ( { top : b . minY , left : b . minX , width : b . width , height : b . height , } ) , ShapeInfo . circle ( C [ 0 ] , C [ 1 ] , r ) ) ; } export function getBezierCircleIntersections ( p1 , p2 , p3 , p4 , center , radius ) { return Intersection . intersect ( ShapeInfo . cubicBezier ( { p1 , p2 , p3 , p4 } ) , ShapeInfo . circle ( { center , radius } ) ) ; } export function getBezierLineSegmentIntersections ( p1 , p2 , p3 , p4 , start , end ) { return Intersection . intersect ( ShapeInfo . cubicBezier ( { p1 , p2 , p3 , p4 } ) , ShapeInfo . line ( { p1 : start , p2 : end } ) ) ; } export function getLineLineIntersection ( A , B , C , D ) { const int = Intersection . intersect ( ShapeInfo . line ( { p1 : A , p2 : B } ) , ShapeInfo . line ( { p1 : C , p2 : D } ) ) ; if ( ! int . points [ 0 ] ) return undefined ; return [ int . points [ 0 ] . x , int . points [ 0 ] . y ] ; } param point param glob returns export function getCircleInGlob ( point , glob ) { if ( glob . points === null ) return false ; const { E0 , E0p , E1 , E1p , F0 , F1 , F0p , F1p } = glob . points ; const P = getClosestPointOnCurve ( point , E0 , F0 , F1 , E1 ) ; const Pp = getClosestPointOnCurve ( point , E0p , F0p , F1p , E1p ) ; const N = getNormalOnCurve ( E0 , F0 , F1 , E1 , P . t ) ; const Np = getNormalOnCurve ( E0p , F0p , F1p , E1p , Pp . t ) ; const center = vec . med ( N , Np ) ; const intA = getLineLineIntersection ( vec . sub ( P . point , vec . mul ( N , 1000000 ) ) , vec . add ( P . point , vec . mul ( N , 1000000 ) ) , vec . sub ( Pp . point , vec . mul ( Np , 1000000 ) ) , vec . add ( Pp . point , vec . mul ( Np , 1000000 ) ) ) ; let C , r ; if ( ! intA ) { C = vec . med ( P . point , Pp . point ) ; r = vec . dist ( P . point , Pp . point ) / 2 ; } else { C = getLineLineIntersection ( vec . sub ( P . point , vec . mul ( vec . per ( N ) , 10000000 ) ) , vec . add ( P . point , vec . mul ( vec . per ( N ) , 10000000 ) ) , vec . sub ( intA , vec . mul ( center , 10000000 ) ) , vec . add ( intA , vec . mul ( center , 10000000 ) ) ) ; if ( ! C ) return false ; r = vec . dist ( P . point , C ) ; } if ( vec . dist ( point , C ) > 16 ) { return false ; } return { point : C , radius : r } ; } export function pointInRect ( point , minX , minY , maxX , maxY ) { return ! ( point [ 0 ] < minX || point [ 0 ] > maxX || point [ 1 ] < minY || point [ 1 ] > maxY ) ; } export function getNodeTransform ( node , point ) { const iPoint = [ ... node . point ] ; const iRadius = node . radius ; const iDist = vec . dist ( node . point , point ) ; return function resize ( point , absolute ) { const dist = vec . dist ( iPoint , point ) ; return absolute ? dist : iRadius + ( dist - iDist ) ; } ; } export type NodeTransform = ReturnType < typeof getNodeTransform > ; export function getNewGlob ( A , B ) { const { point : C0 , radius : r0 } = A ; const { point : C1 , radius : r1 } = B ; const [ E0 , E1 , E0p , E1p ] = getOuterTangents ( C0 , r0 , C1 , r1 ) ; const D = vec . med ( E0 , E1 ) , Dp = vec . med ( E0p , E1p ) , a = 0.5 , b = 0.5 , ap = 0.5 , bp = 0.5 ; const id = "glob_" + Math . random ( ) * Date . now ( ) ; return { id , name : "Glob" , type : ICanvasItems . Glob , nodes : [ A . id , B . id ] , D , Dp , a , b , ap , bp , points : getGlob ( C0 , r0 , C1 , r1 , D , Dp , a , b , ap , bp ) , parentId : "0" , childIndex : 1 , locked : false , } ; } export function getGlobClone ( glob ) { const id = "glob_c_" + Math . random ( ) * Date . now ( ) ; const { D , Dp , nodes } = glob ; return { ... glob , id , nodes : [ ... nodes ] , D : [ ... D ] , Dp : [ ... Dp ] , childIndex : glob . childIndex + 1 , } ; } export function getNewNode ( point , radius = 25 ) { const id = "node_" + Math . random ( ) * Date . now ( ) ; return { id , name : "Node" , point , type : ICanvasItems . Node , radius , cap : "round" , childIndex : 1 , parentId : "0" , locked : false , } ; } export function getNodeClone ( node ) { const id = "node_c_" + Math . random ( ) * Date . now ( ) ; return { ... node , id , point : [ ... node . point ] , childIndex : node . childIndex + 1 , } ; } export function screenToWorld ( point , camera ) { return vec . add ( vec . div ( point , camera . zoom ) , camera . point ) ; } export function getAllSelectedBoundingBox ( data ) { const { nodes , globs } = data ; const allGlobs = Object . values ( globs ) ; const allNodes = Object . values ( nodes ) ; if ( allNodes . length + allGlobs . length === 0 ) return ; return getCommonBounds ( ... allGlobs . filter ( ( glob ) => glob . points !== null ) . map ( ( glob ) => getGlobBounds ( glob , nodes [ glob . nodes [ 0 ] ] , nodes [ glob . nodes [ 1 ] ] ) ) , ... allNodes . map ( getNodeBounds ) ) ; } export function getSelectedBoundingBox ( data ) { const { selectedGlobs , selectedNodes , nodes , globs } = data ; if ( selectedGlobs . length + selectedNodes . length === 0 ) return null ; return getCommonBounds ( ... selectedGlobs . map ( ( id ) => globs [ id ] ) . filter ( ( glob ) => glob . points !== null ) . map ( ( glob ) => getGlobBounds ( glob , nodes [ glob . nodes [ 0 ] ] , nodes [ glob . nodes [ 1 ] ] ) ) , ... selectedNodes . map ( ( id ) => getNodeBounds ( nodes [ id ] ) ) ) ; } export function getResizedBounds ( nodes , globs , bounds , pointDelta , sizeDelta , resizeRadius ) { const snapshots = getSnapshots ( nodes , bounds ) ; const snapglobs = getSnapglobs ( globs , bounds ) ; const { minX : x0 , minY : y0 } = bounds ; let { maxX : x1 , maxY : y1 } = bounds ; let { maxX : mx , maxY : my , width : mw , height : mh } = bounds ; const [ x , y ] = [ bounds . minX + bounds . width + sizeDelta [ 0 ] , bounds . minY + bounds . height + sizeDelta [ 1 ] , ] ; y1 = y ; my = y0 ; mh = Math . abs ( y1 - y0 ) ; x1 = x ; mx = x0 ; mw = Math . abs ( x1 - x0 ) ; for ( const node of nodes ) { const { nx , ny , nw , nh } = snapshots [ node . id ] ; node . point = vec . round ( vec . add ( [ mx + nx * mw , my + ny * mh ] , pointDelta ) ) ; if ( resizeRadius ) { node . radius = ( nw * mw + nh * mh ) / 2 ; } } for ( const glob of globs ) { const { D , Dp , a , ap , b , bp } = snapglobs [ glob . id ] ; Object . assign ( glob , { a : a , ap : ap , b : b , bp : bp , } ) ; Object . assign ( glob , { D : [ mx + D . nx * mw , my + D . ny * mh ] , Dp : [ mx + Dp . nx * mw , my + Dp . ny * mh ] , a , ap , b , bp , } ) ; } } export function getSafeHandlePoint ( start , end , handle ) { const { point : C0 , radius : r0 } = start ; const { point : C1 , radius : r1 } = end ; if ( vec . dist ( handle , C0 ) < r0 + 1 ) { handle = getClosestPointOnCircle ( C0 , r0 , handle , 1 ) ; } if ( vec . dist ( handle , C1 ) < r1 + 1 ) { handle = getClosestPointOnCircle ( C1 , r1 , handle , 1 ) ; } return handle ; } export function isInView ( point , document ) { return ! ( point [ 0 ] < document . point [ 0 ] || point [ 0 ] > document . point [ 0 ] + document . size [ 0 ] || point [ 1 ] < document . point [ 1 ] || point [ 1 ] > document . point [ 1 ] + document . size [ 1 ] ) ; } export function saveSelectionState ( data ) { const sHighlightedNodes = [ ... data . highlightNodes ] ; const sHoveredNodes = [ ... data . hoveredNodes ] ; const sSelectedNodes = [ ... data . selectedNodes ] ; const sHighlightedGlobs = [ ... data . highlightGlobs ] ; const sHoveredGlobs = [ ... data . hoveredGlobs ] ; const sSelectedGlobs = [ ... data . selectedGlobs ] ; return function restore ( data ) { data . highlightNodes = sHighlightedNodes ; data . hoveredNodes = sHoveredNodes ; data . selectedNodes = sSelectedNodes ; data . highlightGlobs = sHighlightedGlobs ; data . hoveredGlobs = sHoveredGlobs ; data . selectedGlobs = sSelectedGlobs ; } ; } export function getSelectionSnapshot ( data ) { const nodesToSnapshot = new Set ( data . selectedNodes ) ; for ( const globId of data . selectedGlobs ) { nodesToSnapshot . add ( data . globs [ globId ] . nodes [ 0 ] ) ; nodesToSnapshot . add ( data . globs [ globId ] . nodes [ 1 ] ) ; } const nodes = Object . fromEntries ( Array . from ( nodesToSnapshot . values ( ) ) . map ( ( id ) => { const { radius , point } = data . nodes [ id ] ; return [ id , { id , point : [ ... point ] , radius } ] ; } ) ) ; const globs = Object . fromEntries ( data . selectedGlobs . map ( ( id ) => { const { D , Dp , a , b , ap , bp } = data . globs [ id ] ; return [ id , { id , D : [ ... D ] , Dp : [ ... Dp ] , a , b , ap , bp , } , ] ; } ) ) ; return { nodes , globs , selectedNodes : [ ... data . selectedNodes ] , selectedGlobs : [ ... data . selectedGlobs ] , hoveredNodes : [ ... data . hoveredNodes ] , hoveredGlobs : [ ... data . hoveredGlobs ] , } ; } export function updateGlobPoints ( data ) { const { globs , globIds , nodes , selectedNodes , selectedGlobs } = data ; const nodesToUpdate = new Set ( selectedNodes ) ; const globsToUpdate = new Set ( selectedGlobs . map ( ( id ) => globs [ id ] ) ) ; const sGlobs = globIds . map ( ( id ) => globs [ id ] ) ; for ( const glob of sGlobs ) { nodesToUpdate . add ( glob . nodes [ 0 ] ) ; nodesToUpdate . add ( glob . nodes [ 1 ] ) ; } for ( const glob of sGlobs ) { if ( nodesToUpdate . has ( glob . nodes [ 0 ] ) || nodesToUpdate . has ( glob . nodes [ 1 ] ) ) { globsToUpdate . add ( glob ) ; } } globsToUpdate . forEach ( ( glob ) => { const [ start , end ] = glob . nodes . map ( ( id ) => nodes [ id ] ) ; try { glob . points = getGlobPoints ( glob , start , end ) ; } catch ( e ) { glob . points = null ; } } ) ; } export function worldToScreen ( point , offset , zoom ) { return vec . mul ( vec . sub ( point , offset ) , zoom ) ; } export function getNodeAdjacentHandleSnapshot ( data ) { const selectedNodeIds = new Set ( data . selectedNodes ) ; const globHandleSnapshots = { } ; for ( const globId in data . globs ) { const glob = data . globs [ globId ] ; const hasStart = selectedNodeIds . has ( glob . nodes [ 0 ] ) ; const hasEnd = selectedNodeIds . has ( glob . nodes [ 1 ] ) ; if ( hasStart || hasEnd ) { globHandleSnapshots [ glob . id ] = { D : [ ... glob . D ] , Dp : [ ... glob . Dp ] , E0 : [ ... glob . points . E0 ] , E1 : [ ... glob . points . E1 ] , E0p : [ ... glob . points . E0p ] , E1p : [ ... glob . points . E1p ] , cw : vec . clockwise ( glob . points . E0 , glob . D , glob . points . E1 ) , cwp : vec . clockwise ( glob . points . E0p , glob . Dp , glob . points . E1p ) , type : hasStart && hasEnd ? "both" : hasStart ? "start" : "end" , } ; } } return globHandleSnapshots ; } param p0 param n0 param p1 param n1 returns export function getRayRayIntersection ( p0 , n0 , p1 , n1 ) { const p0e = vec . add ( p0 , n0 ) , p1e = vec . add ( p1 , n1 ) , m0 = ( p0e [ 1 ] - p0 [ 1 ] ) / ( p0e [ 0 ] - p0 [ 0 ] ) , m1 = ( p1e [ 1 ] - p1 [ 1 ] ) / ( p1e [ 0 ] - p1 [ 0 ] ) , b0 = p0 [ 1 ] - m0 * p0 [ 0 ] , b1 = p1 [ 1 ] - m1 * p1 [ 0 ] , x = ( b1 - b0 ) / ( m0 - m1 ) , y = m0 * x + b0 ; return [ x , y ] ; } export async function postJsonToEndpoint ( endpoint , data ) { const d = await fetch ( `${ process . env . NEXT_PUBLIC_BASE_API_URL }/api/${ endpoint }` , { method : "POST" , headers : { "Content-Type" : "application/json" } , body : JSON . stringify ( data ) , } ) ; return await d . json ( ) ; }