Tldraw/packages/editor/src/lib/primitives/Vec.ts

584 wiersze
12 KiB
TypeScript
Czysty Zwykły widok Historia

import { VecModel } from '@tldraw/tlschema'
2023-04-25 11:01:25 +00:00
import { EASINGS } from './easings'
/** @public */
export type VecLike = Vec | VecModel
2023-04-25 11:01:25 +00:00
/** @public */
export class Vec {
constructor(
public x = 0,
public y = 0,
public z = 1
) {}
2023-04-25 11:01:25 +00:00
// eslint-disable-next-line no-restricted-syntax
2023-04-25 11:01:25 +00:00
get pressure() {
return this.z
}
set(x = this.x, y = this.y, z = this.z) {
this.x = x
this.y = y
this.z = z
return this
}
setTo({ x = 0, y = 0, z = 1 }: VecLike) {
this.x = x
this.y = y
this.z = z
return this
}
rot(r: number) {
if (r === 0) return this
const { x, y } = this
const s = Math.sin(r)
const c = Math.cos(r)
this.x = x * c - y * s
this.y = x * s + y * c
return this
}
rotWith(C: VecLike, r: number) {
if (r === 0) return this
const x = this.x - C.x
const y = this.y - C.y
const s = Math.sin(r)
const c = Math.cos(r)
this.x = C.x + (x * c - y * s)
this.y = C.y + (x * s + y * c)
return this
}
clone(): Vec {
2023-04-25 11:01:25 +00:00
const { x, y, z } = this
return new Vec(x, y, z)
2023-04-25 11:01:25 +00:00
}
sub(V: VecLike) {
this.x -= V.x
this.y -= V.y
return this
}
subXY(x: number, y: number) {
this.x -= x
this.y -= y
return this
}
subScalar(n: number) {
this.x -= n
this.y -= n
// this.z -= n
return this
}
add(V: VecLike) {
this.x += V.x
this.y += V.y
return this
}
addXY(x: number, y: number) {
this.x += x
this.y += y
return this
}
addScalar(n: number) {
this.x += n
this.y += n
// this.z += n
return this
}
clamp(min: number, max?: number) {
this.x = Math.max(this.x, min)
this.y = Math.max(this.y, min)
if (max !== undefined) {
this.x = Math.min(this.x, max)
this.y = Math.min(this.y, max)
}
return this
}
div(t: number) {
this.x /= t
this.y /= t
// this.z /= t
return this
}
divV(V: VecLike) {
this.x /= V.x
this.y /= V.y
// this.z /= V.z
return this
}
mul(t: number) {
this.x *= t
this.y *= t
// this.z *= t
return this
}
mulV(V: VecLike) {
this.x *= V.x
this.y *= V.y
// this.z *= V.z
return this
}
abs() {
this.x = Math.abs(this.x)
this.y = Math.abs(this.y)
return this
}
nudge(B: VecLike, distance: number) {
const tan = Vec.Tan(B, this)
2023-04-25 11:01:25 +00:00
return this.add(tan.mul(distance))
}
neg() {
this.x *= -1
this.y *= -1
// this.z *= -1
return this
}
cross(V: VecLike) {
this.x = this.y * V.z! - this.z * V.y
this.y = this.z * V.x - this.x * V.z!
// this.z = this.x * V.y - this.y * V.x
return this
}
dpr(V: VecLike): number {
return Vec.Dpr(this, V)
2023-04-25 11:01:25 +00:00
}
cpr(V: VecLike) {
return Vec.Cpr(this, V)
2023-04-25 11:01:25 +00:00
}
len2(): number {
return Vec.Len2(this)
2023-04-25 11:01:25 +00:00
}
len(): number {
return Vec.Len(this)
2023-04-25 11:01:25 +00:00
}
pry(V: VecLike): number {
return Vec.Pry(this, V)
2023-04-25 11:01:25 +00:00
}
per() {
const { x, y } = this
this.x = y
this.y = -x
return this
}
uni() {
return Vec.Uni(this)
2023-04-25 11:01:25 +00:00
}
tan(V: VecLike): Vec {
return Vec.Tan(this, V)
2023-04-25 11:01:25 +00:00
}
dist(V: VecLike): number {
return Vec.Dist(this, V)
2023-04-25 11:01:25 +00:00
}
distanceToLineSegment(A: VecLike, B: VecLike): number {
return Vec.DistanceToLineSegment(A, B, this)
2023-04-25 11:01:25 +00:00
}
slope(B: VecLike): number {
return Vec.Slope(this, B)
2023-04-25 11:01:25 +00:00
}
snapToGrid(gridSize: number) {
this.x = Math.round(this.x / gridSize) * gridSize
this.y = Math.round(this.y / gridSize) * gridSize
return this
}
angle(B: VecLike): number {
return Vec.Angle(this, B)
2023-04-25 11:01:25 +00:00
}
toAngle() {
return Vec.ToAngle(this)
2023-04-25 11:01:25 +00:00
}
lrp(B: VecLike, t: number): Vec {
2023-04-25 11:01:25 +00:00
this.x = this.x + (B.x - this.x) * t
this.y = this.y + (B.y - this.y) * t
return this
}
equals(B: VecLike) {
return Vec.Equals(this, B)
2023-04-25 11:01:25 +00:00
}
equalsXY(x: number, y: number) {
return Vec.EqualsXY(this, x, y)
2023-04-25 11:01:25 +00:00
}
norm() {
const l = this.len()
this.x = l === 0 ? 0 : this.x / l
this.y = l === 0 ? 0 : this.y / l
return this
}
toFixed() {
return Vec.ToFixed(this)
2023-04-25 11:01:25 +00:00
}
toString() {
return Vec.ToString(Vec.ToFixed(this))
2023-04-25 11:01:25 +00:00
}
toJson(): VecModel {
return Vec.ToJson(this)
2023-04-25 11:01:25 +00:00
}
toArray(): number[] {
return Vec.ToArray(this)
2023-04-25 11:01:25 +00:00
}
static Add(A: VecLike, B: VecLike): Vec {
return new Vec(A.x + B.x, A.y + B.y)
2023-04-25 11:01:25 +00:00
}
static AddXY(A: VecLike, x: number, y: number): Vec {
return new Vec(A.x + x, A.y + y)
2023-04-25 11:01:25 +00:00
}
static Sub(A: VecLike, B: VecLike): Vec {
return new Vec(A.x - B.x, A.y - B.y)
2023-04-25 11:01:25 +00:00
}
static SubXY(A: VecLike, x: number, y: number): Vec {
return new Vec(A.x - x, A.y - y)
2023-04-25 11:01:25 +00:00
}
static AddScalar(A: VecLike, n: number): Vec {
return new Vec(A.x + n, A.y + n)
2023-04-25 11:01:25 +00:00
}
static SubScalar(A: VecLike, n: number): Vec {
return new Vec(A.x - n, A.y - n)
2023-04-25 11:01:25 +00:00
}
static Div(A: VecLike, t: number): Vec {
return new Vec(A.x / t, A.y / t)
2023-04-25 11:01:25 +00:00
}
static Mul(A: VecLike, t: number): Vec {
return new Vec(A.x * t, A.y * t)
2023-04-25 11:01:25 +00:00
}
static DivV(A: VecLike, B: VecLike): Vec {
return new Vec(A.x / B.x, A.y / B.y)
2023-04-25 11:01:25 +00:00
}
static MulV(A: VecLike, B: VecLike): Vec {
return new Vec(A.x * B.x, A.y * B.y)
2023-04-25 11:01:25 +00:00
}
static Neg(A: VecLike): Vec {
return new Vec(-A.x, -A.y)
2023-04-25 11:01:25 +00:00
}
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
/**
* Get the perpendicular vector to A.
*/
static Per(A: VecLike): Vec {
return new Vec(A.y, -A.x)
2023-04-25 11:01:25 +00:00
}
static Abs(A: VecLike): Vec {
return new Vec(Math.abs(A.x), Math.abs(A.y))
2023-04-25 11:01:25 +00:00
}
// Get the distance between two points.
2023-04-25 11:01:25 +00:00
static Dist(A: VecLike, B: VecLike): number {
return Math.hypot(A.y - B.y, A.x - B.x)
}
// Get whether a distance between two points is less than a number. This is faster to calulate than using `Vec.Dist(a, b) < n`.
static DistMin(A: VecLike, B: VecLike, n: number): boolean {
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y) < n ** 2
}
// Get the squared distance between two points. This is faster to calculate (no square root) so useful for "minimum distance" checks where the actual measurement does not matter.
static Dist2(A: VecLike, B: VecLike): number {
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y)
}
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
/**
* Dot product of two vectors which is used to calculate the angle between them.
*/
2023-04-25 11:01:25 +00:00
static Dpr(A: VecLike, B: VecLike): number {
return A.x * B.x + A.y * B.y
}
static Cross(A: VecLike, V: VecLike) {
return new Vec(
2023-04-25 11:01:25 +00:00
A.y * V.z! - A.z! * V.y,
A.z! * V.x - A.x * V.z!
// A.z = A.x * V.y - A.y * V.x
)
}
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
/**
* Cross product of two vectors which is used to calculate the area of a parallelogram.
*/
2023-04-25 11:01:25 +00:00
static Cpr(A: VecLike, B: VecLike) {
return A.x * B.y - B.x * A.y
}
static Len2(A: VecLike): number {
return A.x * A.x + A.y * A.y
}
static Len(A: VecLike): number {
return Math.hypot(A.x, A.y)
2023-04-25 11:01:25 +00:00
}
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
/**
* Get the projection of A onto B.
*/
2023-04-25 11:01:25 +00:00
static Pry(A: VecLike, B: VecLike): number {
return Vec.Dpr(A, B) / Vec.Len(B)
2023-04-25 11:01:25 +00:00
}
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
/**
* Get the unit vector of A.
*/
2023-04-25 11:01:25 +00:00
static Uni(A: VecLike) {
return Vec.Div(A, Vec.Len(A))
2023-04-25 11:01:25 +00:00
}
static Tan(A: VecLike, B: VecLike): Vec {
return Vec.Uni(Vec.Sub(A, B))
2023-04-25 11:01:25 +00:00
}
static Min(A: VecLike, B: VecLike): Vec {
return new Vec(Math.min(A.x, B.x), Math.min(A.y, B.y))
2023-04-25 11:01:25 +00:00
}
static Max(A: VecLike, B: VecLike): Vec {
return new Vec(Math.max(A.x, B.x), Math.max(A.y, B.y))
2023-04-25 11:01:25 +00:00
}
static From({ x, y, z = 1 }: VecModel) {
return new Vec(x, y, z)
2023-04-25 11:01:25 +00:00
}
static FromArray(v: number[]): Vec {
return new Vec(v[0], v[1])
2023-04-25 11:01:25 +00:00
}
static Rot(A: VecLike, r = 0): Vec {
2023-04-25 11:01:25 +00:00
const s = Math.sin(r)
const c = Math.cos(r)
return new Vec(A.x * c - A.y * s, A.x * s + A.y * c)
2023-04-25 11:01:25 +00:00
}
static RotWith(A: VecLike, C: VecLike, r: number): Vec {
2023-04-25 11:01:25 +00:00
const x = A.x - C.x
const y = A.y - C.y
const s = Math.sin(r)
const c = Math.cos(r)
return new Vec(C.x + (x * c - y * s), C.y + (x * s + y * c))
2023-04-25 11:01:25 +00:00
}
/**
* Get the nearest point on a line with a known unit vector that passes through point A
*
* ```ts
* Vec.nearestPointOnLineThroughPoint(A, u, Point)
* ```
*
* @param A - Any point on the line
* @param u - The unit vector for the line.
* @param P - A point not on the line to test.
*/
static NearestPointOnLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): Vec {
return Vec.Mul(u, Vec.Sub(P, A).pry(u)).add(A)
2023-04-25 11:01:25 +00:00
}
static NearestPointOnLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp = true): Vec {
if (Vec.Equals(A, P)) return Vec.From(P)
if (Vec.Equals(B, P)) return Vec.From(P)
const u = Vec.Tan(B, A)
const C = Vec.Add(A, Vec.Mul(u, Vec.Sub(P, A).pry(u)))
2023-04-25 11:01:25 +00:00
if (clamp) {
if (C.x < Math.min(A.x, B.x)) return Vec.Cast(A.x < B.x ? A : B)
if (C.x > Math.max(A.x, B.x)) return Vec.Cast(A.x > B.x ? A : B)
if (C.y < Math.min(A.y, B.y)) return Vec.Cast(A.y < B.y ? A : B)
if (C.y > Math.max(A.y, B.y)) return Vec.Cast(A.y > B.y ? A : B)
2023-04-25 11:01:25 +00:00
}
return C
}
static DistanceToLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): number {
return Vec.Dist(P, Vec.NearestPointOnLineThroughPoint(A, u, P))
2023-04-25 11:01:25 +00:00
}
static DistanceToLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp = true): number {
return Vec.Dist(P, Vec.NearestPointOnLineSegment(A, B, P, clamp))
2023-04-25 11:01:25 +00:00
}
static Snap(A: VecLike, step = 1) {
return new Vec(Math.round(A.x / step) * step, Math.round(A.y / step) * step)
2023-04-25 11:01:25 +00:00
}
static Cast(A: VecLike): Vec {
if (A instanceof Vec) return A
return Vec.From(A)
2023-04-25 11:01:25 +00:00
}
static Slope(A: VecLike, B: VecLike): number {
if (A.x === B.y) return NaN
return (A.y - B.y) / (A.x - B.x)
}
static IsNaN(A: VecLike): boolean {
return isNaN(A.x) || isNaN(A.y)
}
2023-04-25 11:01:25 +00:00
static Angle(A: VecLike, B: VecLike): number {
return Math.atan2(B.y - A.y, B.x - A.x)
}
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
/**
* Linearly interpolate between two points.
* @param A - The first point.
* @param B - The second point.
* @param t - The interpolation value between 0 and 1.
* @returns The interpolated point.
*/
static Lrp(A: VecLike, B: VecLike, t: number): Vec {
return Vec.Sub(B, A).mul(t).add(A)
2023-04-25 11:01:25 +00:00
}
static Med(A: VecLike, B: VecLike): Vec {
return new Vec((A.x + B.x) / 2, (A.y + B.y) / 2)
2023-04-25 11:01:25 +00:00
}
static Equals(A: VecLike, B: VecLike): boolean {
return Math.abs(A.x - B.x) < 0.0001 && Math.abs(A.y - B.y) < 0.0001
}
static EqualsXY(A: VecLike, x: number, y: number): boolean {
return A.x === x && A.y === y
}
static Clockwise(A: VecLike, B: VecLike, C: VecLike): boolean {
return (C.x - A.x) * (B.y - A.y) - (B.x - A.x) * (C.y - A.y) < 0
}
static Rescale(A: VecLike, n: number) {
const l = Vec.Len(A)
return new Vec((n * A.x) / l, (n * A.y) / l)
2023-04-25 11:01:25 +00:00
}
static ScaleWithOrigin(A: VecLike, scale: number, origin: VecLike) {
return Vec.Sub(A, origin).mul(scale).add(origin)
2023-04-25 11:01:25 +00:00
}
static ToFixed(A: VecLike, n = 2) {
return new Vec(+A.x.toFixed(n), +A.y.toFixed(n), +A.z!.toFixed(n))
2023-04-25 11:01:25 +00:00
}
static Nudge(A: VecLike, B: VecLike, distance: number) {
return Vec.Add(A, Vec.Tan(B, A).mul(distance))
2023-04-25 11:01:25 +00:00
}
static ToString(A: VecLike) {
return `${A.x}, ${A.y}`
}
static ToAngle(A: VecLike) {
let r = Math.atan2(A.y, A.x)
if (r < 0) r += Math.PI * 2
return r
}
static FromAngle(r: number, length = 1) {
return new Vec(Math.cos(r) * length, Math.sin(r) * length)
}
2023-04-25 11:01:25 +00:00
static ToArray(A: VecLike) {
return [A.x, A.y, A.z!]
}
static ToJson(A: VecLike) {
const { x, y, z } = A
return { x, y, z }
}
static Average(arr: VecLike[]) {
const len = arr.length
const avg = new Vec(0, 0)
if (len === 0) {
return avg
}
2023-04-25 11:01:25 +00:00
for (let i = 0; i < len; i++) {
avg.add(arr[i])
}
return avg.div(len)
}
static Clamp(A: Vec, min: number, max?: number) {
2023-04-25 11:01:25 +00:00
if (max === undefined) {
return new Vec(Math.min(Math.max(A.x, min)), Math.min(Math.max(A.y, min)))
2023-04-25 11:01:25 +00:00
}
return new Vec(Math.min(Math.max(A.x, min), max), Math.min(Math.max(A.y, min), max))
2023-04-25 11:01:25 +00:00
}
/**
* Get an array of points (with simulated pressure) between two points.
*
* @param A - The first point.
* @param B - The second point.
* @param steps - The number of points to return.
*/
static PointsBetween(A: VecModel, B: VecModel, steps = 6): Vec[] {
const results: Vec[] = []
2023-04-25 11:01:25 +00:00
for (let i = 0; i < steps; i++) {
const t = EASINGS.easeInQuad(i / (steps - 1))
const point = Vec.Lrp(A, B, t)
2023-04-25 11:01:25 +00:00
point.z = Math.min(1, 0.5 + Math.abs(0.5 - ease(t)) * 0.65)
results.push(point)
}
return results
}
static SnapToGrid(A: VecLike, gridSize = 8) {
return new Vec(Math.round(A.x / gridSize) * gridSize, Math.round(A.y / gridSize) * gridSize)
2023-04-25 11:01:25 +00:00
}
}
const ease = (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)