snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,403 @@
import { ElbowArrowRoute, ElbowArrowSide } from '../definitions'
import { ElbowArrowRouteBuilder } from './ElbowArrowRouteBuilder'
import { ElbowArrowTransform, ElbowArrowWorkingInfo } from './ElbowArrowWorkingInfo'
/**
* Draw one of these arrows:
*
* ```
* 1: 2: 3: 4: 5:
* ┌───┐ ┌───┐ ┌───┐ ┌───────┐ ┌───────┐ ┌───┐
* │ A ├─┐ │ A ├─┐ │ A ├───┐ │ ┌───┐ │ │ ┌───┐ │ │ A ├─┐
* └───┘ │ ┌───┐ └───┘ │ └───┘ │ │ │ A ├─┘ └─► B │ │ └───┘ │
* └─► B │ ┌────┘ ┌───┐ │ │ └───┘ └───┘ └───────┘
* └───┘ │ ┌───┐ ┌►│ B │ │ │ ┌───┐
* └─► B │ │ └───┘ │ └───► B │
* └───┘ └───────┘ └───┘
* ```
*/
export function routeRightToLeft(info: ElbowArrowWorkingInfo): ElbowArrowRoute | null {
const aEdge = info.A.edges.right
const bEdge = info.B.edges.left
// we can't draw this arrow if we don't have the proper edge we want:
if (!aEdge || !bEdge) return null
// just so we get to handle less edge cases, flip the boxes if A is below B:
if (aEdge.crossTarget > bEdge.crossTarget) {
info.apply(ElbowArrowTransform.FlipY)
}
if (info.gapX > 0 && info.midX !== null) {
// Arrow 1:
return new ElbowArrowRouteBuilder(info, 'to left 1')
.add(aEdge.value, aEdge.crossTarget)
.add(info.midX, aEdge.crossTarget)
.add(info.midX, bEdge.crossTarget)
.midpointHandle('x')
.add(bEdge.value, bEdge.crossTarget)
.build()
}
if (aEdge.expanded === null || bEdge.expanded === null) return null
if (info.midY !== null) {
// Arrow 2:
return new ElbowArrowRouteBuilder(info, 'to left 2')
.add(aEdge.value, aEdge.crossTarget)
.add(aEdge.expanded, aEdge.crossTarget)
.add(aEdge.expanded, info.midY)
.add(bEdge.expanded, info.midY)
.midpointHandle('y')
.add(bEdge.expanded, bEdge.crossTarget)
.add(bEdge.value, bEdge.crossTarget)
.build()
}
const arrow3Distance =
Math.abs(aEdge.value - info.common.expanded.right) +
Math.abs(aEdge.crossTarget - info.common.expanded.bottom) +
Math.abs(info.common.expanded.right - bEdge.expanded) +
Math.abs(info.common.expanded.bottom - bEdge.crossTarget) +
info.options.expandElbowLegLength +
6 // 6 points in this arrow
const arrow4Distance =
info.options.expandElbowLegLength +
Math.abs(aEdge.crossTarget - info.common.expanded.top) +
Math.abs(aEdge.expanded - info.common.expanded.left) +
Math.abs(info.common.expanded.top - bEdge.crossTarget) +
Math.abs(info.common.expanded.left - bEdge.value) +
// 6 points in this arrow, plus bias towards down/right:
6 +
info.bias.y
const arrow5Distance =
info.gapX < 0 && info.midX !== null
? info.options.expandElbowLegLength +
Math.abs(aEdge.crossTarget - info.A.expanded.bottom) +
info.common.expanded.width +
Math.abs(info.A.expanded.bottom - info.B.expanded.top) +
Math.abs(info.B.expanded.top - bEdge.crossTarget) +
info.options.expandElbowLegLength +
// 8 points in this arrow
8
: Infinity
if (arrow3Distance < arrow4Distance && arrow3Distance < arrow5Distance) {
// Arrow 3:
return new ElbowArrowRouteBuilder(info, 'to left 3')
.add(aEdge.value, aEdge.crossTarget)
.add(info.common.expanded.right, aEdge.crossTarget)
.add(info.common.expanded.right, info.common.expanded.bottom)
.add(bEdge.expanded, info.common.expanded.bottom)
.add(bEdge.expanded, bEdge.crossTarget)
.add(bEdge.value, bEdge.crossTarget)
.build()
}
if (arrow4Distance < arrow5Distance) {
// Arrow 4:
return new ElbowArrowRouteBuilder(info, 'to left 4')
.add(aEdge.value, aEdge.crossTarget)
.add(aEdge.expanded, aEdge.crossTarget)
.add(aEdge.expanded, info.common.expanded.top)
.add(info.common.expanded.left, info.common.expanded.top)
.add(info.common.expanded.left, bEdge.crossTarget)
.add(bEdge.value, bEdge.crossTarget)
.build()
}
if (info.midX !== null) {
// Arrow 5:
return new ElbowArrowRouteBuilder(info, 'to left 5')
.add(aEdge.value, aEdge.crossTarget)
.add(aEdge.expanded, aEdge.crossTarget)
.add(aEdge.expanded, info.A.expanded.bottom)
.add(info.midX, info.A.expanded.bottom)
.add(info.midX, info.B.expanded.top)
.midpointHandle('y')
.add(bEdge.expanded, info.B.expanded.top)
.add(bEdge.expanded, bEdge.crossTarget)
.add(bEdge.value, bEdge.crossTarget)
.build()
}
return null
}
/**
* Draw one of these arrows:
* ```
* 1: 2: 3:
* ┌───┐ ┌───┐ ┌───┐
* │ A ├─────┐ ┌───┐ │ ┌─▼─┐ │ A ├─┐
* └───┘ │ │ A ├─┘ │ B │ └───┘ │
* ┌─▼─┐ └───┘ └───┘ ┌───┘
* │ B │ ┌─▼─┐
* └───┘ │ B │
* └───┘
* 4: 5: 6:
* ┌───┐ ┌───┐ ┌───┐ ┌───┐
* ┌─▼─┐ │ │ ┌─▼─┐ ┌─▼─┐ │ │ A ├─┐
* │ B │ │ │ │ B │ │ B │ │ └───┘ │
* └───┘ │ │ └───┘ └───┘ └───────┘
* ┌───┐ │ └───┐
* │ A ├─┘ ┌───┐ │
* └───┘ │ A ├─┘
* └───┘
* ```
*/
export function routeRightToTop(info: ElbowArrowWorkingInfo): ElbowArrowRoute | null {
const aEdge = info.A.edges.right
const bEdge = info.B.edges.top
if (!aEdge || !bEdge) return null
if (
(aEdge.crossTarget < (bEdge.expanded ?? bEdge.value) &&
bEdge.crossTarget > (aEdge.expanded ?? aEdge.value)) ||
(info.A.isPoint && info.B.expanded.containsPoint(info.A.original.center))
) {
// Arrow 1:
return new ElbowArrowRouteBuilder(info, 'to top 1')
.add(aEdge.value, aEdge.crossTarget)
.add(bEdge.crossTarget, aEdge.crossTarget)
.add(bEdge.crossTarget, bEdge.value)
.build()
}
if (info.gapX > 0 && info.midX !== null && bEdge.expanded !== null) {
// Arrow 2:
return new ElbowArrowRouteBuilder(info, 'to top 2')
.add(aEdge.value, aEdge.crossTarget)
.add(info.midX, aEdge.crossTarget)
.add(info.midX, bEdge.expanded)
.midpointHandle('x')
.add(bEdge.crossTarget, bEdge.expanded)
.add(bEdge.crossTarget, bEdge.value)
.build()
}
if (
info.gapY > 0 &&
aEdge.expanded !== null &&
bEdge.crossTarget < aEdge.expanded &&
info.midY !== null
) {
// Arrow 3:
return new ElbowArrowRouteBuilder(info, 'to top 3')
.add(aEdge.value, aEdge.crossTarget)
.add(aEdge.expanded, aEdge.crossTarget)
.add(aEdge.expanded, info.midY)
.add(bEdge.crossTarget, info.midY)
.midpointHandle('y')
.add(bEdge.crossTarget, bEdge.value)
.build()
}
const arrow4Length =
Math.abs(aEdge.value - info.common.expanded.right) +
Math.abs(aEdge.crossTarget - info.common.expanded.top) +
Math.abs(bEdge.crossTarget - info.common.expanded.right) +
Math.abs(bEdge.value - info.common.expanded.top)
const arrow5Length =
aEdge.expanded !== null && info.midY !== null && bEdge.expanded !== null
? Math.abs(aEdge.value - aEdge.expanded) +
Math.abs(info.B.expanded.left - aEdge.expanded) +
Math.abs(info.B.expanded.left - bEdge.crossTarget) +
Math.abs(aEdge.crossTarget - info.B.expanded.top) +
Math.abs(bEdge.value - bEdge.expanded)
: Infinity
const arrow6Length =
aEdge.expanded !== null && info.midX !== null && bEdge.expanded !== null
? Math.abs(aEdge.value - info.common.expanded.right) +
Math.abs(aEdge.crossTarget - info.A.expanded.bottom) +
Math.abs(aEdge.expanded - bEdge.crossTarget) +
Math.abs(info.A.expanded.bottom - bEdge.expanded) +
Math.abs(bEdge.expanded - bEdge.value)
: Infinity
if (arrow4Length < arrow5Length && arrow4Length < arrow6Length) {
// Arrow 4:
return new ElbowArrowRouteBuilder(info, 'to top 4')
.add(aEdge.value, aEdge.crossTarget)
.add(info.common.expanded.right, aEdge.crossTarget)
.add(info.common.expanded.right, info.common.expanded.top)
.add(bEdge.crossTarget, info.common.expanded.top)
.add(bEdge.crossTarget, bEdge.value)
.build()
}
if (
bEdge.expanded !== null &&
aEdge.expanded !== null &&
info.midY !== null &&
arrow5Length < arrow6Length
) {
// Arrow 5:
return new ElbowArrowRouteBuilder(info, 'to top 5')
.add(aEdge.value, aEdge.crossTarget)
.add(aEdge.expanded, aEdge.crossTarget)
.add(aEdge.expanded, info.midY)
.add(info.B.expanded.left, info.midY)
.midpointHandle('y')
.add(info.B.expanded.left, bEdge.expanded)
.add(bEdge.crossTarget, bEdge.expanded)
.add(bEdge.crossTarget, bEdge.value)
.build()
}
if (bEdge.expanded !== null && aEdge.expanded !== null && info.midX !== null) {
// Arrow 6:
return new ElbowArrowRouteBuilder(info, 'to top 6')
.add(aEdge.value, aEdge.crossTarget)
.add(aEdge.expanded, aEdge.crossTarget)
.add(aEdge.expanded, info.A.expanded.bottom)
.add(info.midX, info.A.expanded.bottom)
.add(info.midX, bEdge.expanded)
.midpointHandle('x')
.add(bEdge.crossTarget, bEdge.expanded)
.add(bEdge.crossTarget, bEdge.value)
.build()
}
return null
}
/**
* See `routeRightToTop`.
*/
export function routeRightToBottom(info: ElbowArrowWorkingInfo): ElbowArrowRoute | null {
info.apply(ElbowArrowTransform.FlipY)
return routeRightToTop(info)
}
/**
* Arrows may be mirrored - Y flipped
* ```
* 1: 2: 3:
* ┌───┐ ┌───┐ ┌───────┐ ┌───┐
* │ A ├─┐ │ A ├─┘ ┌───┐ │ ┌───┐ │ A ├─┐
* └───┘ │ └───┘ │ B ◄─┘ │ B ◄─┐ └───┘ │
* ┌───┐ │ └───┘ └───┘ └───────┘
* │ B ◄─┘
* └───┘
* ```
*/
export function routeRightToRight(info: ElbowArrowWorkingInfo): ElbowArrowRoute | null {
const aEdge = info.A.edges.right
const bEdge = info.B.edges.right
if (!aEdge || !bEdge) return null
if (
(info.gapX <= 0 ||
aEdge.crossTarget > info.B.expanded.bottom ||
aEdge.crossTarget < info.B.expanded.top) &&
(bEdge.value > info.A.original.left ||
bEdge.crossTarget > info.A.expanded.bottom ||
bEdge.crossTarget < info.A.expanded.top)
) {
// Arrow 1
return new ElbowArrowRouteBuilder(info, 'to right 1')
.add(aEdge.value, aEdge.crossTarget)
.add(info.common.expanded.right, aEdge.crossTarget)
.add(info.common.expanded.right, bEdge.crossTarget)
.add(bEdge.value, bEdge.crossTarget)
.build()
}
if (info.midX === null) return null
if (bEdge.expanded !== null && info.gapX >= 0) {
const viaBottomLength =
Math.abs(bEdge.crossTarget - info.B.expanded.bottom) +
Math.abs(aEdge.crossTarget - info.B.expanded.bottom)
const viaTopLength =
Math.abs(bEdge.crossTarget - info.B.expanded.top) +
Math.abs(aEdge.crossTarget - info.B.expanded.top)
const topOrBottom = viaBottomLength < viaTopLength ? 'bottom' : 'top'
// Arrow 2:
return new ElbowArrowRouteBuilder(info, `to right 2 via ${topOrBottom}`)
.add(aEdge.value, aEdge.crossTarget)
.add(info.midX, aEdge.crossTarget)
.add(info.midX, info.B.expanded[topOrBottom])
.midpointHandle('x')
.add(bEdge.expanded, info.B.expanded[topOrBottom])
.add(bEdge.expanded, bEdge.crossTarget)
.add(bEdge.value, bEdge.crossTarget)
.build()
}
if (aEdge.expanded !== null && info.gapX <= 0) {
const viaBottomLength =
Math.abs(bEdge.crossTarget - info.A.expanded.bottom) +
Math.abs(aEdge.crossTarget - info.A.expanded.bottom)
const viaTopLength =
Math.abs(bEdge.crossTarget - info.A.expanded.top) +
Math.abs(aEdge.crossTarget - info.A.expanded.top)
const topOrBottom = viaBottomLength < viaTopLength ? 'bottom' : 'top'
// Arrow 3:
return new ElbowArrowRouteBuilder(info, `to right 3 via ${topOrBottom}`)
.add(aEdge.value, aEdge.crossTarget)
.add(aEdge.expanded, aEdge.crossTarget)
.add(aEdge.expanded, info.A.expanded[topOrBottom])
.add(info.midX, info.A.expanded[topOrBottom])
.add(info.midX, bEdge.crossTarget)
.midpointHandle('x')
.add(bEdge.value, bEdge.crossTarget)
.build()
}
return null
}
const routes = {
top: {
top: [ElbowArrowTransform.Rotate270, routeRightToRight],
left: [ElbowArrowTransform.Rotate270, routeRightToTop],
bottom: [ElbowArrowTransform.Rotate270, routeRightToLeft],
right: [ElbowArrowTransform.Rotate270, routeRightToBottom],
},
right: {
top: [ElbowArrowTransform.Identity, routeRightToTop],
right: [ElbowArrowTransform.Identity, routeRightToRight],
bottom: [ElbowArrowTransform.Identity, routeRightToBottom],
left: [ElbowArrowTransform.Identity, routeRightToLeft],
},
bottom: {
top: [ElbowArrowTransform.Rotate90, routeRightToLeft],
left: [ElbowArrowTransform.Rotate90, routeRightToBottom],
bottom: [ElbowArrowTransform.Rotate90, routeRightToRight],
right: [ElbowArrowTransform.Rotate90, routeRightToTop],
},
left: {
top: [ElbowArrowTransform.Rotate180, routeRightToBottom],
left: [ElbowArrowTransform.Rotate180, routeRightToRight],
bottom: [ElbowArrowTransform.Rotate180, routeRightToTop],
right: [ElbowArrowTransform.Rotate180, routeRightToLeft],
},
} satisfies Record<
ElbowArrowSide,
Record<
ElbowArrowSide,
[ElbowArrowTransform, (info: ElbowArrowWorkingInfo) => ElbowArrowRoute | null]
>
>
export function tryRouteArrow(
info: ElbowArrowWorkingInfo,
aEdge: ElbowArrowSide,
bEdge: ElbowArrowSide
): ElbowArrowRoute | null {
const [transform, routeFn] = routes[aEdge][bEdge]
info.apply(transform)
const route = routeFn(info)
info.reset()
return route
}