import { Rect , RefLineMeta , MatchedLine , LineType , LineGroup , RefLinePosition , Delta , AdsorbLine , AdsorbVLine , AdsorbHLine , } from "./types" ; import { toNumber as fixNumber , getRectRefLines , getBoundingRect , groupBy , find , getLineTypeFromPosition , mergeRefLineMeta , getMatchedLine , coordinateRotation , isNil , } from "./utils" ; export const version = "%VERSION%" ; export * from "./types" ; export { fixNumber , coordinateRotation , getBoundingRect , getRectRefLines , mergeRefLineMeta } ; export interface RefLineOpts < T extends Rect = Rect > { rects : T [ ] ; current ? : T | string ; lineFilter ? : ( line : RefLineMeta ) => boolean ; adsorbVLines ? : Omit < AdsorbLine , "type" > [ ] ; adsorbHLines ? : Omit < AdsorbLine , "type" > [ ] ; } export class RefLine < T extends Rect = Rect > { protected opts : Omit < RefLineOpts < T > , "rects" | "current" > = { } ; private seq : number = 1 ; protected __rects : T [ ] = [ ] ; protected _rects : T [ ] = [ ] ; protected current : null | T = null ; protected _dirty : boolean = true ; protected _vLines : LineGroup < T > [ ] = [ ] ; protected _hLines : LineGroup < T > [ ] = [ ] ; protected _vLineMap : Map < string , RefLineMeta < T > [ ] > = new Map ( ) ; protected _hLineMap : Map < string , RefLineMeta < T > [ ] > = new Map ( ) ; protected _adsorbVLines : AdsorbVLine [ ] = [ ] ; protected _adsorbHLines : AdsorbHLine [ ] = [ ] ; protected _lineFilter : ( ( line : RefLineMeta ) => boolean ) | null = null ; get rects ( ) { if ( this . _dirty ) { this . initRefLines ( ) ; } return this . _rects ; } set rects ( rects ) { this . __rects = rects ; this . _dirty = true ; } get vLines ( ) { if ( this . _dirty ) { this . initRefLines ( ) ; } return this . _vLines ; } get hLines ( ) { if ( this . _dirty ) { this . initRefLines ( ) ; } return this . _hLines ; } get vLineMap ( ) { if ( this . _dirty ) { this . initRefLines ( ) ; } return this . _vLineMap ; } get hLineMap ( ) { if ( this . _dirty ) { this . initRefLines ( ) ; } return this . _hLineMap ; } get adsorbVLines ( ) { return this . _adsorbHLines ; } set adsorbVLines ( lines ) { this . _adsorbVLines = lines ; this . _dirty = true ; } get adsorbHLines ( ) { return this . _adsorbHLines ; } set adsorbHLines ( lines ) { this . _adsorbHLines = lines ; this . _dirty = true ; } constructor ( opts ? ) { this . opts = opts || { } ; this . __rects = opts ?. rects || [ ] ; this . _lineFilter = opts ?. lineFilter || null ; this . _adsorbVLines = opts ?. adsorbVLines || [ ] ; this . _adsorbHLines = opts ?. adsorbHLines || [ ] ; if ( opts ?. current ) { this . setCurrent ( opts . current ) ; } this . _dirty = true ; } getRectByKey ( key ) { return find ( this . __rects , rect => rect . key === key ) ; } getOffsetRefLineMetaList ( type , offset ) { offset = fixNumber ( offset , 0 ) ; const lineMap = type === "vertical" ? this . vLineMap : this . hLineMap ; return lineMap . get ( this . toLineMapKey ( offset ) ) || [ ] ; } deprecated param rects setRects ( rects ) { this . __rects = rects ; this . _dirty = true ; } protected getRect ( rect ) { let current ; if ( typeof rect === "string" ) { current = this . getRectByKey ( rect ) ; } else { current = rect ; } return current ; } setCurrent ( current ) { const old = this . getCurrent ( ) ; this . current = current ? this . getRect ( current ) : null ; if ( old ?. key !== this . current ?. key ) { this . _dirty = true ; } } getCurrent ( ) { return this . current ; } setLineFilter ( filter ) { this . _lineFilter = filter ; this . _dirty = true ; } getLineFilter ( ) { return this . _lineFilter ; } protected toLineMapKey < S > ( v ) { return v + "" ; } protected getLineMapKey ( line ) { return this . toLineMapKey ( fixNumber ( line . offset , 0 ) ) ; } protected isEnableLine ( line ) { if ( line . adsorbOnly ) return true ; const filter = this . getLineFilter ( ) ; if ( filter ) { return filter ( line ) ; } return true ; } protected getRectRefLines ( rect ) { let lines = getRectRefLines ( rect ) ; if ( this . getLineFilter ( ) ) { lines = lines . filter ( line => this . isEnableLine ( line ) ) ; } return lines ; } protected initRefLines ( ) { const current = this . getCurrent ( ) ; let vLines = [ ] ; let hLines = [ ] ; this . _rects = current ? this . __rects . filter ( rect => rect . key !== current . key ) : [ ... this . __rects ] ; this . _rects . forEach ( rect => { const lines = this . getRectRefLines ( rect ) ; lines . forEach ( line => { const mKey = this . getLineMapKey ( line ) ; if ( line . type === "vertical" ) { vLines . push ( line ) ; const matched = this . _vLineMap . get ( mKey ) || [ ] ; matched . push ( line ) ; this . _vLineMap . set ( mKey , matched ) ; } else { hLines . push ( line ) ; const matched = this . _hLineMap . get ( mKey ) || [ ] ; matched . push ( line ) ; this . _hLineMap . set ( mKey , matched ) ; } } ) ; } ) ; this . _adsorbVLines . forEach ( line => { const refLineMeta = { type : "vertical" , position : "vl" , offset : line . offset , start : line . offset , end : 0 , rect : { key : "v_" + line . key , width : 0 , height : 0 , left : line . offset , top : 0 , } as T , adsorbOnly : true , } ; vLines . push ( refLineMeta ) ; const mKey = this . getLineMapKey ( line ) ; const matched = this . _vLineMap . get ( mKey ) || [ ] ; matched . push ( refLineMeta ) ; this . _vLineMap . set ( mKey , matched ) ; } ) ; this . _adsorbHLines . forEach ( line => { const refLineMeta = { type : "horizontal" , position : "ht" , offset : line . offset , start : line . offset , end : 0 , rect : { key : "h_" + line . key , width : 0 , height : 0 , left : 0 , top : line . offset , } as T , adsorbOnly : true , } ; hLines . push ( refLineMeta ) ; const mKey = this . getLineMapKey ( line ) ; const matched = this . _hLineMap . get ( mKey ) || [ ] ; matched . push ( refLineMeta ) ; this . _hLineMap . set ( mKey , matched ) ; } ) ; vLines = vLines . sort ( ( a , b ) => a . offset - b . offset ) ; hLines = hLines . sort ( ( a , b ) => a . offset - b . offset ) ; let vGroup = groupBy ( vLines , line => this . getLineMapKey ( line ) ) ; let hGroup = groupBy ( hLines , line => this . getLineMapKey ( line ) ) ; this . _vLines = vGroup . keys . map ( key => { const lines = vGroup . group [ key ] ; return { offset : fixNumber ( lines [ 0 ] . offset , 0 ) , min : Math . min ( ... lines . map ( line => line . offset ) ) , max : Math . max ( ... lines . map ( line => line . offset ) ) , refLineMetaList : lines , } ; } ) ; this . _hLines = hGroup . keys . map ( key => { const lines = hGroup . group [ key ] ; return { offset : fixNumber ( lines [ 0 ] . offset , 0 ) , min : Math . min ( ... lines . map ( line => line . offset ) ) , max : Math . max ( ... lines . map ( line => line . offset ) ) , refLineMetaList : lines , } ; } ) ; this . _dirty = false ; } param type param rect returns matchRefLines ( type ) { const current = this . getCurrent ( ) ; if ( ! current ) return [ ] ; const isVertical = type === "vertical" ; const lineMap = isVertical ? this . vLineMap : this . hLineMap ; const matchedLines = [ ] ; if ( ! current ) return matchedLines ; const cLines = this . getRectRefLines ( current ) ; cLines . forEach ( line => { if ( line . type !== type ) return ; const mKey = this . getLineMapKey ( line ) ; const lines = lineMap . get ( mKey ) || [ ] ; const matchedLine = getMatchedLine ( line , lines . filter ( line => ! line . adsorbOnly ) ) ; if ( matchedLine ) { matchedLines . push ( matchedLine ) ; } } ) ; return matchedLines ; } param type param offset returns getNearestOffsetFromOffset ( type , offset ) { const isVertical = type === "vertical" ; const lines = isVertical ? this . vLines : this . hLines ; let prev = - Infinity ; let prevDist = Infinity ; let next = Infinity ; let nextDist = Infinity ; lines . forEach ( line => { const d = Math . abs ( line . min - offset ) ; if ( line . min < offset && d >= 0.5 ) { prev = Math . max ( line . min , prev ) ; prevDist = Math . min ( d , prevDist ) ; } if ( line . min > offset && d >= 0.5 ) { next = Math . min ( line . min , next ) ; nextDist = Math . min ( d , nextDist ) ; } } ) ; return [ prev !== - Infinity ? [ prev , prevDist ] : null , next !== Infinity ? [ next , nextDist ] : null , ] ; } param position returns hasMatchedRefLine ( position ) { const current = this . getCurrent ( ) ; if ( ! current ) return false ; const lines = this . getRectRefLines ( current ) ; const line = find ( lines , line => line . position === position ) ; if ( ! line ) return false ; const mKey = this . getLineMapKey ( line ) ; const lineMap = getLineTypeFromPosition ( position ) === "vertical" ? this . vLineMap : this . hLineMap ; const matched = lineMap . get ( mKey ) ; if ( ! matched ) return false ; return ! ! matched . length ; } returns matchVRefLines ( ) { return this . matchRefLines ( "vertical" ) ; } getVRefLines ( ) { return this . matchRefLines ( "vertical" ) ; } returns matchHRefLines ( ) { return this . matchRefLines ( "horizontal" ) ; } getHRefLines ( ) { return this . matchRefLines ( "horizontal" ) ; } returns matchAllRefLines ( ) { return [ ... this . matchVRefLines ( ) , ... this . matchHRefLines ( ) ] ; } getAllRefLines ( ) { return [ ... this . matchVRefLines ( ) , ... this . matchHRefLines ( ) ] ; } param type param offset param delta param adsorbDistance returns getOffsetAdsorbDelta ( type , offset , delta , adsorbDistance = 5 ) { adsorbDistance = Math . abs ( adsorbDistance ) ; if ( adsorbDistance < 1 ) return delta ; const hasMatchedLine = this . getOffsetRefLineMetaList ( type , offset ) . length ; const n = this . getNearestOffsetFromOffset ( type , offset ) ; const isMoveTopOrLeft = delta < 0 ; const useIndex = isMoveTopOrLeft ? 0 : 1 ; const nearestOffset = n [ useIndex ] ; const value = nearestOffset ? nearestOffset [ 0 ] : 0 ; const dist = nearestOffset ? nearestOffset [ 1 ] : 0 ; if ( nearestOffset && dist <= adsorbDistance ) { adsorbDistance = dist ; } else { adsorbDistance += 1 ; } if ( hasMatchedLine ) { if ( Math . abs ( delta ) < adsorbDistance ) { delta = 0 ; } else if ( Math . abs ( delta ) === 1 && adsorbDistance < 1 ) { delta = adsorbDistance ; } } else if ( delta !== 0 ) { const dist = value - ( offset + delta ) ; if ( Math . abs ( dist ) < adsorbDistance && ( isMoveTopOrLeft ? dist <= 0 : dist >= 0 ) ) { delta += dist ; } } return delta ; } param delta param adsorbDistance returns getAdsorbDelta ( delta , adsorbDistance , dir ) { const rect = this . getCurrent ( ) ; if ( ! rect ) return delta ; const dirX = dir ?. x || "none" ; const dirY = dir ?. y || "none" ; adsorbDistance = Math . abs ( adsorbDistance || 5 ) ; const origAdsorbDistance = adsorbDistance ; const newDelta = { ... delta , } ; if ( adsorbDistance < 1 ) return newDelta ; const refLineMetaList = getRectRefLines ( rect ) ; const lineStatus = [ this . isEnableLine ( refLineMetaList [ 0 ] ) , this . isEnableLine ( refLineMetaList [ 1 ] ) , this . isEnableLine ( refLineMetaList [ 2 ] ) , this . isEnableLine ( refLineMetaList [ 3 ] ) , this . isEnableLine ( refLineMetaList [ 4 ] ) , this . isEnableLine ( refLineMetaList [ 5 ] ) , ] ; const hasMatchedVRefLine = this . hasMatchedRefLine ( "vl" ) || this . hasMatchedRefLine ( "vc" ) || this . hasMatchedRefLine ( "vr" ) ; const hasMatchedHRefLine = this . hasMatchedRefLine ( "ht" ) || this . hasMatchedRefLine ( "hc" ) || this . hasMatchedRefLine ( "hb" ) ; if ( delta . left !== 0 && dirX !== "none" ) { adsorbDistance = origAdsorbDistance ; const currentOffsetList = [ refLineMetaList [ 0 ] . offset , refLineMetaList [ 1 ] . offset , refLineMetaList [ 2 ] . offset , ] ; const n1 = this . getNearestOffsetFromOffset ( "vertical" , refLineMetaList [ 0 ] . offset ) ; const n2 = this . getNearestOffsetFromOffset ( "vertical" , refLineMetaList [ 1 ] . offset ) ; const n3 = this . getNearestOffsetFromOffset ( "vertical" , refLineMetaList [ 2 ] . offset ) ; const isMoveLeft = dirX === "left" ; const values = [ Infinity , Infinity , Infinity ] ; const distValues = [ Infinity , Infinity , Infinity ] ; const useIndex = isMoveLeft ? 0 : 1 ; if ( lineStatus [ 0 ] && n1 [ useIndex ] ) { values [ 0 ] = n1 [ useIndex ] ! [ 0 ] ; distValues [ 0 ] = n1 [ useIndex ] ! [ 1 ] ; } if ( lineStatus [ 1 ] && n2 [ useIndex ] ) { values [ 1 ] = n2 [ useIndex ] ! [ 0 ] ; distValues [ 1 ] = n2 [ useIndex ] ! [ 1 ] ; } if ( lineStatus [ 2 ] && n3 [ useIndex ] ) { values [ 2 ] = n3 [ useIndex ] ! [ 0 ] ; distValues [ 2 ] = n3 [ useIndex ] ! [ 1 ] ; } const minDist = Math . min ( ... distValues ) ; const minIndex = distValues . indexOf ( minDist ) ; const minOffset = values [ minIndex ] ; if ( minDist <= adsorbDistance ) { adsorbDistance = minDist ; } else { adsorbDistance += 1 ; } if ( hasMatchedVRefLine ) { if ( isMoveLeft && delta . left > 0 ) { newDelta . left = 0 ; } else if ( ! isMoveLeft && delta . left < 0 ) { newDelta . left = 0 ; } else if ( Math . abs ( delta . left ) < adsorbDistance ) { newDelta . left = 0 ; } } else { const currentOffset = currentOffsetList [ minIndex ] ; const dist = minOffset - ( currentOffset + delta . left ) ; if ( isMoveLeft && delta . left > 0 ) { newDelta . left = 0 ; } else if ( ! isMoveLeft && delta . left < 0 ) { newDelta . left = 0 ; } else if ( Math . abs ( dist ) < adsorbDistance && ( isMoveLeft ? dist <= 0 : dist >= 0 ) ) { newDelta . left = dist + delta . left ; } } } if ( delta . top !== 0 && dirY !== "none" ) { adsorbDistance = origAdsorbDistance ; const currentOffsetList = [ refLineMetaList [ 3 ] . offset , refLineMetaList [ 4 ] . offset , refLineMetaList [ 5 ] . offset , ] ; const n1 = this . getNearestOffsetFromOffset ( "horizontal" , refLineMetaList [ 3 ] . offset ) ; const n2 = this . getNearestOffsetFromOffset ( "horizontal" , refLineMetaList [ 4 ] . offset ) ; const n3 = this . getNearestOffsetFromOffset ( "horizontal" , refLineMetaList [ 5 ] . offset ) ; const isMoveTop = dirY === "up" ; const values = [ Infinity , Infinity , Infinity ] ; const distValues = [ Infinity , Infinity , Infinity ] ; const useIndex = isMoveTop ? 0 : 1 ; if ( lineStatus [ 3 ] && n1 [ useIndex ] ) { values [ 0 ] = n1 [ useIndex ] ! [ 0 ] ; distValues [ 0 ] = n1 [ useIndex ] ! [ 1 ] ; } if ( lineStatus [ 4 ] && n2 [ useIndex ] ) { values [ 1 ] = n2 [ useIndex ] ! [ 0 ] ; distValues [ 1 ] = n2 [ useIndex ] ! [ 1 ] ; } if ( lineStatus [ 5 ] && n3 [ useIndex ] ) { values [ 2 ] = n3 [ useIndex ] ! [ 0 ] ; distValues [ 2 ] = n3 [ useIndex ] ! [ 1 ] ; } const minDist = Math . min ( ... distValues ) ; const minIndex = distValues . indexOf ( minDist ) ; const minOffset = values [ minIndex ] ; if ( minDist <= adsorbDistance ) { adsorbDistance = minDist ; } else { adsorbDistance += 1 ; } if ( hasMatchedHRefLine ) { if ( isMoveTop && delta . top > 0 ) { newDelta . top = 0 ; } else if ( ! isMoveTop && delta . top < 0 ) { newDelta . top = 0 ; } else if ( Math . abs ( delta . top ) < adsorbDistance ) { newDelta . top = 0 ; } } else { const currentOffset = currentOffsetList [ minIndex ] ; const dist = minOffset - ( currentOffset + delta . top ) ; if ( isMoveTop && delta . top > 0 ) { newDelta . top = 0 ; } else if ( ! isMoveTop && delta . top < 0 ) { newDelta . top = 0 ; } else if ( Math . abs ( dist ) < adsorbDistance && ( isMoveTop ? dist <= 0 : dist >= 0 ) ) { newDelta . top = dist + delta . top ; } } } return newDelta ; } adsorbCreator ( { pageX , pageY , current = this . getCurrent ( ) , distance = 5 , disableAdsorb = false , scale , } ) { if ( ! current ) { throw new Error ( "[refline.js] current rect does not exist!" ) ; } let currentRect = { ... current , } ; const startX = pageX ; const startY = pageY ; const startLeft = currentRect . left ; const startTop = currentRect . top ; const _scale = scale || 1 ; const defaultDisableAdsorb = disableAdsorb ; let lastX = pageX ; let lastY = pageY ; let lastDirX , lastDirY ; return ( data ) => { let disableAdsorb = defaultDisableAdsorb ; if ( data . current ) { currentRect = { ... data . current , } ; } if ( ! isNil ( data . distance ) ) { distance = data . distance as number ; } if ( ! isNil ( data . disableAdsorb ) ) { disableAdsorb = data . disableAdsorb as boolean ; } const currentX = isNil ( data . pageX ) ? startX : ( data . pageX as number ) ; const currentY = isNil ( data . pageY ) ? startY : ( data . pageY as number ) ; const scale = isNil ( data . scale ) ? _scale : ( data . scale as number ) ; const dir = { x : "none" , y : "none" , } as { x : "left" | "right" | "none" ; y : "up" | "down" | "none" ; } ; if ( ! isNil ( data . pageX ) ) { if ( currentX === lastX ) { dir . x = lastDirX ; } else { dir . x = currentX < lastX ? "left" : "right" ; } lastDirX = dir . x ; } if ( ! isNil ( data . pageY ) ) { if ( currentY === lastY ) { dir . y = lastDirY ; } else { dir . y = currentY < lastY ? "up" : "down" ; } lastDirY = dir . y ; } const offsetX = isNil ( data . offsetX ) ? ( currentX - startX ) / scale : ( data . offsetX as number ) ; const offsetY = isNil ( data . offsetY ) ? ( currentY - startY ) / scale : ( data . offsetY as number ) ; const left = startLeft + offsetX ; const top = startTop + offsetY ; let delta = { left : left - currentRect . left , top : top - currentRect . top , } ; if ( ! isNil ( data . pageX ) && currentX === lastX ) { delta . left = 0 ; } if ( ! isNil ( data . pageY ) && currentY === lastY ) { delta . top = 0 ; } if ( ! isNil ( data . pageX ) && ! isNil ( data . pageY ) && currentX === lastX && currentY === lastY ) { disableAdsorb = true ; } const raw = delta ; if ( ! disableAdsorb ) { this . setCurrent ( currentRect ) ; delta = this . getAdsorbDelta ( delta , distance || 5 , dir ) ; } currentRect . left += delta . left ; currentRect . top += delta . top ; lastX = currentX ; lastY = currentY ; return { raw , delta , offset : { left : currentRect . left - startLeft , top : currentRect . top - startTop , } , rect : { ... currentRect , } , } ; } ; } } export function createRefLine < T extends Rect = Rect > ( opts ) { return new RefLine ( opts ) ; } export default RefLine ;