Tldraw/packages/editor/src/lib/editor/shapes/shared/arrow/straight-arrow.ts

266 wiersze
7.3 KiB
TypeScript

import { TLArrowShape } from '@tldraw/tlschema'
import { Mat, MatModel } from '../../../../primitives/Mat'
import { Vec, VecLike } from '../../../../primitives/Vec'
import {
intersectLineSegmentPolygon,
intersectLineSegmentPolyline,
} from '../../../../primitives/intersect'
import { Editor } from '../../../Editor'
import { TLArrowInfo } from './arrow-types'
import {
BOUND_ARROW_OFFSET,
BoundShapeInfo,
MIN_ARROW_LENGTH,
STROKE_SIZES,
TLArrowBindings,
getArrowTerminalsInArrowSpace,
getBoundShapeInfoForTerminal,
getBoundShapeRelationships,
} from './shared'
export function getStraightArrowInfo(
editor: Editor,
shape: TLArrowShape,
bindings: TLArrowBindings
): TLArrowInfo {
const { arrowheadStart, arrowheadEnd } = shape.props
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(editor, shape, bindings)
const a = terminalsInArrowSpace.start.clone()
const b = terminalsInArrowSpace.end.clone()
const c = Vec.Med(a, b)
if (Vec.Equals(a, b)) {
return {
bindings,
isStraight: true,
start: {
handle: a,
point: a,
arrowhead: shape.props.arrowheadStart,
},
end: {
handle: b,
point: b,
arrowhead: shape.props.arrowheadEnd,
},
middle: c,
isValid: false,
length: 0,
}
}
const uAB = Vec.Sub(b, a).uni()
// Update the arrowhead points using intersections with the bound shapes, if any.
const startShapeInfo = getBoundShapeInfoForTerminal(editor, shape, 'start')
const endShapeInfo = getBoundShapeInfoForTerminal(editor, shape, 'end')
const arrowPageTransform = editor.getShapePageTransform(shape)!
// Update the position of the arrowhead's end point
updateArrowheadPointWithBoundShape(
b, // <-- will be mutated
terminalsInArrowSpace.start,
arrowPageTransform,
endShapeInfo
)
// Then update the position of the arrowhead's end point
updateArrowheadPointWithBoundShape(
a, // <-- will be mutated
terminalsInArrowSpace.end,
arrowPageTransform,
startShapeInfo
)
let offsetA = 0
let offsetB = 0
let strokeOffsetA = 0
let strokeOffsetB = 0
let minLength = MIN_ARROW_LENGTH
const isSelfIntersection =
startShapeInfo && endShapeInfo && startShapeInfo.shape === endShapeInfo.shape
const relationship =
startShapeInfo && endShapeInfo
? getBoundShapeRelationships(editor, startShapeInfo.shape.id, endShapeInfo.shape.id)
: 'safe'
if (
relationship === 'safe' &&
startShapeInfo &&
endShapeInfo &&
!isSelfIntersection &&
!startShapeInfo.isExact &&
!endShapeInfo.isExact
) {
if (endShapeInfo.didIntersect && !startShapeInfo.didIntersect) {
// ...and if only the end shape intersected, then make it
// a short arrow ending at the end shape intersection.
if (startShapeInfo.isClosed) {
a.setTo(b.clone().add(uAB.clone().mul(MIN_ARROW_LENGTH)))
}
} else if (!endShapeInfo.didIntersect) {
// ...and if only the end shape intersected, or if neither
// shape intersected, then make it a short arrow starting
// at the start shape intersection.
if (endShapeInfo.isClosed) {
b.setTo(a.clone().sub(uAB.clone().mul(MIN_ARROW_LENGTH)))
}
}
}
const distance = Vec.Sub(b, a)
// Check for divide-by-zero before we call uni()
const u = Vec.Len(distance) ? distance.uni() : Vec.From(distance)
const didFlip = !Vec.Equals(u, uAB)
// If the arrow is bound non-exact to a start shape and the
// start point has an arrowhead, then offset the start point
if (!isSelfIntersection) {
if (
relationship !== 'start-contains-end' &&
startShapeInfo &&
arrowheadStart !== 'none' &&
!startShapeInfo.isExact
) {
strokeOffsetA =
STROKE_SIZES[shape.props.size] / 2 +
('size' in startShapeInfo.shape.props
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
: 0)
offsetA = BOUND_ARROW_OFFSET + strokeOffsetA
minLength += strokeOffsetA
}
// If the arrow is bound non-exact to an end shape and the
// end point has an arrowhead offset the end point
if (
relationship !== 'end-contains-start' &&
endShapeInfo &&
arrowheadEnd !== 'none' &&
!endShapeInfo.isExact
) {
strokeOffsetB =
STROKE_SIZES[shape.props.size] / 2 +
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
offsetB = BOUND_ARROW_OFFSET + strokeOffsetB
minLength += strokeOffsetB
}
}
// Adjust offsets if the length of the arrow is too small
const tA = a.clone().add(u.clone().mul(offsetA * (didFlip ? -1 : 1)))
const tB = b.clone().sub(u.clone().mul(offsetB * (didFlip ? -1 : 1)))
if (Vec.DistMin(tA, tB, minLength)) {
if (offsetA !== 0 && offsetB !== 0) {
// both bound + offset
offsetA *= -1.5
offsetB *= -1.5
} else if (offsetA !== 0) {
// start bound + offset
offsetA *= -1
} else if (offsetB !== 0) {
// end bound + offset
offsetB *= -1
} else {
// noop, its just a really short arrow
}
}
a.add(u.clone().mul(offsetA * (didFlip ? -1 : 1)))
b.sub(u.clone().mul(offsetB * (didFlip ? -1 : 1)))
// If the handles flipped their order, then set the center handle
// to the midpoint of the terminals (rather than the midpoint of the
// arrow body); otherwise, it may not be "between" the other terminals.
if (didFlip) {
if (startShapeInfo && endShapeInfo) {
// If we have two bound shapes...then make the arrow a short arrow from
// the start point towards where the end point should be.
b.setTo(Vec.Add(a, u.clone().mul(-MIN_ARROW_LENGTH)))
}
c.setTo(Vec.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end))
} else {
c.setTo(Vec.Med(a, b))
}
const length = Vec.Dist(a, b)
return {
bindings,
isStraight: true,
start: {
handle: terminalsInArrowSpace.start,
point: a,
arrowhead: shape.props.arrowheadStart,
},
end: {
handle: terminalsInArrowSpace.end,
point: b,
arrowhead: shape.props.arrowheadEnd,
},
middle: c,
isValid: length > 0,
length,
}
}
/** Get an intersection point from A -> B with bound shape (target) from shape (arrow). */
function updateArrowheadPointWithBoundShape(
point: Vec,
opposite: Vec,
arrowPageTransform: MatModel,
targetShapeInfo?: BoundShapeInfo
) {
if (targetShapeInfo === undefined) {
// No bound shape? The arrowhead point will be at the arrow terminal.
return
}
if (targetShapeInfo.isExact) {
// Exact type binding? The arrowhead point will be at the arrow terminal.
return
}
// From and To in page space
const pageFrom = Mat.applyToPoint(arrowPageTransform, opposite)
const pageTo = Mat.applyToPoint(arrowPageTransform, point)
// From and To in local space of the target shape
const targetFrom = Mat.applyToPoint(Mat.Inverse(targetShapeInfo.transform), pageFrom)
const targetTo = Mat.applyToPoint(Mat.Inverse(targetShapeInfo.transform), pageTo)
const isClosed = targetShapeInfo.isClosed
const fn = isClosed ? intersectLineSegmentPolygon : intersectLineSegmentPolyline
const intersection = fn(targetFrom, targetTo, targetShapeInfo.outline)
let targetInt: VecLike | undefined
if (intersection !== null) {
targetInt =
intersection.sort((p1, p2) => Vec.Dist2(p1, targetFrom) - Vec.Dist2(p2, targetFrom))[0] ??
(isClosed ? undefined : targetTo)
}
if (targetInt === undefined) {
// No intersection? The arrowhead point will be at the arrow terminal.
return
}
const pageInt = Mat.applyToPoint(targetShapeInfo.transform, targetInt)
const arrowInt = Mat.applyToPoint(Mat.Inverse(arrowPageTransform), pageInt)
point.setTo(arrowInt)
targetShapeInfo.didIntersect = true
}