From 7f0cfd2c5a8d9e8b1308176edae6120c015873f7 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 12 Sep 2021 13:21:44 +0100 Subject: [PATCH] Splits vectors and intersections into new packages esbuild isn't currently tree shaking intersections, but that's a file where we could save some room --- .vscode/tasks.json | 17 +- package.json | 10 +- packages/core/package.json | 4 +- packages/core/scripts/build.js | 20 +- .../core/src/components/handles/handles.tsx | 2 +- packages/core/src/hooks/useShapeTree.tsx | 3 +- packages/core/src/hooks/useZoomEvents.ts | 2 +- packages/core/src/inputs.ts | 10 +- packages/core/src/test/box.tsx | 11 +- packages/core/src/types.ts | 5 +- packages/core/src/utils/index.ts | 2 - packages/core/src/utils/intersect.ts | 680 ---------- packages/core/src/utils/utils.ts | 104 +- packages/core/tsconfig.json | 12 +- packages/dev/package.json | 3 +- packages/dev/tsconfig.json | 3 +- packages/intersect/README.md | 3 + packages/intersect/package.json | 57 + packages/intersect/scripts/build.js | 50 + packages/intersect/scripts/dev.js | 31 + packages/intersect/scripts/pre-dev.js | 28 + packages/intersect/src/index.ts | 1147 +++++++++++++++++ packages/intersect/tsconfig.build.json | 19 + packages/intersect/tsconfig.json | 11 + packages/tldraw/package.json | 4 +- packages/tldraw/scripts/build.js | 20 +- .../tldraw/src/shape/shapes/arrow/arrow.tsx | 44 +- .../tldraw/src/shape/shapes/draw/draw.tsx | 14 +- .../src/shape/shapes/ellipse/ellipse.tsx | 43 +- .../tldraw/src/shape/shapes/group/group.tsx | 9 +- .../src/shape/shapes/post-it/post-it.tsx | 17 +- .../src/shape/shapes/rectangle/rectangle.tsx | 28 +- .../tldraw/src/shape/shapes/text/text.tsx | 9 +- .../command/duplicate/duplicate.command.ts | 3 +- .../move-to-page/move-to-page.command.ts | 3 +- .../state/command/rotate/rotate.command.ts | 3 +- .../command/translate/translate.command.ts | 2 +- .../session/sessions/arrow/arrow.session.ts | 3 +- .../session/sessions/brush/brush.session.ts | 3 +- .../session/sessions/draw/draw.session.ts | 3 +- .../session/sessions/handle/handle.session.ts | 2 +- .../session/sessions/rotate/rotate.session.ts | 3 +- .../transform-single.session.ts | 3 +- .../sessions/transform/transform.session.ts | 3 +- .../sessions/translate/translate.session.ts | 3 +- packages/tldraw/src/state/tldr.ts | 3 +- packages/tldraw/src/state/tlstate.ts | 2 +- packages/tldraw/tsconfig.json | 8 +- packages/vec/README.md | 3 + packages/vec/package.json | 55 + packages/vec/scripts/build.js | 52 + packages/vec/scripts/dev.js | 31 + packages/vec/scripts/pre-dev.js | 27 + .../src/utils/vec.tsx => vec/src/index.ts} | 0 packages/vec/tsconfig.build.json | 19 + packages/vec/tsconfig.json | 10 + tsconfig.base.json | 2 + tsconfig.json | 13 +- 58 files changed, 1801 insertions(+), 880 deletions(-) delete mode 100644 packages/core/src/utils/intersect.ts create mode 100644 packages/intersect/README.md create mode 100644 packages/intersect/package.json create mode 100644 packages/intersect/scripts/build.js create mode 100644 packages/intersect/scripts/dev.js create mode 100644 packages/intersect/scripts/pre-dev.js create mode 100644 packages/intersect/src/index.ts create mode 100644 packages/intersect/tsconfig.build.json create mode 100644 packages/intersect/tsconfig.json create mode 100644 packages/vec/README.md create mode 100644 packages/vec/package.json create mode 100644 packages/vec/scripts/build.js create mode 100644 packages/vec/scripts/dev.js create mode 100644 packages/vec/scripts/pre-dev.js rename packages/{core/src/utils/vec.tsx => vec/src/index.ts} (100%) create mode 100644 packages/vec/tsconfig.build.json create mode 100644 packages/vec/tsconfig.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d92be8b96..a96ff102a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,17 +2,12 @@ "version": "2.0.0", "tasks": [ { - "label": "tsc", - "type": "shell", - "command": "./node_modules/.bin/tsc", - "args": ["--noEmit"], - "presentation": { - "reveal": "never", - "echo": false, - "focus": false, - "panel": "dedicated" - }, - "problemMatcher": "$tsc-watch" + "label": "Check for type errors", + "type": "typescript", + "tsconfig": "tsconfig.json", + "option": "watch", + "problemMatcher": ["$tsc-watch"], + "group": "build" } ] } diff --git a/package.json b/package.json index a7f20d454..b0a52be7b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "packages/core", "packages/tldraw", "packages/dev", - "packages/www" + "packages/www", + "packages/vec", + "packages/intersect" ], "scripts": { "test": "jest", @@ -22,7 +24,7 @@ "start": "lerna run start:pre && lerna run start --stream --parallel", "start:www": "yarn build:packages && lerna run start --parallel & cd packages/www && yarn dev", "build": "yarn build:packages && cd packages/www && yarn build", - "build:packages": "cd packages/core && yarn build && cd ../tldraw && yarn build", + "build:packages": "cd packages/vec && yarn build && cd ../intersect && yarn build && cd ../core && yarn build && cd ../tldraw && yarn build", "publish:patch": "yarn build:packages && lerna publish patch", "docs": "lerna run docs --stream", "docs:watch": "lerna run docs:watch --stream" @@ -105,11 +107,15 @@ }, "testEnvironment": "jsdom", "modulePathIgnorePatterns": [ + "/packages/vec/dist/", + "/packages/intersect/dist/", "/packages/core/dist/", "/packages/tldraw/dist/", "/packages/tldraw/test-utils/" ], "moduleNameMapper": { + "@tldraw/vec": "/packages/vec/src", + "@tldraw/intersect": "/packages/intersect/src", "@tldraw/core": "/packages/core/src", "@tldraw/tldraw": "/packages/tldraw/src", "\\~(.*)": "/packages/tldraw/src/$1", diff --git a/packages/core/package.json b/packages/core/package.json index b3774320e..db64b17f2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -55,7 +55,9 @@ "react-dom": "^17.0.2" }, "dependencies": { + "@tldraw/vec": "^0.0.86", + "@tldraw/intersect": "^0.0.86", "@use-gesture/react": "^10.0.0-beta.24" }, "gitHead": "55da8880eb3d8ab5fb62b5eb7853065922c95dcf" -} +} \ No newline at end of file diff --git a/packages/core/scripts/build.js b/packages/core/scripts/build.js index 0ab6f331d..e44c1ee4a 100644 --- a/packages/core/scripts/build.js +++ b/packages/core/scripts/build.js @@ -1,6 +1,7 @@ /* eslint-disable */ const fs = require('fs') const esbuild = require('esbuild') +const { gzip } = require('zlib') const name = process.env.npm_package_name || '' @@ -25,9 +26,10 @@ async function main() { jsxFragment: 'React.Fragment', tsconfig: './tsconfig.build.json', external: ['react', 'react-dom'], + metafile: true, }) - esbuild.buildSync({ + const esmResult = esbuild.buildSync({ entryPoints: ['./src/index.ts'], outdir: 'dist/esm', minify: true, @@ -38,9 +40,23 @@ async function main() { jsxFactory: 'React.createElement', jsxFragment: 'React.Fragment', external: ['react', 'react-dom'], + metafile: true, }) - console.log(`✔ ${name}: Built package.`) + let esmSize = 0 + Object.values(esmResult.metafile.outputs).forEach((output) => { + esmSize += output.bytes + }) + + fs.readFile('./dist/esm/index.js', (_err, data) => { + gzip(data, (_err, result) => { + console.log( + `✔ ${name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${( + result.length / 1000 + ).toFixed(2)}kb minified)` + ) + }) + }) } catch (e) { console.log(`× ${name}: Build failed due to an error.`) console.log(e) diff --git a/packages/core/src/components/handles/handles.tsx b/packages/core/src/components/handles/handles.tsx index 8a62b215d..392f93985 100644 --- a/packages/core/src/components/handles/handles.tsx +++ b/packages/core/src/components/handles/handles.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Vec } from '+utils' +import { Vec } from '@tldraw/vec' import type { TLShape } from '+types' import { Handle } from './handle' diff --git a/packages/core/src/hooks/useShapeTree.tsx b/packages/core/src/hooks/useShapeTree.tsx index 8645b0d64..1a0cfe0a1 100644 --- a/packages/core/src/hooks/useShapeTree.tsx +++ b/packages/core/src/hooks/useShapeTree.tsx @@ -11,7 +11,8 @@ import type { TLBinding, TLBounds, } from '+types' -import { Utils, Vec } from '+utils' +import { Utils } from '+utils' +import { Vec } from '@tldraw/vec' function addToShapeTree>( shape: T, diff --git a/packages/core/src/hooks/useZoomEvents.ts b/packages/core/src/hooks/useZoomEvents.ts index 98c40f9d8..a8cd0e968 100644 --- a/packages/core/src/hooks/useZoomEvents.ts +++ b/packages/core/src/hooks/useZoomEvents.ts @@ -2,8 +2,8 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import * as React from 'react' import { useTLContext } from './useTLContext' -import Utils, { Vec } from '+utils' import { useGesture } from '@use-gesture/react' +import { Vec } from '@tldraw/vec' // Capture zoom gestures (pinches, wheels and pans) export function useZoomEvents(ref: React.RefObject) { diff --git a/packages/core/src/inputs.ts b/packages/core/src/inputs.ts index 48b67a4ec..0600293c4 100644 --- a/packages/core/src/inputs.ts +++ b/packages/core/src/inputs.ts @@ -1,6 +1,7 @@ import type React from 'react' import type { TLKeyboardInfo, TLPointerInfo } from './types' -import { Vec, Utils } from './utils' +import { Utils } from './utils' +import { Vec } from '@tldraw/vec' const DOUBLE_CLICK_DURATION = 250 @@ -354,14 +355,11 @@ export class Inputs { e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent, offset = [0, 0] ): number[] { - return [ - Number(e.clientX.toPrecision(5)) - offset[0], - Number(e.clientY.toPrecision(5)) - offset[1], - ] + return [+e.clientX.toFixed(2) - offset[0], +e.clientY.toFixed(2) - offset[1]] } static getPressure(e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent) { - return 'pressure' in e ? Number(e.pressure.toPrecision(5)) || 0.5 : 0.5 + return 'pressure' in e ? +e.pressure.toFixed(2) || 0.5 : 0.5 } static commandKey(): string { diff --git a/packages/core/src/test/box.tsx b/packages/core/src/test/box.tsx index f1e18bc07..a59c152c8 100644 --- a/packages/core/src/test/box.tsx +++ b/packages/core/src/test/box.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import * as React from 'react' import { TLShapeUtil, TLShape, TLShapeProps, TLBounds, TLRenderInfo, TLTransformInfo } from '+types' -import Utils, { Intersect } from '+utils' +import Utils from '+utils' export interface BoxShape extends TLShape { size: number[] @@ -62,15 +62,6 @@ export class Box extends TLShapeUtil { return Utils.pointInBounds(point, this.getBounds(shape)) } - hitTestBounds(shape: BoxShape, bounds: TLBounds) { - const rotatedCorners = Utils.getRotatedCorners(this.getBounds(shape), shape.rotation) - - return ( - rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) || - Intersect.polyline.bounds(rotatedCorners, bounds).length > 0 - ) - } - transform(shape: BoxShape, bounds: TLBounds, _info: TLTransformInfo): BoxShape { return { ...shape, point: [bounds.minX, bounds.minY] } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1a3992d10..23b6acad5 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2,8 +2,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* --------------------- Primary -------------------- */ -import { Intersect, Vec } from '+utils' +import { Vec } from '@tldraw/vec' import React, { ForwardedRef } from 'react' +import { intersectPolylineBounds } from '@tldraw/intersect' export type Patch = Partial<{ [P in keyof T]: T | Partial | Patch }> @@ -426,7 +427,7 @@ export abstract class TLShapeUtil { point[1] < bounds.minY || point[1] > bounds.maxY ) - ) || Intersect.polyline.bounds(corners, bounds).length > 0 + ) || intersectPolylineBounds(corners, bounds).length > 0 ) } } diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 62b52f608..c29ab98a1 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,7 +1,5 @@ import { Utils } from './utils' -export { Intersect } from './intersect' export { Utils } from './utils' export { Svg } from './svg' -export { Vec } from './vec' export default Utils diff --git a/packages/core/src/utils/intersect.ts b/packages/core/src/utils/intersect.ts deleted file mode 100644 index f0f0a6bf1..000000000 --- a/packages/core/src/utils/intersect.ts +++ /dev/null @@ -1,680 +0,0 @@ -import type { TLBounds, TLIntersection } from '../types' -import { Vec } from './vec' -import { Utils } from './utils' - -/* ----------------- Start Copy Here ---------------- */ - -function getIntersection(message: string, ...points: number[][]): TLIntersection { - const didIntersect = points.length > 0 - return { didIntersect, message, points } -} - -export class Intersect { - static ray = { - // Intersect a ray with a ray. - 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] - const u = (dy * n1[0] - dx * n1[1]) / det - const v = (dy * n0[0] - dx * n0[1]) / det - if (u < 0 || v < 0) return getIntersection('miss') - - const m0 = n0[1] / n0[0] - const m1 = n1[1] / n1[0] - const b0 = p0[1] - m0 * p0[0] - const b1 = p1[1] - m1 * p1[0] - const x = (b1 - b0) / (m0 - m1) - const y = m0 * x + b0 - - return Number.isFinite(x) - ? getIntersection('intersection', [x, y]) - : getIntersection('parallel') - }, - - // Interseg a ray with a line segment. - lineSegment(origin: number[], direction: number[], a1: number[], a2: number[]): TLIntersection { - const [x, y] = origin - const [dx, dy] = direction - const [x1, y1] = a1 - const [x2, y2] = a2 - - if (dy / dx !== (y2 - y1) / (x2 - x1)) { - const d = dx * (y2 - y1) - dy * (x2 - x1) - if (d !== 0) { - const r = ((y - y1) * (x2 - x1) - (x - x1) * (y2 - y1)) / d - const s = ((y - y1) * dx - (x - x1) * dy) / d - if (r >= 0 && s >= 0 && s <= 1) { - return getIntersection('intersection', [x + r * dx, y + r * dy]) - } - } - } - return getIntersection('no intersection') - }, - - // Intersect a ray with a rectangle. - rectangle( - origin: number[], - direction: number[], - point: number[], - size: number[], - rotation = 0 - ): TLIntersection[] { - return Intersect.rectangle.ray(point, size, rotation, origin, direction) - }, - - // Intersect a ray with an ellipse. - ellipse( - origin: number[], - direction: number[], - center: number[], - rx: number, - ry: number, - rotation: number - ): TLIntersection { - const a1 = origin - const a2 = Vec.mul(direction, 999999999) - return Intersect.lineSegment.ellipse(a1, a2, center, rx, ry, rotation) - }, - - // Intersect a ray with a bounding box. - bounds( - origin: number[], - direction: number[], - bounds: TLBounds, - rotation = 0 - ): TLIntersection[] { - const { minX, minY, width, height } = bounds - 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 { - 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 { - const AB = Vec.sub(a1, b1) - const BV = Vec.sub(b2, b1) - const AV = Vec.sub(a2, a1) - - const ua_t = BV[0] * AB[1] - BV[1] * AB[0] - const ub_t = AV[0] * AB[1] - AV[1] * AB[0] - const u_b = BV[1] * AV[0] - BV[0] * AV[1] - - if (ua_t === 0 || ub_t === 0) { - return getIntersection('coincident') - } - - if (u_b === 0) { - return getIntersection('parallel') - } - - if (u_b !== 0) { - const ua = ua_t / u_b - const ub = ub_t / u_b - if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { - return getIntersection('intersection', Vec.add(a1, Vec.mul(AV, ua))) - } - } - - return getIntersection('no intersection') - }, - - // Intersect a line segment with a rectangle - rectangle(a1: number[], a2: number[], point: number[], size: number[]): TLIntersection[] { - return Intersect.rectangle.lineSegment(point, size, a1, a2) - }, - - // Intersect a line segment with an arc. - arc( - a1: number[], - a2: number[], - center: number[], - radius: number, - start: number[], - end: number[] - ): TLIntersection { - const sa = Vec.angle(center, start) - const ea = Vec.angle(center, end) - const ellipseTest = Intersect.ellipse.lineSegment(center, radius, radius, 0, a1, a2) - - if (!ellipseTest.didIntersect) return getIntersection('No intersection') - - const points = ellipseTest.points.filter((point) => - Utils.isAngleBetween(sa, ea, Vec.angle(center, point)) - ) - - if (points.length === 0) { - return getIntersection('No intersection') - } - - return getIntersection('intersection', ...points) - }, - - // 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 cc = - c[0] * c[0] + - c[1] * c[1] + - a1[0] * a1[0] + - a1[1] * a1[1] - - 2 * (c[0] * a1[0] + c[1] * a1[1]) - - r * r - - const deter = b * b - 4 * a * cc - - if (deter < 0) { - return getIntersection('outside') - } - - if (deter === 0) { - return getIntersection('tangent') - } - - const e = Math.sqrt(deter) - const u1 = (-b + e) / (2 * a) - const u2 = (-b - e) / (2 * a) - if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) { - if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) { - return getIntersection('outside') - } else { - return getIntersection('inside') - } - } - - const results: number[][] = [] - if (0 <= u1 && u1 <= 1) results.push(Vec.lrp(a1, a2, u1)) - if (0 <= u2 && u2 <= 1) results.push(Vec.lrp(a1, a2, u2)) - - return getIntersection('intersection', ...results) - }, - - // Intersect a line segment with an ellipse. - ellipse( - a1: number[], - a2: number[], - center: number[], - rx: number, - ry: number, - rotation = 0 - ): TLIntersection { - // If the ellipse or line segment are empty, return no tValues. - if (rx === 0 || ry === 0 || Vec.isEqual(a1, a2)) { - return getIntersection('No intersection') - } - - // Get the semimajor and semiminor axes. - rx = rx < 0 ? rx : -rx - ry = ry < 0 ? ry : -ry - - // Rotate points and translate so the ellipse is centered at the origin. - a1 = Vec.sub(Vec.rotWith(a1, center, -rotation), center) - a2 = Vec.sub(Vec.rotWith(a2, center, -rotation), center) - - // Calculate the quadratic parameters. - 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 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). - const tValues: number[] = [] - - // Calculate the discriminant. - const discriminant = B * B - 4 * A * C - - if (discriminant === 0) { - // One real solution. - tValues.push(-B / 2 / A) - } else if (discriminant > 0) { - const root = Math.sqrt(discriminant) - // Two real solutions. - tValues.push((-B + root) / 2 / A) - tValues.push((-B - root) / 2 / A) - } - - // Filter to only points that are on the segment. - // Solve for points, then counter-rotate points. - const points = tValues - .filter((t) => t >= 0 && t <= 1) - .map((t) => Vec.add(center, Vec.add(a1, Vec.mul(Vec.sub(a2, a1), t)))) - .map((p) => Vec.rotWith(p, center, rotation)) - - return getIntersection('intersection', ...points) - }, - - // Intersect a line segment with a bounding box. - bounds(a1: number[], a2: number[], bounds: TLBounds): TLIntersection[] { - return Intersect.bounds.lineSegment(bounds, a1, a2) - }, - - // Intersect a line segment with a polyline - polyline(a1: number[], a2: number[], points: number[][]): TLIntersection[] { - const intersections: TLIntersection[] = [] - - for (let i = 1; i < points.length + 1; i++) { - const int = Intersect.lineSegment.lineSegment( - a1, - a2, - points[i - 1], - points[i % points.length] - ) - - if (int) { - intersections.push(int) - } - } - - return intersections - }, - } - - static rectangle = { - // Intersect a rectangle with a ray. - ray( - point: number[], - size: number[], - rotation: number, - origin: number[], - direction: number[] - ): TLIntersection[] { - const sideIntersections = Utils.getRectangleSides(point, size, rotation).reduce< - TLIntersection[] - >((acc, [message, [a1, a2]]) => { - const intersection = Intersect.ray.lineSegment(origin, direction, a1, a2) - - if (intersection) { - acc.push(getIntersection(message, ...intersection.points)) - } - - return acc - }, []) - - return sideIntersections.filter((int) => int.didIntersect) - }, - - // Intersect a rectangle with a line segment. - 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)) - } - - return acc - }, - [] - ) - - return sideIntersections.filter((int) => int.didIntersect) - }, - - // Intersect a rectangle with a rectangle. - rectangle( - point1: number[], - size1: number[], - point2: number[], - size2: number[] - ): TLIntersection[] { - 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) - ) - ) - - return acc - }, - [] - ) - - return sideIntersections.filter((int) => int.didIntersect) - }, - - // Intersect a rectangle with an arc. - arc( - point: number[], - size: number[], - center: number[], - radius: number, - start: number[], - end: number[] - ): TLIntersection[] { - 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 }) - } - - 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( - (acc, [message, [a1, a2]]) => { - const intersection = Intersect.lineSegment.circle(a1, a2, c, r) - - if (intersection) { - acc.push({ ...intersection, message }) - } - - return acc - }, - [] - ) - - return sideIntersections.filter((int) => int.didIntersect) - }, - - // Intersect a rectangle with an ellipse. - ellipse( - point: number[], - size: number[], - c: number[], - rx: number, - ry: number, - rotation = 0 - ): TLIntersection[] { - 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 }) - } - - return acc - }, - [] - ) - - return sideIntersections.filter((int) => int.didIntersect) - }, - - // Intersect a rectangle with a bounding box. - bounds(point: number[], size: number[], bounds: TLBounds): TLIntersection[] { - const { minX, minY, width, height } = bounds - 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( - (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))) - } - - return acc - }, - [] - ) - - return sideIntersections.filter((int) => int.didIntersect) - }, - } - - static arc = { - // Intersect an arc with a line segment. - lineSegment( - center: number[], - radius: number, - start: number[], - end: number[], - a1: number[], - a2: number[] - ): TLIntersection { - return Intersect.lineSegment.arc(a1, a2, center, radius, start, end) - }, - - // Intersect an arc with a rectangle. - rectangle( - center: number[], - radius: number, - start: number[], - end: number[], - point: number[], - size: number[] - ): TLIntersection[] { - return Intersect.rectangle.arc(point, size, center, radius, start, end) - }, - - // Intersect an arc with a bounding box. - bounds( - center: number[], - radius: number, - start: number[], - end: number[], - bounds: TLBounds - ): TLIntersection[] { - const { minX, minY, width, height } = bounds - 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 { - return Intersect.lineSegment.circle(a1, a2, c, r) - }, - - // Intersect a circle with a circle. - circle(c1: number[], r1: number, c2: number[], r2: number): TLIntersection { - let dx = c2[0] - c1[0], - dy = c2[1] - c1[1] - - const d = Math.sqrt(dx * dx + dy * dy), - x = (d * d - r2 * r2 + r1 * r1) / (2 * d), - y = Math.sqrt(r1 * r1 - x * x) - - dx /= d - dy /= d - - return getIntersection( - 'intersection', - [c1[0] + dx * x - dy * y, c1[1] + dy * x + dx * y], - [c1[0] + dx * x + dy * y, c1[1] + dy * x - dx * y] - ) - }, - - // Intersect a circle with a rectangle. - rectangle(c: number[], r: number, point: number[], size: number[]): TLIntersection[] { - return Intersect.rectangle.circle(point, size, c, r) - }, - - // Intersect a circle with a bounding box. - bounds(c: number[], r: number, bounds: TLBounds): TLIntersection[] { - const { minX, minY, width, height } = bounds - return Intersect.circle.rectangle(c, r, [minX, minY], [width, height]) - }, - } - - static ellipse = { - // Intersect an ellipse with a ray. - ray( - center: number[], - rx: number, - ry: number, - rotation: number, - point: number[], - direction: number[] - ): TLIntersection { - return Intersect.ray.ellipse(point, direction, center, rx, ry, rotation) - }, - - // Intersect an ellipse with a line segment. - lineSegment( - center: number[], - rx: number, - ry: number, - rotation = 0, - a1: number[], - a2: number[] - ): TLIntersection { - if (rx === ry) { - return Intersect.lineSegment.circle(a1, a2, center, rx) - } - - return Intersect.lineSegment.ellipse(a1, a2, center, rx, ry, rotation) - }, - - // Intersect an ellipse with a rectangle. - rectangle( - center: number[], - rx: number, - ry: number, - rotation = 0, - point: number[], - size: number[] - ): TLIntersection[] { - if (rx === ry) { - return Intersect.rectangle.circle(point, size, center, rx) - } - - return Intersect.rectangle.ellipse(point, size, center, rx, ry, rotation) - }, - - // Get an intersection between an ellipse and a second ellipse. - // Adapted from https://gist.github.com/drawable/92792f59b6ff8869d8b1 - ellipse( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _c1: number[], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _rx1: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _ry1: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _r1: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _c2: number[], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _rx2: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _ry2: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _r2: number - ): TLIntersection { - // TODO - return getIntersection('no intersection') - }, - - circle( - c: number[], - rx: number, - ry: number, - rotation: number, - c2: number[], - r2: number - ): TLIntersection { - return Intersect.ellipse.ellipse(c, rx, ry, rotation, c2, r2, r2, 0) - }, - - // Intersect an ellipse with a bounding box. - bounds( - c: number[], - rx: number, - ry: number, - rotation: number, - bounds: TLBounds - ): TLIntersection[] { - const { minX, minY, width, height } = bounds - return Intersect.ellipse.rectangle(c, rx, ry, rotation, [minX, minY], [width, height]) - }, - } - - static bounds = { - ray(bounds: TLBounds, origin: number[], direction: number[]): TLIntersection[] { - const { minX, minY, width, height } = bounds - return Intersect.ray.rectangle(origin, direction, [minX, minY], [width, height]) - }, - - lineSegment(bounds: TLBounds, a1: number[], a2: number[]): TLIntersection[] { - const { minX, minY, width, height } = bounds - return Intersect.lineSegment.rectangle(a1, a2, [minX, minY], [width, height]) - }, - - rectangle(bounds: TLBounds, point: number[], size: number[]): TLIntersection[] { - const { minX, minY, width, height } = bounds - return Intersect.rectangle.rectangle(point, size, [minX, minY], [width, height]) - }, - - bounds(bounds1: TLBounds, bounds2: TLBounds): TLIntersection[] { - return Intersect.rectangle.rectangle( - [bounds1.minX, bounds1.minY], - [bounds1.width, bounds1.height], - [bounds2.minX, bounds2.minY], - [bounds2.width, bounds2.height] - ) - }, - - arc( - bounds: TLBounds, - center: number[], - radius: number, - start: number[], - end: number[] - ): TLIntersection[] { - const { minX, minY, width, height } = bounds - return Intersect.arc.rectangle(center, radius, start, end, [minX, minY], [width, height]) - }, - - circle(bounds: TLBounds, c: number[], r: number): TLIntersection[] { - const { minX, minY, width, height } = bounds - return Intersect.circle.rectangle(c, r, [minX, minY], [width, height]) - }, - - 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]) - }, - - polyline(bounds: TLBounds, points: number[][]): TLIntersection[] { - return Intersect.polyline.bounds(points, bounds) - }, - } - - static polyline = { - // Intersect a polyline with a line segment. - 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[] { - return Intersect.rectangle.polyline(point, size, points) - }, - - // Intersect a polyline with a bounding box. - bounds(points: number[][], bounds: TLBounds): TLIntersection[] { - return Intersect.rectangle.polyline( - [bounds.minX, bounds.minY], - [bounds.width, bounds.height], - points - ) - }, - } -} - -export default Intersect diff --git a/packages/core/src/utils/utils.ts b/packages/core/src/utils/utils.ts index 83c546deb..39fd8b0d1 100644 --- a/packages/core/src/utils/utils.ts +++ b/packages/core/src/utils/utils.ts @@ -4,7 +4,7 @@ /* eslint-disable no-redeclare */ import type React from 'react' import { TLBezierCurveSegment, TLBounds, TLBoundsCorner, TLBoundsEdge } from '../types' -import vec from './vec' +import { Vec } from '@tldraw/vec' import './polyfills' import type { Patch } from '+index' @@ -204,10 +204,10 @@ export class Utils { 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) + 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]], @@ -258,10 +258,10 @@ export class Utils { P: number[], side: number ): number[] | null { - const B = vec.lrp(C, P, 0.5) - const r1 = vec.dist(C, B) - const delta = vec.sub(B, C) - const d = vec.len(delta) + const B = Vec.lrp(C, P, 0.5) + const r1 = Vec.dist(C, B) + const delta = Vec.sub(B, C) + const d = Vec.len(delta) if (!(d <= r + r1 && d >= Math.abs(r - r1))) { return null @@ -269,11 +269,11 @@ export class Utils { const a = (r * r - r1 * r1 + d * d) / (2.0 * d) const n = 1 / d - const p = vec.add(C, vec.mul(delta, a * n)) + const p = Vec.add(C, Vec.mul(delta, a * n)) const h = Math.sqrt(r * r - a * a) - const k = vec.mul(vec.per(delta), h * n) + const k = Vec.mul(Vec.per(delta), h * n) - return side === 0 ? vec.add(p, k) : vec.sub(p, k) + return side === 0 ? Vec.add(p, k) : Vec.sub(p, k) } /** @@ -292,8 +292,8 @@ export class Utils { C1: number[], r1: number ): number[][] | null { - const a0 = vec.angle(C0, C1) - const d = vec.dist(C0, C1) + const a0 = Vec.angle(C0, C1) + const d = Vec.dist(C0, C1) // Circles are overlapping, no tangents if (d < Math.abs(r1 - r0)) { @@ -319,8 +319,8 @@ export class Utils { * @param P The point. */ 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)) + const v = Vec.sub(C, P) + return Vec.sub(C, Vec.mul(Vec.div(v, Vec.len(v)), r)) } /** @@ -411,7 +411,7 @@ export class Utils { * @param B */ static getSweep(C: number[], A: number[], B: number[]): number { - return Utils.angleDelta(vec.angle(C, A), vec.angle(C, B)) + return Utils.angleDelta(Vec.angle(C, A), Vec.angle(C, B)) } /** @@ -766,7 +766,7 @@ export class Utils { const len = pts.length const res: number[][] = [] // results - let t1x: number, // tension vectors + let t1x: number, // tension Vectors t2x: number, t1y: number, t2y: number, @@ -871,7 +871,7 @@ export class Utils { * @returns */ static pointInCircle(A: number[], C: number[], r: number): boolean { - return vec.dist(A, C) <= r + return Vec.dist(A, C) <= r } /** @@ -887,7 +887,7 @@ export class Utils { rotation = rotation || 0 const cos = Math.cos(rotation) const sin = Math.sin(rotation) - const delta = vec.sub(A, C) + const delta = Vec.sub(A, C) const tdx = cos * delta[0] + sin * delta[1] const tdy = sin * delta[0] - cos * delta[1] @@ -914,10 +914,10 @@ export class Utils { points.forEach((a, i) => { const b = points[(i + 1) % points.length] if (a[1] <= p[1]) { - if (b[1] > p[1] && vec.cross(a, b, p) > 0) { + if (b[1] > p[1] && Vec.cross(a, b, p) > 0) { wn += 1 } - } else if (b[1] <= p[1] && vec.cross(a, b, p) < 0) { + } else if (b[1] <= p[1] && Vec.cross(a, b, p) < 0) { wn -= 1 } }) @@ -1024,7 +1024,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)) ) } @@ -1074,8 +1074,8 @@ export class Utils { * @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) + const [minX, minY] = Vec.rotWith([bounds.minX, bounds.minY], center, rotation) + const [maxX, maxY] = Vec.rotWith([bounds.maxX, bounds.maxY], center, rotation) return { minX, @@ -1158,7 +1158,7 @@ export class Utils { [b.maxX, b.minY], [b.maxX, b.maxY], [b.minX, b.maxY], - ].map((point) => vec.rotWith(point, center, rotation)) + ].map((point) => Vec.rotWith(point, center, rotation)) } static getTransformedBoundingBox( @@ -1192,7 +1192,7 @@ export class Utils { // Counter rotate the delta. This lets us make changes as if // the (possibly rotated) boxes were axis aligned. - const [dx, dy] = vec.rot(delta, -rotation) + const [dx, dy] = Vec.rot(delta, -rotation) /* 1. Delta @@ -1299,67 +1299,67 @@ new box's aspect ratio matches the original aspect ratio. /* 3. Rotation -If the bounds are rotated, get a vector from the rotated anchor +If the bounds are rotated, get a Vector from the rotated anchor corner in the inital bounds to the rotated anchor corner in the -result's bounds. Subtract this vector from the result's corners, +result's bounds. Subtract this Vector from the result's corners, so that the two anchor points (initial and result) will be equal. */ if (rotation % (Math.PI * 2) !== 0) { let cv = [0, 0] - const c0 = vec.med([ax0, ay0], [ax1, ay1]) - const c1 = vec.med([bx0, by0], [bx1, by1]) + const c0 = Vec.med([ax0, ay0], [ax1, ay1]) + const c1 = Vec.med([bx0, by0], [bx1, by1]) 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: { - cv = vec.sub( - vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation), - vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation) + cv = Vec.sub( + Vec.rotWith(Vec.med([bx0, by1], [bx1, by1]), c1, rotation), + Vec.rotWith(Vec.med([ax0, ay1], [ax1, ay1]), c0, rotation) ) break } case TLBoundsEdge.Left: { - cv = vec.sub( - vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation), - vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation) + cv = Vec.sub( + Vec.rotWith(Vec.med([bx1, by0], [bx1, by1]), c1, rotation), + Vec.rotWith(Vec.med([ax1, ay0], [ax1, ay1]), c0, rotation) ) break } case TLBoundsEdge.Bottom: { - cv = vec.sub( - vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation), - vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation) + cv = Vec.sub( + Vec.rotWith(Vec.med([bx0, by0], [bx1, by0]), c1, rotation), + Vec.rotWith(Vec.med([ax0, ay0], [ax1, ay0]), c0, rotation) ) break } case TLBoundsEdge.Right: { - cv = vec.sub( - vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation), - vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation) + cv = Vec.sub( + Vec.rotWith(Vec.med([bx0, by0], [bx0, by1]), c1, rotation), + Vec.rotWith(Vec.med([ax0, ay0], [ax0, ay1]), c0, rotation) ) break } } - ;[bx0, by0] = vec.sub([bx0, by0], cv) - ;[bx1, by1] = vec.sub([bx1, by1], cv) + ;[bx0, by0] = Vec.sub([bx0, by0], cv) + ;[bx1, by1] = Vec.sub([bx1, by1], cv) } /* @@ -1500,10 +1500,10 @@ left past the initial left edge) then swap points on that axis. * @param rotation */ static getRotatedSize(size: number[], rotation: number): number[] { - const center = vec.div(size, 2) + const center = Vec.div(size, 2) const points = [[0, 0], [size[0], 0], size, [0, size[1]]].map((point) => - vec.rotWith(point, center, rotation) + Vec.rotWith(point, center, rotation) ) const bounds = Utils.getBoundsFromPoints(points) @@ -1534,7 +1534,7 @@ left past the initial left edge) then swap points on that axis. */ static removeDuplicatePoints(points: number[][]) { return points.reduce((acc, pt, i) => { - if (i === 0 || !vec.isEqual(pt, acc[i - 1])) { + if (i === 0 || !Vec.isEqual(pt, acc[i - 1])) { acc.push(pt) } return acc diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 929fcf50a..4dec22113 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -3,13 +3,17 @@ "include": ["src"], "exclude": ["node_modules", "dist", "docs"], "compilerOptions": { - "composite": true, - "emitDeclarationOnly": true, - "rootDir": "src", "outDir": "./dist/types", + "rootDir": "src", "baseUrl": "src", "paths": { "+*": ["./*"] } - } + }, + "references": [ + { + "path": "../../packages/intersect" + }, + { "path": "../../packages/vec" } + ] } diff --git a/packages/dev/package.json b/packages/dev/package.json index ad4a74b72..ce192c9d8 100644 --- a/packages/dev/package.json +++ b/packages/dev/package.json @@ -17,6 +17,7 @@ "README.md", "src" ], + "sideEffects": false, "dependencies": { "@tldraw/tldraw": "^0.0.86", "idb": "^6.1.2", @@ -37,4 +38,4 @@ "typescript": "4.2.3" }, "gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6" -} +} \ No newline at end of file diff --git a/packages/dev/tsconfig.json b/packages/dev/tsconfig.json index 984070ad1..2df98ef05 100644 --- a/packages/dev/tsconfig.json +++ b/packages/dev/tsconfig.json @@ -3,7 +3,8 @@ "include": ["src"], "exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts", "dist"], "compilerOptions": { - "composite": true + "composite": true, + "rootDir": "." }, "references": [ { diff --git a/packages/intersect/README.md b/packages/intersect/README.md new file mode 100644 index 000000000..c8d3b3631 --- /dev/null +++ b/packages/intersect/README.md @@ -0,0 +1,3 @@ +# Dev Server + +Dev server with fast refresh. diff --git a/packages/intersect/package.json b/packages/intersect/package.json new file mode 100644 index 000000000..a0ec59543 --- /dev/null +++ b/packages/intersect/package.json @@ -0,0 +1,57 @@ +{ + "name": "@tldraw/intersect", + "version": "0.0.86", + "private": false, + "description": "A tiny little drawing app (intersect)", + "author": "@steveruizok", + "repository": { + "type": "git", + "url": "git+https://github.com/tldraw/tldraw.git", + "directory": "packages/intersect" + }, + "license": "MIT", + "keywords": [], + "files": [ + "dist/**/*" + ], + "sideEffects": false, + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "typings": "./dist/types/index.d.ts", + "scripts": { + "start:pre": "node scripts/pre-dev && yarn types:pre", + "start": "node scripts/dev & yarn types:dev", + "build": "node scripts/build && yarn types:build", + "types:pre": "tsc", + "types:dev": "tsc --watch", + "types:build": "tsc --project tsconfig.build.json", + "lint": "eslint src/ --ext .ts,.tsx", + "clean": "rm -rf dist", + "ts-node": "ts-node", + "docs": "typedoc", + "docs:watch": "typedoc --watch" + }, + "devDependencies": { + "@babel/core": "^7.15.5", + "@babel/preset-env": "^7.15.4", + "@types/jest": "^27.0.1", + "@types/node": "^16.7.10", + "@typescript-eslint/eslint-plugin": "^4.30.0", + "@typescript-eslint/parser": "^4.30.0", + "esbuild": "^0.12.24", + "eslint": "^7.32.0", + "lerna": "^4.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "ts-node": "^10.2.1", + "tslib": "^2.3.1", + "typedoc": "^0.21.9", + "typescript": "^4.4.2" + }, + "peerDependencies": {}, + "dependencies": { + "@tldraw/vec": "^0.0.86" + }, + "gitHead": "55da8880eb3d8ab5fb62b5eb7853065922c95dcf" +} \ No newline at end of file diff --git a/packages/intersect/scripts/build.js b/packages/intersect/scripts/build.js new file mode 100644 index 000000000..0ab6f331d --- /dev/null +++ b/packages/intersect/scripts/build.js @@ -0,0 +1,50 @@ +/* eslint-disable */ +const fs = require('fs') +const esbuild = require('esbuild') + +const name = process.env.npm_package_name || '' + +async function main() { + if (fs.existsSync('./dist')) { + fs.rmSync('./dist', { recursive: true }, (e) => { + if (e) { + throw e + } + }) + } + + try { + esbuild.buildSync({ + entryPoints: ['./src/index.ts'], + outdir: 'dist/cjs', + minify: true, + bundle: true, + format: 'cjs', + target: 'es6', + jsxFactory: 'React.createElement', + jsxFragment: 'React.Fragment', + tsconfig: './tsconfig.build.json', + external: ['react', 'react-dom'], + }) + + esbuild.buildSync({ + entryPoints: ['./src/index.ts'], + outdir: 'dist/esm', + minify: true, + bundle: true, + format: 'esm', + target: 'es6', + tsconfig: './tsconfig.build.json', + jsxFactory: 'React.createElement', + jsxFragment: 'React.Fragment', + external: ['react', 'react-dom'], + }) + + console.log(`✔ ${name}: Built package.`) + } catch (e) { + console.log(`× ${name}: Build failed due to an error.`) + console.log(e) + } +} + +main() diff --git a/packages/intersect/scripts/dev.js b/packages/intersect/scripts/dev.js new file mode 100644 index 000000000..050de844d --- /dev/null +++ b/packages/intersect/scripts/dev.js @@ -0,0 +1,31 @@ +/* eslint-disable */ +const esbuild = require('esbuild') + +const name = process.env.npm_package_name || '' + +async function main() { + esbuild.build({ + entryPoints: ['./src/index.ts'], + outdir: 'dist/cjs', + minify: false, + bundle: true, + format: 'cjs', + target: 'es6', + jsxFactory: 'React.createElement', + jsxFragment: 'React.Fragment', + tsconfig: './tsconfig.json', + external: ['react', 'react-dom'], + incremental: true, + watch: { + onRebuild(error) { + if (error) { + console.log(`× ${name}: An error in prevented the rebuild.`) + return + } + console.log(`✔ ${name}: Rebuilt.`) + }, + }, + }) +} + +main() diff --git a/packages/intersect/scripts/pre-dev.js b/packages/intersect/scripts/pre-dev.js new file mode 100644 index 000000000..531792366 --- /dev/null +++ b/packages/intersect/scripts/pre-dev.js @@ -0,0 +1,28 @@ +/* eslint-disable */ +const fs = require('fs') +const esbuild = require('esbuild') + +async function main() { + if (fs.existsSync('./dist')) { + fs.rmSync('./dist', { recursive: true }, (e) => { + if (e) { + throw e + } + }) + } + + esbuild.build({ + entryPoints: ['./src/index.ts'], + outdir: 'dist/cjs', + minify: false, + bundle: true, + format: 'cjs', + target: 'es6', + jsxFactory: 'React.createElement', + jsxFragment: 'React.Fragment', + tsconfig: './tsconfig.json', + external: ['react', 'react-dom'], + }) +} + +main() diff --git a/packages/intersect/src/index.ts b/packages/intersect/src/index.ts new file mode 100644 index 000000000..207a9ed87 --- /dev/null +++ b/packages/intersect/src/index.ts @@ -0,0 +1,1147 @@ +import { Vec } from '@tldraw/vec' + +export type TLIntersection = { + didIntersect: boolean + message: string + points: number[][] +} + +export interface TLBounds { + minX: number + minY: number + maxX: number + maxY: number + width: number + height: number + rotation?: number +} + +/** + * Get an intersection. + * @param message + * @param points + * @internal + */ +function createIntersection(message: string, ...points: number[][]): TLIntersection { + const didIntersect = points.length > 0 + return { didIntersect, message, points } +} + +/** + * + * @param point + * @param size + * @param rotation + * @internal + */ +function 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]], + ['right', [tr, br]], + ['bottom', [br, bl]], + ['left', [bl, tl]], + ] +} + +/** + * Get whether angle c lies between angles a and b. + * @param a + * @param b + * @param c + * @internal + */ +function isAngleBetween(a: number, b: number, c: number): boolean { + if (c === a || c === b) return true + const PI2 = Math.PI * 2 + const AB = (b - a + PI2) % PI2 + const AC = (c - a + PI2) % PI2 + return AB <= Math.PI !== AC > AB +} + +/* -------------------------------------------------- */ +/* Ray */ +/* -------------------------------------------------- */ + +/** + * Find the intersection between a ray and a ray. + * @param p0 The first ray's point + * @param n0 The first ray's direction vector. + * @param p1 The second ray's point. + * @param n1 The second ray's direction vector. + */ +export function /*#__PURE__*/ intersectRayRay( + 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] + const u = (dy * n1[0] - dx * n1[1]) / det + const v = (dy * n0[0] - dx * n0[1]) / det + if (u < 0 || v < 0) return createIntersection('miss') + + const m0 = n0[1] / n0[0] + const m1 = n1[1] / n1[0] + const b0 = p0[1] - m0 * p0[0] + const b1 = p1[1] - m1 * p1[0] + const x = (b1 - b0) / (m0 - m1) + const y = m0 * x + b0 + + return Number.isFinite(x) + ? createIntersection('intersection', [x, y]) + : createIntersection('parallel') +} + +/** + * Find the intersections between a ray and a line segment. + * @param origin + * @param direction + * @param a1 + * @param a2 + */ +export function /*#__PURE__*/ intersectRayLineSegment( + origin: number[], + direction: number[], + a1: number[], + a2: number[] +): TLIntersection { + const [x, y] = origin + const [dx, dy] = direction + const [x1, y1] = a1 + const [x2, y2] = a2 + + if (dy / dx !== (y2 - y1) / (x2 - x1)) { + const d = dx * (y2 - y1) - dy * (x2 - x1) + if (d !== 0) { + const r = ((y - y1) * (x2 - x1) - (x - x1) * (y2 - y1)) / d + const s = ((y - y1) * dx - (x - x1) * dy) / d + if (r >= 0 && s >= 0 && s <= 1) { + return createIntersection('intersection', [x + r * dx, y + r * dy]) + } + } + } + return createIntersection('no intersection') +} + +/** + * Find the intersections between a ray and a rectangle. + * @param origin + * @param direction + * @param point + * @param size + * @param rotation + */ +export function /*#__PURE__*/ intersectRayRectangle( + origin: number[], + direction: number[], + point: number[], + size: number[], + rotation = 0 +): TLIntersection[] { + return intersectRectangleRay(point, size, rotation, origin, direction) +} + +/** + * Find the intersections between a ray and an ellipse. + * @param origin + * @param direction + * @param center + * @param rx + * @param ry + * @param rotation + */ +export function /*#__PURE__*/ intersectRayEllipse( + origin: number[], + direction: number[], + center: number[], + rx: number, + ry: number, + rotation: number +): TLIntersection { + const a1 = origin + const a2 = Vec.mul(direction, 999999999) + return intersectLineSegmentEllipse(a1, a2, center, rx, ry, rotation) +} + +/** + * Find the intersections between a ray and a bounding box. + * @param origin + * @param direction + * @param bounds + * @param rotation + */ +export function /*#__PURE__*/ intersectRayBounds( + origin: number[], + direction: number[], + bounds: TLBounds, + rotation = 0 +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectRayRectangle(origin, direction, [minX, minY], [width, height], rotation) +} + +/* -------------------------------------------------- */ +/* Line Segment */ +/* -------------------------------------------------- */ + +/** + * Find the intersection between a line segment and a ray. + * @param a1 + * @param a2 + * @param origin + * @param direction + */ +export function /*#__PURE__*/ intersectLineSegmentRay( + a1: number[], + a2: number[], + origin: number[], + direction: number[] +): TLIntersection { + return intersectRayLineSegment(origin, direction, a1, a2) +} + +/** + * Find the intersection between a line segment and a line segment. + * @param a1 + * @param a2 + * @param b1 + * @param b2 + */ +export function /*#__PURE__*/ intersectLineSegmentLineSegment( + 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) + + const ua_t = BV[0] * AB[1] - BV[1] * AB[0] + const ub_t = AV[0] * AB[1] - AV[1] * AB[0] + const u_b = BV[1] * AV[0] - BV[0] * AV[1] + + if (ua_t === 0 || ub_t === 0) { + return createIntersection('coincident') + } + + if (u_b === 0) { + return createIntersection('parallel') + } + + if (u_b !== 0) { + const ua = ua_t / u_b + const ub = ub_t / u_b + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + return createIntersection('intersection', Vec.add(a1, Vec.mul(AV, ua))) + } + } + + return createIntersection('no intersection') +} + +/** + * Find the intersections between a line segment and a rectangle. + * @param a1 + * @param a2 + * @param point + * @param size + */ +export function /*#__PURE__*/ intersectLineSegmentRectangle( + a1: number[], + a2: number[], + point: number[], + size: number[] +): TLIntersection[] { + return intersectRectangleLineSegment(point, size, a1, a2) +} + +/** + * Find the intersections between a line segment and an arc. + * @param a1 + * @param a2 + * @param center + * @param radius + * @param start + * @param end + */ +export function /*#__PURE__*/ intersectLineSegmentArc( + a1: number[], + a2: number[], + center: number[], + radius: number, + start: number[], + end: number[] +): TLIntersection { + const sa = Vec.angle(center, start) + const ea = Vec.angle(center, end) + const ellipseTest = intersectEllipseLineSegment(center, radius, radius, 0, a1, a2) + + if (!ellipseTest.didIntersect) return createIntersection('No intersection') + + const points = ellipseTest.points.filter((point) => + isAngleBetween(sa, ea, Vec.angle(center, point)) + ) + + if (points.length === 0) { + return createIntersection('No intersection') + } + + return createIntersection('intersection', ...points) +} + +/** + * Find the intersections between a line segment and a circle. + * @param a1 + * @param a2 + * @param c + * @param r + */ +export function /*#__PURE__*/ intersectLineSegmentCircle( + 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 cc = + c[0] * c[0] + + c[1] * c[1] + + a1[0] * a1[0] + + a1[1] * a1[1] - + 2 * (c[0] * a1[0] + c[1] * a1[1]) - + r * r + + const deter = b * b - 4 * a * cc + + if (deter < 0) { + return createIntersection('outside') + } + + if (deter === 0) { + return createIntersection('tangent') + } + + const e = Math.sqrt(deter) + const u1 = (-b + e) / (2 * a) + const u2 = (-b - e) / (2 * a) + if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) { + if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) { + return createIntersection('outside') + } else { + return createIntersection('inside') + } + } + + const results: number[][] = [] + if (0 <= u1 && u1 <= 1) results.push(Vec.lrp(a1, a2, u1)) + if (0 <= u2 && u2 <= 1) results.push(Vec.lrp(a1, a2, u2)) + + return createIntersection('intersection', ...results) +} + +/** + * Find the intersections between a line segment and an ellipse. + * @param a1 + * @param a2 + * @param center + * @param rx + * @param ry + * @param rotation + */ +export function /*#__PURE__*/ intersectLineSegmentEllipse( + a1: number[], + a2: number[], + center: number[], + rx: number, + ry: number, + rotation = 0 +): TLIntersection { + // If the ellipse or line segment are empty, return no tValues. + if (rx === 0 || ry === 0 || Vec.isEqual(a1, a2)) { + return createIntersection('No intersection') + } + + // Get the semimajor and semiminor axes. + rx = rx < 0 ? rx : -rx + ry = ry < 0 ? ry : -ry + + // Rotate points and translate so the ellipse is centered at the origin. + a1 = Vec.sub(Vec.rotWith(a1, center, -rotation), center) + a2 = Vec.sub(Vec.rotWith(a2, center, -rotation), center) + + // Calculate the quadratic parameters. + 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 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). + const tValues: number[] = [] + + // Calculate the discriminant. + const discriminant = B * B - 4 * A * C + + if (discriminant === 0) { + // One real solution. + tValues.push(-B / 2 / A) + } else if (discriminant > 0) { + const root = Math.sqrt(discriminant) + // Two real solutions. + tValues.push((-B + root) / 2 / A) + tValues.push((-B - root) / 2 / A) + } + + // Filter to only points that are on the segment. + // Solve for points, then counter-rotate points. + const points = tValues + .filter((t) => t >= 0 && t <= 1) + .map((t) => Vec.add(center, Vec.add(a1, Vec.mul(Vec.sub(a2, a1), t)))) + .map((p) => Vec.rotWith(p, center, rotation)) + + return createIntersection('intersection', ...points) +} + +/** + * Find the intersections between a line segment and a bounding box. + * @param a1 + * @param a2 + * @param bounds + */ +export function /*#__PURE__*/ intersectLineSegmentBounds( + a1: number[], + a2: number[], + bounds: TLBounds +): TLIntersection[] { + return intersectBoundsLineSegment(bounds, a1, a2) +} + +/** + * Find the intersections between a line segment and a polyline. + * @param a1 + * @param a2 + * @param points + */ +export function /*#__PURE__*/ intersectLineSegmentPolyline( + a1: number[], + a2: number[], + points: number[][] +): TLIntersection[] { + const intersections: TLIntersection[] = [] + + for (let i = 1; i < points.length + 1; i++) { + const int = intersectLineSegmentLineSegment(a1, a2, points[i - 1], points[i % points.length]) + + if (int) { + intersections.push(int) + } + } + + return intersections +} + +/* -------------------------------------------------- */ +/* Rectangle */ +/* -------------------------------------------------- */ + +/** + * Find the intersections between a rectangle and a ray. + * @param point + * @param size + * @param rotation + * @param origin + * @param direction + */ +export function /*#__PURE__*/ intersectRectangleRay( + point: number[], + size: number[], + rotation: number, + origin: number[], + direction: number[] +): TLIntersection[] { + const sideIntersections = getRectangleSides(point, size, rotation).reduce( + (acc, [message, [a1, a2]]) => { + const intersection = intersectRayLineSegment(origin, direction, a1, a2) + + if (intersection) { + acc.push(createIntersection(message, ...intersection.points)) + } + + return acc + }, + [] + ) + + return sideIntersections.filter((int) => int.didIntersect) +} + +/** + * Find the intersections between a rectangle and a line segment. + * @param point + * @param size + * @param a1 + * @param a2 + */ +export function /*#__PURE__*/ intersectRectangleLineSegment( + point: number[], + size: number[], + a1: number[], + a2: number[] +): TLIntersection[] { + const sideIntersections = getRectangleSides(point, size).reduce( + (acc, [message, [b1, b2]]) => { + const intersection = intersectLineSegmentLineSegment(a1, a2, b1, b2) + + if (intersection) { + acc.push(createIntersection(message, ...intersection.points)) + } + + return acc + }, + [] + ) + + return sideIntersections.filter((int) => int.didIntersect) +} + +/** + * Find the intersections between a rectangle and a rectangle. + * @param point1 + * @param size1 + * @param point2 + * @param size2 + */ +export function /*#__PURE__*/ intersectRectangleRectangle( + point1: number[], + size1: number[], + point2: number[], + size2: number[] +): TLIntersection[] { + const sideIntersections = getRectangleSides(point1, size1).reduce( + (acc, [message, [a1, a2]]) => { + const intersections = intersectRectangleLineSegment(point2, size2, a1, a2) + + acc.push( + ...intersections.map((int) => + createIntersection(`${message} ${int.message}`, ...int.points) + ) + ) + + return acc + }, + [] + ) + + return sideIntersections.filter((int) => int.didIntersect) +} + +/** + * Find the intersections between a rectangle and an arc. + * @param point + * @param size + * @param center + * @param radius + * @param start + * @param end + */ +export function /*#__PURE__*/ intersectRectangleArc( + point: number[], + size: number[], + center: number[], + radius: number, + start: number[], + end: number[] +): TLIntersection[] { + const sideIntersections = getRectangleSides(point, size).reduce( + (acc, [message, [a1, a2]]) => { + const intersection = intersectArcLineSegment(center, radius, start, end, a1, a2) + + if (intersection) { + acc.push({ ...intersection, message }) + } + + return acc + }, + [] + ) + + return sideIntersections.filter((int) => int.didIntersect) +} + +/** + * Find the intersections between a rectangle and a circle. + * @param point + * @param size + * @param c + * @param r + */ +export function /*#__PURE__*/ intersectRectangleCircle( + point: number[], + size: number[], + c: number[], + r: number +): TLIntersection[] { + const sideIntersections = getRectangleSides(point, size).reduce( + (acc, [message, [a1, a2]]) => { + const intersection = intersectLineSegmentCircle(a1, a2, c, r) + + if (intersection) { + acc.push({ ...intersection, message }) + } + + return acc + }, + [] + ) + + return sideIntersections.filter((int) => int.didIntersect) +} + +/** + * Find the intersections between a rectangle and an ellipse. + * @param point + * @param size + * @param c + * @param rx + * @param ry + * @param rotation + */ +export function /*#__PURE__*/ intersectRectangleEllipse( + point: number[], + size: number[], + c: number[], + rx: number, + ry: number, + rotation = 0 +): TLIntersection[] { + const sideIntersections = getRectangleSides(point, size).reduce( + (acc, [message, [a1, a2]]) => { + const intersection = intersectLineSegmentEllipse(a1, a2, c, rx, ry, rotation) + + if (intersection) { + acc.push({ ...intersection, message }) + } + + return acc + }, + [] + ) + + return sideIntersections.filter((int) => int.didIntersect) +} + +/** + * Find the intersections between a rectangle and a bounding box. + * @param point + * @param size + * @param bounds + */ +export function /*#__PURE__*/ intersectRectangleBounds( + point: number[], + size: number[], + bounds: TLBounds +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectRectangleRectangle(point, size, [minX, minY], [width, height]) +} + +/** + * Find the intersections between a rectangle and a polyline. + * @param point + * @param size + * @param points + */ +export function /*#__PURE__*/ intersectRectanglePolyline( + point: number[], + size: number[], + points: number[][] +): TLIntersection[] { + const sideIntersections = getRectangleSides(point, size).reduce( + (acc, [message, [a1, a2]]) => { + const intersections = intersectLineSegmentPolyline(a1, a2, points) + + if (intersections.length > 0) { + acc.push(createIntersection(message, ...intersections.flatMap((i) => i.points))) + } + + return acc + }, + [] + ) + + return sideIntersections.filter((int) => int.didIntersect) +} + +/* -------------------------------------------------- */ +/* Arc */ +/* -------------------------------------------------- */ + +/** + * Find the intersections between a arc and a line segment. + * @param center + * @param radius + * @param start + * @param end + * @param a1 + * @param a2 + */ +export function /*#__PURE__*/ intersectArcLineSegment( + center: number[], + radius: number, + start: number[], + end: number[], + a1: number[], + a2: number[] +): TLIntersection { + return intersectLineSegmentArc(a1, a2, center, radius, start, end) +} + +/** + * Find the intersections between a arc and a rectangle. + * @param center + * @param radius + * @param start + * @param end + * @param point + * @param size + */ +export function /*#__PURE__*/ intersectArcRectangle( + center: number[], + radius: number, + start: number[], + end: number[], + point: number[], + size: number[] +): TLIntersection[] { + return intersectRectangleArc(point, size, center, radius, start, end) +} + +/** + * Find the intersections between a arc and a bounding box. + * @param center + * @param radius + * @param start + * @param end + * @param bounds + */ +export function /*#__PURE__*/ intersectArcBounds( + center: number[], + radius: number, + start: number[], + end: number[], + bounds: TLBounds +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectArcRectangle(center, radius, start, end, [minX, minY], [width, height]) +} + +/* -------------------------------------------------- */ +/* Circle */ +/* -------------------------------------------------- */ + +/** + * Find the intersections between a circle and a line segment. + * @param c + * @param r + * @param a1 + * @param a2 + */ +export function /*#__PURE__*/ intersectCircleLineSegment( + c: number[], + r: number, + a1: number[], + a2: number[] +): TLIntersection { + return intersectLineSegmentCircle(a1, a2, c, r) +} + +/** + * Find the intersections between a circle and a circle. + * @param c1 + * @param r1 + * @param c2 + * @param r2 + */ +export function /*#__PURE__*/ intersectCircleCircle( + c1: number[], + r1: number, + c2: number[], + r2: number +): TLIntersection { + let dx = c2[0] - c1[0], + dy = c2[1] - c1[1] + + const d = Math.sqrt(dx * dx + dy * dy), + x = (d * d - r2 * r2 + r1 * r1) / (2 * d), + y = Math.sqrt(r1 * r1 - x * x) + + dx /= d + dy /= d + + return createIntersection( + 'intersection', + [c1[0] + dx * x - dy * y, c1[1] + dy * x + dx * y], + [c1[0] + dx * x + dy * y, c1[1] + dy * x - dx * y] + ) +} + +/** + * Find the intersections between a circle and a rectangle. + * @param c + * @param r + * @param point + * @param size + */ +export function /*#__PURE__*/ intersectCircleRectangle( + c: number[], + r: number, + point: number[], + size: number[] +): TLIntersection[] { + return intersectRectangleCircle(point, size, c, r) +} + +/** + * Find the intersections between a circle and a bounding box. + * @param c + * @param r + * @param bounds + */ +export function /*#__PURE__*/ intersectCircleBounds( + c: number[], + r: number, + bounds: TLBounds +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectCircleRectangle(c, r, [minX, minY], [width, height]) +} + +/* -------------------------------------------------- */ +/* Ellipse */ +/* -------------------------------------------------- */ + +/** + * Find the intersections between an ellipse and a ray. + * @param center + * @param rx + * @param ry + * @param rotation + * @param point + * @param direction + */ +export function /*#__PURE__*/ intersectEllipseRay( + center: number[], + rx: number, + ry: number, + rotation: number, + point: number[], + direction: number[] +): TLIntersection { + return intersectRayEllipse(point, direction, center, rx, ry, rotation) +} + +/** + * Find the intersections between an ellipse and a line segment. + * @param center + * @param rx + * @param ry + * @param rotation + * @param a1 + * @param a2 + */ +export function /*#__PURE__*/ intersectEllipseLineSegment( + center: number[], + rx: number, + ry: number, + rotation = 0, + a1: number[], + a2: number[] +): TLIntersection { + if (rx === ry) { + return intersectLineSegmentCircle(a1, a2, center, rx) + } + + return intersectLineSegmentEllipse(a1, a2, center, rx, ry, rotation) +} + +/** + * Find the intersections between an ellipse and a rectangle. + * @param center + * @param rx + * @param ry + * @param rotation + * @param point + * @param size + */ +export function /*#__PURE__*/ intersectEllipseRectangle( + center: number[], + rx: number, + ry: number, + rotation = 0, + point: number[], + size: number[] +): TLIntersection[] { + if (rx === ry) { + return intersectRectangleCircle(point, size, center, rx) + } + + return intersectRectangleEllipse(point, size, center, rx, ry, rotation) +} + +/** + * Find the intersections between an ellipse and an ellipse. + * Adapted from https://gist.github.com/drawable/92792f59b6ff8869d8b1 + * @param _c1 + * @param _rx1 + * @param _ry1 + * @param _r1 + * @param _c2 + * @param _rx2 + * @param _ry2 + * @param _r2 + */ +export function /*#__PURE__*/ intersectEllipseEllipse( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _c1: number[], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _rx1: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _ry1: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _r1: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _c2: number[], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _rx2: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _ry2: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _r2: number +): TLIntersection { + // TODO + return createIntersection('no intersection') +} + +/** + * Find the intersections between an ellipse and a circle. + * @param c + * @param rx + * @param ry + * @param rotation + * @param c2 + * @param r2 + */ +export function /*#__PURE__*/ intersectEllipseCircle( + c: number[], + rx: number, + ry: number, + rotation: number, + c2: number[], + r2: number +): TLIntersection { + return intersectEllipseEllipse(c, rx, ry, rotation, c2, r2, r2, 0) +} + +/** + * Find the intersections between an ellipse and a bounding box. + * @param c + * @param rx + * @param ry + * @param rotation + * @param bounds + */ +export function /*#__PURE__*/ intersectEllipseBounds( + c: number[], + rx: number, + ry: number, + rotation: number, + bounds: TLBounds +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectEllipseRectangle(c, rx, ry, rotation, [minX, minY], [width, height]) +} + +/** + * Find the intersections between a bounding box and a ray. + * @param bounds + * @param origin + * @param direction + */ +export function /*#__PURE__*/ intersectBoundsRay( + bounds: TLBounds, + origin: number[], + direction: number[] +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectRayRectangle(origin, direction, [minX, minY], [width, height]) +} + +/** + * Find the intersections between a bounding box and a line segment. + * @param bounds + * @param a1 + * @param a2 + */ +export function /*#__PURE__*/ intersectBoundsLineSegment( + bounds: TLBounds, + a1: number[], + a2: number[] +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectLineSegmentRectangle(a1, a2, [minX, minY], [width, height]) +} + +/** + * Find the intersections between a bounding box and a rectangle. + * @param bounds + * @param point + * @param size + */ +export function /*#__PURE__*/ intersectBoundsRectangle( + bounds: TLBounds, + point: number[], + size: number[] +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectRectangleRectangle(point, size, [minX, minY], [width, height]) +} + +/** + * Find the intersections between a bounding box and a bounding box. + * @param bounds1 + * @param bounds2 + */ +export function /*#__PURE__*/ intersectBoundsBounds( + bounds1: TLBounds, + bounds2: TLBounds +): TLIntersection[] { + return intersectRectangleRectangle( + [bounds1.minX, bounds1.minY], + [bounds1.width, bounds1.height], + [bounds2.minX, bounds2.minY], + [bounds2.width, bounds2.height] + ) +} + +/** + * Find the intersections between a bounding box and an arc. + * @param bounds + * @param center + * @param radius + * @param start + * @param end + */ +export function /*#__PURE__*/ intersectBoundsArc( + bounds: TLBounds, + center: number[], + radius: number, + start: number[], + end: number[] +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectArcRectangle(center, radius, start, end, [minX, minY], [width, height]) +} + +/** + * Find the intersections between a bounding box and a circle. + * @param bounds + * @param c + * @param r + */ +export function /*#__PURE__*/ intersectBoundsCircle( + bounds: TLBounds, + c: number[], + r: number +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectCircleRectangle(c, r, [minX, minY], [width, height]) +} + +/** + * Find the intersections between a bounding box and an ellipse. + * @param bounds + * @param c + * @param rx + * @param ry + * @param rotation + */ +export function /*#__PURE__*/ intersectBoundsEllipse( + bounds: TLBounds, + c: number[], + rx: number, + ry: number, + rotation = 0 +): TLIntersection[] { + const { minX, minY, width, height } = bounds + return intersectEllipseRectangle(c, rx, ry, rotation, [minX, minY], [width, height]) +} + +/** + * Find the intersections between a bounding box and a polyline. + * @param bounds + * @param points + */ +export function /*#__PURE__*/ intersectBoundsPolyline( + bounds: TLBounds, + points: number[][] +): TLIntersection[] { + return intersectPolylineBounds(points, bounds) +} + +/* -------------------------------------------------- */ +/* Polyline */ +/* -------------------------------------------------- */ + +/** + * Find the intersections between a polyline and a line segment. + * @param points + * @param a1 + * @param a2 + */ +export function /*#__PURE__*/ intersectPolylineLineSegment( + points: number[][], + a1: number[], + a2: number[] +): TLIntersection[] { + return intersectLineSegmentPolyline(a1, a2, points) +} + +/** + * Find the intersections between a polyline and a rectangle. + * @param points + * @param point + * @param size + */ +export function /*#__PURE__*/ intersectPolylineRectangle( + points: number[][], + point: number[], + size: number[] +): TLIntersection[] { + return intersectRectanglePolyline(point, size, points) +} + +/** + * Find the intersections between a polyline and a bounding box. + * @param points + * @param bounds + */ +export function /*#__PURE__*/ intersectPolylineBounds( + points: number[][], + bounds: TLBounds +): TLIntersection[] { + return intersectRectanglePolyline( + [bounds.minX, bounds.minY], + [bounds.width, bounds.height], + points + ) +} diff --git a/packages/intersect/tsconfig.build.json b/packages/intersect/tsconfig.build.json new file mode 100644 index 000000000..292943845 --- /dev/null +++ b/packages/intersect/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "node_modules", + "**/*.test.tsx", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.spec.ts", + "src/test", + "dist", + "docs" + ], + "compilerOptions": { + "composite": false, + "incremental": false, + "declarationMap": false, + "sourceMap": false + } +} diff --git a/packages/intersect/tsconfig.json b/packages/intersect/tsconfig.json new file mode 100644 index 000000000..0f7eeae4f --- /dev/null +++ b/packages/intersect/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "exclude": ["node_modules", "dist", "docs"], + "compilerOptions": { + "outDir": "./dist/types", + "rootDir": "src", + "baseUrl": "src" + }, + "references": [{ "path": "../../packages/vec" }] +} diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index 8e2a53dc8..d97ce5951 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -65,9 +65,11 @@ "@radix-ui/react-tooltip": "^0.0.20", "@stitches/react": "^1.0.0", "@tldraw/core": "^0.0.86", + "@tldraw/vec": "^0.0.86", + "@tldraw/intersect": "^0.0.86", "perfect-freehand": "^0.5.3", "react-hotkeys-hook": "^3.4.0", "rko": "^0.5.25" }, "gitHead": "55da8880eb3d8ab5fb62b5eb7853065922c95dcf" -} +} \ No newline at end of file diff --git a/packages/tldraw/scripts/build.js b/packages/tldraw/scripts/build.js index 36aa13ddc..f07c9f56a 100644 --- a/packages/tldraw/scripts/build.js +++ b/packages/tldraw/scripts/build.js @@ -1,6 +1,7 @@ /* eslint-disable */ const fs = require('fs') const esbuild = require('esbuild') +const { gzip } = require('zlib') const name = process.env.npm_package_name || '' @@ -25,9 +26,10 @@ async function main() { jsxFragment: 'React.Fragment', tsconfig: './tsconfig.json', external: ['react', 'react-dom'], + metafile: true, }) - esbuild.buildSync({ + const esmResult = esbuild.buildSync({ entryPoints: ['./src/index.ts'], outdir: 'dist/esm', minify: true, @@ -38,6 +40,22 @@ async function main() { jsxFactory: 'React.createElement', jsxFragment: 'React.Fragment', external: ['react', 'react-dom'], + metafile: true, + }) + + let esmSize = 0 + Object.values(esmResult.metafile.outputs).forEach((output) => { + esmSize += output.bytes + }) + + fs.readFile('./dist/esm/index.js', (_err, data) => { + gzip(data, (_err, result) => { + console.log( + `✔ ${name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${( + result.length / 1000 + ).toFixed(2)}kb minified)` + ) + }) }) console.log(`✔ ${name}: Built package.`) diff --git a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx index c2ad77582..320c6b835 100644 --- a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx +++ b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx @@ -3,13 +3,13 @@ import { SVGContainer, TLBounds, Utils, - Vec, TLTransformInfo, - Intersect, TLHandle, TLPointerInfo, TLShapeProps, } from '@tldraw/core' +import { Vec } from '@tldraw/vec' + import getStroke from 'perfect-freehand' import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles' import { @@ -22,6 +22,14 @@ import { TLDrawShape, ArrowBinding, } from '~types' +import { + intersectArcBounds, + intersectCircleCircle, + intersectCircleLineSegment, + intersectLineSegmentBounds, + intersectRayBounds, + intersectRayEllipse, +} from '@tldraw/intersect' export class Arrow extends TLDrawShapeUtil { type = TLDrawShapeType.Arrow as const @@ -299,12 +307,12 @@ export class Arrow extends TLDrawShapeUtil { } if (Vec.isEqual(Vec.med(start.point, end.point), bend.point)) { - return Intersect.lineSegment.bounds(sp, ep, brushBounds).length > 0 + return intersectLineSegmentBounds(sp, ep, brushBounds).length > 0 } else { const [cx, cy, r] = getCtp(shape) const cp = Vec.add(shape.point, [cx, cy]) - return Intersect.arc.bounds(cp, r, sp, ep, brushBounds).length > 0 + return intersectArcBounds(cp, r, sp, ep, brushBounds).length > 0 } } @@ -430,15 +438,13 @@ export class Arrow extends TLDrawShapeUtil { const direction = Vec.uni(Vec.sub(Vec.add(anchor, shape.point), origin)) if ([TLDrawShapeType.Rectangle, TLDrawShapeType.Text].includes(target.type)) { - let hits = Intersect.ray - .bounds(origin, direction, intersectBounds, target.rotation) + let hits = intersectRayBounds(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)) if (hits.length < 2) { - hits = Intersect.ray - .bounds(origin, Vec.neg(direction), intersectBounds) + hits = intersectRayBounds(origin, Vec.neg(direction), intersectBounds) .filter((int) => int.didIntersect) .map((int) => int.points[0]) .sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin)) @@ -451,16 +457,14 @@ export class Arrow extends TLDrawShapeUtil { handlePoint = Vec.sub(hits[0], shape.point) } else if (target.type === TLDrawShapeType.Ellipse) { - 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)) + const hits = intersectRayEllipse( + 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)) if (!hits[0]) { console.warn('No intersections') @@ -701,7 +705,7 @@ function getCurvedArrowHeadPoints( r2: number, sweep: boolean ) { - const ints = Intersect.circle.circle(A, r1 * 0.618, C, r2).points + const ints = intersectCircleCircle(A, r1 * 0.618, C, r2).points if (!ints) { console.warn('Could not find an intersection for the arrow head.') return { left: A, right: A } @@ -714,7 +718,7 @@ function getCurvedArrowHeadPoints( } function getStraightArrowHeadPoints(A: number[], B: number[], r: number) { - const ints = Intersect.circle.lineSegment(A, r, A, B).points + const ints = intersectCircleLineSegment(A, r, A, B).points if (!ints) { console.warn('Could not find an intersection for the arrow head.') return { left: A, right: A } diff --git a/packages/tldraw/src/shape/shapes/draw/draw.tsx b/packages/tldraw/src/shape/shapes/draw/draw.tsx index 23226ccaf..14963e1bc 100644 --- a/packages/tldraw/src/shape/shapes/draw/draw.tsx +++ b/packages/tldraw/src/shape/shapes/draw/draw.tsx @@ -1,5 +1,7 @@ import * as React from 'react' -import { SVGContainer, TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core' +import { SVGContainer, TLBounds, Utils, TLTransformInfo } from '@tldraw/core' +import { Vec } from '@tldraw/vec' +import { intersectBoundsBounds, intersectBoundsPolyline } from '@tldraw/intersect' import getStroke, { getStrokePoints } from 'perfect-freehand' import { defaultStyle, getShapeStyle } from '~shape/shape-styles' import { @@ -202,10 +204,10 @@ export class Draw extends TLDrawShapeUtil { return ( Utils.boundsContain(brushBounds, bounds) || ((Utils.boundsContain(bounds, brushBounds) || - Intersect.bounds.bounds(bounds, brushBounds).length > 0) && - Intersect.polyline.bounds( - shape.points, - Utils.translateBounds(brushBounds, Vec.neg(shape.point)) + intersectBoundsBounds(bounds, brushBounds).length > 0) && + intersectBoundsPolyline( + Utils.translateBounds(brushBounds, Vec.neg(shape.point)), + shape.points ).length > 0) ) } @@ -220,7 +222,7 @@ export class Draw extends TLDrawShapeUtil { return ( Utils.boundsContain(brushBounds, rBounds) || - Intersect.bounds.polyline( + intersectBoundsPolyline( Utils.translateBounds(brushBounds, Vec.neg(shape.point)), rotatedBounds ).length > 0 diff --git a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx index f10cb5605..625af3188 100644 --- a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx +++ b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx @@ -1,13 +1,6 @@ import * as React from 'react' -import { - SVGContainer, - Utils, - TLTransformInfo, - TLBounds, - Intersect, - TLShapeProps, - Vec, -} from '@tldraw/core' +import { SVGContainer, Utils, TLTransformInfo, TLBounds, TLShapeProps } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { ArrowShape, DashStyle, @@ -18,6 +11,11 @@ import { } from '~types' import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles' import getStroke from 'perfect-freehand' +import { + intersectLineSegmentEllipse, + intersectPolylineBounds, + intersectRayEllipse, +} from '@tldraw/intersect' // TODO // [ ] Improve indicator shape for drawn shapes @@ -181,7 +179,7 @@ export class Ellipse extends TLDrawShapeUtil { return ( rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) || - Intersect.polyline.bounds(rotatedCorners, bounds).length > 0 + intersectPolylineBounds(rotatedCorners, bounds).length > 0 ) } @@ -226,15 +224,24 @@ export class Ellipse extends TLDrawShapeUtil { // .map((int) => int.points[0]) // .sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0] - let 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] + let intersection = intersectRayEllipse( + 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) { - intersection = Intersect.lineSegment - .ellipse(point, center, center, shape.radius[0], shape.radius[1], shape.rotation || 0) - .points.sort((a, b) => Vec.dist(a, point) - Vec.dist(b, point))[0] + intersection = intersectLineSegmentEllipse( + point, + center, + center, + shape.radius[0], + shape.radius[1], + shape.rotation || 0 + ).points.sort((a, b) => Vec.dist(a, point) - Vec.dist(b, point))[0] } // The anchor is a point between the handle and the intersection @@ -258,7 +265,7 @@ export class Ellipse extends TLDrawShapeUtil { distance = 16 } else { // Find the distance between the point and the ellipse - const innerIntersection = Intersect.lineSegment.ellipse( + const innerIntersection = intersectLineSegmentEllipse( point, center, center, diff --git a/packages/tldraw/src/shape/shapes/group/group.tsx b/packages/tldraw/src/shape/shapes/group/group.tsx index fc167d050..71717b049 100644 --- a/packages/tldraw/src/shape/shapes/group/group.tsx +++ b/packages/tldraw/src/shape/shapes/group/group.tsx @@ -1,5 +1,7 @@ import * as React from 'react' -import { SVGContainer, TLBounds, Utils, Vec, Intersect, TLShapeProps } from '@tldraw/core' +import { SVGContainer, TLBounds, Utils, TLShapeProps } from '@tldraw/core' +import { Vec } from '@tldraw/vec' +import { intersectRayBounds, intersectPolylineBounds } from '@tldraw/intersect' import { defaultStyle, getPerfectDashProps } from '~shape/shape-styles' import { GroupShape, @@ -179,8 +181,7 @@ export class Group extends TLDrawShapeUtil { // origin through point and expanded bounds. // TODO: Make this a ray vs rounded rect intersection - const intersection = Intersect.ray - .bounds(origin, direction, expandedBounds) + const intersection = intersectRayBounds(origin, direction, expandedBounds) .filter((int) => int.didIntersect) .map((int) => int.points[0]) .sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0] @@ -228,7 +229,7 @@ export class Group extends TLDrawShapeUtil { return ( rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) || - Intersect.polyline.bounds(rotatedCorners, bounds).length > 0 + intersectPolylineBounds(rotatedCorners, bounds).length > 0 ) } diff --git a/packages/tldraw/src/shape/shapes/post-it/post-it.tsx b/packages/tldraw/src/shape/shapes/post-it/post-it.tsx index 28656989a..212f3a8d8 100644 --- a/packages/tldraw/src/shape/shapes/post-it/post-it.tsx +++ b/packages/tldraw/src/shape/shapes/post-it/post-it.tsx @@ -1,15 +1,9 @@ import * as React from 'react' -import { - TLBounds, - Utils, - Vec, - TLTransformInfo, - Intersect, - TLShapeProps, - HTMLContainer, -} from '@tldraw/core' +import { TLBounds, Utils, TLTransformInfo, TLShapeProps, HTMLContainer } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { defaultStyle, getShapeStyle } from '~shape/shape-styles' import { PostItShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, ArrowShape } from '~types' +import { intersectPolylineBounds, intersectRayBounds } from '@tldraw/intersect' // TODO // [ ] - Make sure that fill does not extend drawn shape at corners @@ -154,8 +148,7 @@ export class PostIt extends TLDrawShapeUtil { // origin through point and expanded bounds. // TODO: Make this a ray vs rounded rect intersection - const intersection = Intersect.ray - .bounds(origin, direction, expandedBounds) + const intersection = intersectRayBounds(origin, direction, expandedBounds) .filter((int) => int.didIntersect) .map((int) => int.points[0]) .sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0] @@ -198,7 +191,7 @@ export class PostIt extends TLDrawShapeUtil { return ( rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) || - Intersect.polyline.bounds(rotatedCorners, bounds).length > 0 + intersectPolylineBounds(rotatedCorners, bounds).length > 0 ) } diff --git a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx index 4b55df950..f9e7d0cf1 100644 --- a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx +++ b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx @@ -1,14 +1,7 @@ import * as React from 'react' -import { - TLBounds, - Utils, - Vec, - TLTransformInfo, - Intersect, - TLShapeProps, - SVGContainer, - HTMLContainer, -} from '@tldraw/core' +import { TLBounds, Utils, TLTransformInfo, TLShapeProps, SVGContainer } from '@tldraw/core' +import { intersectRayBounds } from '@tldraw/intersect' +import { Vec } from '@tldraw/vec' import getStroke from 'perfect-freehand' import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles' import { @@ -234,8 +227,7 @@ export class Rectangle extends TLDrawShapeUtil { // origin through point and expanded bounds. // TODO: Make this a ray vs rounded rect intersection - const intersection = Intersect.ray - .bounds(origin, direction, expandedBounds) + const intersection = intersectRayBounds(origin, direction, expandedBounds) .filter((int) => int.didIntersect) .map((int) => int.points[0]) .sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0] @@ -278,12 +270,7 @@ export class Rectangle extends TLDrawShapeUtil { bounds: TLBounds, { initialShape, transformOrigin, scaleX, scaleY }: TLTransformInfo ) { - if (!shape.rotation && !shape.isAspectRatioLocked) { - return { - point: Vec.round([bounds.minX, bounds.minY]), - size: Vec.round([bounds.width, bounds.height]), - } - } else { + if (shape.rotation || shape.isAspectRatioLocked) { const size = Vec.round( Vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY))) ) @@ -309,6 +296,11 @@ export class Rectangle extends TLDrawShapeUtil { point, rotation, } + } else { + return { + point: Vec.round([bounds.minX, bounds.minY]), + size: Vec.round([bounds.width, bounds.height]), + } } } diff --git a/packages/tldraw/src/shape/shapes/text/text.tsx b/packages/tldraw/src/shape/shapes/text/text.tsx index 0adc1cfd4..69e9411a1 100644 --- a/packages/tldraw/src/shape/shapes/text/text.tsx +++ b/packages/tldraw/src/shape/shapes/text/text.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' -import { HTMLContainer, TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core' +import { HTMLContainer, TLBounds, Utils, TLTransformInfo } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { getShapeStyle, getFontStyle, defaultStyle } from '~shape/shape-styles' import { TextShape, @@ -12,6 +13,7 @@ import { } from '~types' import styled from '~styles' import TextAreaUtils from './text-utils' +import { intersectPolylineBounds, intersectRayBounds } from '@tldraw/intersect' const LETTER_SPACING = -1.5 @@ -255,7 +257,7 @@ export class Text extends TLDrawShapeUtil { return ( rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) || - Intersect.polyline.bounds(rotatedCorners, bounds).length > 0 + intersectPolylineBounds(rotatedCorners, bounds).length > 0 ) } @@ -373,8 +375,7 @@ export class Text extends TLDrawShapeUtil { // origin through point and expanded bounds. // TODO: Make this a ray vs rounded rect intersection - const intersection = Intersect.ray - .bounds(origin, direction, expandedBounds) + const intersection = intersectRayBounds(origin, direction, expandedBounds) .filter((int) => int.didIntersect) .map((int) => int.points[0]) .sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0] diff --git a/packages/tldraw/src/state/command/duplicate/duplicate.command.ts b/packages/tldraw/src/state/command/duplicate/duplicate.command.ts index db45ec07e..17743a42d 100644 --- a/packages/tldraw/src/state/command/duplicate/duplicate.command.ts +++ b/packages/tldraw/src/state/command/duplicate/duplicate.command.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { Utils, Vec } from '@tldraw/core' +import { Utils } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { TLDR } from '~state/tldr' import type { Data, PagePartial, TLDrawCommand } from '~types' diff --git a/packages/tldraw/src/state/command/move-to-page/move-to-page.command.ts b/packages/tldraw/src/state/command/move-to-page/move-to-page.command.ts index a0cbbe97e..dd820a300 100644 --- a/packages/tldraw/src/state/command/move-to-page/move-to-page.command.ts +++ b/packages/tldraw/src/state/command/move-to-page/move-to-page.command.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { ArrowShape, Data, PagePartial, TLDrawCommand, TLDrawShape } from '~types' import { TLDR } from '~state/tldr' -import { Utils, Vec } from '@tldraw/core' +import { Utils } from '@tldraw/core' +import { Vec } from '@tldraw/vec' export function moveToPage( data: Data, diff --git a/packages/tldraw/src/state/command/rotate/rotate.command.ts b/packages/tldraw/src/state/command/rotate/rotate.command.ts index cc831ffe5..9d283361e 100644 --- a/packages/tldraw/src/state/command/rotate/rotate.command.ts +++ b/packages/tldraw/src/state/command/rotate/rotate.command.ts @@ -1,4 +1,5 @@ -import { Utils, Vec } from '@tldraw/core' +import { Utils } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import type { TLDrawCommand, Data } from '~types' import { TLDR } from '~state/tldr' diff --git a/packages/tldraw/src/state/command/translate/translate.command.ts b/packages/tldraw/src/state/command/translate/translate.command.ts index d0e8a1b67..2ba337f2a 100644 --- a/packages/tldraw/src/state/command/translate/translate.command.ts +++ b/packages/tldraw/src/state/command/translate/translate.command.ts @@ -1,4 +1,4 @@ -import { Vec } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import type { Data, TLDrawCommand, PagePartial } from '~types' import { TLDR } from '~state/tldr' diff --git a/packages/tldraw/src/state/session/sessions/arrow/arrow.session.ts b/packages/tldraw/src/state/session/sessions/arrow/arrow.session.ts index 278f4321e..2eb1e6f0f 100644 --- a/packages/tldraw/src/state/session/sessions/arrow/arrow.session.ts +++ b/packages/tldraw/src/state/session/sessions/arrow/arrow.session.ts @@ -7,7 +7,8 @@ import { Session, TLDrawStatus, } from '~types' -import { Vec, Utils, TLHandle } from '@tldraw/core' +import { Vec } from '@tldraw/vec' +import { Utils } from '@tldraw/core' import { TLDR } from '~state/tldr' export class ArrowSession implements Session { diff --git a/packages/tldraw/src/state/session/sessions/brush/brush.session.ts b/packages/tldraw/src/state/session/sessions/brush/brush.session.ts index 2b93b8279..ca8ceed2e 100644 --- a/packages/tldraw/src/state/session/sessions/brush/brush.session.ts +++ b/packages/tldraw/src/state/session/sessions/brush/brush.session.ts @@ -1,4 +1,5 @@ -import { brushUpdater, Utils, Vec } from '@tldraw/core' +import { brushUpdater, Utils } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { Data, Session, TLDrawPatch, TLDrawStatus } from '~types' import { getShapeUtils } from '~shape' import { TLDR } from '~state/tldr' diff --git a/packages/tldraw/src/state/session/sessions/draw/draw.session.ts b/packages/tldraw/src/state/session/sessions/draw/draw.session.ts index 0397de7bb..10e58b223 100644 --- a/packages/tldraw/src/state/session/sessions/draw/draw.session.ts +++ b/packages/tldraw/src/state/session/sessions/draw/draw.session.ts @@ -1,4 +1,5 @@ -import { Utils, Vec } from '@tldraw/core' +import { Utils } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { Data, DrawShape, Session, TLDrawStatus } from '~types' import { TLDR } from '~state/tldr' diff --git a/packages/tldraw/src/state/session/sessions/handle/handle.session.ts b/packages/tldraw/src/state/session/sessions/handle/handle.session.ts index 11a0c0942..49f185a4e 100644 --- a/packages/tldraw/src/state/session/sessions/handle/handle.session.ts +++ b/packages/tldraw/src/state/session/sessions/handle/handle.session.ts @@ -1,4 +1,4 @@ -import { Vec } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { ShapesWithProp, TLDrawStatus } from '~types' import type { Session } from '~types' import type { Data } from '~types' diff --git a/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts b/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts index a883f85d4..469daeed3 100644 --- a/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts +++ b/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts @@ -1,4 +1,5 @@ -import { Utils, Vec } from '@tldraw/core' +import { Utils } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { Session, TLDrawShape, TLDrawStatus } from '~types' import type { Data } from '~types' import { TLDR } from '~state/tldr' diff --git a/packages/tldraw/src/state/session/sessions/transform-single/transform-single.session.ts b/packages/tldraw/src/state/session/sessions/transform-single/transform-single.session.ts index 588f25712..d161621dd 100644 --- a/packages/tldraw/src/state/session/sessions/transform-single/transform-single.session.ts +++ b/packages/tldraw/src/state/session/sessions/transform-single/transform-single.session.ts @@ -1,4 +1,5 @@ -import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core' +import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { TLDrawShape, TLDrawStatus } from '~types' import type { Session } from '~types' import type { Data } from '~types' diff --git a/packages/tldraw/src/state/session/sessions/transform/transform.session.ts b/packages/tldraw/src/state/session/sessions/transform/transform.session.ts index d68fc2216..e324a2ce2 100644 --- a/packages/tldraw/src/state/session/sessions/transform/transform.session.ts +++ b/packages/tldraw/src/state/session/sessions/transform/transform.session.ts @@ -1,4 +1,5 @@ -import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core' +import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { Session, TLDrawShape, TLDrawStatus } from '~types' import type { Data } from '~types' import { TLDR } from '~state/tldr' diff --git a/packages/tldraw/src/state/session/sessions/translate/translate.session.ts b/packages/tldraw/src/state/session/sessions/translate/translate.session.ts index dbafaf07e..128cb4dfa 100644 --- a/packages/tldraw/src/state/session/sessions/translate/translate.session.ts +++ b/packages/tldraw/src/state/session/sessions/translate/translate.session.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { TLPageState, Utils, Vec } from '@tldraw/core' +import { TLPageState, Utils } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { TLDrawShape, TLDrawBinding, diff --git a/packages/tldraw/src/state/tldr.ts b/packages/tldraw/src/state/tldr.ts index 9168a48d1..c672002e3 100644 --- a/packages/tldraw/src/state/tldr.ts +++ b/packages/tldraw/src/state/tldr.ts @@ -1,4 +1,4 @@ -import { TLBounds, TLTransformInfo, Vec, Utils, TLPageState } from '@tldraw/core' +import { TLBounds, TLTransformInfo, Utils, TLPageState } from '@tldraw/core' import { getShapeUtils } from '~shape' import type { Data, @@ -11,6 +11,7 @@ import type { TLDrawCommand, TLDrawPatch, } from '~types' +import { Vec } from '@tldraw/vec' export class TLDR { static getShapeUtils( diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 102372d6b..fc5951d0c 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -11,11 +11,11 @@ import { TLPointerEventHandler, TLWheelEventHandler, Utils, - Vec, brushUpdater, TLPointerInfo, TLBounds, } from '@tldraw/core' +import { Vec } from '@tldraw/vec' import { FlipType, TextShape, diff --git a/packages/tldraw/tsconfig.json b/packages/tldraw/tsconfig.json index 875ea21fb..22c7cd686 100644 --- a/packages/tldraw/tsconfig.json +++ b/packages/tldraw/tsconfig.json @@ -6,12 +6,16 @@ "strict": true, "composite": true, "emitDeclarationOnly": true, - "rootDir": "src", + "rootDir": ".", "outDir": "./dist/types", "baseUrl": "src", "paths": { "~*": ["./*"] } }, - "references": [{ "path": "../../packages/core" }] + "references": [ + { "path": "../../packages/intersect" }, + { "path": "../../packages/vec" }, + { "path": "../../packages/core" } + ] } diff --git a/packages/vec/README.md b/packages/vec/README.md new file mode 100644 index 000000000..c8d3b3631 --- /dev/null +++ b/packages/vec/README.md @@ -0,0 +1,3 @@ +# Dev Server + +Dev server with fast refresh. diff --git a/packages/vec/package.json b/packages/vec/package.json new file mode 100644 index 000000000..756b591ae --- /dev/null +++ b/packages/vec/package.json @@ -0,0 +1,55 @@ +{ + "name": "@tldraw/vec", + "version": "0.0.86", + "private": false, + "description": "A tiny little drawing app (vec)", + "author": "@steveruizok", + "repository": { + "type": "git", + "url": "git+https://github.com/tldraw/tldraw.git", + "directory": "packages/vec" + }, + "license": "MIT", + "keywords": [], + "files": [ + "dist/**/*" + ], + "sideEffects": false, + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "typings": "./dist/types/index.d.ts", + "scripts": { + "start:pre": "node scripts/pre-dev && yarn types:pre", + "start": "node scripts/dev & yarn types:dev", + "build": "node scripts/build && yarn types:build", + "types:pre": "tsc", + "types:dev": "tsc --watch", + "types:build": "tsc --project tsconfig.build.json", + "lint": "eslint src/ --ext .ts,.tsx", + "clean": "rm -rf dist", + "ts-node": "ts-node", + "docs": "typedoc", + "docs:watch": "typedoc --watch" + }, + "devDependencies": { + "@babel/core": "^7.15.5", + "@babel/preset-env": "^7.15.4", + "@types/jest": "^27.0.1", + "@types/node": "^16.7.10", + "@typescript-eslint/eslint-plugin": "^4.30.0", + "@typescript-eslint/parser": "^4.30.0", + "esbuild": "^0.12.24", + "eslint": "^7.32.0", + "lerna": "^4.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "ts-node": "^10.2.1", + "tslib": "^2.3.1", + "typedoc": "^0.21.9", + "typescript": "^4.4.2" + }, + "peerDependencies": {}, + "dependencies": {}, + "gitHead": "55da8880eb3d8ab5fb62b5eb7853065922c95dcf" +} \ No newline at end of file diff --git a/packages/vec/scripts/build.js b/packages/vec/scripts/build.js new file mode 100644 index 000000000..b8677c06b --- /dev/null +++ b/packages/vec/scripts/build.js @@ -0,0 +1,52 @@ +/* eslint-disable */ +const fs = require('fs') +const esbuild = require('esbuild') + +const name = process.env.npm_package_name || '' + +async function main() { + if (fs.existsSync('./dist')) { + fs.rmSync('./dist', { recursive: true }, (e) => { + if (e) { + throw e + } + }) + } + + try { + esbuild.buildSync({ + entryPoints: ['./src/index.ts'], + outdir: 'dist/cjs', + minify: true, + bundle: true, + sourcemap: true, + format: 'cjs', + target: 'es6', + jsxFactory: 'React.createElement', + jsxFragment: 'React.Fragment', + tsconfig: './tsconfig.build.json', + external: ['react', 'react-dom'], + }) + + esbuild.buildSync({ + entryPoints: ['./src/index.ts'], + outdir: 'dist/esm', + minify: true, + bundle: true, + sourcemap: true, + format: 'esm', + target: 'es6', + tsconfig: './tsconfig.build.json', + jsxFactory: 'React.createElement', + jsxFragment: 'React.Fragment', + external: ['react', 'react-dom'], + }) + + console.log(`✔ ${name}: Built package.`) + } catch (e) { + console.log(`× ${name}: Build failed due to an error.`) + console.log(e) + } +} + +main() diff --git a/packages/vec/scripts/dev.js b/packages/vec/scripts/dev.js new file mode 100644 index 000000000..050de844d --- /dev/null +++ b/packages/vec/scripts/dev.js @@ -0,0 +1,31 @@ +/* eslint-disable */ +const esbuild = require('esbuild') + +const name = process.env.npm_package_name || '' + +async function main() { + esbuild.build({ + entryPoints: ['./src/index.ts'], + outdir: 'dist/cjs', + minify: false, + bundle: true, + format: 'cjs', + target: 'es6', + jsxFactory: 'React.createElement', + jsxFragment: 'React.Fragment', + tsconfig: './tsconfig.json', + external: ['react', 'react-dom'], + incremental: true, + watch: { + onRebuild(error) { + if (error) { + console.log(`× ${name}: An error in prevented the rebuild.`) + return + } + console.log(`✔ ${name}: Rebuilt.`) + }, + }, + }) +} + +main() diff --git a/packages/vec/scripts/pre-dev.js b/packages/vec/scripts/pre-dev.js new file mode 100644 index 000000000..ba8c1eb4d --- /dev/null +++ b/packages/vec/scripts/pre-dev.js @@ -0,0 +1,27 @@ +/* eslint-disable */ +const fs = require('fs') +const esbuild = require('esbuild') + +async function main() { + if (fs.existsSync('./dist')) { + fs.rmSync('./dist', { recursive: true }, (e) => { + if (e) { + throw e + } + }) + } + + esbuild.build({ + entryPoints: ['./src/index.ts'], + outdir: 'dist/cjs', + minify: true, + bundle: true, + sourcemap: true, + format: 'cjs', + target: 'es6', + tsconfig: './tsconfig.json', + external: ['react', 'react-dom'], + }) +} + +main() diff --git a/packages/core/src/utils/vec.tsx b/packages/vec/src/index.ts similarity index 100% rename from packages/core/src/utils/vec.tsx rename to packages/vec/src/index.ts diff --git a/packages/vec/tsconfig.build.json b/packages/vec/tsconfig.build.json new file mode 100644 index 000000000..292943845 --- /dev/null +++ b/packages/vec/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "node_modules", + "**/*.test.tsx", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.spec.ts", + "src/test", + "dist", + "docs" + ], + "compilerOptions": { + "composite": false, + "incremental": false, + "declarationMap": false, + "sourceMap": false + } +} diff --git a/packages/vec/tsconfig.json b/packages/vec/tsconfig.json new file mode 100644 index 000000000..d72359299 --- /dev/null +++ b/packages/vec/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "exclude": ["node_modules", "dist", "docs"], + "compilerOptions": { + "outDir": "./dist/types", + "rootDir": "src", + "baseUrl": "src" + } +} diff --git a/tsconfig.base.json b/tsconfig.base.json index a00d3ac32..556ea81b7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -3,7 +3,9 @@ // For references "declaration": true, "declarationMap": true, + "composite": true, "sourceMap": true, + "emitDeclarationOnly": true, // Other "allowSyntheticDefaultImports": true, "esModuleInterop": true, diff --git a/tsconfig.json b/tsconfig.json index b0219f154..c702d4527 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,12 +3,19 @@ "extends": "./tsconfig.base.json", "exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts"], "files": [], - "references": [{ "path": "./packages/tldraw" }, { "path": "./packages/core" }], + "references": [ + { "path": "./packages/vec" }, + { "path": "./packages/intersect" }, + { "path": "./packages/tldraw" }, + { "path": "./packages/core" } + ], "compilerOptions": { "baseUrl": ".", "paths": { - "@tldraw/tldraw": ["./packages/tldraw/dist"], - "@tldraw/core": ["./packages/core/dist"], + "@tldraw/tldraw": ["./packages/tldraw"], + "@tldraw/core": ["./packages/core"], + "@tldraw/vec": ["./packages/vec"], + "@tldraw/intersect": ["./packages/intersect"], "+*": ["./packages/core/src/*"], "~*": ["./packages/tldraw/src/*"] }