Tldraw/packages/tldraw/src/lib/shapes/arrow/arrowLabel.ts

352 wiersze
11 KiB
TypeScript
Czysty Zwykły widok Historia

arrows: add ability to change label placement (#2557) This adds the ability to drag the label on an arrow to a different location within the line segment/arc. https://github.com/tldraw/tldraw/assets/469604/dbd2ee35-bebc-48d6-b8ee-fcf12ce91fa5 - A lot of the complexity lay in ensuring a fixed distance from the ends of the arrowheads. - I added a new type of handle `text-adjust` that makes the text box the very handle itself. - I added a `ARROW_HANDLES` enum - we should use more enums! - The bulk of the changes are in ArrowShapeUtil — check that out in particular obviously :) Along the way, I tried to improve a couple spots as I touched them: - added some more documentation to Vec.ts because some of the functions in there were obscure/new to me. (at least the naming, hah) - added `getPointOnCircle` which was being done in a couple places independently and refactored those places. ### Questions - the `getPointOnCircle` API changed. Is this considered breaking and/or should I leave the signature the same? Wasn't sure if it was a big deal or not. - I made `labelPosition` in the schema always but I guess it could have been optional? Lemme know if there's a preference. - Any feedback on tests? Happy to expand those if necessary. ### Change Type - [ ] `patch` — Bug fix - [x] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. For arrow in [straightArrow, curvedArrow] test the following: a. Label in the middle b. Label at both ends of the arrow c. Test arrows in different directions d. Rotating the endpoints and seeing that the label stays at the end of the arrow at a fixed width. e. Test different stroke widths. f. Test with different arrowheads. 2. Also, test arcs that are more circle like than arc-like. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Adds ability to change label position on arrows. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> Co-authored-by: alex <alex@dytry.ch>
2024-01-24 10:19:20 +00:00
import {
Arc2d,
Box,
Circle2d,
Edge2d,
Editor,
Geometry2d,
Polygon2d,
TLArrowInfo,
TLArrowShape,
Vec,
VecLike,
angleDistance,
clamp,
getPointOnCircle,
intersectCirclePolygon,
intersectLineSegmentPolygon,
} from '@tldraw/editor'
import {
ARROW_LABEL_FONT_SIZES,
ARROW_LABEL_PADDING,
FONT_FAMILIES,
LABEL_TO_ARROW_PADDING,
STROKE_SIZES,
TEXT_PROPS,
} from '../shared/default-shape-constants'
const labelSizeCache = new WeakMap<TLArrowShape, Vec>()
export function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
const cachedSize = labelSizeCache.get(shape)
if (cachedSize) return cachedSize
const info = editor.getArrowInfo(shape)!
let width = 0
let height = 0
const bodyGeom = info.isStraight
? new Edge2d({
start: Vec.From(info.start.point),
end: Vec.From(info.end.point),
})
arrows: add ability to change label placement (#2557) This adds the ability to drag the label on an arrow to a different location within the line segment/arc. https://github.com/tldraw/tldraw/assets/469604/dbd2ee35-bebc-48d6-b8ee-fcf12ce91fa5 - A lot of the complexity lay in ensuring a fixed distance from the ends of the arrowheads. - I added a new type of handle `text-adjust` that makes the text box the very handle itself. - I added a `ARROW_HANDLES` enum - we should use more enums! - The bulk of the changes are in ArrowShapeUtil — check that out in particular obviously :) Along the way, I tried to improve a couple spots as I touched them: - added some more documentation to Vec.ts because some of the functions in there were obscure/new to me. (at least the naming, hah) - added `getPointOnCircle` which was being done in a couple places independently and refactored those places. ### Questions - the `getPointOnCircle` API changed. Is this considered breaking and/or should I leave the signature the same? Wasn't sure if it was a big deal or not. - I made `labelPosition` in the schema always but I guess it could have been optional? Lemme know if there's a preference. - Any feedback on tests? Happy to expand those if necessary. ### Change Type - [ ] `patch` — Bug fix - [x] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. For arrow in [straightArrow, curvedArrow] test the following: a. Label in the middle b. Label at both ends of the arrow c. Test arrows in different directions d. Rotating the endpoints and seeing that the label stays at the end of the arrow at a fixed width. e. Test different stroke widths. f. Test with different arrowheads. 2. Also, test arcs that are more circle like than arc-like. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Adds ability to change label position on arrows. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> Co-authored-by: alex <alex@dytry.ch>
2024-01-24 10:19:20 +00:00
: new Arc2d({
center: Vec.Cast(info.handleArc.center),
radius: info.handleArc.radius,
start: Vec.Cast(info.start.point),
end: Vec.Cast(info.end.point),
sweepFlag: info.bodyArc.sweepFlag,
largeArcFlag: info.bodyArc.largeArcFlag,
})
arrows: add ability to change label placement (#2557) This adds the ability to drag the label on an arrow to a different location within the line segment/arc. https://github.com/tldraw/tldraw/assets/469604/dbd2ee35-bebc-48d6-b8ee-fcf12ce91fa5 - A lot of the complexity lay in ensuring a fixed distance from the ends of the arrowheads. - I added a new type of handle `text-adjust` that makes the text box the very handle itself. - I added a `ARROW_HANDLES` enum - we should use more enums! - The bulk of the changes are in ArrowShapeUtil — check that out in particular obviously :) Along the way, I tried to improve a couple spots as I touched them: - added some more documentation to Vec.ts because some of the functions in there were obscure/new to me. (at least the naming, hah) - added `getPointOnCircle` which was being done in a couple places independently and refactored those places. ### Questions - the `getPointOnCircle` API changed. Is this considered breaking and/or should I leave the signature the same? Wasn't sure if it was a big deal or not. - I made `labelPosition` in the schema always but I guess it could have been optional? Lemme know if there's a preference. - Any feedback on tests? Happy to expand those if necessary. ### Change Type - [ ] `patch` — Bug fix - [x] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. For arrow in [straightArrow, curvedArrow] test the following: a. Label in the middle b. Label at both ends of the arrow c. Test arrows in different directions d. Rotating the endpoints and seeing that the label stays at the end of the arrow at a fixed width. e. Test different stroke widths. f. Test with different arrowheads. 2. Also, test arcs that are more circle like than arc-like. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Adds ability to change label position on arrows. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> Co-authored-by: alex <alex@dytry.ch>
2024-01-24 10:19:20 +00:00
if (shape.props.text.trim()) {
const bodyBounds = bodyGeom.bounds
const { w, h } = editor.textMeasure.measureText(shape.props.text, {
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
maxWidth: null,
})
width = w
height = h
if (bodyBounds.width > bodyBounds.height) {
width = Math.max(Math.min(w, 64), Math.min(bodyBounds.width - 64, w))
const { w: squishedWidth, h: squishedHeight } = editor.textMeasure.measureText(
shape.props.text,
{
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
maxWidth: width,
}
)
width = squishedWidth
height = squishedHeight
}
if (width > 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]) {
width = 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]
const { w: squishedWidth, h: squishedHeight } = editor.textMeasure.measureText(
shape.props.text,
{
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
maxWidth: width,
}
)
width = squishedWidth
height = squishedHeight
}
}
const size = new Vec(width, height).addScalar(ARROW_LABEL_PADDING * 2)
labelSizeCache.set(shape, size)
return size
}
function getLabelToArrowPadding(editor: Editor, shape: TLArrowShape) {
const strokeWidth = STROKE_SIZES[shape.props.size]
const labelToArrowPadding =
LABEL_TO_ARROW_PADDING +
(strokeWidth - STROKE_SIZES.s) * 2 +
(strokeWidth === STROKE_SIZES.xl ? 20 : 0)
return labelToArrowPadding
}
/**
* Return the range of possible label positions for a straight arrow. The full possible range is 0
* to 1, but as the label itself takes up space the usable range is smaller.
*/
function getStraightArrowLabelRange(
editor: Editor,
shape: TLArrowShape,
info: Extract<TLArrowInfo, { isStraight: true }>
): { start: number; end: number } {
const labelSize = getArrowLabelSize(editor, shape)
const labelToArrowPadding = getLabelToArrowPadding(editor, shape)
// take the start and end points of the arrow, and nudge them in a bit to give some spare space:
const startOffset = Vec.Nudge(info.start.point, info.end.point, labelToArrowPadding)
const endOffset = Vec.Nudge(info.end.point, info.start.point, labelToArrowPadding)
// assuming we just stick the label in the middle of the shape, where does the arrow intersect the label?
const intersectionPoints = intersectLineSegmentPolygon(
startOffset,
endOffset,
Box.FromCenter(info.middle, labelSize).corners
)
if (!intersectionPoints || intersectionPoints.length !== 2) {
return { start: 0.5, end: 0.5 }
}
// there should be two intersection points - one near the start, and one near the end
let [startIntersect, endIntersect] = intersectionPoints
if (Vec.Dist2(startIntersect, startOffset) > Vec.Dist2(endIntersect, startOffset)) {
;[endIntersect, startIntersect] = intersectionPoints
}
// take our nudged start and end points and scooch them in even further to give us the possible
// range for the position of the _center_ of the label
const startConstrained = startOffset.add(Vec.Sub(info.middle, startIntersect))
const endConstrained = endOffset.add(Vec.Sub(info.middle, endIntersect))
// now we can work out the range of possible label positions
const start = Vec.Dist(info.start.point, startConstrained) / info.length
const end = Vec.Dist(info.start.point, endConstrained) / info.length
return { start, end }
}
/**
* Return the range of possible label positions for a curved arrow. The full possible range is 0
* to 1, but as the label itself takes up space the usable range is smaller.
*/
function getCurvedArrowLabelRange(
editor: Editor,
shape: TLArrowShape,
info: Extract<TLArrowInfo, { isStraight: false }>
): { start: number; end: number; dbg?: Geometry2d[] } {
const labelSize = getArrowLabelSize(editor, shape)
const labelToArrowPadding = getLabelToArrowPadding(editor, shape)
const direction = Math.sign(shape.props.bend)
// take the start and end points of the arrow, and nudge them in a bit to give some spare space:
const labelToArrowPaddingRad = (labelToArrowPadding / info.handleArc.radius) * direction
const startOffsetAngle = Vec.Angle(info.bodyArc.center, info.start.point) - labelToArrowPaddingRad
const endOffsetAngle = Vec.Angle(info.bodyArc.center, info.end.point) + labelToArrowPaddingRad
const startOffset = getPointOnCircle(info.bodyArc.center, info.bodyArc.radius, startOffsetAngle)
const endOffset = getPointOnCircle(info.bodyArc.center, info.bodyArc.radius, endOffsetAngle)
const dbg: Geometry2d[] = []
// unlike the straight arrow, we can't just stick the label in the middle of the shape when
// we're working out the range. this is because as the label moves along the curve, the place
// where the arrow intersects with label changes. instead, we have to stick the label center on
// the `startOffset` (the start-most place where it can go), then find where it intersects with
// the arc. because of the symmetry of the label rectangle, we can move the label to that new
// center and take that as the start-most possible point.
const startIntersections = intersectArcPolygon(
info.bodyArc.center,
info.bodyArc.radius,
startOffsetAngle,
endOffsetAngle,
direction,
Box.FromCenter(startOffset, labelSize).corners
)
dbg.push(
new Polygon2d({
points: Box.FromCenter(startOffset, labelSize).corners,
debugColor: 'lime',
isFilled: false,
ignore: true,
})
)
const endIntersections = intersectArcPolygon(
info.bodyArc.center,
info.bodyArc.radius,
startOffsetAngle,
endOffsetAngle,
direction,
Box.FromCenter(endOffset, labelSize).corners
)
dbg.push(
new Polygon2d({
points: Box.FromCenter(endOffset, labelSize).corners,
debugColor: 'lime',
isFilled: false,
ignore: true,
})
)
for (const pt of [
...(startIntersections ?? []),
...(endIntersections ?? []),
startOffset,
endOffset,
]) {
dbg.push(
new Circle2d({
x: pt.x - 3,
y: pt.y - 3,
radius: 3,
isFilled: false,
debugColor: 'magenta',
ignore: true,
})
)
}
// if we have one or more intersections (we shouldn't have more than two) then the one we need
// is the one furthest from the arrow terminal
const startConstrained =
(startIntersections && furthest(info.start.point, startIntersections)) ?? info.middle
const endConstrained =
(endIntersections && furthest(info.end.point, endIntersections)) ?? info.middle
const startAngle = Vec.Angle(info.bodyArc.center, info.start.point)
const endAngle = Vec.Angle(info.bodyArc.center, info.end.point)
const constrainedStartAngle = Vec.Angle(info.bodyArc.center, startConstrained)
const constrainedEndAngle = Vec.Angle(info.bodyArc.center, endConstrained)
// if the arc is small enough that there's no room for the label to move, we constrain it to the middle.
if (
angleDistance(startAngle, constrainedStartAngle, direction) >
angleDistance(startAngle, constrainedEndAngle, direction)
) {
return { start: 0.5, end: 0.5, dbg }
}
// now we can work out the range of possible label positions
const fullDistance = angleDistance(startAngle, endAngle, direction)
const start = angleDistance(startAngle, constrainedStartAngle, direction) / fullDistance
const end = angleDistance(startAngle, constrainedEndAngle, direction) / fullDistance
return { start, end, dbg }
}
export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
let labelCenter
const debugGeom: Geometry2d[] = []
const info = editor.getArrowInfo(shape)!
const hasStartArrowhead = info.start.arrowhead !== 'none'
const hasEndArrowhead = info.end.arrowhead !== 'none'
arrows: add ability to change label placement (#2557) This adds the ability to drag the label on an arrow to a different location within the line segment/arc. https://github.com/tldraw/tldraw/assets/469604/dbd2ee35-bebc-48d6-b8ee-fcf12ce91fa5 - A lot of the complexity lay in ensuring a fixed distance from the ends of the arrowheads. - I added a new type of handle `text-adjust` that makes the text box the very handle itself. - I added a `ARROW_HANDLES` enum - we should use more enums! - The bulk of the changes are in ArrowShapeUtil — check that out in particular obviously :) Along the way, I tried to improve a couple spots as I touched them: - added some more documentation to Vec.ts because some of the functions in there were obscure/new to me. (at least the naming, hah) - added `getPointOnCircle` which was being done in a couple places independently and refactored those places. ### Questions - the `getPointOnCircle` API changed. Is this considered breaking and/or should I leave the signature the same? Wasn't sure if it was a big deal or not. - I made `labelPosition` in the schema always but I guess it could have been optional? Lemme know if there's a preference. - Any feedback on tests? Happy to expand those if necessary. ### Change Type - [ ] `patch` — Bug fix - [x] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. For arrow in [straightArrow, curvedArrow] test the following: a. Label in the middle b. Label at both ends of the arrow c. Test arrows in different directions d. Rotating the endpoints and seeing that the label stays at the end of the arrow at a fixed width. e. Test different stroke widths. f. Test with different arrowheads. 2. Also, test arcs that are more circle like than arc-like. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Adds ability to change label position on arrows. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> Co-authored-by: alex <alex@dytry.ch>
2024-01-24 10:19:20 +00:00
if (info.isStraight) {
const range = getStraightArrowLabelRange(editor, shape, info)
let clampedPosition = clamp(
shape.props.labelPosition,
hasStartArrowhead ? range.start : 0,
hasEndArrowhead ? range.end : 1
)
// This makes the position snap in the middle.
clampedPosition = clampedPosition >= 0.48 && clampedPosition <= 0.52 ? 0.5 : clampedPosition
arrows: add ability to change label placement (#2557) This adds the ability to drag the label on an arrow to a different location within the line segment/arc. https://github.com/tldraw/tldraw/assets/469604/dbd2ee35-bebc-48d6-b8ee-fcf12ce91fa5 - A lot of the complexity lay in ensuring a fixed distance from the ends of the arrowheads. - I added a new type of handle `text-adjust` that makes the text box the very handle itself. - I added a `ARROW_HANDLES` enum - we should use more enums! - The bulk of the changes are in ArrowShapeUtil — check that out in particular obviously :) Along the way, I tried to improve a couple spots as I touched them: - added some more documentation to Vec.ts because some of the functions in there were obscure/new to me. (at least the naming, hah) - added `getPointOnCircle` which was being done in a couple places independently and refactored those places. ### Questions - the `getPointOnCircle` API changed. Is this considered breaking and/or should I leave the signature the same? Wasn't sure if it was a big deal or not. - I made `labelPosition` in the schema always but I guess it could have been optional? Lemme know if there's a preference. - Any feedback on tests? Happy to expand those if necessary. ### Change Type - [ ] `patch` — Bug fix - [x] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. For arrow in [straightArrow, curvedArrow] test the following: a. Label in the middle b. Label at both ends of the arrow c. Test arrows in different directions d. Rotating the endpoints and seeing that the label stays at the end of the arrow at a fixed width. e. Test different stroke widths. f. Test with different arrowheads. 2. Also, test arcs that are more circle like than arc-like. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Adds ability to change label position on arrows. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> Co-authored-by: alex <alex@dytry.ch>
2024-01-24 10:19:20 +00:00
labelCenter = Vec.Lrp(info.start.point, info.end.point, clampedPosition)
} else {
const range = getCurvedArrowLabelRange(editor, shape, info)
if (range.dbg) debugGeom.push(...range.dbg)
let clampedPosition = clamp(
shape.props.labelPosition,
hasStartArrowhead ? range.start : 0,
hasEndArrowhead ? range.end : 1
)
// This makes the position snap in the middle.
clampedPosition = clampedPosition >= 0.48 && clampedPosition <= 0.52 ? 0.5 : clampedPosition
arrows: add ability to change label placement (#2557) This adds the ability to drag the label on an arrow to a different location within the line segment/arc. https://github.com/tldraw/tldraw/assets/469604/dbd2ee35-bebc-48d6-b8ee-fcf12ce91fa5 - A lot of the complexity lay in ensuring a fixed distance from the ends of the arrowheads. - I added a new type of handle `text-adjust` that makes the text box the very handle itself. - I added a `ARROW_HANDLES` enum - we should use more enums! - The bulk of the changes are in ArrowShapeUtil — check that out in particular obviously :) Along the way, I tried to improve a couple spots as I touched them: - added some more documentation to Vec.ts because some of the functions in there were obscure/new to me. (at least the naming, hah) - added `getPointOnCircle` which was being done in a couple places independently and refactored those places. ### Questions - the `getPointOnCircle` API changed. Is this considered breaking and/or should I leave the signature the same? Wasn't sure if it was a big deal or not. - I made `labelPosition` in the schema always but I guess it could have been optional? Lemme know if there's a preference. - Any feedback on tests? Happy to expand those if necessary. ### Change Type - [ ] `patch` — Bug fix - [x] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. For arrow in [straightArrow, curvedArrow] test the following: a. Label in the middle b. Label at both ends of the arrow c. Test arrows in different directions d. Rotating the endpoints and seeing that the label stays at the end of the arrow at a fixed width. e. Test different stroke widths. f. Test with different arrowheads. 2. Also, test arcs that are more circle like than arc-like. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Adds ability to change label position on arrows. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> Co-authored-by: alex <alex@dytry.ch>
2024-01-24 10:19:20 +00:00
const labelAngle = interpolateArcAngles(
Vec.Angle(info.bodyArc.center, info.start.point),
Vec.Angle(info.bodyArc.center, info.end.point),
Math.sign(shape.props.bend),
clampedPosition
)
labelCenter = getPointOnCircle(info.bodyArc.center, info.bodyArc.radius, labelAngle)
}
const labelSize = getArrowLabelSize(editor, shape)
return { box: Box.FromCenter(labelCenter, labelSize), debugGeom }
}
function intersectArcPolygon(
center: VecLike,
radius: number,
angleStart: number,
angleEnd: number,
direction: number,
polygon: VecLike[]
) {
const intersections = intersectCirclePolygon(center, radius, polygon)
// filter the circle intersections to just the ones from the arc
const fullArcDistance = angleDistance(angleStart, angleEnd, direction)
return intersections?.filter((pt) => {
const pDistance = angleDistance(angleStart, Vec.Angle(center, pt), direction)
return pDistance >= 0 && pDistance <= fullArcDistance
})
}
function furthest(from: VecLike, candidates: VecLike[]): VecLike | null {
let furthest: VecLike | null = null
let furthestDist = -Infinity
for (const candidate of candidates) {
const dist = Vec.Dist2(from, candidate)
if (dist > furthestDist) {
furthest = candidate
furthestDist = dist
}
}
return furthest
}
/**
*
* @param angleStart - The angle of the start of the arc
* @param angleEnd - The angle of the end of the arc
* @param direction - The direction of the arc (1 = counter-clockwise, -1 = clockwise)
* @param t - A number between 0 and 1 representing the position along the arc
* @returns
*/
function interpolateArcAngles(angleStart: number, angleEnd: number, direction: number, t: number) {
const dist = angleDistance(angleStart, angleEnd, direction)
return angleStart + dist * t * direction * -1
}