import { exhaustiveSwitchError } from '@tldraw/editor' import { ElbowArrowRoute, ElbowArrowSide, ElbowArrowSideOpposites, ElbowArrowSideReason, ElbowArrowSides, } from '../definitions' import { tryRouteArrow } from './elbowArrowRoutes' import { ElbowArrowWorkingInfo } from './ElbowArrowWorkingInfo' export function routeArrowWithAutoEdgePicking( info: ElbowArrowWorkingInfo, reason: ElbowArrowSideReason ): ElbowArrowRoute | null { let idealRoute = null if ( // +1 to bias us towards the x-axis. without this, we get flicker as we move an arrow locket // to 45 deg (as gapx/gapy are almost equal and the result depends on floating point // precision) Math.abs(info.gapX) + 1 > Math.abs(info.gapY) && info.midX !== null ) { if (info.gapX > 0) { idealRoute = tryRouteArrow(info, 'right', 'left') } else { idealRoute = tryRouteArrow(info, 'left', 'right') } } else { const aRight = info.A.edges.right const aLeft = info.A.edges.left const bTop = info.B.edges.top const bBottom = info.B.edges.bottom if (info.A.isPoint && info.B.isPoint) { if (info.gapY > 0) { idealRoute = tryRouteArrow(info, 'bottom', 'top') } else { idealRoute = tryRouteArrow(info, 'top', 'bottom') } } else if ( aRight && bTop && (aRight.expanded ?? aRight.value) <= bTop.crossTarget && aRight.crossTarget <= (bTop.expanded ?? bTop.value) ) { idealRoute = tryRouteArrow(info, 'right', 'top') } else if ( aRight && bBottom && (aRight.expanded ?? aRight.value) <= bBottom.crossTarget && aRight.crossTarget >= (bBottom.expanded ?? bBottom.value) ) { idealRoute = tryRouteArrow(info, 'right', 'bottom') } else if ( aLeft && bTop && (aLeft.expanded ?? aLeft.value) >= bTop.crossTarget && aLeft.crossTarget <= (bTop.expanded ?? bTop.value) ) { idealRoute = tryRouteArrow(info, 'left', 'top') } else if ( aLeft && bBottom && (aLeft.expanded ?? aLeft.value) >= bBottom.crossTarget && aLeft.crossTarget >= (bBottom.expanded ?? bBottom.value) ) { idealRoute = tryRouteArrow(info, 'left', 'bottom') } else if (info.gapY > 0 && info.midY !== null) { idealRoute = tryRouteArrow(info, 'bottom', 'top') } else if (info.gapY < 0 && info.midY !== null) { idealRoute = tryRouteArrow(info, 'top', 'bottom') } } if (idealRoute) { idealRoute.aEdgePicking = reason idealRoute.bEdgePicking = reason return idealRoute } const aAvailableSide = ElbowArrowSides.filter((side) => info.A.edges[side]) const bAvailableSides = ElbowArrowSides.filter((side) => info.B.edges[side]) const nonPartialRouteCandidates = aAvailableSide.flatMap((aSide) => bAvailableSides.map((bSide) => [aSide, bSide, reason, reason] as const) ) return pickBest(info, nonPartialRouteCandidates) } export function routeArrowWithPartialEdgePicking( info: ElbowArrowWorkingInfo, aSide: ElbowArrowSide ) { let idealRoute = null const aRight = info.A.edges.right const aLeft = info.A.edges.left const bTop = info.B.edges.top const bBottom = info.B.edges.bottom switch (aSide) { case 'right': if (info.gapX > 0 && info.gapX > Math.abs(info.gapY) && info.midX !== null) { idealRoute = tryRouteArrow(info, 'right', 'left') } else if ( aRight && bTop && (aRight.expanded ?? aRight.value) <= bTop.crossTarget && aRight.crossTarget <= (bTop.expanded ?? bTop.value) ) { idealRoute = tryRouteArrow(info, 'right', 'top') } else if ( aRight && bBottom && (aRight.expanded ?? aRight.value) <= bBottom.crossTarget && aRight.crossTarget >= (bBottom.expanded ?? bBottom.value) ) { idealRoute = tryRouteArrow(info, 'right', 'bottom') } break case 'left': if (info.gapX < 0 && Math.abs(info.gapX) > Math.abs(info.gapY) && info.midX !== null) { idealRoute = tryRouteArrow(info, 'left', 'right') } else if ( aLeft && bTop && (aLeft.expanded ?? aLeft.value) >= bTop.crossTarget && aLeft.crossTarget <= (bTop.expanded ?? bTop.value) ) { idealRoute = tryRouteArrow(info, 'left', 'top') } else if ( aLeft && bBottom && (aLeft.expanded ?? aLeft.value) >= bBottom.crossTarget && aLeft.crossTarget >= (bBottom.expanded ?? bBottom.value) ) { idealRoute = tryRouteArrow(info, 'left', 'bottom') } break case 'top': case 'bottom': // top and bottom are handled by the pickShortest approach below - it automatically // picks the path we would pick with heuristics anyway. break default: exhaustiveSwitchError(aSide) } if (idealRoute) { idealRoute.aEdgePicking = 'manual' idealRoute.bEdgePicking = 'auto' return idealRoute } switch (aSide) { case 'top': return pickBest(info, [ ['top', 'bottom', 'manual', 'auto'], ['top', 'right', 'manual', 'auto'], ['top', 'left', 'manual', 'auto'], ['top', 'top', 'manual', 'auto'], ]) case 'bottom': return pickBest(info, [ ['bottom', 'top', 'manual', 'auto'], ['bottom', 'right', 'manual', 'auto'], ['bottom', 'left', 'manual', 'auto'], ['bottom', 'bottom', 'manual', 'auto'], ]) case 'left': return pickBest(info, [ ['left', 'right', 'manual', 'auto'], ['left', 'bottom', 'manual', 'auto'], ['left', 'left', 'manual', 'auto'], ['left', 'top', 'manual', 'auto'], ]) case 'right': return pickBest(info, [ ['right', 'left', 'manual', 'auto'], ['right', 'bottom', 'manual', 'auto'], ['right', 'right', 'manual', 'auto'], ['right', 'top', 'manual', 'auto'], ]) } } export function routeArrowWithManualEdgePicking( info: ElbowArrowWorkingInfo, aSide: ElbowArrowSide, bSide: ElbowArrowSide ) { const route = tryRouteArrow(info, aSide, bSide) if (route) return route if (info.A.isPoint && info.B.isPoint) { return pickBest(info, [ [ElbowArrowSideOpposites[aSide], ElbowArrowSideOpposites[bSide], 'manual', 'manual'], [aSide, ElbowArrowSideOpposites[bSide], 'manual', 'auto'], [ElbowArrowSideOpposites[aSide], bSide, 'auto', 'manual'], ]) } else if (info.A.isPoint) { return tryRouteArrow(info, ElbowArrowSideOpposites[aSide], bSide) } else if (info.B.isPoint) { return tryRouteArrow(info, aSide, ElbowArrowSideOpposites[bSide]) } return null } function pickBest( info: ElbowArrowWorkingInfo, edges: ReadonlyArray< readonly [ElbowArrowSide, ElbowArrowSide, ElbowArrowSideReason, ElbowArrowSideReason] > ) { let bestRoute: ElbowArrowRoute | null = null let bestCornerCount = Infinity let bestDistance = Infinity let distanceBias = 0 for (const [aSide, bSide, aEdgePicking, bEdgePicking] of edges) { distanceBias += 1 const route = tryRouteArrow(info, aSide, bSide) if (route) { route.aEdgePicking = aEdgePicking route.bEdgePicking = bEdgePicking if (route.points.length < bestCornerCount) { bestCornerCount = route.points.length bestDistance = route.distance bestRoute = route } else if ( route.points.length === bestCornerCount && route.distance + distanceBias < bestDistance ) { bestDistance = route.distance bestRoute = route } } } return bestRoute }