From b737a42ca94df985c54c368ddc0a864e7be6a652 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 11 Aug 2021 15:51:24 +0100 Subject: [PATCH] Adds ellipse binding --- packages/core/src/utils/intersect.ts | 363 +++++------------- packages/core/src/utils/utils.ts | 197 ++-------- .../tldraw/src/shape/shapes/arrow/arrow.tsx | 40 +- .../src/shape/shapes/ellipse/ellipse.tsx | 94 ++++- packages/tldraw/src/state/tlstate.ts | 30 ++ 5 files changed, 281 insertions(+), 443 deletions(-) diff --git a/packages/core/src/utils/intersect.ts b/packages/core/src/utils/intersect.ts index ada941eda..d970c072b 100644 --- a/packages/core/src/utils/intersect.ts +++ b/packages/core/src/utils/intersect.ts @@ -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,23 +301,19 @@ 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]]) => { - const intersection = Intersect.lineSegment.lineSegment(a1, a2, b1, b2) + lineSegment(point: number[], size: number[], a1: number[], a2: number[]): TLIntersection[] { + const sideIntersections = Utils.getRectangleSides(point, size).reduce( + (acc, [message, [b1, b2]]) => { + const intersection = Intersect.lineSegment.lineSegment(a1, a2, b1, b2) - if (intersection) { - acc.push(getIntersection(message, ...intersection.points)) - } + if (intersection) { + acc.push(getIntersection(message, ...intersection.points)) + } - return acc - }, []) + return acc + }, + [] + ) return sideIntersections.filter((int) => int.didIntersect) }, @@ -375,24 +325,20 @@ 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( + (acc, [message, [a1, a2]]) => { + const intersections = Intersect.rectangle.lineSegment(point2, size2, a1, a2) - acc.push( - ...intersections.map((int) => - getIntersection(`${message} ${int.message}`, ...int.points) + acc.push( + ...intersections.map((int) => + getIntersection(`${message} ${int.message}`, ...int.points) + ) ) - ) - return acc - }, []) + return acc + }, + [] + ) return sideIntersections.filter((int) => int.didIntersect) }, @@ -406,46 +352,36 @@ 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( + (acc, [message, [a1, a2]]) => { + const intersection = Intersect.arc.lineSegment(center, radius, start, end, a1, a2) - if (intersection) { - acc.push({ ...intersection, message }) - } + if (intersection) { + acc.push({ ...intersection, message }) + } - return acc - }, []) + 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]]) => { - const intersection = Intersect.lineSegment.circle(a1, a2, c, r) + circle(point: number[], size: number[], c: number[], r: number): TLIntersection[] { + const sideIntersections = Utils.getRectangleSides(point, size).reduce( + (acc, [message, [a1, a2]]) => { + const intersection = Intersect.lineSegment.circle(a1, a2, c, r) - if (intersection) { - acc.push({ ...intersection, message }) - } + if (intersection) { + acc.push({ ...intersection, message }) + } - return acc - }, []) + 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( + (acc, [message, [a1, a2]]) => { + const intersection = Intersect.lineSegment.ellipse(a1, a2, c, rx, ry, rotation) - if (intersection) { - acc.push({ ...intersection, message }) - } + if (intersection) { + acc.push({ ...intersection, message }) + } - return acc - }, []) + 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]]) => { - const intersections = Intersect.lineSegment.polyline(a1, a2, points) + polyline(point: number[], size: number[], points: number[][]): TLIntersection[] { + const sideIntersections = Utils.getRectangleSides(point, size).reduce( + (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)) - ) - } + if (intersections.length > 0) { + acc.push(getIntersection(message, ...intersections.flatMap((i) => i.points))) + } - return acc - }, []) + 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) }, diff --git a/packages/core/src/utils/utils.ts b/packages/core/src/utils/utils.ts index d0a0e0ab5..422665e64 100644 --- a/packages/core/src/utils/utils.ts +++ b/packages/core/src/utils/utils.ts @@ -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, i?: number, arr?: Entry[]) => boolean ) { - return Object.fromEntries( - (Object.entries(obj) as Entry[]).filter(fn) - ) as Partial + return Object.fromEntries((Object.entries(obj) as Entry[]).filter(fn)) as Partial } static deepMerge(a: T, b: DeepPartial): 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>( - objA: T, - objB: T - ): boolean { + static shallowEqual>(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( - cache: WeakMap, - item: I, - getNext: () => V - ): V { + static getFromCache(cache: WeakMap, 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(a: T[], b: K[], fn?: (item: K) => T): boolean static arrsIntersect(a: T[], b: T[]): boolean - static arrsIntersect( - a: T[], - b: unknown[], - fn?: (item: unknown) => T - ): boolean { + static arrsIntersect(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 ) } diff --git a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx index feb4f90da..bd57631a7 100644 --- a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx +++ b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx @@ -433,7 +433,10 @@ export class Arrow extends TLDrawShapeUtil { 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 { 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 { handlePoint = Vec.sub(hits[0], shape.point) } 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( - Vec.sub( - Intersect.ray - .ellipse( - origin, - 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 - ) + if (!hits[0]) { + console.warn('No intersections') + } + + handlePoint = Vec.sub(hits[0], shape.point) } } diff --git a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx index b15e87f23..137459234 100644 --- a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx +++ b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx @@ -155,7 +155,7 @@ export class Ellipse extends TLDrawShapeUtil { } 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 { ) } + 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, diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 753fee29a..d3dbfbe1f 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -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 + } } }