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

View file

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

View file

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

View file

@ -155,7 +155,7 @@ export class Ellipse extends TLDrawShapeUtil<EllipseShape> {
} }
getCenter(shape: EllipseShape): number[] { 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[]) { 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( transform(
_shape: EllipseShape, _shape: EllipseShape,
bounds: TLBounds, bounds: TLBounds,

View file

@ -1261,6 +1261,21 @@ export class TLDrawState implements TLCallbacks {
} }
break 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 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
}
} }
} }