Adds ellipse binding

This commit is contained in:
Steve Ruiz 2021-08-11 15:51:24 +01:00
parent 94f0273ca4
commit b737a42ca9
5 changed files with 281 additions and 443 deletions

View file

@ -4,10 +4,7 @@ import { Utils } from './utils'
/* ----------------- Start Copy Here ---------------- */
function getIntersection(
message: string,
...points: number[][]
): TLIntersection {
function getIntersection(message: string, ...points: number[][]): TLIntersection {
const didIntersect = points.length > 0
return { didIntersect, message, points }
}
@ -15,12 +12,7 @@ function getIntersection(
export class Intersect {
static ray = {
// Intersect a ray with a ray.
ray(
p0: number[],
n0: number[],
p1: number[],
n1: number[]
): TLIntersection {
ray(p0: number[], n0: number[], p1: number[], n1: number[]): TLIntersection {
const dx = p1[0] - p0[0]
const dy = p1[1] - p0[1]
const det = n1[0] * n0[1] - n1[1] * n0[0]
@ -41,12 +33,7 @@ export class Intersect {
},
// Interseg a ray with a line segment.
lineSegment(
origin: number[],
direction: number[],
a1: number[],
a2: number[]
): TLIntersection {
lineSegment(origin: number[], direction: number[], a1: number[], a2: number[]): TLIntersection {
const [x, y] = origin
const [dx, dy] = direction
const [x1, y1] = a1
@ -70,9 +57,10 @@ export class Intersect {
origin: number[],
direction: number[],
point: number[],
size: number[]
size: number[],
rotation = 0
): TLIntersection[] {
return Intersect.rectangle.ray(point, size, origin, direction)
return Intersect.rectangle.ray(point, size, rotation, origin, direction)
},
// Intersect a ray with an ellipse.
@ -93,36 +81,22 @@ export class Intersect {
bounds(
origin: number[],
direction: number[],
bounds: TLBounds
bounds: TLBounds,
rotation = 0
): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.ray.rectangle(
origin,
direction,
[minX, minY],
[width, height]
)
return Intersect.ray.rectangle(origin, direction, [minX, minY], [width, height], rotation)
},
}
static lineSegment = {
// Intersect a line segment with a ray.
ray(
a1: number[],
a2: number[],
origin: number[],
direction: number[]
): TLIntersection {
ray(a1: number[], a2: number[], origin: number[], direction: number[]): TLIntersection {
return Intersect.ray.lineSegment(origin, direction, a1, a2)
},
// Intersect a line segment with a line segment.
lineSegment(
a1: number[],
a2: number[],
b1: number[],
b2: number[]
): TLIntersection {
lineSegment(a1: number[], a2: number[], b1: number[], b2: number[]): TLIntersection {
const AB = Vec.sub(a1, b1)
const BV = Vec.sub(b2, b1)
const AV = Vec.sub(a2, a1)
@ -151,12 +125,7 @@ export class Intersect {
},
// Intersect a line segment with a rectangle
rectangle(
a1: number[],
a2: number[],
point: number[],
size: number[]
): TLIntersection[] {
rectangle(a1: number[], a2: number[], point: number[], size: number[]): TLIntersection[] {
return Intersect.rectangle.lineSegment(point, size, a1, a2)
},
@ -171,14 +140,7 @@ export class Intersect {
): TLIntersection {
const sa = Vec.angle(center, start)
const ea = Vec.angle(center, end)
const ellipseTest = Intersect.ellipse.lineSegment(
center,
radius,
radius,
0,
a1,
a2
)
const ellipseTest = Intersect.ellipse.lineSegment(center, radius, radius, 0, a1, a2)
if (!ellipseTest.didIntersect) return getIntersection('No intersection')
@ -195,11 +157,8 @@ export class Intersect {
// Intersect a line segment with a circle.
circle(a1: number[], a2: number[], c: number[], r: number): TLIntersection {
const a =
(a2[0] - a1[0]) * (a2[0] - a1[0]) + (a2[1] - a1[1]) * (a2[1] - a1[1])
const b =
2 *
((a2[0] - a1[0]) * (a1[0] - c[0]) + (a2[1] - a1[1]) * (a1[1] - c[1]))
const a = (a2[0] - a1[0]) * (a2[0] - a1[0]) + (a2[1] - a1[1]) * (a2[1] - a1[1])
const b = 2 * ((a2[0] - a1[0]) * (a1[0] - c[0]) + (a2[1] - a1[1]) * (a1[1] - c[1]))
const cc =
c[0] * c[0] +
c[1] * c[1] +
@ -262,8 +221,7 @@ export class Intersect {
const diff = Vec.sub(a2, a1)
const A = (diff[0] * diff[0]) / rx / rx + (diff[1] * diff[1]) / ry / ry
const B =
(2 * a1[0] * diff[0]) / rx / rx + (2 * a1[1] * diff[1]) / ry / ry
const B = (2 * a1[0] * diff[0]) / rx / rx + (2 * a1[1] * diff[1]) / ry / ry
const C = (a1[0] * a1[0]) / rx / rx + (a1[1] * a1[1]) / ry / ry - 1
// Make a list of t values (normalized points on the line where intersections occur).
@ -323,18 +281,14 @@ export class Intersect {
ray(
point: number[],
size: number[],
rotation: number,
origin: number[],
direction: number[]
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<
const sideIntersections = Utils.getRectangleSides(point, size, rotation).reduce<
TLIntersection[]
>((acc, [message, [a1, a2]]) => {
const intersection = Intersect.ray.lineSegment(
origin,
direction,
a1,
a2
)
const intersection = Intersect.ray.lineSegment(origin, direction, a1, a2)
if (intersection) {
acc.push(getIntersection(message, ...intersection.points))
@ -347,15 +301,9 @@ export class Intersect {
},
// Intersect a rectangle with a line segment.
lineSegment(
point: number[],
size: number[],
a1: number[],
a2: number[]
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<
TLIntersection[]
>((acc, [message, [b1, b2]]) => {
lineSegment(point: number[], size: number[], a1: number[], a2: number[]): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<TLIntersection[]>(
(acc, [message, [b1, b2]]) => {
const intersection = Intersect.lineSegment.lineSegment(a1, a2, b1, b2)
if (intersection) {
@ -363,7 +311,9 @@ export class Intersect {
}
return acc
}, [])
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
@ -375,15 +325,9 @@ export class Intersect {
point2: number[],
size2: number[]
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point1, size1).reduce<
TLIntersection[]
>((acc, [message, [a1, a2]]) => {
const intersections = Intersect.rectangle.lineSegment(
point2,
size2,
a1,
a2
)
const sideIntersections = Utils.getRectangleSides(point1, size1).reduce<TLIntersection[]>(
(acc, [message, [a1, a2]]) => {
const intersections = Intersect.rectangle.lineSegment(point2, size2, a1, a2)
acc.push(
...intersections.map((int) =>
@ -392,7 +336,9 @@ export class Intersect {
)
return acc
}, [])
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
@ -406,38 +352,26 @@ export class Intersect {
start: number[],
end: number[]
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<
TLIntersection[]
>((acc, [message, [a1, a2]]) => {
const intersection = Intersect.arc.lineSegment(
center,
radius,
start,
end,
a1,
a2
)
const sideIntersections = Utils.getRectangleSides(point, size).reduce<TLIntersection[]>(
(acc, [message, [a1, a2]]) => {
const intersection = Intersect.arc.lineSegment(center, radius, start, end, a1, a2)
if (intersection) {
acc.push({ ...intersection, message })
}
return acc
}, [])
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
// Intersect a rectangle with a circle.
circle(
point: number[],
size: number[],
c: number[],
r: number
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<
TLIntersection[]
>((acc, [message, [a1, a2]]) => {
circle(point: number[], size: number[], c: number[], r: number): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<TLIntersection[]>(
(acc, [message, [a1, a2]]) => {
const intersection = Intersect.lineSegment.circle(a1, a2, c, r)
if (intersection) {
@ -445,7 +379,9 @@ export class Intersect {
}
return acc
}, [])
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
@ -459,62 +395,42 @@ export class Intersect {
ry: number,
rotation = 0
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<
TLIntersection[]
>((acc, [message, [a1, a2]]) => {
const intersection = Intersect.lineSegment.ellipse(
a1,
a2,
c,
rx,
ry,
rotation
)
const sideIntersections = Utils.getRectangleSides(point, size).reduce<TLIntersection[]>(
(acc, [message, [a1, a2]]) => {
const intersection = Intersect.lineSegment.ellipse(a1, a2, c, rx, ry, rotation)
if (intersection) {
acc.push({ ...intersection, message })
}
return acc
}, [])
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
// Intersect a rectangle with a bounding box.
bounds(
point: number[],
size: number[],
bounds: TLBounds
): TLIntersection[] {
bounds(point: number[], size: number[], bounds: TLBounds): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.rectangle.rectangle(
point,
size,
[minX, minY],
[width, height]
)
return Intersect.rectangle.rectangle(point, size, [minX, minY], [width, height])
},
// Intersect a rectangle with a polyline
polyline(
point: number[],
size: number[],
points: number[][]
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<
TLIntersection[]
>((acc, [message, [a1, a2]]) => {
polyline(point: number[], size: number[], points: number[][]): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<TLIntersection[]>(
(acc, [message, [a1, a2]]) => {
const intersections = Intersect.lineSegment.polyline(a1, a2, points)
if (intersections.length > 0) {
acc.push(
getIntersection(message, ...intersections.flatMap((i) => i.points))
)
acc.push(getIntersection(message, ...intersections.flatMap((i) => i.points)))
}
return acc
}, [])
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
@ -554,25 +470,13 @@ export class Intersect {
bounds: TLBounds
): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.arc.rectangle(
center,
radius,
start,
end,
[minX, minY],
[width, height]
)
return Intersect.arc.rectangle(center, radius, start, end, [minX, minY], [width, height])
},
}
static circle = {
// Intersect a circle with a line segment.
lineSegment(
c: number[],
r: number,
a1: number[],
a2: number[]
): TLIntersection {
lineSegment(c: number[], r: number, a1: number[], a2: number[]): TLIntersection {
return Intersect.lineSegment.circle(a1, a2, c, r)
},
@ -596,12 +500,7 @@ export class Intersect {
},
// Intersect a circle with a rectangle.
rectangle(
c: number[],
r: number,
point: number[],
size: number[]
): TLIntersection[] {
rectangle(c: number[], r: number, point: number[], size: number[]): TLIntersection[] {
return Intersect.rectangle.circle(point, size, c, r)
},
@ -693,58 +592,24 @@ export class Intersect {
bounds: TLBounds
): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.ellipse.rectangle(
c,
rx,
ry,
rotation,
[minX, minY],
[width, height]
)
return Intersect.ellipse.rectangle(c, rx, ry, rotation, [minX, minY], [width, height])
},
}
static bounds = {
ray(
bounds: TLBounds,
origin: number[],
direction: number[]
): TLIntersection[] {
ray(bounds: TLBounds, origin: number[], direction: number[]): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.ray.rectangle(
origin,
direction,
[minX, minY],
[width, height]
)
return Intersect.ray.rectangle(origin, direction, [minX, minY], [width, height])
},
lineSegment(
bounds: TLBounds,
a1: number[],
a2: number[]
): TLIntersection[] {
lineSegment(bounds: TLBounds, a1: number[], a2: number[]): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.lineSegment.rectangle(
a1,
a2,
[minX, minY],
[width, height]
)
return Intersect.lineSegment.rectangle(a1, a2, [minX, minY], [width, height])
},
rectangle(
bounds: TLBounds,
point: number[],
size: number[]
): TLIntersection[] {
rectangle(bounds: TLBounds, point: number[], size: number[]): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.rectangle.rectangle(
point,
size,
[minX, minY],
[width, height]
)
return Intersect.rectangle.rectangle(point, size, [minX, minY], [width, height])
},
bounds(bounds1: TLBounds, bounds2: TLBounds): TLIntersection[] {
@ -764,14 +629,7 @@ export class Intersect {
end: number[]
): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.arc.rectangle(
center,
radius,
start,
end,
[minX, minY],
[width, height]
)
return Intersect.arc.rectangle(center, radius, start, end, [minX, minY], [width, height])
},
circle(bounds: TLBounds, c: number[], r: number): TLIntersection[] {
@ -779,22 +637,9 @@ export class Intersect {
return Intersect.circle.rectangle(c, r, [minX, minY], [width, height])
},
ellipse(
bounds: TLBounds,
c: number[],
rx: number,
ry: number,
rotation = 0
): TLIntersection[] {
ellipse(bounds: TLBounds, c: number[], rx: number, ry: number, rotation = 0): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.ellipse.rectangle(
c,
rx,
ry,
rotation,
[minX, minY],
[width, height]
)
return Intersect.ellipse.rectangle(c, rx, ry, rotation, [minX, minY], [width, height])
},
polyline(bounds: TLBounds, points: number[][]): TLIntersection[] {
@ -804,20 +649,12 @@ export class Intersect {
static polyline = {
// Intersect a polyline with a line segment.
lineSegment(
points: number[][],
a1: number[],
a2: number[]
): TLIntersection[] {
lineSegment(points: number[][], a1: number[], a2: number[]): TLIntersection[] {
return Intersect.lineSegment.polyline(a1, a2, points)
},
// Interesct a polyline with a rectangle.
rectangle(
points: number[][],
point: number[],
size: number[]
): TLIntersection[] {
rectangle(points: number[][], point: number[], size: number[]): TLIntersection[] {
return Intersect.rectangle.polyline(point, size, points)
},

View file

@ -4,12 +4,7 @@
import type React from 'react'
import deepmerge from 'deepmerge'
import isMobilePkg from 'ismobilejs'
import {
TLBezierCurveSegment,
TLBounds,
TLBoundsCorner,
TLBoundsEdge,
} from '../types'
import { TLBezierCurveSegment, TLBounds, TLBoundsCorner, TLBoundsEdge } from '../types'
import vec from './vec'
import './polyfills'
@ -22,9 +17,7 @@ export class Utils {
obj: T,
fn: (entry: Entry<T>, i?: number, arr?: Entry<T>[]) => boolean
) {
return Object.fromEntries(
(Object.entries(obj) as Entry<T>[]).filter(fn)
) as Partial<T>
return Object.fromEntries((Object.entries(obj) as Entry<T>[]).filter(fn)) as Partial<T>
}
static deepMerge<T>(a: T, b: DeepPartial<T>): T {
@ -52,29 +45,16 @@ export class Utils {
*```
*/
static lerpColor(
color1: string,
color2: string,
factor = 0.5
): string | undefined {
static lerpColor(color1: string, color2: string, factor = 0.5): string | undefined {
function h2r(hex: string) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16),
]
? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
: null
}
function r2h(rgb: number[]) {
return (
'#' +
((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2])
.toString(16)
.slice(1)
)
return '#' + ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1)
}
const c1 = h2r(color1) || [0, 0, 0]
@ -96,12 +76,7 @@ export class Utils {
* @param rangeB to [low, high]
* @param clamp
*/
static modulate(
value: number,
rangeA: number[],
rangeB: number[],
clamp = false
): number {
static modulate(value: number, rangeA: number[], rangeB: number[], clamp = false): number {
const [fromLow, fromHigh] = rangeA
const [v0, v1] = rangeB
const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0)
@ -193,14 +168,12 @@ export class Utils {
/* ---------------------- Boxes --------------------- */
static getRectangleSides(
point: number[],
size: number[]
): [string, number[][]][] {
const tl = point
const tr = vec.add(point, [size[0], 0])
const br = vec.add(point, size)
const bl = vec.add(point, [0, size[1]])
static getRectangleSides(point: number[], size: number[], rotation = 0): [string, number[][]][] {
const center = [point[0] + size[0] / 2, point[1] + size[1] / 2]
const tl = vec.rotWith(point, center, rotation)
const tr = vec.rotWith(vec.add(point, [size[0], 0]), center, rotation)
const br = vec.rotWith(vec.add(point, size), center, rotation)
const bl = vec.rotWith(vec.add(point, [0, size[1]]), center, rotation)
return [
['top', [tl, tr]],
@ -211,16 +184,10 @@ export class Utils {
}
static getBoundsSides(bounds: TLBounds): [string, number[][]][] {
return this.getRectangleSides(
[bounds.minX, bounds.minY],
[bounds.width, bounds.height]
)
return this.getRectangleSides([bounds.minX, bounds.minY], [bounds.width, bounds.height])
}
static shallowEqual<T extends Record<string, unknown>>(
objA: T,
objB: T
): boolean {
static shallowEqual<T extends Record<string, unknown>>(objA: T, objB: T): boolean {
if (objA === objB) return true
if (!objA || !objB) return false
@ -234,10 +201,7 @@ export class Utils {
for (let i = 0; i < len; i++) {
const key = aKeys[i]
if (
objA[key] !== objB[key] ||
!Object.prototype.hasOwnProperty.call(objB, key)
) {
if (objA[key] !== objB[key] || !Object.prototype.hasOwnProperty.call(objB, key)) {
return false
}
}
@ -320,11 +284,7 @@ export class Utils {
* @param r The circle's radius.
* @param P The point.
*/
static getClosestPointOnCircle(
C: number[],
r: number,
P: number[]
): number[] {
static getClosestPointOnCircle(C: number[], r: number, P: number[]): number[] {
const v = vec.sub(C, P)
return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r))
}
@ -336,11 +296,7 @@ export class Utils {
* @param C
* @returns [x, y, r]
*/
static circleFromThreePoints(
A: number[],
B: number[],
C: number[]
): number[] {
static circleFromThreePoints(A: number[], B: number[], C: number[]): number[] {
const [x1, y1] = A
const [x2, y2] = B
const [x3, y3] = C
@ -500,12 +456,7 @@ export class Utils {
* @param A
* @param B
*/
static getArcLength(
C: number[],
r: number,
A: number[],
B: number[]
): number {
static getArcLength(C: number[], r: number, A: number[], B: number[]): number {
const sweep = Utils.getSweep(C, A, B)
return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
}
@ -518,13 +469,7 @@ export class Utils {
* @param B
* @param step
*/
static getArcDashOffset(
C: number[],
r: number,
A: number[],
B: number[],
step: number
): number {
static getArcDashOffset(C: number[], r: number, A: number[], B: number[], step: number): number {
const del0 = Utils.getSweep(C, A, B)
const len0 = Utils.getArcLength(C, r, A, B)
const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0
@ -548,10 +493,7 @@ export class Utils {
* @param points
* @param tension
*/
static getTLBezierCurveSegments(
points: number[][],
tension = 0.4
): TLBezierCurveSegment[] {
static getTLBezierCurveSegments(points: number[][], tension = 0.4): TLBezierCurveSegment[] {
const len = points.length
const cpoints: number[][] = [...points]
@ -681,13 +623,7 @@ export class Utils {
* @param x2
* @param y2
*/
static cubicBezier(
tx: number,
x1: number,
y1: number,
x2: number,
y2: number
): number {
static cubicBezier(tx: number, x1: number, y1: number, x2: number, y2: number): number {
// Inspired by Don Lancaster's two articles
// http://www.tinaja.com/glib/cubemath.pdf
// http://www.tinaja.com/text/bezmath.html
@ -876,8 +812,7 @@ export class Utils {
for (let i = 1; i < len - 1; i++) {
const [x0, y0] = points[i]
const d =
Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / max
const d = Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / max
if (distance > d) continue
@ -914,13 +849,7 @@ export class Utils {
* @param rotation
* @returns
*/
static pointInEllipse(
A: number[],
C: number[],
rx: number,
ry: number,
rotation = 0
): boolean {
static pointInEllipse(A: number[], C: number[], rx: number, ry: number, rotation = 0): boolean {
rotation = rotation || 0
const cos = Math.cos(rotation)
const sin = Math.sin(rotation)
@ -984,12 +913,7 @@ export class Utils {
* @returns
*/
static boundsCollide(a: TLBounds, b: TLBounds): boolean {
return !(
a.maxX < b.minX ||
a.minX > b.maxX ||
a.maxY < b.minY ||
a.minY > b.maxY
)
return !(a.maxX < b.minX || a.minX > b.maxX || a.maxY < b.minY || a.minY > b.maxY)
}
/**
@ -999,9 +923,7 @@ export class Utils {
* @returns
*/
static boundsContain(a: TLBounds, b: TLBounds): boolean {
return (
a.minX < b.minX && a.minY < b.minY && a.maxY > b.maxY && a.maxX > b.maxX
)
return a.minX < b.minX && a.minY < b.minY && a.maxY > b.maxY && a.maxX > b.maxX
}
/**
@ -1021,12 +943,7 @@ export class Utils {
* @returns
*/
static boundsAreEqual(a: TLBounds, b: TLBounds): boolean {
return !(
b.maxX !== a.maxX ||
b.minX !== a.minX ||
b.maxY !== a.maxY ||
b.minY !== a.minY
)
return !(b.maxX !== a.maxX || b.minX !== a.minX || b.maxY !== a.maxY || b.minY !== a.minY)
}
/**
@ -1056,9 +973,7 @@ export class Utils {
if (rotation !== 0) {
return Utils.getBoundsFromPoints(
points.map((pt) =>
vec.rotWith(pt, [(minX + maxX) / 2, (minY + maxY) / 2], rotation)
)
points.map((pt) => vec.rotWith(pt, [(minX + maxX) / 2, (minY + maxY) / 2], rotation))
)
}
@ -1107,21 +1022,9 @@ export class Utils {
* @param center
* @param rotation
*/
static rotateBounds(
bounds: TLBounds,
center: number[],
rotation: number
): TLBounds {
const [minX, minY] = vec.rotWith(
[bounds.minX, bounds.minY],
center,
rotation
)
const [maxX, maxY] = vec.rotWith(
[bounds.maxX, bounds.maxY],
center,
rotation
)
static rotateBounds(bounds: TLBounds, center: number[], rotation: number): TLBounds {
const [minX, minY] = vec.rotWith([bounds.minX, bounds.minY], center, rotation)
const [maxX, maxY] = vec.rotWith([bounds.maxX, bounds.maxY], center, rotation)
return {
minX,
@ -1359,31 +1262,19 @@ so that the two anchor points (initial and result) will be equal.
switch (handle) {
case TLBoundsCorner.TopLeft: {
cv = vec.sub(
vec.rotWith([bx1, by1], c1, rotation),
vec.rotWith([ax1, ay1], c0, rotation)
)
cv = vec.sub(vec.rotWith([bx1, by1], c1, rotation), vec.rotWith([ax1, ay1], c0, rotation))
break
}
case TLBoundsCorner.TopRight: {
cv = vec.sub(
vec.rotWith([bx0, by1], c1, rotation),
vec.rotWith([ax0, ay1], c0, rotation)
)
cv = vec.sub(vec.rotWith([bx0, by1], c1, rotation), vec.rotWith([ax0, ay1], c0, rotation))
break
}
case TLBoundsCorner.BottomRight: {
cv = vec.sub(
vec.rotWith([bx0, by0], c1, rotation),
vec.rotWith([ax0, ay0], c0, rotation)
)
cv = vec.sub(vec.rotWith([bx0, by0], c1, rotation), vec.rotWith([ax0, ay0], c0, rotation))
break
}
case TLBoundsCorner.BottomLeft: {
cv = vec.sub(
vec.rotWith([bx1, by0], c1, rotation),
vec.rotWith([ax1, ay0], c0, rotation)
)
cv = vec.sub(vec.rotWith([bx1, by0], c1, rotation), vec.rotWith([ax1, ay0], c0, rotation))
break
}
case TLBoundsEdge.Top: {
@ -1613,11 +1504,7 @@ left past the initial left edge) then swap points on that axis.
*```
*/
// eslint-disable-next-line @typescript-eslint/ban-types
static getFromCache<V, I extends object>(
cache: WeakMap<I, V>,
item: I,
getNext: () => V
): V {
static getFromCache<V, I extends object>(cache: WeakMap<I, V>, item: I, getNext: () => V): V {
let value = cache.get(item)
if (value === undefined) {
@ -1678,11 +1565,7 @@ left past the initial left edge) then swap points on that axis.
*/
static arrsIntersect<T, K>(a: T[], b: K[], fn?: (item: K) => T): boolean
static arrsIntersect<T>(a: T[], b: T[]): boolean
static arrsIntersect<T>(
a: T[],
b: unknown[],
fn?: (item: unknown) => T
): boolean {
static arrsIntersect<T>(a: T[], b: unknown[], fn?: (item: unknown) => T): boolean {
return a.some((item) => b.includes(fn ? fn(item) : item))
}
@ -1732,9 +1615,7 @@ left past the initial left edge) then swap points on that axis.
d.push(' Z')
return d
.join('')
.replaceAll(/(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g, '$1')
return d.join('').replaceAll(/(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g, '$1')
}
/* -------------------------------------------------- */
@ -1774,9 +1655,7 @@ left past the initial left edge) then swap points on that axis.
*/
static isTouchDisplay(): boolean {
return (
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0
)
}

View file

@ -433,7 +433,10 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
const anchor = Vec.sub(
Vec.add(
[expandedBounds.minX, expandedBounds.minY],
Vec.mulV([expandedBounds.width, expandedBounds.height], binding.point)
Vec.mulV(
[expandedBounds.width, expandedBounds.height],
Vec.rotWith(binding.point, [0.5, 0.5], target.rotation || 0)
)
),
shape.point
)
@ -455,7 +458,7 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
if ([TLDrawShapeType.Rectangle, TLDrawShapeType.Text].includes(target.type)) {
let hits = Intersect.ray
.bounds(origin, direction, intersectBounds)
.bounds(origin, direction, intersectBounds, target.rotation)
.filter((int) => int.didIntersect)
.map((int) => int.points[0])
.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))
@ -475,25 +478,22 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
handlePoint = Vec.sub(hits[0], shape.point)
} else if (target.type === TLDrawShapeType.Ellipse) {
// const center = getShapeUtils(target).getCenter(target)
handlePoint = Vec.nudge(
Vec.sub(
Intersect.ray
const hits = Intersect.ray
.ellipse(
origin,
direction,
center,
target.radius[0],
target.radius[1],
target.radius[0] + binding.distance,
target.radius[1] + binding.distance,
target.rotation || 0
)
.points.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))[0],
shape.point
),
origin,
binding.distance
)
.points.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))
if (!hits[0]) {
console.warn('No intersections')
}
handlePoint = Vec.sub(hits[0], shape.point)
}
}

View file

@ -155,7 +155,7 @@ export class Ellipse extends TLDrawShapeUtil<EllipseShape> {
}
getCenter(shape: EllipseShape): number[] {
return Utils.getBoundsCenter(this.getBounds(shape))
return [shape.point[0] + shape.radius[0], shape.point[1] + shape.radius[1]]
}
hitTest(shape: EllipseShape, point: number[]) {
@ -171,6 +171,98 @@ export class Ellipse extends TLDrawShapeUtil<EllipseShape> {
)
}
getBindingPoint(
shape: EllipseShape,
point: number[],
origin: number[],
direction: number[],
padding: number,
anywhere: boolean
) {
{
const bounds = this.getBounds(shape)
const expandedBounds = Utils.expandBounds(bounds, padding)
const center = this.getCenter(shape)
let bindingPoint: number[]
let distance: number
if (!Utils.pointInEllipse(point, center, shape.radius[0] + 32, shape.radius[1] + 32)) return
if (anywhere) {
if (Vec.dist(point, this.getCenter(shape)) < 12) {
bindingPoint = [0.5, 0.5]
} else {
bindingPoint = Vec.divV(Vec.sub(point, [expandedBounds.minX, expandedBounds.minY]), [
expandedBounds.width,
expandedBounds.height,
])
}
distance = 0
} else {
// Find furthest intersection between ray from
// origin through point and expanded bounds.
// const intersection = Intersect.ray
// .bounds(origin, direction, expandedBounds)
// .filter((int) => int.didIntersect)
// .map((int) => int.points[0])
// .sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0]
const intersection = Intersect.ray
.ellipse(origin, direction, center, shape.radius[0], shape.radius[1], shape.rotation || 0)
.points.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))[0]
if (!intersection) {
console.log('could not find an intersection')
return undefined
}
// The anchor is a point between the handle and the intersection
const anchor = Vec.med(point, intersection)
if (Vec.distanceToLineSegment(point, anchor, this.getCenter(shape)) < 12) {
// If we're close to the center, snap to the center
bindingPoint = [0.5, 0.5]
} else {
// Or else calculate a normalized point
bindingPoint = Vec.divV(Vec.sub(anchor, [expandedBounds.minX, expandedBounds.minY]), [
expandedBounds.width,
expandedBounds.height,
])
}
if (Utils.pointInBounds(point, bounds)) {
distance = 16
} else {
// Find the distance between the point and the ellipse
const innerIntersection = Intersect.lineSegment.ellipse(
point,
center,
center,
shape.radius[0],
shape.radius[1],
shape.rotation || 0
).points[0]
if (!innerIntersection) {
console.log('could not find an intersection')
return undefined
}
distance = Math.max(16, Vec.dist(point, innerIntersection))
}
}
return {
point: bindingPoint,
distance,
}
}
}
transform(
_shape: EllipseShape,
bounds: TLBounds,

View file

@ -1261,6 +1261,21 @@ export class TLDrawState implements TLCallbacks {
}
break
}
case 'translatingHandle': {
if (key === 'Escape') {
this.cancelSession(this.getPagePoint(info.point))
}
if (key === 'Meta' || key === 'Control') {
this.updateHandleSession(
this.getPagePoint(info.point),
info.shiftKey,
info.altKey,
info.metaKey
)
}
break
}
}
}
@ -1284,6 +1299,21 @@ export class TLDrawState implements TLCallbacks {
}
break
}
case 'translatingHandle': {
if (key === 'Escape') {
this.cancelSession(this.getPagePoint(info.point))
}
if (key === 'Meta' || key === 'Control') {
this.updateHandleSession(
this.getPagePoint(info.point),
info.shiftKey,
info.altKey,
info.metaKey
)
}
break
}
}
}