diff --git a/.vscode/tasks.json b/.vscode/tasks.json
deleted file mode 100644
index a96ff102a..000000000
--- a/.vscode/tasks.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "version": "2.0.0",
- "tasks": [
- {
- "label": "Check for type errors",
- "type": "typescript",
- "tsconfig": "tsconfig.json",
- "option": "watch",
- "problemMatcher": ["$tsc-watch"],
- "group": "build"
- }
- ]
-}
diff --git a/apps/www/components/Editor.tsx b/apps/www/components/Editor.tsx
index cd6ec81e3..c54887113 100644
--- a/apps/www/components/Editor.tsx
+++ b/apps/www/components/Editor.tsx
@@ -1,7 +1,7 @@
-import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
-import * as gtag from '-utils/gtag'
import React from 'react'
-import { useAccountHandlers } from '-hooks/useAccountHandlers'
+import * as gtag from 'utils/gtag'
+import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
+import { useAccountHandlers } from 'hooks/useAccountHandlers'
declare const window: Window & { app: TldrawApp }
@@ -19,9 +19,9 @@ export default function Editor({ id = 'home', isUser = false, isSponsor = false
// Send events to gtag as actions.
const handlePersist = React.useCallback((_app: TldrawApp, reason?: string) => {
gtag.event({
- action: reason,
+ action: reason ?? '',
category: 'editor',
- label: reason || 'persist',
+ label: reason ?? 'persist',
value: 0,
})
}, [])
diff --git a/apps/www/components/MultiplayerEditor.tsx b/apps/www/components/MultiplayerEditor.tsx
index c59f73d8f..bce6d387b 100644
--- a/apps/www/components/MultiplayerEditor.tsx
+++ b/apps/www/components/MultiplayerEditor.tsx
@@ -3,9 +3,9 @@ import * as React from 'react'
import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
import { createClient } from '@liveblocks/client'
import { LiveblocksProvider, RoomProvider } from '@liveblocks/react'
-import { useAccountHandlers } from '-hooks/useAccountHandlers'
-import { styled } from '-styles'
-import { useMultiplayerState } from '-hooks/useMultiplayerState'
+import { useAccountHandlers } from 'hooks/useAccountHandlers'
+import { styled } from 'styles'
+import { useMultiplayerState } from 'hooks/useMultiplayerState'
const client = createClient({
publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_API_KEY || '',
diff --git a/apps/www/hooks/useMultiplayerState.ts b/apps/www/hooks/useMultiplayerState.ts
index 6e0849e9e..bb97ebbc9 100644
--- a/apps/www/hooks/useMultiplayerState.ts
+++ b/apps/www/hooks/useMultiplayerState.ts
@@ -129,10 +129,17 @@ export function useMultiplayerState(roomId: string) {
page: { shapes, bindings },
},
},
- } = doc.toObject()
+ } = doc.toObject() as { document: TDDocument }
- Object.values(shapes).forEach((shape) => lShapes.set(shape.id, shape))
- Object.values(bindings).forEach((binding) => lBindings.set(binding.id, binding))
+ for (const key in shapes) {
+ const shape = shapes[key]
+ lShapes.set(shape.id, shape)
+ }
+
+ for (const key in bindings) {
+ const binding = bindings[key]
+ lBindings.set(binding.id, binding)
+ }
}
}
@@ -175,21 +182,23 @@ export function useMultiplayerState(roomId: string) {
if (!(lShapes && lBindings)) return
- Object.entries(shapes).forEach(([id, shape]) => {
+ for (const id in shapes) {
+ const shape = shapes[id]
if (!shape) {
lShapes.delete(id)
} else {
lShapes.set(shape.id, shape)
}
- })
+ }
- Object.entries(bindings).forEach(([id, binding]) => {
+ for (const id in bindings) {
+ const binding = bindings[id]
if (!binding) {
lBindings.delete(id)
} else {
lBindings.set(binding.id, binding)
}
- })
+ }
rExpectingUpdate.current = true
})
diff --git a/apps/www/package.json b/apps/www/package.json
index 1aade9c66..e62c5b7ad 100644
--- a/apps/www/package.json
+++ b/apps/www/package.json
@@ -25,6 +25,7 @@
"@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.13.2",
"@stitches/react": "^1.2.5",
+ "@tldraw/core": "^1.1.4",
"@tldraw/tldraw": "^1.1.4",
"@types/next-auth": "^3.15.0",
"next": "^12.0.1",
@@ -42,7 +43,7 @@
"cors": "^2.8.5",
"eslint": "7.32.0",
"eslint-config-next": "11.1.2",
- "typescript": "^4.4.2"
+ "typescript": "^4.5.2"
},
"gitHead": "838fabdbff1a66d4d7ee8aa5c5d117bc55acbff2"
}
diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx
index 1ba10f5a8..a84700ac6 100644
--- a/apps/www/pages/_app.tsx
+++ b/apps/www/pages/_app.tsx
@@ -1,7 +1,9 @@
import '../styles/globals.css'
-import { init } from '-utils/sentry'
import Head from 'next/head'
-import useGtag from '-utils/useGtag'
+import useGtag from 'utils/useGtag'
+import { init } from 'utils/sentry'
+import type { AppProps } from 'next/app'
+import type React from 'react'
init()
@@ -10,7 +12,7 @@ const APP_DESCRIPTION = 'A tiny little drawing app.'
const APP_URL = 'https://tldraw.com'
const IMAGE = 'https://tldraw.com/social-image.png'
-function MyApp({ Component, pageProps }) {
+function MyApp({ Component, pageProps }: AppProps) {
useGtag()
return (
diff --git a/apps/www/pages/_document.tsx b/apps/www/pages/_document.tsx
index f1b73b987..dbfaba7d8 100644
--- a/apps/www/pages/_document.tsx
+++ b/apps/www/pages/_document.tsx
@@ -1,29 +1,19 @@
import NextDocument, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
-import { getCssText } from '../styles'
-import { GA_TRACKING_ID } from '../utils/gtag'
+import { getCssText } from 'styles'
+import { GA_TRACKING_ID } from 'utils/gtag'
class MyDocument extends NextDocument {
- static async getInitialProps(ctx: DocumentContext): Promise<{
- styles: JSX.Element
- html: string
- head?: JSX.Element[]
- }> {
- try {
- const initialProps = await NextDocument.getInitialProps(ctx)
+ static async getInitialProps(ctx: DocumentContext) {
+ const initialProps = await NextDocument.getInitialProps(ctx)
- return {
- ...initialProps,
- styles: (
- <>
- {initialProps.styles}
-
- >
- ),
- }
- } catch (e) {
- console.error(e.message)
- } finally {
- null
+ return {
+ ...initialProps,
+ styles: (
+ <>
+ {initialProps.styles}
+
+ >
+ ),
}
}
diff --git a/apps/www/pages/api/auth/[...nextauth].ts b/apps/www/pages/api/auth/[...nextauth].ts
index 9583f9509..49e6994b3 100644
--- a/apps/www/pages/api/auth/[...nextauth].ts
+++ b/apps/www/pages/api/auth/[...nextauth].ts
@@ -1,5 +1,5 @@
-import { isSponsoringMe } from '-utils/isSponsoringMe'
-import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
+import { isSponsoringMe } from 'utils/isSponsoringMe'
+import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
@@ -20,13 +20,15 @@ export default function Auth(
return baseUrl
},
async signIn(user, account, profile: { login?: string }) {
- const canLogin = await isSponsoringMe(profile?.login)
+ if (profile?.login) {
+ const canLogin = await isSponsoringMe(profile.login)
- if (canLogin) {
- return canLogin
- } else {
- return '/sponsorware'
+ if (canLogin) {
+ return canLogin
+ }
}
+
+ return '/'
},
},
})
diff --git a/apps/www/pages/api/sponsors.ts b/apps/www/pages/api/sponsors.ts
index dc3149e0b..84477ed23 100644
--- a/apps/www/pages/api/sponsors.ts
+++ b/apps/www/pages/api/sponsors.ts
@@ -1,4 +1,4 @@
-import { NextApiRequest, NextApiResponse } from 'next'
+import type { NextApiRequest, NextApiResponse } from 'next'
const AV_SIZE = 32
const PADDING = 4
diff --git a/apps/www/pages/index.tsx b/apps/www/pages/index.tsx
index bb171df00..d57579118 100644
--- a/apps/www/pages/index.tsx
+++ b/apps/www/pages/index.tsx
@@ -1,9 +1,9 @@
import dynamic from 'next/dynamic'
-import { GetServerSideProps } from 'next'
+import type { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/client'
import Head from 'next/head'
-const Editor = dynamic(() => import('-components/Editor'), { ssr: false })
+const Editor = dynamic(() => import('components/Editor'), { ssr: false })
interface PageProps {
isUser: boolean
diff --git a/apps/www/pages/r/[id].tsx b/apps/www/pages/r/[id].tsx
index df0f7ab43..d19ce441e 100644
--- a/apps/www/pages/r/[id].tsx
+++ b/apps/www/pages/r/[id].tsx
@@ -2,7 +2,7 @@ import * as React from 'react'
import type { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/client'
import dynamic from 'next/dynamic'
-const MultiplayerEditor = dynamic(() => import('-components/MultiplayerEditor'), { ssr: false })
+const MultiplayerEditor = dynamic(() => import('components/MultiplayerEditor'), { ssr: false })
interface RoomProps {
id: string
diff --git a/apps/www/pages/sponsorware.tsx b/apps/www/pages/sponsorware.tsx
index 8684f85ea..631f6fe4d 100644
--- a/apps/www/pages/sponsorware.tsx
+++ b/apps/www/pages/sponsorware.tsx
@@ -1,6 +1,6 @@
import { styled } from 'styles'
import { getSession, signin, signout, useSession } from 'next-auth/client'
-import { GetServerSideProps } from 'next'
+import type { GetServerSideProps } from 'next'
import Link from 'next/link'
import React from 'react'
import Head from 'next/head'
diff --git a/apps/www/tsconfig.json b/apps/www/tsconfig.json
index f9e126caf..8d39a9399 100644
--- a/apps/www/tsconfig.json
+++ b/apps/www/tsconfig.json
@@ -1,28 +1,37 @@
{
"compilerOptions": {
- "composite": true,
+ "composite": false,
+ "incremental": false,
+ "resolveJsonModule": true,
"target": "es6",
- "lib": ["dom", "dom.iterable", "esnext"],
+ "lib": ["dom", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
+ "emitDeclarationOnly": false,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"isolatedModules": true,
"jsx": "preserve",
- "baseUrl": ".",
"rootDir": ".",
+ "baseUrl": ".",
"paths": {
- "-*": ["./*"],
- "@tldraw/core": ["../../packages/core"],
- "@tldraw/tldraw": ["../../packages/tldraw"]
- },
- "incremental": true,
- "resolveJsonModule": true
+ "*": ["./*"],
+ "@tldraw/tldraw": ["./packages/tldraw"],
+ "@tldraw/core": ["./packages/core"],
+ "@tldraw/intersect": ["./packages/intersect"],
+ "@tldraw/vec": ["./packages/vec"]
+ }
},
+ "references": [
+ { "path": "../../packages/vec" },
+ { "path": "../../packages/intersect" },
+ { "path": "../../packages/core" },
+ { "path": "../../packages/tldraw" }
+ ],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
diff --git a/apps/www/types.ts b/apps/www/types.ts
index ae3d978f6..336ce12bb 100644
--- a/apps/www/types.ts
+++ b/apps/www/types.ts
@@ -1,6 +1 @@
-import { TDDocument } from '@tldraw/tldraw'
-import { LiveObject } from '@liveblocks/client'
-
-export interface TldrawStorage {
- doc: LiveObject<{ uuid: string; document: TDDocument }>
-}
+export {}
diff --git a/apps/www/utils/sentry.ts b/apps/www/utils/sentry.ts
index 1a4463bb2..c5b83f009 100644
--- a/apps/www/utils/sentry.ts
+++ b/apps/www/utils/sentry.ts
@@ -13,11 +13,11 @@ export function init(): void {
integrations.push(
new RewriteFrames({
iteratee: (frame) => {
- frame.filename = frame.filename.replace(
- process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR,
+ frame.filename = frame?.filename?.replace(
+ process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR as string,
'app:///'
)
- frame.filename = frame.filename.replace('.next', '_next')
+ frame.filename = frame?.filename?.replace('.next', '_next')
return frame
},
})
diff --git a/examples/core-example-advanced/package.json b/examples/core-example-advanced/package.json
index 1c78a04a2..ea02f7fbd 100644
--- a/examples/core-example-advanced/package.json
+++ b/examples/core-example-advanced/package.json
@@ -18,9 +18,9 @@
"devDependencies": {
"@state-designer/react": "3.0.0",
"@stitches/react": "^1.2.5",
- "@tldraw/core": "^1.1.3",
- "@tldraw/intersect": "latest",
- "@tldraw/vec": "latest",
+ "@tldraw/core": "^1.1.4",
+ "@tldraw/intersect": "^1.1.4",
+ "@tldraw/vec": "^1.1.4",
"@types/node": "^14.14.35",
"@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9",
@@ -40,4 +40,4 @@
"typescript": "4.2.3"
},
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
-}
+}
\ No newline at end of file
diff --git a/examples/core-example-advanced/src/shapes/CustomShapeUtil.ts b/examples/core-example-advanced/src/shapes/CustomShapeUtil.ts
index c61dd4a5a..4d730d38b 100644
--- a/examples/core-example-advanced/src/shapes/CustomShapeUtil.ts
+++ b/examples/core-example-advanced/src/shapes/CustomShapeUtil.ts
@@ -8,6 +8,8 @@ export abstract class CustomShapeUtil<
canBind = false
+ hideBounds = false
+
abstract getCenter: (shape: T) => number[]
abstract getShape: (shape: Partial) => T
diff --git a/examples/core-example-advanced/src/state/actions/camera/pinchCamera.ts b/examples/core-example-advanced/src/state/actions/camera/pinchCamera.ts
index ea2991120..7abcd1817 100644
--- a/examples/core-example-advanced/src/state/actions/camera/pinchCamera.ts
+++ b/examples/core-example-advanced/src/state/actions/camera/pinchCamera.ts
@@ -8,6 +8,6 @@ export const pinchCamera: Action = (data, payload: TLPointerInfo) => {
const nextPoint = Vec.sub(camera.point, Vec.div(payload.delta, camera.zoom))
const p0 = Vec.sub(Vec.div(payload.point, camera.zoom), nextPoint)
const p1 = Vec.sub(Vec.div(payload.point, nextZoom), nextPoint)
- data.pageState.camera.point = Vec.round(Vec.add(nextPoint, Vec.sub(p1, p0)))
+ data.pageState.camera.point = Vec.toFixed(Vec.add(nextPoint, Vec.sub(p1, p0)))
data.pageState.camera.zoom = nextZoom
}
diff --git a/examples/core-example-advanced/src/state/actions/camera/zoomIn.ts b/examples/core-example-advanced/src/state/actions/camera/zoomIn.ts
index a816238b0..53599ad85 100644
--- a/examples/core-example-advanced/src/state/actions/camera/zoomIn.ts
+++ b/examples/core-example-advanced/src/state/actions/camera/zoomIn.ts
@@ -10,7 +10,7 @@ export const zoomIn: Action = (data) => {
const center = [mutables.rendererBounds.width / 2, mutables.rendererBounds.height / 2]
const p0 = Vec.sub(Vec.div(center, camera.zoom), center)
const p1 = Vec.sub(Vec.div(center, zoom), center)
- const point = Vec.round(Vec.add(camera.point, Vec.sub(p1, p0)))
+ const point = Vec.toFixed(Vec.add(camera.point, Vec.sub(p1, p0)))
data.pageState.camera.zoom = zoom
data.pageState.camera.point = point
diff --git a/examples/core-example-advanced/src/state/actions/camera/zoomOut.ts b/examples/core-example-advanced/src/state/actions/camera/zoomOut.ts
index 54511fc98..922f8266f 100644
--- a/examples/core-example-advanced/src/state/actions/camera/zoomOut.ts
+++ b/examples/core-example-advanced/src/state/actions/camera/zoomOut.ts
@@ -10,7 +10,7 @@ export const zoomOut: Action = (data) => {
const center = [mutables.rendererBounds.width / 2, mutables.rendererBounds.height / 2]
const p0 = Vec.sub(Vec.div(center, camera.zoom), center)
const p1 = Vec.sub(Vec.div(center, zoom), center)
- const point = Vec.round(Vec.add(camera.point, Vec.sub(p1, p0)))
+ const point = Vec.toFixed(Vec.add(camera.point, Vec.sub(p1, p0)))
data.pageState.camera.zoom = zoom
data.pageState.camera.point = point
diff --git a/examples/core-example-advanced/src/state/helpers.ts b/examples/core-example-advanced/src/state/helpers.ts
index e6226a591..f6c188c85 100644
--- a/examples/core-example-advanced/src/state/helpers.ts
+++ b/examples/core-example-advanced/src/state/helpers.ts
@@ -38,5 +38,5 @@ export function getZoomFitCamera(
export function getZoomedCameraPoint(nextZoom: number, center: number[], pageState: TLPageState) {
const p0 = Vec.sub(Vec.div(center, pageState.camera.zoom), pageState.camera.point)
const p1 = Vec.sub(Vec.div(center, nextZoom), pageState.camera.point)
- return Vec.round(Vec.add(pageState.camera.point, Vec.sub(p1, p0)))
+ return Vec.toFixed(Vec.add(pageState.camera.point, Vec.sub(p1, p0)))
}
diff --git a/examples/core-example-advanced/tsconfig.json b/examples/core-example-advanced/tsconfig.json
index b057334ac..4150d5c6b 100644
--- a/examples/core-example-advanced/tsconfig.json
+++ b/examples/core-example-advanced/tsconfig.json
@@ -4,10 +4,11 @@
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"outDir": "./dist/types",
+ "baseUrl": ".",
"rootDir": "src",
- "baseUrl": "src",
"emitDeclarationOnly": false,
"paths": {
+ "*": ["src/*"],
"@tldraw/core": ["../../packages/core"]
}
},
diff --git a/examples/core-example/package.json b/examples/core-example/package.json
index a773d2c5b..55c0c2405 100644
--- a/examples/core-example/package.json
+++ b/examples/core-example/package.json
@@ -1,5 +1,5 @@
{
- "version": "1.1.3",
+ "version": "1.1.4",
"name": "@tldraw/core-example-simple",
"description": "A simple example project for @tldraw/core.",
"author": "@steveruizok",
@@ -15,8 +15,8 @@
},
"files": [],
"devDependencies": {
- "@tldraw/core": "^1.1.3",
- "@tldraw/vec": "^0.0.130",
+ "@tldraw/core": "^1.1.4",
+ "@tldraw/vec": "^1.1.4",
"@types/node": "^14.14.35",
"@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9",
@@ -30,4 +30,4 @@
"typescript": "4.2.3"
},
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
-}
+}
\ No newline at end of file
diff --git a/examples/core-example/tsconfig.json b/examples/core-example/tsconfig.json
index dce1c6de1..457581440 100644
--- a/examples/core-example/tsconfig.json
+++ b/examples/core-example/tsconfig.json
@@ -4,10 +4,11 @@
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"outDir": "./dist/types",
+ "baseUrl": ".",
"rootDir": "src",
- "baseUrl": "src",
"emitDeclarationOnly": false,
"paths": {
+ "*": ["src/*"],
"@tldraw/core": ["../packages/core"]
}
},
diff --git a/examples/tldraw-example/tsconfig.json b/examples/tldraw-example/tsconfig.json
index 11abf005e..4c2247330 100644
--- a/examples/tldraw-example/tsconfig.json
+++ b/examples/tldraw-example/tsconfig.json
@@ -4,11 +4,11 @@
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"outDir": "./dist/types",
+ "baseUrl": ".",
"rootDir": "src",
- "baseUrl": "src",
"emitDeclarationOnly": false,
"paths": {
- "+*": ["./*"],
+ "~*": ["./src/*"],
"@tldraw/tldraw": ["../../packages/tldraw"]
}
},
diff --git a/package.json b/package.json
index 7e262db3a..869ed2229 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,10 @@
},
"license": "MIT",
"workspaces": [
- "packages/tldraw",
+ "packages/vec",
+ "packages/intersect",
"packages/core",
+ "packages/tldraw",
"apps/www",
"apps/electron",
"apps/vscode/editor",
@@ -38,7 +40,7 @@
"test:watch": "lerna run test:watch --stream",
"docs": "lerna run typedoc",
"docs:watch": "lerna run typedoc --watch",
- "postinstall": "husky install & yarn build:packages"
+ "postinstall": "husky install"
},
"devDependencies": {
"@swc-node/jest": "^1.3.3",
@@ -62,7 +64,7 @@
"resize-observer-polyfill": "^1.5.1",
"tslib": "^2.3.0",
"typedoc": "^0.22.3",
- "typescript": "^4.4.2"
+ "typescript": "^4.5.2"
},
"husky": {
"hooks": {
@@ -73,4 +75,4 @@
"lint-staged": {
"*": "fix:style && eslint"
}
-}
\ No newline at end of file
+}
diff --git a/packages/core/package.json b/packages/core/package.json
index e726b79a7..323b2f1c3 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,5 +1,5 @@
{
- "version": "1.1.3",
+ "version": "1.1.4",
"name": "@tldraw/core",
"description": "The tldraw core renderer and utilities.",
"author": "@steveruizok",
@@ -37,8 +37,8 @@
"test:watch": "jest --watchAll"
},
"dependencies": {
- "@tldraw/intersect": "latest",
- "@tldraw/vec": "latest",
+ "@tldraw/intersect": "^1.1.4",
+ "@tldraw/vec": "^1.1.4",
"@use-gesture/react": "^10.1.3"
},
"peerDependencies": {
@@ -81,4 +81,4 @@
"\\~(.*)": "/src/$1"
}
}
-}
+}
\ No newline at end of file
diff --git a/packages/core/src/TLShapeUtil/TLShapeUtil.tsx b/packages/core/src/TLShapeUtil/TLShapeUtil.tsx
index 0f16aea8f..99fee77d7 100644
--- a/packages/core/src/TLShapeUtil/TLShapeUtil.tsx
+++ b/packages/core/src/TLShapeUtil/TLShapeUtil.tsx
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react'
import Utils from '../utils'
-import { intersectPolylineBounds } from '@tldraw/intersect'
import type { TLBounds, TLComponentProps, TLForwardedRef, TLShape, TLUser } from '../types'
+import { intersectPolylineBounds } from '@tldraw/intersect'
export abstract class TLShapeUtil {
refMap = new Map>()
diff --git a/packages/core/src/components/canvas/canvas.tsx b/packages/core/src/components/canvas/canvas.tsx
index c03a6946c..c92abc237 100644
--- a/packages/core/src/components/canvas/canvas.tsx
+++ b/packages/core/src/components/canvas/canvas.tsx
@@ -18,6 +18,7 @@ import { useResizeObserver } from '~hooks/useResizeObserver'
import { inputs } from '~inputs'
import { UsersIndicators } from '~components/users-indicators'
import { SnapLines } from '~components/snap-lines/snap-lines'
+import { Grid } from '~components/grid'
import { Overlay } from '~components/overlay'
function resetError() {
@@ -28,6 +29,7 @@ interface CanvasProps> {
page: TLPage
pageState: TLPageState
snapLines?: TLSnapLine[]
+ grid?: number
users?: TLUsers
userId?: string
hideBounds: boolean
@@ -37,6 +39,7 @@ interface CanvasProps> {
hideCloneHandles: boolean
hideResizeHandles: boolean
hideRotateHandle: boolean
+ hideGrid: boolean
externalContainerRef?: React.RefObject
meta?: M
id?: string
@@ -48,6 +51,7 @@ export function Canvas>({
page,
pageState,
snapLines,
+ grid,
users,
userId,
meta,
@@ -59,6 +63,7 @@ export function Canvas>({
hideCloneHandles,
hideResizeHandles,
hideRotateHandle,
+ hideGrid,
onBoundsChange,
}: CanvasProps): JSX.Element {
const rCanvas = React.useRef(null)
@@ -85,6 +90,7 @@ export function Canvas>({
+ {!hideGrid && grid && }
+
+ {STEPS.map(([min, mid, size], i) => {
+ const s = size * grid * camera.zoom
+ const xo = camera.point[0] * camera.zoom
+ const yo = camera.point[1] * camera.zoom
+ const gxo = xo > 0 ? xo % s : s + (xo % s)
+ const gyo = yo > 0 ? yo % s : s + (yo % s)
+ const opacity = camera.zoom < mid ? Utils.modulate(camera.zoom, [min, mid], [0, 1]) : 1
+
+ return (
+
+
+
+ )
+ })}
+
+ {STEPS.map((_, i) => (
+
+ ))}
+
+ )
+}
diff --git a/packages/core/src/components/grid/index.ts b/packages/core/src/components/grid/index.ts
new file mode 100644
index 000000000..55d044ade
--- /dev/null
+++ b/packages/core/src/components/grid/index.ts
@@ -0,0 +1 @@
+export * from './grid'
diff --git a/packages/core/src/components/renderer/renderer.tsx b/packages/core/src/components/renderer/renderer.tsx
index f3625ecc1..71f535706 100644
--- a/packages/core/src/components/renderer/renderer.tsx
+++ b/packages/core/src/components/renderer/renderer.tsx
@@ -86,6 +86,14 @@ export interface RendererProps extends Partial>({
theme,
meta,
snapLines,
+ grid,
containerRef,
hideHandles = false,
hideIndicators = false,
@@ -122,6 +131,7 @@ export function Renderer>({
hideResizeHandles = false,
hideRotateHandles = false,
hideBounds = false,
+ hideGrid = true,
...rest
}: RendererProps): JSX.Element {
useTLTheme(theme, '#' + id)
@@ -164,6 +174,7 @@ export function Renderer>({
page={page}
pageState={pageState}
snapLines={snapLines}
+ grid={grid}
users={users}
userId={userId}
externalContainerRef={containerRef}
@@ -174,6 +185,7 @@ export function Renderer>({
hideBindingHandles={hideBindingHandles}
hideRotateHandle={hideRotateHandles}
hideResizeHandles={hideResizeHandles}
+ hideGrid={hideGrid}
onBoundsChange={onBoundsChange}
meta={meta}
/>
diff --git a/packages/core/src/hooks/useStyle.tsx b/packages/core/src/hooks/useStyle.tsx
index d04c6f823..01f63e004 100644
--- a/packages/core/src/hooks/useStyle.tsx
+++ b/packages/core/src/hooks/useStyle.tsx
@@ -73,6 +73,7 @@ const defaultTheme: TLTheme = {
selectFill: 'rgba(65, 132, 244, 0.05)',
background: 'rgb(248, 249, 250)',
foreground: 'rgb(51, 51, 51)',
+ grid: 'rgba(144, 144, 144, 1)',
}
const tlcss = css`
@@ -142,6 +143,15 @@ const tlcss = css`
pointer-events: none;
}
+ .tl-grid {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ touch-action: none;
+ pointer-events: none;
+ user-select: none;
+ }
+
.tl-snap-line {
stroke: var(--tl-accent);
stroke-width: calc(1px * var(--tl-scale));
@@ -394,6 +404,10 @@ const tlcss = css`
stroke: var(--tl-selectStroke);
stroke-width: calc(2px * var(--tl-scale));
}
+
+ .tl-grid-dot {
+ fill: var(--tl-grid);
+ }
`
export function useTLTheme(theme?: Partial, selector?: string) {
diff --git a/packages/core/src/inputs.ts b/packages/core/src/inputs.ts
index f0d7884eb..8ecf7b19f 100644
--- a/packages/core/src/inputs.ts
+++ b/packages/core/src/inputs.ts
@@ -360,7 +360,7 @@ export class Inputs {
target: 'pinch',
origin,
delta: delta,
- point: Vec.sub(Vec.round(point), [this.bounds.minX, this.bounds.minY]),
+ point: Vec.sub(Vec.toFixed(point), [this.bounds.minX, this.bounds.minY]),
pressure: 0.5,
shiftKey,
ctrlKey,
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index a6323ca88..6ea42cd29 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -108,6 +108,7 @@ export interface TLTheme {
selectStroke?: string
background?: string
foreground?: string
+ grid?: string
}
export type TLWheelEventHandler = (
diff --git a/packages/core/src/utils/utils.ts b/packages/core/src/utils/utils.ts
index 3e496c07c..3303342c0 100644
--- a/packages/core/src/utils/utils.ts
+++ b/packages/core/src/utils/utils.ts
@@ -1089,6 +1089,26 @@ export class Utils {
return this.translateBounds(bounds, [dx, dy])
}
+ /**
+ * Snap a bounding box to a grid size.
+ * @param bounds
+ * @param gridSize
+ */
+ static snapBoundsToGrid(bounds: TLBounds, gridSize: number): TLBounds {
+ const minX = Math.round(bounds.minX / gridSize) * gridSize
+ const minY = Math.round(bounds.minY / gridSize) * gridSize
+ const maxX = Math.round(bounds.maxX / gridSize) * gridSize
+ const maxY = Math.round(bounds.maxY / gridSize) * gridSize
+ return {
+ minX,
+ minY,
+ maxX,
+ maxY,
+ width: Math.max(1, maxX - minX),
+ height: Math.max(1, maxY - minY),
+ }
+ }
+
/**
* Move a bounding box without recalculating it.
* @param bounds
@@ -1509,12 +1529,10 @@ left past the initial left edge) then swap points on that axis.
(isFlippedX
? initialBounds.maxX - initialShapeBounds.maxX
: initialShapeBounds.minX - initialBounds.minX) / initialBounds.width
-
const ny =
(isFlippedY
? initialBounds.maxY - initialShapeBounds.maxY
: initialShapeBounds.minY - initialBounds.minY) / initialBounds.height
-
const nw = initialShapeBounds.width / initialBounds.width
const nh = initialShapeBounds.height / initialBounds.height
@@ -1562,7 +1580,7 @@ left past the initial left edge) then swap points on that axis.
* Get a bounding box with a midX and midY.
* @param bounds
*/
- static getBoundsWithCenter(bounds: TLBounds): TLBounds & { midX: number; midY: number } {
+ static getBoundsWithCenter(bounds: TLBounds): TLBoundsWithCenter {
const center = Utils.getBoundsCenter(bounds)
return {
...bounds,
diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json
index d3d6e819b..23191e8bd 100644
--- a/packages/core/tsconfig.build.json
+++ b/packages/core/tsconfig.build.json
@@ -13,8 +13,9 @@
"compilerOptions": {
"composite": false,
"incremental": false,
- "declarationMap": false,
- "sourceMap": false,
- "emitDeclarationOnly": true
- }
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "references": [{ "path": "../vec" }, { "path": "../intersect" }]
}
diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json
index 0ada69ff5..612b38f6e 100644
--- a/packages/core/tsconfig.json
+++ b/packages/core/tsconfig.json
@@ -1,15 +1,15 @@
{
"extends": "../../tsconfig.base.json",
- "include": ["src"],
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"outDir": "./dist/types",
"rootDir": "src",
- "baseUrl": "src",
+ "baseUrl": ".",
"paths": {
- "~*": ["./*"]
+ "~*": ["./src/*"]
}
},
+ "references": [{ "path": "../vec" }, { "path": "../intersect" }],
"typedocOptions": {
"entryPoints": ["src/index.ts"],
"out": "docs"
diff --git a/packages/intersect/CHANGELOG.md b/packages/intersect/CHANGELOG.md
new file mode 100644
index 000000000..e470bd611
--- /dev/null
+++ b/packages/intersect/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+## 0.1.4
+
+- Fixes bug in `polyline`, adds `polygon` intersections.
+
+## 0.1.0
+
+- Hello world.
diff --git a/packages/intersect/LICENSE.md b/packages/intersect/LICENSE.md
new file mode 100644
index 000000000..bdcc8b850
--- /dev/null
+++ b/packages/intersect/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Stephen Ruiz Ltd
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/intersect/README.md b/packages/intersect/README.md
new file mode 100644
index 000000000..30cab634d
--- /dev/null
+++ b/packages/intersect/README.md
@@ -0,0 +1,477 @@
+
+
+
+
+# @tldraw/core
+
+This package contains the `Renderer` and core utilities used by [tldraw](https://tldraw.com).
+
+You can use this package to build projects like [tldraw](https://tldraw.com), where React components are rendered on a canvas user interface. Check out the [advanced example](https://core-steveruiz.vercel.app/).
+
+💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
+
+## Installation
+
+Use your package manager of choice to install `@tldraw/core` and its peer dependencies.
+
+```bash
+yarn add @tldraw/core
+# or
+npm i @tldraw/core
+```
+
+## Examples
+
+There are two examples in this repository.
+
+The **simple** example in the `example` folder shows a minimal use of the library. It does not do much but this should be a good reference for the API without too much else built on top.
+
+The **advanced** example in the `example-advanced` folder shows a more realistic use of the library. (Try it [here](https://core-steveruiz.vercel.app/)). While the fundamental patterns are the same, this example contains features such as: panning, pinching, and zooming the camera; creating, cloning, resizing, and deleting shapes; keyboard shortcuts, brush-selection; shape-snapping; undo, redo; and more. Much of the code in the advanced example comes from the [@tldraw/tldraw](https://tldraw.com) codebase.
+
+If you're working on an app that uses this library, I recommend referring back to the advanced example for tips on how you might implement these features for your own project.
+
+## Usage
+
+Import the `Renderer` React component and pass it the required props.
+
+```tsx
+import * as React from "react"
+import { Renderer, TLShape, TLShapeUtil, Vec } from '@tldraw/core'
+import { BoxShape, BoxUtil } from "./shapes/box"
+
+const shapeUtils = { box: new BoxUtil() }
+
+function App() {
+ const [page, setPage] = React.useState({
+ id: "page"
+ shapes: {
+ "box1": {
+ id: 'box1',
+ type: 'box',
+ parentId: 'page',
+ childIndex: 0,
+ point: [0, 0],
+ size: [100, 100],
+ rotation: 0,
+ }
+ },
+ bindings: {}
+ })
+
+ const [pageState, setPageState] = React.useState({
+ id: "page",
+ selectedIds: [],
+ camera: {
+ point: [0,0],
+ zoom: 1
+ }
+ })
+
+ return ()
+}
+```
+
+## Documentation
+
+### `Renderer`
+
+To avoid unnecessary renders, be sure to pass "stable" values as props to the `Renderer`. Either define these values outside of the parent component, or place them in React state, or memoize them with `React.useMemo`.
+
+| Prop | Type | Description |
+| ------------ | ------------------------------- | ---------------------------------------------- |
+| `page` | [`TLPage`](#tlpage) | The current page object. |
+| `pageState` | [`TLPageState`](#tlpagestate) | The current page's state. |
+| `shapeUtils` | [`TLShapeUtils`](#tlshapeutils) | The shape utilities used to render the shapes. |
+
+In addition to these required props, the Renderer accents many other **optional** props.
+
+| Property | Type | Description |
+| -------------------- | ----------------------------- | ----------------------------------------------------------------- |
+| `containerRef` | `React.MutableRefObject` | A React ref for the container, where CSS variables will be added. |
+| `theme` | `object` | An object with overrides for the Renderer's default colors. |
+| `hideBounds` | `boolean` | Do not show the bounding box for selected shapes. |
+| `hideHandles` | `boolean` | Do not show handles for shapes with handles. |
+| `hideBindingHandles` | `boolean` | Do not show binding controls for selected shapes with bindings. |
+| `hideResizeHandles` | `boolean` | Do not show resize handles for selected shapes. |
+| `hideRotateHandles` | `boolean` | Do not show rotate handles for selected shapes. |
+| `snapLines` | [`TLSnapLine`](#tlsnapline)[] | An array of "snap" lines. |
+| `users` | `object` | A table of [`TLUser`](#tluser)s. |
+| `userId` | `object` | The current user's [`TLUser`](#tluser) id. |
+
+The theme object accepts valid CSS colors for the following properties:
+
+| Property | Description |
+| -------------- | ---------------------------------------------------- |
+| `foreground` | The primary (usually "text") color |
+| `background` | The default page's background color |
+| `brushFill` | The fill color of the brush selection box |
+| `brushStroke` | The stroke color of the brush selection box |
+| `selectFill` | The fill color of the selection bounds |
+| `selectStroke` | The stroke color of the selection bounds and handles |
+
+The Renderer also accepts many (optional) event callbacks.
+
+| Prop | Description |
+| --------------------------- | ----------------------------------------------------------- |
+| `onPan` | Panned with the mouse wheel |
+| `onZoom` | Zoomed with the mouse wheel |
+| `onPinchStart` | Began a two-pointer pinch |
+| `onPinch` | Moved their pointers during a pinch |
+| `onPinchEnd` | Stopped a two-pointer pinch |
+| `onPointerDown` | Started pointing |
+| `onPointerMove` | Moved their pointer |
+| `onPointerUp` | Ended a point |
+| `onPointCanvas` | Pointed the canvas |
+| `onDoubleClickCanvas` | Double-pointed the canvas |
+| `onRightPointCanvas` | Right-pointed the canvas |
+| `onDragCanvas` | Dragged the canvas |
+| `onReleaseCanvas` | Stopped pointing the canvas |
+| `onHoverShape` | Moved their pointer onto a shape |
+| `onUnhoverShape` | Moved their pointer off of a shape |
+| `onPointShape` | Pointed a shape |
+| `onDoubleClickShape` | Double-pointed a shape |
+| `onRightPointShape` | Right-pointed a shape |
+| `onDragShape` | Dragged a shape |
+| `onReleaseShape` | Stopped pointing a shape |
+| `onHoverHandle` | Moved their pointer onto a shape handle |
+| `onUnhoverHandle` | Moved their pointer off of a shape handle |
+| `onPointHandle` | Pointed a shape handle |
+| `onDoubleClickHandle` | Double-pointed a shape handle |
+| `onRightPointHandle` | Right-pointed a shape handle |
+| `onDragHandle` | Dragged a shape handle |
+| `onReleaseHandle` | Stopped pointing shape handle |
+| `onHoverBounds` | Moved their pointer onto the selection bounds |
+| `onUnhoverBounds` | Moved their pointer off of the selection bounds |
+| `onPointBounds` | Pointed the selection bounds |
+| `onDoubleClickBounds` | Double-pointed the selection bounds |
+| `onRightPointBounds` | Right-pointed the selection bounds |
+| `onDragBounds` | Dragged the selection bounds |
+| `onReleaseBounds` | Stopped the selection bounds |
+| `onHoverBoundsHandle` | Moved their pointer onto a selection bounds handle |
+| `onUnhoverBoundsHandle` | Moved their pointer off of a selection bounds handle |
+| `onPointBoundsHandle` | Pointed a selection bounds handle |
+| `onDoubleClickBoundsHandle` | Double-pointed a selection bounds handle |
+| `onRightPointBoundsHandle` | Right-pointed a selection bounds handle |
+| `onDragBoundsHandle` | Dragged a selection bounds handle |
+| `onReleaseBoundsHandle` | Stopped a selection bounds handle |
+| `onShapeClone` | Clicked on a shape's clone handle |
+| `onShapeChange` | A shape's component prompted a change |
+| `onShapeBlur` | A shape's component was prompted a blur |
+| `onRenderCountChange` | The number of rendered shapes changed |
+| `onBoundsChange` | The Renderer's screen bounding box of the component changed |
+| `onError` | The Renderer encountered an error |
+
+The `@tldraw/core` library provides types for most of the event handlers:
+
+| Type |
+| ---------------------------- |
+| `TLPinchEventHandler` |
+| `TLPointerEventHandler` |
+| `TLCanvasEventHandler` |
+| `TLBoundsEventHandler` |
+| `TLBoundsHandleEventHandler` |
+| `TLShapeChangeHandler` |
+| `TLShapeBlurHandler` |
+| `TLShapeCloneHandler` |
+
+### `TLPage`
+
+An object describing the current page. It contains:
+
+| Property | Type | Description |
+| ----------------- | --------------------------- | --------------------------------------------------------------------------- |
+| `id` | `string` | A unique id for the page. |
+| `shapes` | [`TLShape{}`](#tlshape) | A table of shapes. |
+| `bindings` | [`TLBinding{}`](#tlbinding) | A table of bindings. |
+| `backgroundColor` | `string` | (optional) The page's background fill color. Will also overwrite the theme. |
+
+### `TLPageState`
+
+An object describing the current page. It contains:
+
+| Property | Type | Description |
+| -------------- | ---------- | --------------------------------------------------- |
+| `id` | `string` | The corresponding page's id |
+| `selectedIds` | `string[]` | An array of selected shape ids |
+| `camera` | `object` | An object describing the camera state |
+| `camera.point` | `number[]` | The camera's `[x, y]` coordinates |
+| `camera.zoom` | `number` | The camera's zoom level |
+| `pointedId` | `string` | (optional) The currently pointed shape id |
+| `hoveredId` | `string` | (optional) The currently hovered shape id |
+| `editingId` | `string` | (optional) The currently editing shape id |
+| `bindingId` | `string` | (optional) The currently editing binding. |
+| `brush` | `TLBounds` | (optional) A `Bounds` for the current selection box |
+
+### `TLShape`
+
+An object that describes a shape on the page. The shapes in your document should extend this interface with other properties. See [Shape Type](#shape-type).
+
+| Property | Type | Description |
+| --------------------- | ---------- | ------------------------------------------------------------------------------------- |
+| `id` | `string` | The shape's id. |
+| `type` | `string` | The type of the shape, corresponding to the `type` of a [`TLShapeUtil`](#tlshapeutil) |
+| `parentId` | `string` | The id of the shape's parent (either the current page or another shape) |
+| `childIndex` | `number` | the order of the shape among its parent's children |
+| `name` | `string` | the name of the shape |
+| `point` | `number[]` | the shape's current `[x, y]` coordinates on the page |
+| `rotation` | `number` | (optiona) The shape's current rotation in radians |
+| `children` | `string[]` | (optional) An array containing the ids of this shape's children |
+| `handles` | `{}` | (optional) A table of [`TLHandle`](#tlhandle) objects |
+| `isGhost` | `boolean` | (optional) True if the shape is "ghosted", e.g. while deleting |
+| `isLocked` | `boolean` | (optional) True if the shape is locked |
+| `isHidden` | `boolean` | (optional) True if the shape is hidden |
+| `isEditing` | `boolean` | (optional) True if the shape is currently editing |
+| `isGenerated` | `boolean` | optional) True if the shape is generated programatically |
+| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked |
+
+### `TLHandle`
+
+An object that describes a relationship between two shapes on the page.
+
+| Property | Type | Description |
+| -------- | ---------- | --------------------------------------------- |
+| `id` | `string` | An id for the handle |
+| `index` | `number` | The handle's order within the shape's handles |
+| `point` | `number[]` | The handle's `[x, y]` coordinates |
+
+When a shape with handles is the only selected shape, the `Renderer` will display its handles. You can respond to interactions with these handles using the `on
+
+### `TLBinding`
+
+An object that describes a relationship between two shapes on the page.
+
+| Property | Type | Description |
+| -------- | -------- | -------------------------------------------- |
+| `id` | `string` | A unique id for the binding |
+| `fromId` | `string` | The id of the shape where the binding begins |
+| `toId` | `string` | The id of the shape where the binding begins |
+
+### `TLSnapLine`
+
+A snapline is an array of points (formatted as `[x, y]`) that represent a "snapping" line.
+
+### `TLShapeUtil`
+
+The `TLShapeUtil` is an abstract class that you can extend to create utilities for your custom shapes. See the [Creating Shapes](#creating-shapes) guide to learn more.
+
+### `TLUser`
+
+A `TLUser` is the presence information for a multiplayer user. The user's pointer location and selections will be shown on the canvas. If the `TLUser`'s id matches the `Renderer`'s `userId` prop, then the user's cursor and selections will not be shown.
+
+| Property | Type | Description |
+| --------------- | ---------- | --------------------------------------- |
+| `id` | `string` | A unique id for the user |
+| `color` | `string` | The user's color, used for indicators |
+| `point` | `number[]` | The user's pointer location on the page |
+| `selectedIds[]` | `string[]` | The user's selected shape ids |
+
+### `Utils`
+
+A general purpose utility class. See source for more.
+
+## Guide: Creating Shapes
+
+The `Renderer` component has no built-in shapes. It's up to you to define every shape that you want to see on the canvas. While these shapes are highly reusable between projects, you'll need to define them using the API described below.
+
+> For several example shapes, see the folder `/example/src/shapes/`.
+
+### Shape Type
+
+Your first task is to define an interface for the shape that extends `TLShape`. It must have a `type` property.
+
+```ts
+// BoxShape.ts
+import type { TLShape } from '@tldraw/core'
+
+export interface BoxShape extends TLShape {
+ type: 'box'
+ size: number[]
+}
+```
+
+### Component
+
+Next, use `TLShapeUtil.Component` to create a second component for your shape's `Component`. The `Renderer` will use this component to display the shape on the canvas.
+
+```tsx
+// BoxComponent.ts
+
+import * as React from 'react'
+import { shapeComponent, SVGContainer } from '@tldraw/core'
+import type { BoxShape } from './BoxShape'
+
+export const BoxComponent = TLShapeUtil.Component(
+ ({ shape, events, meta }, ref) => {
+ const color = meta.isDarkMode ? 'white' : 'black'
+
+ return (
+
+
+
+ )
+ }
+)
+```
+
+Your component can return HTML elements or SVG elements. If your shape is returning only SVG elements, wrap it in an `SVGContainer`. If your shape returns HTML elements, wrap it in an `HTMLContainer`. Not that you must set `pointerEvents` manually on the shapes you wish to receive pointer events.
+
+The component will receive the following props:
+
+| Name | Type | Description |
+| ------------------- | ---------- | ------------------------------------------------------------------ |
+| `shape` | `TLShape` | The shape from `page.shapes` that is being rendered |
+| `meta` | `{}` | The value provided to the `Renderer`'s `meta` prop |
+| `events` | `{}` | Several pointer events that should be set on the container element |
+| `isSelected` | `boolean` | The shape is selected (its `id` is in `pageState.selectedIds`) |
+| `isHovered` | `boolean` | The shape is hovered (its `id` is `pageState.hoveredId`) |
+| `isEditing` | `boolean` | The shape is being edited (its `id` is `pageState.editingId`) |
+| `isGhost` | `boolean` | The shape is ghosted or is the child of a ghosted shape. |
+| `isChildOfSelected` | `boolean` | The shape is the child of a selected shape. |
+| `onShapeChange` | `Function` | The callback provided to the `Renderer`'s `onShapeChange` prop |
+| `onShapeBlur` | `Function` | The callback provided to the `Renderer`'s `onShapeBlur` prop |
+
+### Indicator
+
+Next, use `TLShapeUtil.Indicator` to create a second component for your shape's `Indicator`. This component is shown when the shape is hovered or selected. Your `Indicator` must return SVG elements only.
+
+```tsx
+// BoxIndicator.ts
+
+export const BoxIndicator = TLShapeUtil.Indicator(({ shape }) => {
+ return (
+
+ )
+})
+```
+
+The indicator component will receive the following props:
+
+| Name | Type | Description |
+| ------------ | --------- | -------------------------------------------------------------------------------------- |
+| `shape` | `TLShape` | The shape from `page.shapes` that is being rendered |
+| `meta` | {} | The value provided to the `Renderer`'s `meta` prop |
+| `user` | `TLUser` | The user when shown in a multiplayer session |
+| `isSelected` | `boolean` | Whether the current shape is selected (true if its `id` is in `pageState.selectedIds`) |
+| `isHovered` | `boolean` | Whether the current shape is hovered (true if its `id` is `pageState.hoveredId`) |
+
+### ShapeUtil
+
+Next, create a "shape util" for your shape. This is a class that extends `TLShapeUtil`. The `Renderer` will use an instance of this class to answer questions about the shape: what it should look like, where it is on screen, whether it can rotate, etc.
+
+```ts
+// BoxUtil.ts
+
+import { Utils, TLBounds, TLShapeUtil } from '@tldraw/core'
+import { BoxComponent } from './BoxComponent'
+import { BoxIndicator } from './BoxIndicator'
+import type { BoxShape } from './BoxShape'
+
+export class BoxUtil extends TLShapeUtil {
+ Component = BoxComponent
+
+ Indicator = BoxIndicator
+
+ getBounds = (shape: BoxShape): TLBounds => {
+ const [width, height] = shape.size
+
+ const bounds = {
+ minX: 0,
+ maxX: width,
+ minY: 0,
+ maxY: height,
+ width,
+ height,
+ }
+
+ return Utils.translateBounds(bounds, shape.point)
+ }
+}
+```
+
+Set the `Component` field to your component and the `Indicator` field to your indicator component. Then define the `getBounds` method. This method will receive a shape and should return a `TLBounds` object.
+
+You may also set the following fields:
+
+| Name | Type | Default | Description |
+| ------------------ | --------- | ------- | ----------------------------------------------------------------------------------------------------- |
+| `showCloneHandles` | `boolean` | `false` | Whether to display clone handles when the shape is the only selected shape |
+| `hideBounds` | `boolean` | `false` | Whether to hide the bounds when the shape is the only selected shape |
+| `isStateful` | `boolean` | `false` | Whether the shape has its own React state. When true, the shape will not be unmounted when off-screen |
+
+### ShapeUtils Object
+
+Finally, create a mapping of your project's shape utils and the `type` properties of their corresponding shapes. Pass this object to the `Renderer`'s `shapeUtils` prop.
+
+```tsx
+// App.tsx
+
+const shapeUtils = {
+ box: new BoxUtil(),
+ circle: new CircleUtil(),
+ text: new TextUtil(),
+}
+
+export function App() {
+ // ...
+
+ return
+}
+```
+
+## Local Development
+
+To start the development servers for the package and the advanced example:
+
+- Run `yarn` to install dependencies.
+- Run `yarn start`.
+- Open `localhost:5420`.
+
+You can also run:
+
+- `start:advanced` to start development servers for the package and the advanced example.
+- `start:simple` to start development servers for the package and the simple example.
+- `test` to execute unit tests via [Jest](https://jestjs.io).
+- `docs` to build the docs via [ts-doc](https://typedoc.org/).
+- `build` to build the package.
+
+## Example
+
+See the `example` folder or this [CodeSandbox](https://codesandbox.io/s/laughing-elion-gp0kx) example.
+
+## Community
+
+### Support
+
+Need help? Please [open an issue](https://github.com/tldraw/tldraw/issues/new) for support.
+
+### Discussion
+
+Want to connect with other devs? Visit the [Discord channel](https://discord.gg/SBBEVCA4PG).
+
+### License
+
+This project is licensed under MIT.
+
+If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
+
+## Author
+
+- [@steveruizok](https://twitter.com/steveruizok)
diff --git a/packages/intersect/card-repo.png b/packages/intersect/card-repo.png
new file mode 100644
index 000000000..2b3997cbd
Binary files /dev/null and b/packages/intersect/card-repo.png differ
diff --git a/packages/intersect/package.json b/packages/intersect/package.json
new file mode 100644
index 000000000..99bdb7d47
--- /dev/null
+++ b/packages/intersect/package.json
@@ -0,0 +1,39 @@
+{
+ "version": "1.1.4",
+ "name": "@tldraw/intersect",
+ "description": "2D intersection utilities for TLDraw and maybe you, too.",
+ "author": "@steveruizok",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/tldraw/tldraw.git"
+ },
+ "license": "MIT",
+ "keywords": [
+ "2d",
+ "vector",
+ "intersection",
+ "typescript",
+ "javascript"
+ ],
+ "files": [
+ "dist/**/*"
+ ],
+ "main": "./dist/cjs/index.js",
+ "module": "./dist/esm/index.js",
+ "types": "./dist/types/index.d.ts",
+ "scripts": {
+ "start:packages": "yarn start",
+ "start:core": "yarn start",
+ "start": "node scripts/dev & yarn types:dev",
+ "build:packages": "yarn build",
+ "build:core": "yarn build",
+ "build": "node scripts/build && yarn types:build",
+ "types:dev": "tsc -w --p tsconfig.build.json",
+ "types:build": "tsc -p tsconfig.build.json",
+ "lint": "eslint src/ --ext .ts,.tsx",
+ "clean": "rm -rf dist"
+ },
+ "dependencies": {
+ "@tldraw/vec": "^1.1.4"
+ }
+}
\ 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..9de0d663d
--- /dev/null
+++ b/packages/intersect/scripts/build.js
@@ -0,0 +1,63 @@
+/* eslint-disable */
+const fs = require('fs')
+const esbuild = require('esbuild')
+const { gzip } = require('zlib')
+const pkg = require('../package.json')
+
+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: false,
+ bundle: true,
+ format: 'cjs',
+ target: 'es6',
+ tsconfig: './tsconfig.build.json',
+ external: Object.keys(pkg.dependencies),
+ metafile: true,
+ sourcemap: true,
+ })
+
+ const esmResult = esbuild.buildSync({
+ entryPoints: ['./src/index.ts'],
+ outdir: 'dist/esm',
+ minify: false,
+ bundle: true,
+ format: 'esm',
+ target: 'es6',
+ tsconfig: './tsconfig.build.json',
+ external: Object.keys(pkg.dependencies),
+ metafile: true,
+ sourcemap: 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(
+ `✔ ${pkg.name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${(
+ result.length / 1000
+ ).toFixed(2)}kb minified)`
+ )
+ })
+ })
+ } catch (e) {
+ console.log(`× ${pkg.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..44bcf930f
--- /dev/null
+++ b/packages/intersect/scripts/dev.js
@@ -0,0 +1,29 @@
+/* eslint-disable */
+const esbuild = require('esbuild')
+const pkg = require('../package.json')
+
+async function main() {
+ try {
+ await esbuild.build({
+ entryPoints: ['src/index.tsx'],
+ outfile: 'dist/index.js',
+ bundle: true,
+ minify: false,
+ sourcemap: true,
+ incremental: true,
+ target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
+ define: {
+ 'process.env.NODE_ENV': '"development"',
+ },
+ watch: {
+ onRebuild(err) {
+ err ? error('❌ Failed') : log('✅ Updated')
+ },
+ },
+ })
+ } catch (err) {
+ process.exit(1)
+ }
+}
+
+main()
diff --git a/packages/intersect/src/index.d.ts b/packages/intersect/src/index.d.ts
new file mode 100644
index 000000000..39a98876f
--- /dev/null
+++ b/packages/intersect/src/index.d.ts
@@ -0,0 +1,430 @@
+export declare 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;
+}
+/**
+ * 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 declare function intersectRayRay(p0: number[], n0: number[], p1: number[], n1: number[]): TLIntersection;
+/**
+ * Find the intersections between a ray and a line segment.
+ * @param origin
+ * @param direction
+ * @param a1
+ * @param a2
+ */
+export declare function intersectRayLineSegment(origin: number[], direction: number[], a1: number[], a2: number[]): TLIntersection;
+/**
+ * Find the intersections between a ray and a rectangle.
+ * @param origin
+ * @param direction
+ * @param point
+ * @param size
+ * @param rotation
+ */
+export declare function intersectRayRectangle(origin: number[], direction: number[], point: number[], size: number[], rotation?: number): TLIntersection[];
+/**
+ * Find the intersections between a ray and an ellipse.
+ * @param origin
+ * @param direction
+ * @param center
+ * @param rx
+ * @param ry
+ * @param rotation
+ */
+export declare function intersectRayEllipse(origin: number[], direction: number[], center: number[], rx: number, ry: number, rotation: number): TLIntersection;
+/**
+ * Find the intersections between a ray and a bounding box.
+ * @param origin
+ * @param direction
+ * @param bounds
+ * @param rotation
+ */
+export declare function intersectRayBounds(origin: number[], direction: number[], bounds: TLBounds, rotation?: number): TLIntersection[];
+/**
+ * Find the intersection between a line segment and a ray.
+ * @param a1
+ * @param a2
+ * @param origin
+ * @param direction
+ */
+export declare function intersectLineSegmentRay(a1: number[], a2: number[], origin: number[], direction: number[]): TLIntersection;
+/**
+ * Find the intersection between a line segment and a line segment.
+ * @param a1
+ * @param a2
+ * @param b1
+ * @param b2
+ */
+export declare function intersectLineSegmentLineSegment(a1: number[], a2: number[], b1: number[], b2: number[]): TLIntersection;
+/**
+ * Find the intersections between a line segment and a rectangle.
+ * @param a1
+ * @param a2
+ * @param point
+ * @param size
+ */
+export declare function intersectLineSegmentRectangle(a1: number[], a2: number[], point: number[], size: number[]): TLIntersection[];
+/**
+ * Find the intersections between a line segment and an arc.
+ * @param a1
+ * @param a2
+ * @param center
+ * @param radius
+ * @param start
+ * @param end
+ */
+export declare function intersectLineSegmentArc(a1: number[], a2: number[], center: number[], radius: number, start: number[], end: number[]): TLIntersection;
+/**
+ * Find the intersections between a line segment and a circle.
+ * @param a1
+ * @param a2
+ * @param c
+ * @param r
+ */
+export declare function intersectLineSegmentCircle(a1: number[], a2: number[], c: number[], r: number): TLIntersection;
+/**
+ * Find the intersections between a line segment and an ellipse.
+ * @param a1
+ * @param a2
+ * @param center
+ * @param rx
+ * @param ry
+ * @param rotation
+ */
+export declare function intersectLineSegmentEllipse(a1: number[], a2: number[], center: number[], rx: number, ry: number, rotation?: number): TLIntersection;
+/**
+ * Find the intersections between a line segment and a bounding box.
+ * @param a1
+ * @param a2
+ * @param bounds
+ */
+export declare function intersectLineSegmentBounds(a1: number[], a2: number[], bounds: TLBounds): TLIntersection[];
+/**
+ * Find the intersections between a line segment and a polyline.
+ * @param a1
+ * @param a2
+ * @param points
+ */
+export declare function intersectLineSegmentPolyline(a1: number[], a2: number[], points: number[][]): TLIntersection;
+/**
+ * Find the intersections between a line segment and a closed polygon.
+ * @param a1
+ * @param a2
+ * @param points
+ */
+export declare function intersectLineSegmentPolygon(a1: number[], a2: number[], points: number[][]): TLIntersection;
+/**
+ * Find the intersections between a rectangle and a ray.
+ * @param point
+ * @param size
+ * @param rotation
+ * @param origin
+ * @param direction
+ */
+export declare function intersectRectangleRay(point: number[], size: number[], rotation: number, origin: number[], direction: number[]): TLIntersection[];
+/**
+ * Find the intersections between a rectangle and a line segment.
+ * @param point
+ * @param size
+ * @param a1
+ * @param a2
+ */
+export declare function intersectRectangleLineSegment(point: number[], size: number[], a1: number[], a2: number[]): TLIntersection[];
+/**
+ * Find the intersections between a rectangle and a rectangle.
+ * @param point1
+ * @param size1
+ * @param point2
+ * @param size2
+ */
+export declare function intersectRectangleRectangle(point1: number[], size1: number[], point2: number[], size2: number[]): TLIntersection[];
+/**
+ * Find the intersections between a rectangle and an arc.
+ * @param point
+ * @param size
+ * @param center
+ * @param radius
+ * @param start
+ * @param end
+ */
+export declare function intersectRectangleArc(point: number[], size: number[], center: number[], radius: number, start: number[], end: number[]): TLIntersection[];
+/**
+ * Find the intersections between a rectangle and a circle.
+ * @param point
+ * @param size
+ * @param c
+ * @param r
+ */
+export declare function intersectRectangleCircle(point: number[], size: number[], c: number[], r: number): TLIntersection[];
+/**
+ * Find the intersections between a rectangle and an ellipse.
+ * @param point
+ * @param size
+ * @param c
+ * @param rx
+ * @param ry
+ * @param rotation
+ */
+export declare function intersectRectangleEllipse(point: number[], size: number[], c: number[], rx: number, ry: number, rotation?: number): TLIntersection[];
+/**
+ * Find the intersections between a rectangle and a bounding box.
+ * @param point
+ * @param size
+ * @param bounds
+ */
+export declare function intersectRectangleBounds(point: number[], size: number[], bounds: TLBounds): TLIntersection[];
+/**
+ * Find the intersections between a rectangle and a polyline.
+ * @param point
+ * @param size
+ * @param points
+ */
+export declare function intersectRectanglePolyline(point: number[], size: number[], points: number[][]): TLIntersection[];
+/**
+ * Find the intersections between a rectangle and a polygon.
+ * @param point
+ * @param size
+ * @param points
+ */
+export declare function intersectRectanglePolygon(point: number[], size: number[], points: number[][]): TLIntersection[];
+/**
+ * Find the intersections between a arc and a line segment.
+ * @param center
+ * @param radius
+ * @param start
+ * @param end
+ * @param a1
+ * @param a2
+ */
+export declare function intersectArcLineSegment(center: number[], radius: number, start: number[], end: number[], a1: number[], a2: number[]): TLIntersection;
+/**
+ * Find the intersections between a arc and a rectangle.
+ * @param center
+ * @param radius
+ * @param start
+ * @param end
+ * @param point
+ * @param size
+ */
+export declare function intersectArcRectangle(center: number[], radius: number, start: number[], end: number[], point: number[], size: number[]): TLIntersection[];
+/**
+ * Find the intersections between a arc and a bounding box.
+ * @param center
+ * @param radius
+ * @param start
+ * @param end
+ * @param bounds
+ */
+export declare function intersectArcBounds(center: number[], radius: number, start: number[], end: number[], bounds: TLBounds): TLIntersection[];
+/**
+ * Find the intersections between a circle and a line segment.
+ * @param c
+ * @param r
+ * @param a1
+ * @param a2
+ */
+export declare function intersectCircleLineSegment(c: number[], r: number, a1: number[], a2: number[]): TLIntersection;
+/**
+ * Find the intersections between a circle and a circle.
+ * @param c1
+ * @param r1
+ * @param c2
+ * @param r2
+ */
+export declare function intersectCircleCircle(c1: number[], r1: number, c2: number[], r2: number): TLIntersection;
+/**
+ * Find the intersections between a circle and a rectangle.
+ * @param c
+ * @param r
+ * @param point
+ * @param size
+ */
+export declare function intersectCircleRectangle(c: number[], r: number, point: number[], size: number[]): TLIntersection[];
+/**
+ * Find the intersections between a circle and a bounding box.
+ * @param c
+ * @param r
+ * @param bounds
+ */
+export declare function intersectCircleBounds(c: number[], r: number, bounds: TLBounds): TLIntersection[];
+/**
+ * Find the intersections between an ellipse and a ray.
+ * @param center
+ * @param rx
+ * @param ry
+ * @param rotation
+ * @param point
+ * @param direction
+ */
+export declare function intersectEllipseRay(center: number[], rx: number, ry: number, rotation: number, point: number[], direction: number[]): TLIntersection;
+/**
+ * Find the intersections between an ellipse and a line segment.
+ * @param center
+ * @param rx
+ * @param ry
+ * @param rotation
+ * @param a1
+ * @param a2
+ */
+export declare function intersectEllipseLineSegment(center: number[], rx: number, ry: number, rotation: number | undefined, a1: number[], a2: number[]): TLIntersection;
+/**
+ * Find the intersections between an ellipse and a rectangle.
+ * @param center
+ * @param rx
+ * @param ry
+ * @param rotation
+ * @param point
+ * @param size
+ */
+export declare function intersectEllipseRectangle(center: number[], rx: number, ry: number, rotation: number | undefined, point: number[], size: number[]): TLIntersection[];
+/**
+ * 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 declare function intersectEllipseEllipse(_c1: number[], _rx1: number, _ry1: number, _r1: number, _c2: number[], _rx2: number, _ry2: number, _r2: number): TLIntersection;
+/**
+ * Find the intersections between an ellipse and a circle.
+ * @param c
+ * @param rx
+ * @param ry
+ * @param rotation
+ * @param c2
+ * @param r2
+ */
+export declare function intersectEllipseCircle(c: number[], rx: number, ry: number, rotation: number, c2: number[], r2: number): TLIntersection;
+/**
+ * Find the intersections between an ellipse and a bounding box.
+ * @param c
+ * @param rx
+ * @param ry
+ * @param rotation
+ * @param bounds
+ */
+export declare function intersectEllipseBounds(c: number[], rx: number, ry: number, rotation: number, bounds: TLBounds): TLIntersection[];
+/**
+ * Find the intersections between a bounding box and a ray.
+ * @param bounds
+ * @param origin
+ * @param direction
+ */
+export declare function intersectBoundsRay(bounds: TLBounds, origin: number[], direction: number[]): TLIntersection[];
+/**
+ * Find the intersections between a bounding box and a line segment.
+ * @param bounds
+ * @param a1
+ * @param a2
+ */
+export declare function intersectBoundsLineSegment(bounds: TLBounds, a1: number[], a2: number[]): TLIntersection[];
+/**
+ * Find the intersections between a bounding box and a rectangle.
+ * @param bounds
+ * @param point
+ * @param size
+ */
+export declare function intersectBoundsRectangle(bounds: TLBounds, point: number[], size: number[]): TLIntersection[];
+/**
+ * Find the intersections between a bounding box and a bounding box.
+ * @param bounds1
+ * @param bounds2
+ */
+export declare function intersectBoundsBounds(bounds1: TLBounds, bounds2: TLBounds): TLIntersection[];
+/**
+ * Find the intersections between a bounding box and an arc.
+ * @param bounds
+ * @param center
+ * @param radius
+ * @param start
+ * @param end
+ */
+export declare function intersectBoundsArc(bounds: TLBounds, center: number[], radius: number, start: number[], end: number[]): TLIntersection[];
+/**
+ * Find the intersections between a bounding box and a circle.
+ * @param bounds
+ * @param c
+ * @param r
+ */
+export declare function intersectBoundsCircle(bounds: TLBounds, c: number[], r: number): TLIntersection[];
+/**
+ * Find the intersections between a bounding box and an ellipse.
+ * @param bounds
+ * @param c
+ * @param rx
+ * @param ry
+ * @param rotation
+ */
+export declare function intersectBoundsEllipse(bounds: TLBounds, c: number[], rx: number, ry: number, rotation?: number): TLIntersection[];
+/**
+ * Find the intersections between a bounding box and a polyline.
+ * @param bounds
+ * @param points
+ */
+export declare function intersectBoundsPolyline(bounds: TLBounds, points: number[][]): TLIntersection[];
+/**
+ * Find the intersections between a bounding box and a polygon.
+ * @param bounds
+ * @param points
+ */
+export declare function intersectBoundsPolygon(bounds: TLBounds, points: number[][]): TLIntersection[];
+/**
+ * Find the intersections between a polyline and a line segment.
+ * @param points
+ * @param a1
+ * @param a2
+ */
+export declare function intersectPolylineLineSegment(points: number[][], a1: number[], a2: number[]): TLIntersection;
+/**
+ * Find the intersections between a polyline and a rectangle.
+ * @param points
+ * @param point
+ * @param size
+ */
+export declare function intersectPolylineRectangle(points: number[][], point: number[], size: number[]): TLIntersection[];
+/**
+ * Find the intersections between a polyline and a bounding box.
+ * @param points
+ * @param bounds
+ */
+export declare function intersectPolylineBounds(points: number[][], bounds: TLBounds): TLIntersection[];
+/**
+ * Find the intersections between a polygon nd a line segment.
+ * @param points
+ * @param a1
+ * @param a2
+ */
+export declare function intersectPolygonLineSegment(points: number[][], a1: number[], a2: number[]): TLIntersection;
+/**
+ * Find the intersections between a polygon and a rectangle.
+ * @param points
+ * @param point
+ * @param size
+ */
+export declare function intersectPolygonRectangle(points: number[][], point: number[], size: number[]): TLIntersection[];
+/**
+ * Find the intersections between a polygon and a bounding box.
+ * @param points
+ * @param bounds
+ */
+export declare function intersectPolygonBounds(points: number[][], bounds: TLBounds): TLIntersection[];
diff --git a/packages/intersect/src/index.ts b/packages/intersect/src/index.ts
new file mode 100644
index 000000000..18d8ea705
--- /dev/null
+++ b/packages/intersect/src/index.ts
@@ -0,0 +1,1241 @@
+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 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 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 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 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 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 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 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 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 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 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 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 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 intersectLineSegmentPolyline(
+ a1: number[],
+ a2: number[],
+ points: number[][]
+): TLIntersection {
+ const pts: number[][] = []
+
+ for (let i = 1; i < points.length; i++) {
+ const int = intersectLineSegmentLineSegment(a1, a2, points[i - 1], points[i])
+
+ if (int) {
+ pts.push(...int.points)
+ }
+ }
+
+ if (pts.length === 0) {
+ return createIntersection('no intersection')
+ }
+
+ return createIntersection('intersection', ...points)
+}
+/**
+ * Find the intersections between a line segment and a closed polygon.
+ * @param a1
+ * @param a2
+ * @param points
+ */
+export function intersectLineSegmentPolygon(
+ a1: number[],
+ a2: number[],
+ points: number[][]
+): TLIntersection {
+ const pts: number[][] = []
+
+ for (let i = 1; i < points.length + 1; i++) {
+ const int = intersectLineSegmentLineSegment(a1, a2, points[i - 1], points[i % points.length])
+
+ if (int) {
+ pts.push(...int.points)
+ }
+ }
+
+ if (pts.length === 0) {
+ return createIntersection('no intersection')
+ }
+
+ return createIntersection('intersection', ...points)
+}
+
+/* -------------------------------------------------- */
+/* Rectangle */
+/* -------------------------------------------------- */
+
+/**
+ * Find the intersections between a rectangle and a ray.
+ * @param point
+ * @param size
+ * @param rotation
+ * @param origin
+ * @param direction
+ */
+export function 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 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 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 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 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 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 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 intersectRectanglePolyline(
+ point: number[],
+ size: number[],
+ points: number[][]
+): TLIntersection[] {
+ const sideIntersections = getRectangleSides(point, size).reduce(
+ (acc, [message, [a1, a2]]) => {
+ const intersection = intersectLineSegmentPolyline(a1, a2, points)
+
+ if (intersection.didIntersect) {
+ acc.push(createIntersection(message, ...intersection.points))
+ }
+
+ return acc
+ },
+ []
+ )
+
+ return sideIntersections.filter((int) => int.didIntersect)
+}
+/**
+ * Find the intersections between a rectangle and a polygon.
+ * @param point
+ * @param size
+ * @param points
+ */
+export function intersectRectanglePolygon(
+ point: number[],
+ size: number[],
+ points: number[][]
+): TLIntersection[] {
+ const sideIntersections = getRectangleSides(point, size).reduce(
+ (acc, [message, [a1, a2]]) => {
+ const intersection = intersectLineSegmentPolygon(a1, a2, points)
+
+ if (intersection.didIntersect) {
+ acc.push(createIntersection(message, ...intersection.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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 intersectBoundsPolyline(bounds: TLBounds, points: number[][]): TLIntersection[] {
+ return intersectPolylineBounds(points, bounds)
+}
+
+/**
+ * Find the intersections between a bounding box and a polygon.
+ * @param bounds
+ * @param points
+ */
+export function intersectBoundsPolygon(bounds: TLBounds, points: number[][]): TLIntersection[] {
+ return intersectPolygonBounds(points, bounds)
+}
+
+/* -------------------------------------------------- */
+/* Polyline */
+/* -------------------------------------------------- */
+
+/**
+ * Find the intersections between a polyline and a line segment.
+ * @param points
+ * @param a1
+ * @param a2
+ */
+export function 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 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 intersectPolylineBounds(points: number[][], bounds: TLBounds): TLIntersection[] {
+ return intersectRectanglePolyline(
+ [bounds.minX, bounds.minY],
+ [bounds.width, bounds.height],
+ points
+ )
+}
+
+/* -------------------------------------------------- */
+/* Polygon */
+/* -------------------------------------------------- */
+
+/**
+ * Find the intersections between a polygon nd a line segment.
+ * @param points
+ * @param a1
+ * @param a2
+ */
+export function intersectPolygonLineSegment(
+ points: number[][],
+ a1: number[],
+ a2: number[]
+): TLIntersection {
+ return intersectLineSegmentPolyline(a1, a2, points)
+}
+
+/**
+ * Find the intersections between a polygon and a rectangle.
+ * @param points
+ * @param point
+ * @param size
+ */
+export function intersectPolygonRectangle(
+ points: number[][],
+ point: number[],
+ size: number[]
+): TLIntersection[] {
+ return intersectRectanglePolyline(point, size, points)
+}
+
+/**
+ * Find the intersections between a polygon and a bounding box.
+ * @param points
+ * @param bounds
+ */
+export function intersectPolygonBounds(points: number[][], bounds: TLBounds): TLIntersection[] {
+ return intersectRectanglePolygon(
+ [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..4eb5595ee
--- /dev/null
+++ b/packages/intersect/tsconfig.build.json
@@ -0,0 +1,21 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": [
+ "node_modules",
+ "**/*.test.tsx",
+ "**/*.test.ts",
+ "**/*.spec.tsx",
+ "**/*.spec.ts",
+ "src/test",
+ "dist",
+ "docs"
+ ],
+ "compilerOptions": {
+ "composite": false,
+ "incremental": false,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "references": [{ "path": "../vec" }]
+}
diff --git a/packages/intersect/tsconfig.json b/packages/intersect/tsconfig.json
new file mode 100644
index 000000000..4b9f2fc35
--- /dev/null
+++ b/packages/intersect/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "exclude": ["node_modules", "dist", "docs"],
+ "compilerOptions": {
+ "outDir": "./dist/types",
+ "rootDir": "src",
+ "baseUrl": "."
+ },
+ "references": [{ "path": "../vec" }],
+ "typedocOptions": {
+ "entryPoints": ["src/index.ts"],
+ "out": "docs"
+ }
+}
diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json
index e08acc6a9..b535b17a5 100644
--- a/packages/tldraw/package.json
+++ b/packages/tldraw/package.json
@@ -49,9 +49,9 @@
"@radix-ui/react-radio-group": "^0.1.1",
"@radix-ui/react-tooltip": "^0.1.1",
"@stitches/react": "^1.2.5",
- "@tldraw/core": "^1.1.3",
- "@tldraw/intersect": "latest",
- "@tldraw/vec": "latest",
+ "@tldraw/core": "^1.1.4",
+ "@tldraw/intersect": "^1.1.4",
+ "@tldraw/vec": "^1.1.4",
"idb-keyval": "^6.0.3",
"perfect-freehand": "^1.0.16",
"react-hotkeys-hook": "^3.4.0",
@@ -62,7 +62,7 @@
"@swc-node/jest": "^1.3.3",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
- "tsconfig-replace-paths": "^0.0.5"
+ "tsconfig-replace-paths": "^0.0.11"
},
"jest": {
"setupFilesAfterEnv": [
@@ -91,4 +91,4 @@
}
},
"gitHead": "325008ff82bd27b63d625ad1b760f8871fb71af9"
-}
+}
\ No newline at end of file
diff --git a/packages/tldraw/src/Tldraw.tsx b/packages/tldraw/src/Tldraw.tsx
index a434e9a58..954d3cecf 100644
--- a/packages/tldraw/src/Tldraw.tsx
+++ b/packages/tldraw/src/Tldraw.tsx
@@ -8,9 +8,10 @@ import { TldrawContext, useStylesheet, useKeyboardShortcuts, useTldrawApp } from
import { shapeUtils } from '~state/shapes'
import { ToolsPanel } from '~components/ToolsPanel'
import { TopPanel } from '~components/TopPanel'
-import { TLDR } from '~state/TLDR'
import { ContextMenu } from '~components/ContextMenu'
-import { FocusButton } from '~components/FocusButton/FocusButton'
+import { FocusButton } from '~components/FocusButton'
+import { TLDR } from '~state/TLDR'
+import { GRID_SIZE } from '~constants'
export interface TldrawProps extends TDCallbacks {
/**
@@ -425,6 +426,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
page={page}
pageState={pageState}
snapLines={appState.snapLines}
+ grid={GRID_SIZE}
users={room?.users}
userId={room?.userId}
theme={theme}
@@ -436,6 +438,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
hideBindingHandles={!settings.showBindingHandles}
hideCloneHandles={!settings.showCloneHandles}
hideRotateHandles={!settings.showRotateHandles}
+ hideGrid={!settings.showGrid}
onPinchStart={app.onPinchStart}
onPinchEnd={app.onPinchEnd}
onPinch={app.onPinch}
diff --git a/packages/tldraw/src/components/FocusButton/index.ts b/packages/tldraw/src/components/FocusButton/index.ts
index e69de29bb..bd2437382 100644
--- a/packages/tldraw/src/components/FocusButton/index.ts
+++ b/packages/tldraw/src/components/FocusButton/index.ts
@@ -0,0 +1 @@
+export * from './FocusButton'
diff --git a/packages/tldraw/src/components/TopPanel/PreferencesMenu/PreferencesMenu.tsx b/packages/tldraw/src/components/TopPanel/PreferencesMenu/PreferencesMenu.tsx
index e83f1efee..41f906876 100644
--- a/packages/tldraw/src/components/TopPanel/PreferencesMenu/PreferencesMenu.tsx
+++ b/packages/tldraw/src/components/TopPanel/PreferencesMenu/PreferencesMenu.tsx
@@ -26,6 +26,10 @@ export function PreferencesMenu() {
app.setSetting('showRotateHandles', (v) => !v)
}, [app])
+ const toggleGrid = React.useCallback(() => {
+ app.setSetting('showGrid', (v) => !v)
+ }, [app])
+
const toggleBoundShapesHandle = React.useCallback(() => {
app.setSetting('showBindingHandles', (v) => !v)
}, [app])
@@ -62,6 +66,9 @@ export function PreferencesMenu() {
Clone Handles
+
+ Grid
+
Always Show Snaps
diff --git a/packages/tldraw/src/constants.ts b/packages/tldraw/src/constants.ts
index db0aed101..796c2b353 100644
--- a/packages/tldraw/src/constants.ts
+++ b/packages/tldraw/src/constants.ts
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
+export const GRID_SIZE = 8
export const BINDING_DISTANCE = 24
export const CLONING_DISTANCE = 32
export const FIT_TO_SCREEN_PADDING = 128
diff --git a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx
index 71ac29610..eb66ce67c 100644
--- a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx
+++ b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx
@@ -129,6 +129,16 @@ export function useKeyboardShortcuts(ref: React.RefObject) {
[app]
)
+ useHotkeys(
+ 'ctrl+shift+g,⌘+shift+g',
+ () => {
+ if (!canHandleEvent()) return
+ app.toggleGrid()
+ },
+ undefined,
+ [app]
+ )
+
// File System
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
diff --git a/packages/tldraw/src/state/TLDR.ts b/packages/tldraw/src/state/TLDR.ts
index 9f528c9e7..4debfe2dd 100644
--- a/packages/tldraw/src/state/TLDR.ts
+++ b/packages/tldraw/src/state/TLDR.ts
@@ -672,7 +672,7 @@ export class TLDR {
const rotatedCenter = Vec.rotWith(center, origin, delta)
// Get the top left point relative to the rotated center
- const nextPoint = Vec.round(Vec.sub(rotatedCenter, relativeCenter))
+ const nextPoint = Vec.toFixed(Vec.sub(rotatedCenter, relativeCenter))
// If the shape has handles, we need to rotate the handles instead
// of rotating the shape. Shapes with handles should never be rotated,
@@ -685,7 +685,7 @@ export class TLDR {
Object.entries(shape.handles).map(([handleId, handle]) => {
// Rotate each handle's point around the shape's center
// (in relative shape space, as the handle's point will be).
- const point = Vec.round(Vec.rotWith(handle.point, relativeCenter, delta))
+ const point = Vec.toFixed(Vec.rotWith(handle.point, relativeCenter, delta))
return [handleId, { ...handle, point }]
})
) as T['handles'],
diff --git a/packages/tldraw/src/state/TldrawApp.ts b/packages/tldraw/src/state/TldrawApp.ts
index c51e42750..f0c24a8cd 100644
--- a/packages/tldraw/src/state/TldrawApp.ts
+++ b/packages/tldraw/src/state/TldrawApp.ts
@@ -48,7 +48,7 @@ import { defaultStyle } from '~state/shapes/shared/shape-styles'
import * as Commands from './commands'
import { SessionArgsOfType, getSession, TldrawSession } from './sessions'
import type { BaseTool } from './tools/BaseTool'
-import { USER_COLORS, FIT_TO_SCREEN_PADDING } from '~constants'
+import { USER_COLORS, FIT_TO_SCREEN_PADDING, GRID_SIZE } from '~constants'
import { SelectTool } from './tools/SelectTool'
import { EraseTool } from './tools/EraseTool'
import { TextTool } from './tools/TextTool'
@@ -790,6 +790,16 @@ export class TldrawApp extends StateManager {
return this
}
+ /**
+ * Toggle grids.
+ */
+ toggleGrid = (): this => {
+ if (this.session) return this
+ this.patchState({ settings: { showGrid: !this.settings.showGrid } }, 'settings:toggled_grid')
+ this.persist()
+ return this
+ }
+
/**
* Select a tool.
* @param tool The tool to select, or "select".
@@ -1472,17 +1482,14 @@ export class TldrawApp extends StateManager {
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
- let center = Vec.round(this.getPagePoint(point || this.centerPoint))
+ let center = Vec.toFixed(this.getPagePoint(point || this.centerPoint))
if (
Vec.dist(center, this.pasteInfo.center) < 2 ||
- Vec.dist(center, Vec.round(Utils.getBoundsCenter(commonBounds))) < 2
+ Vec.dist(center, Vec.toFixed(Utils.getBoundsCenter(commonBounds))) < 2
) {
center = Vec.add(center, this.pasteInfo.offset)
- this.pasteInfo.offset = Vec.add(this.pasteInfo.offset, [
- this.settings.nudgeDistanceLarge,
- this.settings.nudgeDistanceLarge,
- ])
+ this.pasteInfo.offset = Vec.add(this.pasteInfo.offset, [GRID_SIZE, GRID_SIZE])
} else {
this.pasteInfo.center = center
this.pasteInfo.offset = [0, 0]
@@ -1499,7 +1506,7 @@ export class TldrawApp extends StateManager {
shapesToPaste.map((shape) =>
TLDR.getShapeUtil(shape.type).create({
...shape,
- point: Vec.round(Vec.add(shape.point, delta)),
+ point: Vec.toFixed(Vec.add(shape.point, delta)),
parentId: shape.parentId || this.currentPageId,
})
),
@@ -1691,7 +1698,7 @@ export class TldrawApp extends StateManager {
*/
pan = (delta: number[]): this => {
const { camera } = this.pageState
- return this.setCamera(Vec.round(Vec.sub(camera.point, delta)), camera.zoom, `panned`)
+ return this.setCamera(Vec.toFixed(Vec.sub(camera.point, delta)), camera.zoom, `panned`)
}
/**
@@ -1706,7 +1713,11 @@ export class TldrawApp extends StateManager {
const nextZoom = zoom
const p0 = Vec.sub(Vec.div(point, camera.zoom), nextPoint)
const p1 = Vec.sub(Vec.div(point, nextZoom), nextPoint)
- return this.setCamera(Vec.round(Vec.add(nextPoint, Vec.sub(p1, p0))), nextZoom, `pinch_zoomed`)
+ return this.setCamera(
+ Vec.toFixed(Vec.add(nextPoint, Vec.sub(p1, p0))),
+ nextZoom,
+ `pinch_zoomed`
+ )
}
/**
@@ -1718,7 +1729,7 @@ export class TldrawApp extends StateManager {
const { zoom, point } = this.pageState.camera
const p0 = Vec.sub(Vec.div(center, zoom), point)
const p1 = Vec.sub(Vec.div(center, next), point)
- return this.setCamera(Vec.round(Vec.add(point, Vec.sub(p1, p0))), next, `zoomed_camera`)
+ return this.setCamera(Vec.toFixed(Vec.add(point, Vec.sub(p1, p0))), next, `zoomed_camera`)
}
/**
@@ -1767,7 +1778,7 @@ export class TldrawApp extends StateManager {
const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom
return this.setCamera(
- Vec.round(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
+ Vec.toFixed(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
zoom,
`zoomed_to_fit`
)
@@ -1798,7 +1809,7 @@ export class TldrawApp extends StateManager {
const my = (rendererBounds.height - selectedBounds.height * zoom) / 2 / zoom
return this.setCamera(
- Vec.round(Vec.sub([mx, my], [selectedBounds.minX, selectedBounds.minY])),
+ Vec.toFixed(Vec.sub([mx, my], [selectedBounds.minX, selectedBounds.minY])),
zoom,
`zoomed_to_selection`
)
@@ -1821,7 +1832,7 @@ export class TldrawApp extends StateManager {
const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom
return this.setCamera(
- Vec.round(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
+ Vec.toFixed(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
this.pageState.camera.zoom,
`zoomed_to_content`
)
@@ -2119,6 +2130,7 @@ export class TldrawApp extends StateManager {
status: TDStatus.Idle,
},
document: {
+ ...result.document,
pageStates: {
[this.currentPageId]: {
...result.document?.pageStates?.[this.currentPageId],
@@ -2359,7 +2371,15 @@ export class TldrawApp extends StateManager {
*/
nudge = (delta: number[], isMajor = false, ids = this.selectedIds): this => {
if (ids.length === 0) return this
- return this.setState(Commands.translateShapes(this, ids, Vec.mul(delta, isMajor ? 10 : 1)))
+ const size = isMajor
+ ? this.settings.showGrid
+ ? this.currentGrid * 4
+ : 10
+ : this.settings.showGrid
+ ? this.currentGrid
+ : 1
+
+ return this.setState(Commands.translateShapes(this, ids, Vec.mul(delta, size)))
}
/**
@@ -2498,7 +2518,7 @@ export class TldrawApp extends StateManager {
onKeyDown: TLKeyboardEventHandler = (key, info, e) => {
switch (e.key) {
- case '.': {
+ case '/': {
if (this.status === 'idle') {
const { shiftKey, metaKey, altKey, ctrlKey, spaceKey } = this
@@ -2559,7 +2579,7 @@ export class TldrawApp extends StateManager {
if (!info) return
switch (e.key) {
- case '.': {
+ case '/': {
const { currentPoint, shiftKey, metaKey, altKey, ctrlKey, spaceKey } = this
this.onPointerUp(
@@ -2950,7 +2970,18 @@ export class TldrawApp extends StateManager {
// The center of the component (in screen space)
get centerPoint() {
const { width, height } = this.rendererBounds
- return Vec.round([width / 2, height / 2])
+ return Vec.toFixed([width / 2, height / 2])
+ }
+
+ get currentGrid() {
+ const { zoom } = this.pageState.camera
+ if (zoom < 0.15) {
+ return GRID_SIZE * 16
+ } else if (zoom < 1) {
+ return GRID_SIZE * 4
+ } else {
+ return GRID_SIZE * 1
+ }
}
getShapeUtil = TLDR.getShapeUtil
@@ -2996,6 +3027,7 @@ export class TldrawApp extends StateManager {
showRotateHandles: true,
showBindingHandles: true,
showCloneHandles: false,
+ showGrid: false,
},
appState: {
status: TDStatus.Idle,
diff --git a/packages/tldraw/src/state/commands/alignShapes/alignShapes.ts b/packages/tldraw/src/state/commands/alignShapes/alignShapes.ts
index 235c8f8b6..9bb925ae7 100644
--- a/packages/tldraw/src/state/commands/alignShapes/alignShapes.ts
+++ b/packages/tldraw/src/state/commands/alignShapes/alignShapes.ts
@@ -1,9 +1,8 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import { Vec } from '@tldraw/vec'
import { Utils } from '@tldraw/core'
import { AlignType, TldrawCommand, TDShapeType } from '~types'
-import type { TDSnapshot } from '~types'
import { TLDR } from '~state/TLDR'
-import Vec from '@tldraw/vec'
import type { TldrawApp } from '../../internal'
export function alignShapes(app: TldrawApp, ids: string[], type: AlignType): TldrawCommand {
diff --git a/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.ts b/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.ts
index 2cca79f30..9c081c085 100644
--- a/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.ts
+++ b/packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.ts
@@ -170,7 +170,7 @@ export function moveShapesToPage(
const mx = (viewportBounds.width - bounds.width * zoom) / 2 / zoom
const my = (viewportBounds.height - bounds.height * zoom) / 2 / zoom
- const point = Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my]))
+ const point = Vec.toFixed(Vec.add([-bounds.minX, -bounds.minY], [mx, my]))
return {
id: 'move_to_page',
diff --git a/packages/tldraw/src/state/commands/styleShapes/styleShapes.ts b/packages/tldraw/src/state/commands/styleShapes/styleShapes.ts
index efaa41db5..2600b9275 100644
--- a/packages/tldraw/src/state/commands/styleShapes/styleShapes.ts
+++ b/packages/tldraw/src/state/commands/styleShapes/styleShapes.ts
@@ -35,7 +35,7 @@ export function styleShapes(
if (shape.type === TDShapeType.Text) {
beforeShapes[shape.id].point = shape.point
- afterShapes[shape.id].point = Vec.round(
+ afterShapes[shape.id].point = Vec.toFixed(
Vec.add(
shape.point,
Vec.sub(
diff --git a/packages/tldraw/src/state/commands/translateShapes/translateShapes.ts b/packages/tldraw/src/state/commands/translateShapes/translateShapes.ts
index f8671af2f..5393dc39c 100644
--- a/packages/tldraw/src/state/commands/translateShapes/translateShapes.ts
+++ b/packages/tldraw/src/state/commands/translateShapes/translateShapes.ts
@@ -30,7 +30,7 @@ export function translateShapes(app: TldrawApp, ids: string[], delta: number[]):
app.state,
idsToMutate,
(shape) => ({
- point: Vec.round(Vec.add(shape.point, delta)),
+ point: Vec.toFixed(Vec.add(shape.point, delta)),
}),
currentPageId
)
diff --git a/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.ts b/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.ts
index ca01d2403..876764233 100644
--- a/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.ts
+++ b/packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.ts
@@ -10,12 +10,12 @@ import {
TldrawCommand,
} from '~types'
import { Vec } from '@tldraw/vec'
-import { Utils } from '@tldraw/core'
import { TLDR } from '~state/TLDR'
import { BINDING_DISTANCE } from '~constants'
import { shapeUtils } from '~state/shapes'
import { BaseSession } from '../BaseSession'
import type { TldrawApp } from '../../internal'
+import { Utils } from '@tldraw/core'
export class ArrowSession extends BaseSession {
type = SessionType.Arrow
@@ -70,7 +70,14 @@ export class ArrowSession extends BaseSession {
update = (): TldrawPatch | undefined => {
const { initialShape } = this
- const { currentPoint, shiftKey, altKey, metaKey } = this.app
+ const {
+ currentPoint,
+ shiftKey,
+ altKey,
+ metaKey,
+ currentGrid,
+ settings: { showGrid },
+ } = this.app
const shape = this.app.getShape(initialShape.id)
@@ -90,15 +97,18 @@ export class ArrowSession extends BaseSession {
if (shiftKey) {
const A = handles[handleId === 'start' ? 'end' : 'start'].point
const B = handles[handleId].point
- const C = Vec.round(Vec.sub(Vec.add(B, delta), shape.point))
+ const C = Vec.toFixed(Vec.sub(Vec.add(B, delta), shape.point))
const angle = Vec.angle(A, C)
const adjusted = Vec.rotWith(C, A, Utils.snapAngleToSegments(angle, 24) - angle)
delta = Vec.add(delta, Vec.sub(adjusted, C))
}
+ const nextPoint = Vec.sub(Vec.add(handles[handleId].point, delta), shape.point)
+
const handle = {
...handles[handleId],
- point: Vec.round(Vec.sub(Vec.add(handles[handleId].point, delta), shape.point)),
+ point: showGrid ? Vec.snap(nextPoint, currentGrid) : Vec.toFixed(nextPoint),
+
bindingId: undefined,
}
@@ -340,14 +350,20 @@ export class ArrowSession extends BaseSession {
complete = (): TldrawPatch | TldrawCommand | undefined => {
const { initialShape, initialBinding, newStartBindingId, startBindingShapeId, handleId } = this
+ const currentShape = TLDR.onSessionComplete(this.app.page.shapes[initialShape.id]) as ArrowShape
+ const currentBindingId = currentShape.handles[handleId].bindingId
+
+ if (
+ !(currentBindingId || initialBinding) &&
+ Vec.dist(currentShape.handles.start.point, currentShape.handles.end.point) > 2
+ ) {
+ return this.cancel()
+ }
+
const beforeBindings: Partial> = {}
const afterBindings: Partial> = {}
- let afterShape = this.app.page.shapes[initialShape.id] as ArrowShape
-
- const currentBindingId = afterShape.handles[handleId].bindingId
-
if (initialBinding) {
beforeBindings[initialBinding.id] = this.isCreate ? undefined : initialBinding
afterBindings[initialBinding.id] = undefined
@@ -363,8 +379,6 @@ export class ArrowSession extends BaseSession {
afterBindings[newStartBindingId] = this.app.page.bindings[newStartBindingId]
}
- afterShape = TLDR.onSessionComplete(afterShape)
-
return {
id: 'arrow',
before: {
@@ -392,7 +406,7 @@ export class ArrowSession extends BaseSession {
pages: {
[this.app.currentPageId]: {
shapes: {
- [initialShape.id]: afterShape,
+ [initialShape.id]: currentShape,
},
bindings: afterBindings,
},
@@ -441,7 +455,7 @@ export class ArrowSession extends BaseSession {
fromId: shape.id,
toId: target.id,
handleId: handleId,
- point: Vec.round(bindingPoint.point),
+ point: Vec.toFixed(bindingPoint.point),
distance: bindingPoint.distance,
}
}
diff --git a/packages/tldraw/src/state/sessions/DrawSession/DrawSession.ts b/packages/tldraw/src/state/sessions/DrawSession/DrawSession.ts
index b5605cc5e..7d5f85d97 100644
--- a/packages/tldraw/src/state/sessions/DrawSession/DrawSession.ts
+++ b/packages/tldraw/src/state/sessions/DrawSession/DrawSession.ts
@@ -81,7 +81,7 @@ export class DrawSession extends BaseSession {
}
// The new adjusted point
- const newAdjustedPoint = Vec.round(Vec.sub(currentPoint, originPoint)).concat(currentPoint[2])
+ const newAdjustedPoint = Vec.toFixed(Vec.sub(currentPoint, originPoint)).concat(currentPoint[2])
// Don't add duplicate points.
if (Vec.isEqual(this.lastAdjustedPoint, newAdjustedPoint)) return
@@ -112,7 +112,7 @@ export class DrawSession extends BaseSession {
// offset between the new top left and the original top left.
points = this.points.map((pt) => {
- return Vec.round(Vec.sub(pt, delta)).concat(pt[2])
+ return Vec.toFixed(Vec.sub(pt, delta)).concat(pt[2])
})
} else {
// If the new top left is the same as the previous top left,
@@ -197,8 +197,8 @@ export class DrawSession extends BaseSession {
shapes: {
[shapeId]: {
...shape,
- point: Vec.round(shape.point),
- points: shape.points.map((pt) => Vec.round(pt)),
+ point: Vec.toFixed(shape.point),
+ points: shape.points.map((pt) => Vec.toFixed(pt)),
isComplete: true,
},
},
diff --git a/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts b/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts
index d6be9c09a..84614aea3 100644
--- a/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts
+++ b/packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts
@@ -57,7 +57,7 @@ export class EraseSession extends BaseSession {
}
}
- const newPoint = Vec.round(Vec.add(originPoint, Vec.sub(currentPoint, originPoint)))
+ const newPoint = Vec.toFixed(Vec.add(originPoint, Vec.sub(currentPoint, originPoint)))
const deletedShapeIds = new Set([])
diff --git a/packages/tldraw/src/state/sessions/RotateSession/RotateSession.spec.ts b/packages/tldraw/src/state/sessions/RotateSession/RotateSession.spec.ts
index 63acc2fa4..187e46a8c 100644
--- a/packages/tldraw/src/state/sessions/RotateSession/RotateSession.spec.ts
+++ b/packages/tldraw/src/state/sessions/RotateSession/RotateSession.spec.ts
@@ -106,7 +106,7 @@ describe('Rotate session', () => {
it('keeps the center', () => {
app.loadDocument(mockDocument).select('rect1', 'rect2')
- const centerBefore = Vec.round(
+ const centerBefore = Vec.toFixed(
Utils.getBoundsCenter(
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
)
@@ -114,7 +114,7 @@ describe('Rotate session', () => {
app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession()
- const centerAfterA = Vec.round(
+ const centerAfterA = Vec.toFixed(
Utils.getBoundsCenter(
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
)
@@ -122,7 +122,7 @@ describe('Rotate session', () => {
app.pointBoundsHandle('rotate', { x: 100, y: 0 }).movePointer([50, 0]).completeSession()
- const centerAfterB = Vec.round(
+ const centerAfterB = Vec.toFixed(
Utils.getBoundsCenter(
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
)
@@ -142,7 +142,7 @@ describe('Rotate session', () => {
it('changes the center after nudging', () => {
const app = new TldrawTestApp().loadDocument(mockDocument).select('rect1', 'rect2')
- const centerBefore = Vec.round(
+ const centerBefore = Vec.toFixed(
Utils.getBoundsCenter(
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
)
@@ -150,7 +150,7 @@ describe('Rotate session', () => {
app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession()
- const centerAfterA = Vec.round(
+ const centerAfterA = Vec.toFixed(
Utils.getBoundsCenter(
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
)
@@ -163,7 +163,7 @@ describe('Rotate session', () => {
app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession()
- const centerAfterB = Vec.round(
+ const centerAfterB = Vec.toFixed(
Utils.getBoundsCenter(
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
)
diff --git a/packages/tldraw/src/state/sessions/TransformSession/TransformSession.ts b/packages/tldraw/src/state/sessions/TransformSession/TransformSession.ts
index 6413db95d..16118e874 100644
--- a/packages/tldraw/src/state/sessions/TransformSession/TransformSession.ts
+++ b/packages/tldraw/src/state/sessions/TransformSession/TransformSession.ts
@@ -113,7 +113,8 @@ export class TransformSession extends BaseSession {
shiftKey,
altKey,
metaKey,
- settings: { isSnapping },
+ currentGrid,
+ settings: { isSnapping, showGrid },
},
} = this
@@ -138,6 +139,13 @@ export class TransformSession extends BaseSession {
}
}
+ if (showGrid) {
+ newBounds = {
+ ...newBounds,
+ ...Utils.snapBoundsToGrid(newBounds, currentGrid),
+ }
+ }
+
// Should we snap?
const speed = Vec.dist(currentPoint, previousPoint)
@@ -180,7 +188,7 @@ export class TransformSession extends BaseSession {
this.scaleY = newBounds.scaleY
shapeBounds.forEach(({ initialShape, initialShapeBounds, transformOrigin }) => {
- const newShapeBounds = Utils.getRelativeTransformedBoundingBox(
+ let newShapeBounds = Utils.getRelativeTransformedBoundingBox(
newBounds,
initialCommonBounds,
initialShapeBounds,
@@ -188,13 +196,19 @@ export class TransformSession extends BaseSession {
this.scaleY < 0
)
- shapes[initialShape.id] = TLDR.transform(this.app.getShape(initialShape.id), newShapeBounds, {
+ if (showGrid) {
+ newShapeBounds = Utils.snapBoundsToGrid(newShapeBounds, currentGrid)
+ }
+
+ const afterShape = TLDR.transform(this.app.getShape(initialShape.id), newShapeBounds, {
type: this.transformType,
initialShape,
scaleX: this.scaleX,
scaleY: this.scaleY,
transformOrigin,
})
+
+ shapes[initialShape.id] = afterShape
})
return {
diff --git a/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.ts b/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.ts
index 7e5af974c..ffb6b1b3c 100644
--- a/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.ts
+++ b/packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.ts
@@ -70,13 +70,14 @@ export class TransformSingleSession extends BaseSession {
initialShape,
initialShapeBounds,
app: {
- settings: { isSnapping },
+ settings: { isSnapping, showGrid },
currentPageId,
pageState: { camera },
viewport,
currentPoint,
previousPoint,
originPoint,
+ currentGrid,
shiftKey,
altKey,
metaKey,
@@ -85,12 +86,12 @@ export class TransformSingleSession extends BaseSession {
if (initialShape.isLocked) return void null
+ const shapes = {} as Record>
+
const delta = altKey
? Vec.mul(Vec.sub(currentPoint, originPoint), 2)
: Vec.sub(currentPoint, originPoint)
- const shapes = {} as Record>
-
const shape = this.app.getShape(initialShape.id)
const utils = TLDR.getShapeUtil(shape)
@@ -110,6 +111,13 @@ export class TransformSingleSession extends BaseSession {
}
}
+ if (showGrid) {
+ newBounds = {
+ ...newBounds,
+ ...Utils.snapBoundsToGrid(newBounds, currentGrid),
+ }
+ }
+
// Should we snap?
const speed = Vec.dist(currentPoint, previousPoint)
@@ -159,6 +167,10 @@ export class TransformSingleSession extends BaseSession {
shapes[shape.id] = afterShape
}
+ if (showGrid && afterShape && afterShape.point) {
+ afterShape.point = Vec.snap(afterShape.point, currentGrid)
+ }
+
return {
appState: {
snapLines,
diff --git a/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.spec.ts b/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.spec.ts
index 6f56b6a83..e826ee6dc 100644
--- a/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.spec.ts
+++ b/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.spec.ts
@@ -1,3 +1,4 @@
+import { Vec } from '@tldraw/vec'
import { mockDocument, TldrawTestApp } from '~test'
import { GroupShape, SessionType, TDShapeType, TDStatus } from '~types'
@@ -114,15 +115,16 @@ describe('Translate session', () => {
expect(Object.keys(app.getPage().shapes).length).toBe(5)
- app.movePointer({ x: 30, y: 30 })
+ app.movePointer({ x: 20, y: 20, altKey: false })
expect(Object.keys(app.getPage().shapes).length).toBe(3)
app.completeSession()
// Original position + delta
- expect(app.getShape('rect1').point).toStrictEqual([30, 30])
- expect(app.getShape('rect2').point).toStrictEqual([130, 130])
+ const rectPoint = app.getShape('rect1').point
+ expect(app.getShape('rect1').point).toStrictEqual(rectPoint)
+ expect(app.getShape('rect2').point).toStrictEqual([110, 110])
expect(Object.keys(app.page.shapes)).toStrictEqual(['rect1', 'rect2', 'rect3'])
})
@@ -211,6 +213,7 @@ describe('Translate session', () => {
.movePointer({ x: 20, y: 20, altKey: true })
.completeSession()
+ const rectPoint = app.getShape('rect1').point
const children = app.getShape('groupA').children
const newShapeId = children[children.length - 1]
@@ -218,7 +221,7 @@ describe('Translate session', () => {
expect(app.getShape('groupA').children.length).toBe(3)
expect(app.getShape('rect1').point).toStrictEqual([0, 0])
expect(app.getShape('rect2').point).toStrictEqual([100, 100])
- expect(app.getShape(newShapeId).point).toStrictEqual([20, 20])
+ expect(app.getShape(newShapeId).point).toStrictEqual(Vec.add(rectPoint, [10, 10]))
expect(app.getShape(newShapeId).parentId).toBe('groupA')
app.undo()
@@ -235,7 +238,7 @@ describe('Translate session', () => {
expect(app.getShape('groupA').children.length).toBe(3)
expect(app.getShape('rect1').point).toStrictEqual([0, 0])
expect(app.getShape('rect2').point).toStrictEqual([100, 100])
- expect(app.getShape(newShapeId).point).toStrictEqual([20, 20])
+ expect(app.getShape(newShapeId).point).toStrictEqual(Vec.add(rectPoint, [10, 10]))
expect(app.getShape(newShapeId).parentId).toBe('groupA')
})
})
diff --git a/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.ts b/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.ts
index a6732bf5f..6389617fd 100644
--- a/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.ts
+++ b/packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.ts
@@ -24,6 +24,7 @@ type CloneInfo =
}
| {
state: 'ready'
+ cloneMap: Record
clones: TDShape[]
clonedBindings: ArrowBinding[]
}
@@ -172,7 +173,7 @@ export class TranslateSession extends BaseSession {
bindingsToDelete,
app: {
pageState: { camera },
- settings: { isSnapping },
+ settings: { isSnapping, showGrid },
currentPageId,
viewport,
selectedIds,
@@ -182,13 +183,12 @@ export class TranslateSession extends BaseSession {
altKey,
shiftKey,
metaKey,
+ currentGrid,
},
} = this
const nextBindings: Patch> = {}
-
const nextShapes: Patch> = {}
-
const nextPageState: Patch = {}
let delta = Vec.sub(currentPoint, originPoint)
@@ -236,10 +236,12 @@ export class TranslateSession extends BaseSession {
this.speed * camera.zoom < SLOW_SPEED &&
this.snapInfo.state === 'ready'
) {
- const bounds = Utils.getBoundsWithCenter(Utils.translateBounds(initialCommonBounds, delta))
-
const snapResult = Utils.getSnapPoints(
- bounds,
+ Utils.getBoundsWithCenter(
+ showGrid
+ ? Utils.snapBoundsToGrid(Utils.translateBounds(initialCommonBounds, delta), currentGrid)
+ : Utils.translateBounds(initialCommonBounds, delta)
+ ),
(this.isCloning ? this.snapInfo.bounds : this.snapInfo.others).filter(
(bounds) => Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds)
),
@@ -259,8 +261,6 @@ export class TranslateSession extends BaseSession {
// The "movement" is the actual change of position between this
// computed position and the previous computed position.
- const movement = Vec.sub(delta, this.prev)
-
this.prev = delta
// If cloning...
@@ -287,7 +287,7 @@ export class TranslateSession extends BaseSession {
// Add the clones to the page
clones.forEach((clone) => {
- nextShapes[clone.id] = { ...clone, point: Vec.round(Vec.add(clone.point, delta)) }
+ nextShapes[clone.id] = { ...clone }
// Add clones to non-selected parents
if (clone.parentId !== currentPageId && !selectedIds.includes(clone.parentId)) {
@@ -313,13 +313,11 @@ export class TranslateSession extends BaseSession {
// Either way, move the clones
clones.forEach((clone) => {
- const current = (nextShapes[clone.id] || this.app.getShape(clone.id)) as TDShape
-
- if (!current.point) throw Error('No point on that clone!')
-
nextShapes[clone.id] = {
...clone,
- point: Vec.round(Vec.add(current.point, movement)),
+ point: showGrid
+ ? Vec.snap(Vec.toFixed(Vec.add(clone.point, delta)), currentGrid)
+ : Vec.toFixed(Vec.add(clone.point, delta)),
}
})
} else {
@@ -327,14 +325,11 @@ export class TranslateSession extends BaseSession {
const { clones } = this.cloneInfo
- // Either way, move the clones
clones.forEach((clone) => {
- const current = (nextShapes[clone.id] || this.app.getShape(clone.id)) as TDShape
-
- if (!current.point) throw Error('No point on that clone!')
-
nextShapes[clone.id] = {
- point: Vec.round(Vec.add(current.point, movement)),
+ point: showGrid
+ ? Vec.snap(Vec.toFixed(Vec.add(clone.point, delta)), currentGrid)
+ : Vec.toFixed(Vec.add(clone.point, delta)),
}
})
}
@@ -350,7 +345,6 @@ export class TranslateSession extends BaseSession {
this.isCloning = false
// Delete the bindings
-
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined))
// Remove the clones from parents
@@ -369,7 +363,9 @@ export class TranslateSession extends BaseSession {
// Move the original shapes back to the cursor position
initialShapes.forEach((shape) => {
nextShapes[shape.id] = {
- point: Vec.round(Vec.add(shape.point, delta)),
+ point: showGrid
+ ? Vec.snap(Vec.toFixed(Vec.add(shape.point, delta)), currentGrid)
+ : Vec.toFixed(Vec.add(shape.point, delta)),
}
})
@@ -380,18 +376,18 @@ export class TranslateSession extends BaseSession {
// Set selected ids
nextPageState.selectedIds = initialShapes.map((shape) => shape.id)
+ } else {
+ // Move the shapes by the delta
+ initialShapes.forEach((shape) => {
+ // const current = (nextShapes[shape.id] || this.app.getShape(shape.id)) as TDShape
+
+ nextShapes[shape.id] = {
+ point: showGrid
+ ? Vec.snap(Vec.toFixed(Vec.add(shape.point, delta)), currentGrid)
+ : Vec.toFixed(Vec.add(shape.point, delta)),
+ }
+ })
}
-
- // Move the shapes by the delta
- initialShapes.forEach((shape) => {
- const current = (nextShapes[shape.id] || this.app.getShape(shape.id)) as TDShape
-
- if (!current.point) throw Error('No point on that clone!')
-
- nextShapes[shape.id] = {
- point: Vec.round(Vec.add(current.point, movement)),
- }
- })
}
return {
@@ -696,6 +692,7 @@ export class TranslateSession extends BaseSession {
this.cloneInfo = {
state: 'ready',
clones,
+ cloneMap,
clonedBindings,
}
}
diff --git a/packages/tldraw/src/state/shapes/ArrowUtil/ArrowUtil.tsx b/packages/tldraw/src/state/shapes/ArrowUtil/ArrowUtil.tsx
index 80dcb990f..78450649f 100644
--- a/packages/tldraw/src/state/shapes/ArrowUtil/ArrowUtil.tsx
+++ b/packages/tldraw/src/state/shapes/ArrowUtil/ArrowUtil.tsx
@@ -99,7 +99,7 @@ export class ArrowUtil extends TDShapeUtil {
const isDraw = style.dash === DashStyle.Draw
- const isStraightLine = Vec.dist(bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
+ const isStraightLine = Vec.dist(bend.point, Vec.toFixed(Vec.med(start.point, end.point))) < 1
const styles = getShapeStyle(style, meta.isDarkMode)
@@ -122,7 +122,7 @@ export class ArrowUtil extends TDShapeUtil {
if (isStraightLine) {
const path = isDraw
? renderFreehandArrowShaft(shape)
- : 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
+ : 'M' + Vec.toFixed(start.point) + 'L' + Vec.toFixed(end.point)
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
arrowDist,
@@ -398,11 +398,11 @@ export class ArrowUtil extends TDShapeUtil {
nextHandles['bend'] = {
...bend,
- point: Vec.round(Math.abs(bendDist) < 10 ? midPoint : point),
+ point: Vec.toFixed(Math.abs(bendDist) < 10 ? midPoint : point),
}
return {
- point: Vec.round([bounds.minX, bounds.minY]),
+ point: Vec.toFixed([bounds.minX, bounds.minY]),
handles: nextHandles,
}
}
@@ -516,7 +516,7 @@ export class ArrowUtil extends TDShapeUtil {
return this.onHandleChange(shape, {
[handle.id]: {
...handle,
- point: Vec.round(handlePoint),
+ point: Vec.toFixed(handlePoint),
},
})
}
@@ -529,11 +529,11 @@ export class ArrowUtil extends TDShapeUtil {
...nextHandles,
start: {
...nextHandles.start,
- point: Vec.round(nextHandles.start.point),
+ point: Vec.toFixed(nextHandles.start.point),
},
end: {
...nextHandles.end,
- point: Vec.round(nextHandles.end.point),
+ point: Vec.toFixed(nextHandles.end.point),
},
}
@@ -601,10 +601,10 @@ export class ArrowUtil extends TDShapeUtil {
if (!Vec.isEqual(offset, [0, 0])) {
Object.values(nextShape.handles).forEach((handle) => {
- handle.point = Vec.round(Vec.sub(handle.point, offset))
+ handle.point = Vec.toFixed(Vec.sub(handle.point, offset))
})
- nextShape.point = Vec.round(Vec.add(nextShape.point, offset))
+ nextShape.point = Vec.toFixed(Vec.add(nextShape.point, offset))
}
return nextShape
diff --git a/packages/tldraw/src/state/shapes/ArrowUtil/arrowHelpers.ts b/packages/tldraw/src/state/shapes/ArrowUtil/arrowHelpers.ts
index 70e7903c2..3a0928cd2 100644
--- a/packages/tldraw/src/state/shapes/ArrowUtil/arrowHelpers.ts
+++ b/packages/tldraw/src/state/shapes/ArrowUtil/arrowHelpers.ts
@@ -38,7 +38,7 @@ export function getBendPoint(handles: ArrowShape['handles'], bend: number) {
const u = Vec.uni(Vec.vec(start.point, end.point))
- const point = Vec.round(
+ const point = Vec.toFixed(
Math.abs(bendDist) < 10 ? midPoint : Vec.add(midPoint, Vec.mul(Vec.per(u), bendDist))
)
@@ -115,7 +115,7 @@ export function renderCurvedFreehandArrowShaft(
const angle = Utils.lerpAngles(startAngle, endAngle, t)
- points.push(Vec.round(Vec.nudgeAtAngle(center, angle, radius)))
+ points.push(Vec.toFixed(Vec.nudgeAtAngle(center, angle, radius)))
}
const stroke = getStroke([startPoint, ...points, endPoint], {
@@ -221,7 +221,7 @@ export function getArrowPath(shape: ArrowShape) {
const path: (string | number)[] = []
- const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
+ const isStraightLine = Vec.dist(_bend.point, Vec.toFixed(Vec.med(start.point, end.point))) < 1
if (isStraightLine) {
// Path (line segment)
diff --git a/packages/tldraw/src/state/shapes/EllipseUtil/EllipseUtil.tsx b/packages/tldraw/src/state/shapes/EllipseUtil/EllipseUtil.tsx
index 793937e8d..d86711907 100644
--- a/packages/tldraw/src/state/shapes/EllipseUtil/EllipseUtil.tsx
+++ b/packages/tldraw/src/state/shapes/EllipseUtil/EllipseUtil.tsx
@@ -331,7 +331,7 @@ export class EllipseUtil extends TDShapeUtil {
transformSingle = (shape: T, bounds: TLBounds): Partial => {
return {
- point: Vec.round([bounds.minX, bounds.minY]),
+ point: Vec.toFixed([bounds.minX, bounds.minY]),
radius: Vec.div([bounds.width, bounds.height], 2),
}
}
diff --git a/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx b/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx
index 27a0d4029..1a699ca04 100644
--- a/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx
+++ b/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx
@@ -217,7 +217,7 @@ export class StickyUtil extends TDShapeUtil {
bounds: TLBounds,
{ scaleX, scaleY, transformOrigin }: TransformInfo
): Partial => {
- const point = Vec.round([
+ const point = Vec.toFixed([
bounds.minX +
(bounds.width - shape.size[0]) * (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
bounds.minY +
diff --git a/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx b/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx
index f761a6cd7..3fd2db865 100644
--- a/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx
+++ b/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx
@@ -285,7 +285,7 @@ export class TextUtil extends TDShapeUtil {
} = initialShape
return {
- point: Vec.round([bounds.minX, bounds.minY]),
+ point: Vec.toFixed([bounds.minX, bounds.minY]),
style: {
...initialShape.style,
scale: scale * Math.max(Math.abs(scaleY), Math.abs(scaleX)),
@@ -309,7 +309,7 @@ export class TextUtil extends TDShapeUtil {
...shape.style,
scale: 1,
},
- point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))),
+ point: Vec.toFixed(Vec.add(shape.point, Vec.sub(center, newCenter))),
}
}
diff --git a/packages/tldraw/src/state/shapes/shared/shape-styles.ts b/packages/tldraw/src/state/shapes/shared/shape-styles.ts
index b2d999455..0ed2e357f 100644
--- a/packages/tldraw/src/state/shapes/shared/shape-styles.ts
+++ b/packages/tldraw/src/state/shapes/shared/shape-styles.ts
@@ -141,7 +141,8 @@ export function getStickyShapeStyle(style: ShapeStyles, isDarkMode = false) {
const { color } = style
const theme: Theme = isDarkMode ? 'dark' : 'light'
- const adjustedColor = color === ColorStyle.Black ? ColorStyle.Yellow : color
+ const adjustedColor =
+ color === ColorStyle.White || color === ColorStyle.Black ? ColorStyle.Yellow : color
return {
fill: stickyFills[theme][adjustedColor],
diff --git a/packages/tldraw/src/state/shapes/shared/transformRectangle.ts b/packages/tldraw/src/state/shapes/shared/transformRectangle.ts
index a81c274bc..51fd7b052 100644
--- a/packages/tldraw/src/state/shapes/shared/transformRectangle.ts
+++ b/packages/tldraw/src/state/shapes/shared/transformRectangle.ts
@@ -13,9 +13,11 @@ export function transformRectangle(
{ initialShape, transformOrigin, scaleX, scaleY }: TLTransformInfo
) {
if (shape.rotation || initialShape.isAspectRatioLocked) {
- const size = Vec.round(Vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY))))
+ const size = Vec.toFixed(
+ Vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY)))
+ )
- const point = Vec.round([
+ const point = Vec.toFixed([
bounds.minX +
(bounds.width - shape.size[0]) * (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
bounds.minY +
@@ -37,8 +39,8 @@ export function transformRectangle(
}
} else {
return {
- point: Vec.round([bounds.minX, bounds.minY]),
- size: Vec.round([bounds.width, bounds.height]),
+ point: Vec.toFixed([bounds.minX, bounds.minY]),
+ size: Vec.toFixed([bounds.width, bounds.height]),
}
}
}
diff --git a/packages/tldraw/src/state/shapes/shared/transformSingleRectangle.ts b/packages/tldraw/src/state/shapes/shared/transformSingleRectangle.ts
index 9e064fea7..7e965cfa3 100644
--- a/packages/tldraw/src/state/shapes/shared/transformSingleRectangle.ts
+++ b/packages/tldraw/src/state/shapes/shared/transformSingleRectangle.ts
@@ -11,7 +11,7 @@ export function transformSingleRectangle
bounds: TLBounds
) {
return {
- size: Vec.round([bounds.width, bounds.height]),
- point: Vec.round([bounds.minX, bounds.minY]),
+ size: Vec.toFixed([bounds.width, bounds.height]),
+ point: Vec.toFixed([bounds.minX, bounds.minY]),
}
}
diff --git a/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.ts b/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.ts
index 5206a2185..a15f3c2b3 100644
--- a/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.ts
+++ b/packages/tldraw/src/state/tools/ArrowTool/ArrowTool.ts
@@ -1,4 +1,5 @@
import { Utils, TLPointerEventHandler } from '@tldraw/core'
+import Vec from '@tldraw/vec'
import { Arrow } from '~state/shapes'
import { SessionType, TDShapeType } from '~types'
import { BaseTool, Status } from '../BaseTool'
@@ -13,6 +14,8 @@ export class ArrowTool extends BaseTool {
const {
currentPoint,
+ currentGrid,
+ settings: { showGrid },
appState: { currentPageId, currentStyle },
} = this.app
@@ -24,7 +27,7 @@ export class ArrowTool extends BaseTool {
id,
parentId: currentPageId,
childIndex,
- point: currentPoint,
+ point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
style: { ...currentStyle },
})
diff --git a/packages/tldraw/src/state/tools/BaseTool.ts b/packages/tldraw/src/state/tools/BaseTool.ts
index f07e548d9..824153b7f 100644
--- a/packages/tldraw/src/state/tools/BaseTool.ts
+++ b/packages/tldraw/src/state/tools/BaseTool.ts
@@ -89,7 +89,6 @@ export abstract class BaseTool extends TDEventHandler {
return
}
- /* noop */
if (key === 'Meta' || key === 'Control' || key === 'Alt') {
this.app.updateSession()
return
@@ -97,7 +96,6 @@ export abstract class BaseTool extends TDEventHandler {
}
onKeyUp: TLKeyboardEventHandler = (key) => {
- /* noop */
if (key === 'Meta' || key === 'Control' || key === 'Alt') {
this.app.updateSession()
return
diff --git a/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.ts b/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.ts
index 2eb607372..bca039543 100644
--- a/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.ts
+++ b/packages/tldraw/src/state/tools/EllipseTool/EllipseTool.ts
@@ -1,4 +1,5 @@
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core'
+import Vec from '@tldraw/vec'
import { Ellipse } from '~state/shapes'
import { SessionType, TDShapeType } from '~types'
import { BaseTool, Status } from '../BaseTool'
@@ -13,6 +14,8 @@ export class EllipseTool extends BaseTool {
const {
currentPoint,
+ currentGrid,
+ settings: { showGrid },
appState: { currentPageId, currentStyle },
} = this.app
@@ -24,7 +27,7 @@ export class EllipseTool extends BaseTool {
id,
parentId: currentPageId,
childIndex,
- point: currentPoint,
+ point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
style: { ...currentStyle },
})
diff --git a/packages/tldraw/src/state/tools/LineTool/LineTool.ts b/packages/tldraw/src/state/tools/LineTool/LineTool.ts
index 7c0156223..2a74b3399 100644
--- a/packages/tldraw/src/state/tools/LineTool/LineTool.ts
+++ b/packages/tldraw/src/state/tools/LineTool/LineTool.ts
@@ -1,4 +1,5 @@
import { Utils, TLPointerEventHandler } from '@tldraw/core'
+import Vec from '@tldraw/vec'
import { Arrow } from '~state/shapes'
import { SessionType, TDShapeType } from '~types'
import { BaseTool, Status } from '../BaseTool'
@@ -13,6 +14,8 @@ export class LineTool extends BaseTool {
const {
currentPoint,
+ currentGrid,
+ settings: { showGrid },
appState: { currentPageId, currentStyle },
} = this.app
@@ -24,7 +27,7 @@ export class LineTool extends BaseTool {
id,
parentId: currentPageId,
childIndex,
- point: currentPoint,
+ point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
decorations: {
start: undefined,
end: undefined,
diff --git a/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.ts b/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.ts
index 8789e3f5a..a28d07eb4 100644
--- a/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.ts
+++ b/packages/tldraw/src/state/tools/RectangleTool/RectangleTool.ts
@@ -1,4 +1,5 @@
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core'
+import Vec from '@tldraw/vec'
import { Rectangle } from '~state/shapes'
import { SessionType, TDShapeType } from '~types'
import { BaseTool, Status } from '../BaseTool'
@@ -13,6 +14,8 @@ export class RectangleTool extends BaseTool {
const {
currentPoint,
+ currentGrid,
+ settings: { showGrid },
appState: { currentPageId, currentStyle },
} = this.app
@@ -24,7 +27,7 @@ export class RectangleTool extends BaseTool {
id,
parentId: currentPageId,
childIndex,
- point: currentPoint,
+ point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
style: { ...currentStyle },
})
diff --git a/packages/tldraw/src/state/tools/SelectTool/SelectTool.spec.ts b/packages/tldraw/src/state/tools/SelectTool/SelectTool.spec.ts
index f54239430..1deb67972 100644
--- a/packages/tldraw/src/state/tools/SelectTool/SelectTool.spec.ts
+++ b/packages/tldraw/src/state/tools/SelectTool/SelectTool.spec.ts
@@ -65,9 +65,14 @@ describe('When double clicking link controls', () => {
const app = new TldrawTestApp()
.loadDocument(doc)
.select('rect2')
- .pointBoundsHandle('center', { x: 0, y: 0 })
+ .pointBoundsHandle('center', [100, 100])
+ .expectShapesToBeAtPoints({
+ rect1: [0, 0],
+ rect2: [100, 0],
+ rect3: [200, 0],
+ })
- app.movePointer({ x: 100, y: 100 }).expectShapesToBeAtPoints({
+ app.movePointer([200, 200]).expectShapesToBeAtPoints({
rect1: [100, 100],
rect2: [200, 100],
rect3: [300, 100],
diff --git a/packages/tldraw/src/state/tools/StickyTool/StickyTool.ts b/packages/tldraw/src/state/tools/StickyTool/StickyTool.ts
index b892e1ea2..ac6cd9490 100644
--- a/packages/tldraw/src/state/tools/StickyTool/StickyTool.ts
+++ b/packages/tldraw/src/state/tools/StickyTool/StickyTool.ts
@@ -26,6 +26,8 @@ export class StickyTool extends BaseTool {
if (this.status === Status.Idle) {
const {
currentPoint,
+ currentGrid,
+ settings: { showGrid },
appState: { currentPageId, currentStyle },
} = this.app
@@ -39,7 +41,7 @@ export class StickyTool extends BaseTool {
id,
parentId: currentPageId,
childIndex,
- point: currentPoint,
+ point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
style: { ...currentStyle },
})
diff --git a/packages/tldraw/src/state/tools/TextTool/TextTool.ts b/packages/tldraw/src/state/tools/TextTool/TextTool.ts
index ef00a830f..30a0a300b 100644
--- a/packages/tldraw/src/state/tools/TextTool/TextTool.ts
+++ b/packages/tldraw/src/state/tools/TextTool/TextTool.ts
@@ -1,4 +1,5 @@
import type { TLPointerEventHandler, TLKeyboardEventHandler } from '@tldraw/core'
+import Vec from '@tldraw/vec'
import { TDShapeType } from '~types'
import { BaseTool, Status } from '../BaseTool'
@@ -32,8 +33,13 @@ export class TextTool extends BaseTool {
}
if (this.status === Status.Idle) {
- const { currentPoint } = this.app
- this.app.createTextShapeAtPoint(currentPoint)
+ const {
+ currentPoint,
+ currentGrid,
+ settings: { showGrid },
+ } = this.app
+
+ this.app.createTextShapeAtPoint(showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint)
this.setStatus(Status.Creating)
return
}
diff --git a/packages/tldraw/src/test/TldrawTestApp.tsx b/packages/tldraw/src/test/TldrawTestApp.tsx
index 1c36c03a9..3023cb104 100644
--- a/packages/tldraw/src/test/TldrawTestApp.tsx
+++ b/packages/tldraw/src/test/TldrawTestApp.tsx
@@ -162,4 +162,16 @@ export class TldrawTestApp extends TldrawApp {
})
return this
}
+
+ pressKey = (key: string) => {
+ const e = { key } as KeyboardEvent
+ this.onKeyDown(key, inputs.keydown(e), e)
+ return this
+ }
+
+ releaseKey = (key: string) => {
+ const e = { key } as KeyboardEvent
+ this.onKeyUp(key, inputs.keyup(e), e)
+ return this
+ }
}
diff --git a/packages/tldraw/src/types.ts b/packages/tldraw/src/types.ts
index 46d8a31bc..2e5005e0d 100644
--- a/packages/tldraw/src/types.ts
+++ b/packages/tldraw/src/types.ts
@@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-types */
-import type { TLPage, TLUser, TLPageState } from '@tldraw/core'
-import type { FileSystemHandle } from '~state/data/browser-fs-access'
import type {
+ TLPage,
+ TLUser,
+ TLPageState,
TLBinding,
TLBoundsCorner,
TLBoundsEdge,
@@ -89,6 +90,7 @@ export interface TDSnapshot {
showRotateHandles: boolean
showBindingHandles: boolean
showCloneHandles: boolean
+ showGrid: boolean
}
appState: {
currentStyle: ShapeStyles
@@ -470,3 +472,29 @@ export interface Command {
before: Patch
after: Patch
}
+
+export interface FileWithHandle extends File {
+ handle?: FileSystemHandle
+}
+
+export interface FileWithDirectoryHandle extends File {
+ directoryHandle?: FileSystemHandle
+}
+
+// The following typings implement the relevant parts of the File System Access
+// API. This can be removed once the specification reaches the Candidate phase
+// and is implemented as part of microsoft/TSJS-lib-generator.
+
+export interface FileSystemHandlePermissionDescriptor {
+ mode?: 'read' | 'readwrite'
+}
+
+export interface FileSystemHandle {
+ readonly kind: 'file' | 'directory'
+ readonly name: string
+
+ isSameEntry: (other: FileSystemHandle) => Promise
+
+ queryPermission: (descriptor?: FileSystemHandlePermissionDescriptor) => Promise
+ requestPermission: (descriptor?: FileSystemHandlePermissionDescriptor) => Promise
+}
diff --git a/packages/tldraw/tsconfig.build.json b/packages/tldraw/tsconfig.build.json
index 9998c8e39..9cb27be3e 100644
--- a/packages/tldraw/tsconfig.build.json
+++ b/packages/tldraw/tsconfig.build.json
@@ -11,12 +11,11 @@
"docs"
],
"compilerOptions": {
- "rootDir": "src",
- "baseUrl": "src",
"composite": false,
"incremental": false,
- "declarationMap": false,
- "sourceMap": false,
- "emitDeclarationOnly": true
- }
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "references": [{ "path": "../vec" }, { "path": "../intersect" }, { "path": "../core" }]
}
diff --git a/packages/tldraw/tsconfig.json b/packages/tldraw/tsconfig.json
index c8a7d4c89..cf85870e7 100644
--- a/packages/tldraw/tsconfig.json
+++ b/packages/tldraw/tsconfig.json
@@ -1,17 +1,16 @@
{
"extends": "../../tsconfig.base.json",
- "include": ["src", "src/test/*.json"],
"exclude": ["node_modules", "dist", "docs"],
+ "include": ["src"],
"compilerOptions": {
- "resolveJsonModule": true,
"outDir": "./dist/types",
"rootDir": "src",
- "baseUrl": "src",
+ "baseUrl": ".",
"paths": {
- "~*": ["./*"],
- "@tldraw/core": ["../core"]
+ "~*": ["./src/*"]
}
},
+ "references": [{ "path": "../vec" }, { "path": "../intersect" }, { "path": "../core" }],
"typedocOptions": {
"entryPoints": ["src/index.ts"],
"out": "docs"
diff --git a/packages/vec/CHANGELOG.md b/packages/vec/CHANGELOG.md
new file mode 100644
index 000000000..e03d35f1c
--- /dev/null
+++ b/packages/vec/CHANGELOG.md
@@ -0,0 +1,86 @@
+# Changelog
+
+## 0.1.21
+
+New:
+
+- Adds the `isGhost` prop to `TLShape`. In `TLComponentProps`, the `isGhost` prop will be true if either a shape has its `isGhost` set to `true` OR if a shape is the descendant of a shape with `isGhost` set to `true`. A ghost shape will have the `tl-ghost` class name, though this is not used in the Renderer. You can set it yourself in your app.
+- Adds the `isChildOfSelected` prop for `TLComponentProps`. If a shape is the child of a selected shape, its `isChildOfSelected` prop will be true.
+
+Improved:
+
+- Fixes a bug that could occur with the order of grouped shapes.
+- Adds an Eraser tool to the advanced example.
+- Adds a Pencil tool to the advanced example.
+
+## 0.1.20
+
+- Update docs.
+- Adds `hideResizeHandles` prop.
+
+## 0.1.19
+
+- Remove stray `index.js` files.
+
+## 0.1.18
+
+- Even more dependency fixes.
+
+## 0.1.17
+
+- More dependency fixes.
+
+## 0.1.16
+
+- Fix dependencies, remove `@use-gesture/react` from bundle.
+
+## 0.1.15
+
+- Fix README.
+
+## 0.1.14
+
+- Add README to package.
+
+## 0.1.13
+
+- Remove `type` from `TLBinding`.
+
+## 0.1.12
+
+- Fix bug with initial bounds.
+
+## 0.1.12
+
+- Fix bug with initial bounds.
+
+## 0.1.12
+
+- Fix bug with bounds handle events.
+
+## 0.1.11
+
+- Fix bug with initial camera state.
+
+## 0.1.10
+
+- Improve example.
+- Improve types for `TLPage`.
+
+## 0.1.9
+
+- Bug fixes.
+
+## 0.1.8
+
+- Expands README.
+- Removes properties specific to the tldraw app.
+
+## 0.1.7
+
+- Fixes selection bug with SVGContainer.
+- Removes various properties specific to the tldraw app.
+
+## 0.1.0
+
+- Re-writes API for ShapeUtils.
diff --git a/packages/vec/LICENSE.md b/packages/vec/LICENSE.md
new file mode 100644
index 000000000..bdcc8b850
--- /dev/null
+++ b/packages/vec/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Stephen Ruiz Ltd
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/vec/README.md b/packages/vec/README.md
new file mode 100644
index 000000000..30cab634d
--- /dev/null
+++ b/packages/vec/README.md
@@ -0,0 +1,477 @@
+
+
+
+
+# @tldraw/core
+
+This package contains the `Renderer` and core utilities used by [tldraw](https://tldraw.com).
+
+You can use this package to build projects like [tldraw](https://tldraw.com), where React components are rendered on a canvas user interface. Check out the [advanced example](https://core-steveruiz.vercel.app/).
+
+💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
+
+## Installation
+
+Use your package manager of choice to install `@tldraw/core` and its peer dependencies.
+
+```bash
+yarn add @tldraw/core
+# or
+npm i @tldraw/core
+```
+
+## Examples
+
+There are two examples in this repository.
+
+The **simple** example in the `example` folder shows a minimal use of the library. It does not do much but this should be a good reference for the API without too much else built on top.
+
+The **advanced** example in the `example-advanced` folder shows a more realistic use of the library. (Try it [here](https://core-steveruiz.vercel.app/)). While the fundamental patterns are the same, this example contains features such as: panning, pinching, and zooming the camera; creating, cloning, resizing, and deleting shapes; keyboard shortcuts, brush-selection; shape-snapping; undo, redo; and more. Much of the code in the advanced example comes from the [@tldraw/tldraw](https://tldraw.com) codebase.
+
+If you're working on an app that uses this library, I recommend referring back to the advanced example for tips on how you might implement these features for your own project.
+
+## Usage
+
+Import the `Renderer` React component and pass it the required props.
+
+```tsx
+import * as React from "react"
+import { Renderer, TLShape, TLShapeUtil, Vec } from '@tldraw/core'
+import { BoxShape, BoxUtil } from "./shapes/box"
+
+const shapeUtils = { box: new BoxUtil() }
+
+function App() {
+ const [page, setPage] = React.useState({
+ id: "page"
+ shapes: {
+ "box1": {
+ id: 'box1',
+ type: 'box',
+ parentId: 'page',
+ childIndex: 0,
+ point: [0, 0],
+ size: [100, 100],
+ rotation: 0,
+ }
+ },
+ bindings: {}
+ })
+
+ const [pageState, setPageState] = React.useState({
+ id: "page",
+ selectedIds: [],
+ camera: {
+ point: [0,0],
+ zoom: 1
+ }
+ })
+
+ return ()
+}
+```
+
+## Documentation
+
+### `Renderer`
+
+To avoid unnecessary renders, be sure to pass "stable" values as props to the `Renderer`. Either define these values outside of the parent component, or place them in React state, or memoize them with `React.useMemo`.
+
+| Prop | Type | Description |
+| ------------ | ------------------------------- | ---------------------------------------------- |
+| `page` | [`TLPage`](#tlpage) | The current page object. |
+| `pageState` | [`TLPageState`](#tlpagestate) | The current page's state. |
+| `shapeUtils` | [`TLShapeUtils`](#tlshapeutils) | The shape utilities used to render the shapes. |
+
+In addition to these required props, the Renderer accents many other **optional** props.
+
+| Property | Type | Description |
+| -------------------- | ----------------------------- | ----------------------------------------------------------------- |
+| `containerRef` | `React.MutableRefObject` | A React ref for the container, where CSS variables will be added. |
+| `theme` | `object` | An object with overrides for the Renderer's default colors. |
+| `hideBounds` | `boolean` | Do not show the bounding box for selected shapes. |
+| `hideHandles` | `boolean` | Do not show handles for shapes with handles. |
+| `hideBindingHandles` | `boolean` | Do not show binding controls for selected shapes with bindings. |
+| `hideResizeHandles` | `boolean` | Do not show resize handles for selected shapes. |
+| `hideRotateHandles` | `boolean` | Do not show rotate handles for selected shapes. |
+| `snapLines` | [`TLSnapLine`](#tlsnapline)[] | An array of "snap" lines. |
+| `users` | `object` | A table of [`TLUser`](#tluser)s. |
+| `userId` | `object` | The current user's [`TLUser`](#tluser) id. |
+
+The theme object accepts valid CSS colors for the following properties:
+
+| Property | Description |
+| -------------- | ---------------------------------------------------- |
+| `foreground` | The primary (usually "text") color |
+| `background` | The default page's background color |
+| `brushFill` | The fill color of the brush selection box |
+| `brushStroke` | The stroke color of the brush selection box |
+| `selectFill` | The fill color of the selection bounds |
+| `selectStroke` | The stroke color of the selection bounds and handles |
+
+The Renderer also accepts many (optional) event callbacks.
+
+| Prop | Description |
+| --------------------------- | ----------------------------------------------------------- |
+| `onPan` | Panned with the mouse wheel |
+| `onZoom` | Zoomed with the mouse wheel |
+| `onPinchStart` | Began a two-pointer pinch |
+| `onPinch` | Moved their pointers during a pinch |
+| `onPinchEnd` | Stopped a two-pointer pinch |
+| `onPointerDown` | Started pointing |
+| `onPointerMove` | Moved their pointer |
+| `onPointerUp` | Ended a point |
+| `onPointCanvas` | Pointed the canvas |
+| `onDoubleClickCanvas` | Double-pointed the canvas |
+| `onRightPointCanvas` | Right-pointed the canvas |
+| `onDragCanvas` | Dragged the canvas |
+| `onReleaseCanvas` | Stopped pointing the canvas |
+| `onHoverShape` | Moved their pointer onto a shape |
+| `onUnhoverShape` | Moved their pointer off of a shape |
+| `onPointShape` | Pointed a shape |
+| `onDoubleClickShape` | Double-pointed a shape |
+| `onRightPointShape` | Right-pointed a shape |
+| `onDragShape` | Dragged a shape |
+| `onReleaseShape` | Stopped pointing a shape |
+| `onHoverHandle` | Moved their pointer onto a shape handle |
+| `onUnhoverHandle` | Moved their pointer off of a shape handle |
+| `onPointHandle` | Pointed a shape handle |
+| `onDoubleClickHandle` | Double-pointed a shape handle |
+| `onRightPointHandle` | Right-pointed a shape handle |
+| `onDragHandle` | Dragged a shape handle |
+| `onReleaseHandle` | Stopped pointing shape handle |
+| `onHoverBounds` | Moved their pointer onto the selection bounds |
+| `onUnhoverBounds` | Moved their pointer off of the selection bounds |
+| `onPointBounds` | Pointed the selection bounds |
+| `onDoubleClickBounds` | Double-pointed the selection bounds |
+| `onRightPointBounds` | Right-pointed the selection bounds |
+| `onDragBounds` | Dragged the selection bounds |
+| `onReleaseBounds` | Stopped the selection bounds |
+| `onHoverBoundsHandle` | Moved their pointer onto a selection bounds handle |
+| `onUnhoverBoundsHandle` | Moved their pointer off of a selection bounds handle |
+| `onPointBoundsHandle` | Pointed a selection bounds handle |
+| `onDoubleClickBoundsHandle` | Double-pointed a selection bounds handle |
+| `onRightPointBoundsHandle` | Right-pointed a selection bounds handle |
+| `onDragBoundsHandle` | Dragged a selection bounds handle |
+| `onReleaseBoundsHandle` | Stopped a selection bounds handle |
+| `onShapeClone` | Clicked on a shape's clone handle |
+| `onShapeChange` | A shape's component prompted a change |
+| `onShapeBlur` | A shape's component was prompted a blur |
+| `onRenderCountChange` | The number of rendered shapes changed |
+| `onBoundsChange` | The Renderer's screen bounding box of the component changed |
+| `onError` | The Renderer encountered an error |
+
+The `@tldraw/core` library provides types for most of the event handlers:
+
+| Type |
+| ---------------------------- |
+| `TLPinchEventHandler` |
+| `TLPointerEventHandler` |
+| `TLCanvasEventHandler` |
+| `TLBoundsEventHandler` |
+| `TLBoundsHandleEventHandler` |
+| `TLShapeChangeHandler` |
+| `TLShapeBlurHandler` |
+| `TLShapeCloneHandler` |
+
+### `TLPage`
+
+An object describing the current page. It contains:
+
+| Property | Type | Description |
+| ----------------- | --------------------------- | --------------------------------------------------------------------------- |
+| `id` | `string` | A unique id for the page. |
+| `shapes` | [`TLShape{}`](#tlshape) | A table of shapes. |
+| `bindings` | [`TLBinding{}`](#tlbinding) | A table of bindings. |
+| `backgroundColor` | `string` | (optional) The page's background fill color. Will also overwrite the theme. |
+
+### `TLPageState`
+
+An object describing the current page. It contains:
+
+| Property | Type | Description |
+| -------------- | ---------- | --------------------------------------------------- |
+| `id` | `string` | The corresponding page's id |
+| `selectedIds` | `string[]` | An array of selected shape ids |
+| `camera` | `object` | An object describing the camera state |
+| `camera.point` | `number[]` | The camera's `[x, y]` coordinates |
+| `camera.zoom` | `number` | The camera's zoom level |
+| `pointedId` | `string` | (optional) The currently pointed shape id |
+| `hoveredId` | `string` | (optional) The currently hovered shape id |
+| `editingId` | `string` | (optional) The currently editing shape id |
+| `bindingId` | `string` | (optional) The currently editing binding. |
+| `brush` | `TLBounds` | (optional) A `Bounds` for the current selection box |
+
+### `TLShape`
+
+An object that describes a shape on the page. The shapes in your document should extend this interface with other properties. See [Shape Type](#shape-type).
+
+| Property | Type | Description |
+| --------------------- | ---------- | ------------------------------------------------------------------------------------- |
+| `id` | `string` | The shape's id. |
+| `type` | `string` | The type of the shape, corresponding to the `type` of a [`TLShapeUtil`](#tlshapeutil) |
+| `parentId` | `string` | The id of the shape's parent (either the current page or another shape) |
+| `childIndex` | `number` | the order of the shape among its parent's children |
+| `name` | `string` | the name of the shape |
+| `point` | `number[]` | the shape's current `[x, y]` coordinates on the page |
+| `rotation` | `number` | (optiona) The shape's current rotation in radians |
+| `children` | `string[]` | (optional) An array containing the ids of this shape's children |
+| `handles` | `{}` | (optional) A table of [`TLHandle`](#tlhandle) objects |
+| `isGhost` | `boolean` | (optional) True if the shape is "ghosted", e.g. while deleting |
+| `isLocked` | `boolean` | (optional) True if the shape is locked |
+| `isHidden` | `boolean` | (optional) True if the shape is hidden |
+| `isEditing` | `boolean` | (optional) True if the shape is currently editing |
+| `isGenerated` | `boolean` | optional) True if the shape is generated programatically |
+| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked |
+
+### `TLHandle`
+
+An object that describes a relationship between two shapes on the page.
+
+| Property | Type | Description |
+| -------- | ---------- | --------------------------------------------- |
+| `id` | `string` | An id for the handle |
+| `index` | `number` | The handle's order within the shape's handles |
+| `point` | `number[]` | The handle's `[x, y]` coordinates |
+
+When a shape with handles is the only selected shape, the `Renderer` will display its handles. You can respond to interactions with these handles using the `on
+
+### `TLBinding`
+
+An object that describes a relationship between two shapes on the page.
+
+| Property | Type | Description |
+| -------- | -------- | -------------------------------------------- |
+| `id` | `string` | A unique id for the binding |
+| `fromId` | `string` | The id of the shape where the binding begins |
+| `toId` | `string` | The id of the shape where the binding begins |
+
+### `TLSnapLine`
+
+A snapline is an array of points (formatted as `[x, y]`) that represent a "snapping" line.
+
+### `TLShapeUtil`
+
+The `TLShapeUtil` is an abstract class that you can extend to create utilities for your custom shapes. See the [Creating Shapes](#creating-shapes) guide to learn more.
+
+### `TLUser`
+
+A `TLUser` is the presence information for a multiplayer user. The user's pointer location and selections will be shown on the canvas. If the `TLUser`'s id matches the `Renderer`'s `userId` prop, then the user's cursor and selections will not be shown.
+
+| Property | Type | Description |
+| --------------- | ---------- | --------------------------------------- |
+| `id` | `string` | A unique id for the user |
+| `color` | `string` | The user's color, used for indicators |
+| `point` | `number[]` | The user's pointer location on the page |
+| `selectedIds[]` | `string[]` | The user's selected shape ids |
+
+### `Utils`
+
+A general purpose utility class. See source for more.
+
+## Guide: Creating Shapes
+
+The `Renderer` component has no built-in shapes. It's up to you to define every shape that you want to see on the canvas. While these shapes are highly reusable between projects, you'll need to define them using the API described below.
+
+> For several example shapes, see the folder `/example/src/shapes/`.
+
+### Shape Type
+
+Your first task is to define an interface for the shape that extends `TLShape`. It must have a `type` property.
+
+```ts
+// BoxShape.ts
+import type { TLShape } from '@tldraw/core'
+
+export interface BoxShape extends TLShape {
+ type: 'box'
+ size: number[]
+}
+```
+
+### Component
+
+Next, use `TLShapeUtil.Component` to create a second component for your shape's `Component`. The `Renderer` will use this component to display the shape on the canvas.
+
+```tsx
+// BoxComponent.ts
+
+import * as React from 'react'
+import { shapeComponent, SVGContainer } from '@tldraw/core'
+import type { BoxShape } from './BoxShape'
+
+export const BoxComponent = TLShapeUtil.Component(
+ ({ shape, events, meta }, ref) => {
+ const color = meta.isDarkMode ? 'white' : 'black'
+
+ return (
+
+
+
+ )
+ }
+)
+```
+
+Your component can return HTML elements or SVG elements. If your shape is returning only SVG elements, wrap it in an `SVGContainer`. If your shape returns HTML elements, wrap it in an `HTMLContainer`. Not that you must set `pointerEvents` manually on the shapes you wish to receive pointer events.
+
+The component will receive the following props:
+
+| Name | Type | Description |
+| ------------------- | ---------- | ------------------------------------------------------------------ |
+| `shape` | `TLShape` | The shape from `page.shapes` that is being rendered |
+| `meta` | `{}` | The value provided to the `Renderer`'s `meta` prop |
+| `events` | `{}` | Several pointer events that should be set on the container element |
+| `isSelected` | `boolean` | The shape is selected (its `id` is in `pageState.selectedIds`) |
+| `isHovered` | `boolean` | The shape is hovered (its `id` is `pageState.hoveredId`) |
+| `isEditing` | `boolean` | The shape is being edited (its `id` is `pageState.editingId`) |
+| `isGhost` | `boolean` | The shape is ghosted or is the child of a ghosted shape. |
+| `isChildOfSelected` | `boolean` | The shape is the child of a selected shape. |
+| `onShapeChange` | `Function` | The callback provided to the `Renderer`'s `onShapeChange` prop |
+| `onShapeBlur` | `Function` | The callback provided to the `Renderer`'s `onShapeBlur` prop |
+
+### Indicator
+
+Next, use `TLShapeUtil.Indicator` to create a second component for your shape's `Indicator`. This component is shown when the shape is hovered or selected. Your `Indicator` must return SVG elements only.
+
+```tsx
+// BoxIndicator.ts
+
+export const BoxIndicator = TLShapeUtil.Indicator(({ shape }) => {
+ return (
+
+ )
+})
+```
+
+The indicator component will receive the following props:
+
+| Name | Type | Description |
+| ------------ | --------- | -------------------------------------------------------------------------------------- |
+| `shape` | `TLShape` | The shape from `page.shapes` that is being rendered |
+| `meta` | {} | The value provided to the `Renderer`'s `meta` prop |
+| `user` | `TLUser` | The user when shown in a multiplayer session |
+| `isSelected` | `boolean` | Whether the current shape is selected (true if its `id` is in `pageState.selectedIds`) |
+| `isHovered` | `boolean` | Whether the current shape is hovered (true if its `id` is `pageState.hoveredId`) |
+
+### ShapeUtil
+
+Next, create a "shape util" for your shape. This is a class that extends `TLShapeUtil`. The `Renderer` will use an instance of this class to answer questions about the shape: what it should look like, where it is on screen, whether it can rotate, etc.
+
+```ts
+// BoxUtil.ts
+
+import { Utils, TLBounds, TLShapeUtil } from '@tldraw/core'
+import { BoxComponent } from './BoxComponent'
+import { BoxIndicator } from './BoxIndicator'
+import type { BoxShape } from './BoxShape'
+
+export class BoxUtil extends TLShapeUtil {
+ Component = BoxComponent
+
+ Indicator = BoxIndicator
+
+ getBounds = (shape: BoxShape): TLBounds => {
+ const [width, height] = shape.size
+
+ const bounds = {
+ minX: 0,
+ maxX: width,
+ minY: 0,
+ maxY: height,
+ width,
+ height,
+ }
+
+ return Utils.translateBounds(bounds, shape.point)
+ }
+}
+```
+
+Set the `Component` field to your component and the `Indicator` field to your indicator component. Then define the `getBounds` method. This method will receive a shape and should return a `TLBounds` object.
+
+You may also set the following fields:
+
+| Name | Type | Default | Description |
+| ------------------ | --------- | ------- | ----------------------------------------------------------------------------------------------------- |
+| `showCloneHandles` | `boolean` | `false` | Whether to display clone handles when the shape is the only selected shape |
+| `hideBounds` | `boolean` | `false` | Whether to hide the bounds when the shape is the only selected shape |
+| `isStateful` | `boolean` | `false` | Whether the shape has its own React state. When true, the shape will not be unmounted when off-screen |
+
+### ShapeUtils Object
+
+Finally, create a mapping of your project's shape utils and the `type` properties of their corresponding shapes. Pass this object to the `Renderer`'s `shapeUtils` prop.
+
+```tsx
+// App.tsx
+
+const shapeUtils = {
+ box: new BoxUtil(),
+ circle: new CircleUtil(),
+ text: new TextUtil(),
+}
+
+export function App() {
+ // ...
+
+ return
+}
+```
+
+## Local Development
+
+To start the development servers for the package and the advanced example:
+
+- Run `yarn` to install dependencies.
+- Run `yarn start`.
+- Open `localhost:5420`.
+
+You can also run:
+
+- `start:advanced` to start development servers for the package and the advanced example.
+- `start:simple` to start development servers for the package and the simple example.
+- `test` to execute unit tests via [Jest](https://jestjs.io).
+- `docs` to build the docs via [ts-doc](https://typedoc.org/).
+- `build` to build the package.
+
+## Example
+
+See the `example` folder or this [CodeSandbox](https://codesandbox.io/s/laughing-elion-gp0kx) example.
+
+## Community
+
+### Support
+
+Need help? Please [open an issue](https://github.com/tldraw/tldraw/issues/new) for support.
+
+### Discussion
+
+Want to connect with other devs? Visit the [Discord channel](https://discord.gg/SBBEVCA4PG).
+
+### License
+
+This project is licensed under MIT.
+
+If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
+
+## Author
+
+- [@steveruizok](https://twitter.com/steveruizok)
diff --git a/packages/vec/card-repo.png b/packages/vec/card-repo.png
new file mode 100644
index 000000000..2b3997cbd
Binary files /dev/null and b/packages/vec/card-repo.png differ
diff --git a/packages/vec/package.json b/packages/vec/package.json
new file mode 100644
index 000000000..459f3ec94
--- /dev/null
+++ b/packages/vec/package.json
@@ -0,0 +1,35 @@
+{
+ "version": "1.1.4",
+ "name": "@tldraw/vec",
+ "description": "2D vector utilities for TLDraw and maybe you, too.",
+ "author": "@steveruizok",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/tldraw/tldraw.git"
+ },
+ "license": "MIT",
+ "keywords": [
+ "2d",
+ "vector",
+ "typescript",
+ "javascript"
+ ],
+ "files": [
+ "dist/**/*"
+ ],
+ "main": "./dist/cjs/index.js",
+ "module": "./dist/esm/index.js",
+ "types": "./dist/types/index.d.ts",
+ "scripts": {
+ "start:packages": "yarn start",
+ "start:core": "yarn start",
+ "start": "node scripts/dev & yarn types:dev",
+ "build:core": "yarn build",
+ "build:packages": "yarn build",
+ "build": "node scripts/build && yarn types:build",
+ "types:dev": "tsc -w --p tsconfig.build.json",
+ "types:build": "tsc -p tsconfig.build.json",
+ "lint": "eslint src/ --ext .ts,.tsx",
+ "clean": "rm -rf dist"
+ }
+}
\ 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..57466c523
--- /dev/null
+++ b/packages/vec/scripts/build.js
@@ -0,0 +1,61 @@
+/* eslint-disable */
+const fs = require('fs')
+const esbuild = require('esbuild')
+const { gzip } = require('zlib')
+const pkg = require('../package.json')
+
+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: false,
+ bundle: true,
+ format: 'cjs',
+ target: 'es6',
+ tsconfig: './tsconfig.build.json',
+ metafile: false,
+ sourcemap: true,
+ })
+
+ const esmResult = esbuild.buildSync({
+ entryPoints: ['./src/index.ts'],
+ outdir: 'dist/esm',
+ minify: false,
+ bundle: true,
+ format: 'esm',
+ target: 'es6',
+ tsconfig: './tsconfig.build.json',
+ metafile: true,
+ sourcemap: 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(
+ `✔ ${pkg.name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${(
+ result.length / 1000
+ ).toFixed(2)}kb minified)`
+ )
+ })
+ })
+ } catch (e) {
+ console.log(`× ${pkg.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..44bcf930f
--- /dev/null
+++ b/packages/vec/scripts/dev.js
@@ -0,0 +1,29 @@
+/* eslint-disable */
+const esbuild = require('esbuild')
+const pkg = require('../package.json')
+
+async function main() {
+ try {
+ await esbuild.build({
+ entryPoints: ['src/index.tsx'],
+ outfile: 'dist/index.js',
+ bundle: true,
+ minify: false,
+ sourcemap: true,
+ incremental: true,
+ target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
+ define: {
+ 'process.env.NODE_ENV': '"development"',
+ },
+ watch: {
+ onRebuild(err) {
+ err ? error('❌ Failed') : log('✅ Updated')
+ },
+ },
+ })
+ } catch (err) {
+ process.exit(1)
+ }
+}
+
+main()
diff --git a/packages/vec/src/index.d.ts b/packages/vec/src/index.d.ts
new file mode 100644
index 000000000..fa0f2f1eb
--- /dev/null
+++ b/packages/vec/src/index.d.ts
@@ -0,0 +1,311 @@
+export declare class Vec {
+ /**
+ * Clamp a value into a range.
+ * @param n
+ * @param min
+ */
+ static clamp(n: number, min: number): number;
+ static clamp(n: number, min: number, max: number): number;
+ /**
+ * Clamp a value into a range.
+ * @param n
+ * @param min
+ */
+ static clampV(A: number[], min: number): number[];
+ static clampV(A: number[], min: number, max: number): number[];
+ /**
+ * Negate a vector.
+ * @param A
+ */
+ static neg: (A: number[]) => number[];
+ /**
+ * Add vectors.
+ * @param A
+ * @param B
+ */
+ static add: (A: number[], B: number[]) => number[];
+ /**
+ * Add scalar to vector.
+ * @param A
+ * @param B
+ */
+ static addScalar: (A: number[], n: number) => number[];
+ /**
+ * Subtract vectors.
+ * @param A
+ * @param B
+ */
+ static sub: (A: number[], B: number[]) => number[];
+ /**
+ * Subtract scalar from vector.
+ * @param A
+ * @param B
+ */
+ static subScalar: (A: number[], n: number) => number[];
+ /**
+ * Get the vector from vectors A to B.
+ * @param A
+ * @param B
+ */
+ static vec: (A: number[], B: number[]) => number[];
+ /**
+ * Vector multiplication by scalar
+ * @param A
+ * @param n
+ */
+ static mul: (A: number[], n: number) => number[];
+ /**
+ * Multiple two vectors.
+ * @param A
+ * @param B
+ */
+ static mulV: (A: number[], B: number[]) => number[];
+ /**
+ * Vector division by scalar.
+ * @param A
+ * @param n
+ */
+ static div: (A: number[], n: number) => number[];
+ /**
+ * Vector division by vector.
+ * @param A
+ * @param n
+ */
+ static divV: (A: number[], B: number[]) => number[];
+ /**
+ * Perpendicular rotation of a vector A
+ * @param A
+ */
+ static per: (A: number[]) => number[];
+ /**
+ * Dot product
+ * @param A
+ * @param B
+ */
+ static dpr: (A: number[], B: number[]) => number;
+ /**
+ * Cross product (outer product) | A X B |
+ * @param A
+ * @param B
+ */
+ static cpr: (A: number[], B: number[]) => number;
+ /**
+ * Cross (for point in polygon)
+ *
+ */
+ static cross(x: number[], y: number[], z: number[]): number;
+ /**
+ * Length of the vector squared
+ * @param A
+ */
+ static len2: (A: number[]) => number;
+ /**
+ * Length of the vector
+ * @param A
+ */
+ static len: (A: number[]) => number;
+ /**
+ * Project A over B
+ * @param A
+ * @param B
+ */
+ static pry: (A: number[], B: number[]) => number;
+ /**
+ * Get normalized / unit vector.
+ * @param A
+ */
+ static uni: (A: number[]) => number[];
+ /**
+ * Get normalized / unit vector.
+ * @param A
+ */
+ static normalize: (A: number[]) => number[];
+ /**
+ * Get the tangent between two vectors.
+ * @param A
+ * @param B
+ * @returns
+ */
+ static tangent: (A: number[], B: number[]) => number[];
+ /**
+ * Dist length from A to B squared.
+ * @param A
+ * @param B
+ */
+ static dist2: (A: number[], B: number[]) => number;
+ /**
+ * Dist length from A to B
+ * @param A
+ * @param B
+ */
+ static dist: (A: number[], B: number[]) => number;
+ /**
+ * A faster, though less accurate method for testing distances. Maybe faster?
+ * @param A
+ * @param B
+ * @returns
+ */
+ static fastDist: (A: number[], B: number[]) => number[];
+ /**
+ * Angle between vector A and vector B in radians
+ * @param A
+ * @param B
+ */
+ static ang: (A: number[], B: number[]) => number;
+ /**
+ * Angle between vector A and vector B in radians
+ * @param A
+ * @param B
+ */
+ static angle: (A: number[], B: number[]) => number;
+ /**
+ * Mean between two vectors or mid vector between two vectors
+ * @param A
+ * @param B
+ */
+ static med: (A: number[], B: number[]) => number[];
+ /**
+ * Vector rotation by r (radians)
+ * @param A
+ * @param r rotation in radians
+ */
+ static rot: (A: number[], r?: number) => number[];
+ /**
+ * Rotate a vector around another vector by r (radians)
+ * @param A vector
+ * @param C center
+ * @param r rotation in radians
+ */
+ static rotWith: (A: number[], C: number[], r?: number) => number[];
+ /**
+ * Check of two vectors are identical.
+ * @param A
+ * @param B
+ */
+ static isEqual: (A: number[], B: number[]) => boolean;
+ /**
+ * Interpolate vector A to B with a scalar t
+ * @param A
+ * @param B
+ * @param t scalar
+ */
+ static lrp: (A: number[], B: number[], t: number) => number[];
+ /**
+ * Interpolate from A to B when curVAL goes fromVAL: number[] => to
+ * @param A
+ * @param B
+ * @param from Starting value
+ * @param to Ending value
+ * @param s Strength
+ */
+ static int: (A: number[], B: number[], from: number, to: number, s?: number) => number[];
+ /**
+ * Get the angle between the three vectors A, B, and C.
+ * @param p1
+ * @param pc
+ * @param p2
+ */
+ static ang3: (p1: number[], pc: number[], p2: number[]) => number;
+ /**
+ * Absolute value of a vector.
+ * @param A
+ * @returns
+ */
+ static abs: (A: number[]) => number[];
+ static rescale: (a: number[], n: number) => number[];
+ /**
+ * Get whether p1 is left of p2, relative to pc.
+ * @param p1
+ * @param pc
+ * @param p2
+ */
+ static isLeft: (p1: number[], pc: number[], p2: number[]) => number;
+ /**
+ * Get whether p1 is left of p2, relative to pc.
+ * @param p1
+ * @param pc
+ * @param p2
+ */
+ static clockwise: (p1: number[], pc: number[], p2: number[]) => boolean;
+ /**
+ * Round a vector to the a given precision.
+ * @param a
+ * @param d
+ */
+ static toFixed: (a: number[], d?: number) => number[];
+ /**
+ * Snap vector to nearest step.
+ * @param A
+ * @param step
+ * @example
+ * ```ts
+ * Vec.snap([10.5, 28], 10) // [10, 30]
+ * ```
+ */
+ static snap(a: number[], step?: number): number[];
+ /**
+ * Get the nearest point on a line with a known unit vector that passes through point A
+ * @param A Any point on the line
+ * @param u The unit vector for the line.
+ * @param P A point not on the line to test.
+ * @returns
+ */
+ static nearestPointOnLineThroughPoint: (A: number[], u: number[], P: number[]) => number[];
+ /**
+ * Distance between a point and a line with a known unit vector that passes through a point.
+ * @param A Any point on the line
+ * @param u The unit vector for the line.
+ * @param P A point not on the line to test.
+ * @returns
+ */
+ static distanceToLineThroughPoint: (A: number[], u: number[], P: number[]) => number;
+ /**
+ * Get the nearest point on a line segment between A and B
+ * @param A The start of the line segment
+ * @param B The end of the line segment
+ * @param P The off-line point
+ * @param clamp Whether to clamp the point between A and B.
+ * @returns
+ */
+ static nearestPointOnLineSegment: (A: number[], B: number[], P: number[], clamp?: boolean) => number[];
+ /**
+ * Distance between a point and the nearest point on a line segment between A and B
+ * @param A The start of the line segment
+ * @param B The end of the line segment
+ * @param P The off-line point
+ * @param clamp Whether to clamp the point between A and B.
+ * @returns
+ */
+ static distanceToLineSegment: (A: number[], B: number[], P: number[], clamp?: boolean) => number;
+ /**
+ * Push a point A towards point B by a given distance.
+ * @param A
+ * @param B
+ * @param d
+ * @returns
+ */
+ static nudge: (A: number[], B: number[], d: number) => number[];
+ /**
+ * Push a point in a given angle by a given distance.
+ * @param A
+ * @param B
+ * @param d
+ */
+ static nudgeAtAngle: (A: number[], a: number, d: number) => number[];
+ /**
+ * Round a vector to a precision length.
+ * @param a
+ * @param n
+ */
+ static toPrecision: (a: number[], n?: number) => number[];
+ /**
+ * Get an array of points (with simulated pressure) between two points.
+ * @param A The first point.
+ * @param B The second point.
+ * @param steps The number of points to return.
+ * @param ease An easing function to apply to the simulated pressure.
+ */
+ static pointsBetween: (A: number[], B: number[], steps?: number) => number[][];
+}
+export default Vec;
+//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
diff --git a/packages/vec/src/index.d.ts.map b/packages/vec/src/index.d.ts.map
new file mode 100644
index 000000000..553aa2b97
--- /dev/null
+++ b/packages/vec/src/index.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,qBAAa,GAAG;IACd;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;IAC5C,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;IAKzD;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE;IACjD,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE;IAK9D;;;OAGG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAG,MAAM,EAAE,CAEnC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEhD;IAED;;;;OAIG;IACH,MAAM,CAAC,SAAS,MAAO,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAEpD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEhD;IAED;;;;OAIG;IACH,MAAM,CAAC,SAAS,MAAO,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAEpD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAGhD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAE9C;IAED;;;;OAIG;IACH,MAAM,CAAC,IAAI,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEjD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAE9C;IAED;;;;OAIG;IACH,MAAM,CAAC,IAAI,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEjD;IAED;;;OAGG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAG,MAAM,EAAE,CAEnC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAE9C;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAE9C;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM;IAI3D;;;OAGG;IACH,MAAM,CAAC,IAAI,MAAO,MAAM,EAAE,KAAG,MAAM,CAElC;IAED;;;OAGG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAG,MAAM,CAEjC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAE9C;IAED;;;OAGG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAG,MAAM,EAAE,CAEnC;IAED;;;OAGG;IACH,MAAM,CAAC,SAAS,MAAO,MAAM,EAAE,KAAG,MAAM,EAAE,CAEzC;IAED;;;;;OAKG;IACH,MAAM,CAAC,OAAO,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEpD;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAEhD;IAED;;;;OAIG;IACH,MAAM,CAAC,IAAI,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAE/C;IAED;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAMrD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAE9C;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAEhD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEhD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,iBAAU,MAAM,EAAE,CAE1C;IAED;;;;;OAKG;IACH,MAAM,CAAC,OAAO,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,iBAAU,MAAM,EAAE,CAa3D;IAED;;;;OAIG;IACH,MAAM,CAAC,OAAO,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,OAAO,CAEnD;IAED;;;;;OAKG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAE3D;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,QAAQ,MAAM,MAAM,MAAM,iBAAU,MAAM,EAAE,CAGjF;IAED;;;;;OAKG;IACH,MAAM,CAAC,IAAI,OAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,MAAM,EAAE,KAAG,MAAM,CAK/D;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAG,MAAM,EAAE,CAEnC;IAED,MAAM,CAAC,OAAO,MAAO,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAGlD;IAED;;;;;OAKG;IACH,MAAM,CAAC,MAAM,OAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,MAAM,EAAE,KAAG,MAAM,CAKjE;IAED;;;;;OAKG;IACH,MAAM,CAAC,SAAS,OAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,MAAM,EAAE,KAAG,OAAO,CAErE;IAED;;;;OAIG;IACH,MAAM,CAAC,OAAO,MAAO,MAAM,EAAE,iBAAU,MAAM,EAAE,CAE9C;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,SAAI;IAIjC;;;;;;OAMG;IACH,MAAM,CAAC,8BAA8B,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAExF;IAED;;;;;;OAMG;IACH,MAAM,CAAC,0BAA0B,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAElF;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,yBAAyB,MAC3B,MAAM,EAAE,KACR,MAAM,EAAE,KACR,MAAM,EAAE,sBAEV,MAAM,EAAE,CAYV;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,qBAAqB,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,sBAAiB,MAAM,CAE3F;IAED;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAE7D;IAED;;;;;OAKG;IACH,MAAM,CAAC,YAAY,MAAO,MAAM,EAAE,KAAK,MAAM,KAAK,MAAM,KAAG,MAAM,EAAE,CAElE;IAED;;;;OAIG;IACH,MAAM,CAAC,WAAW,MAAO,MAAM,EAAE,iBAAU,MAAM,EAAE,CAElD;IAED;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,qBAAc,MAAM,EAAE,EAAE,CAMvE;CACF;AAED,eAAe,GAAG,CAAA"}
\ No newline at end of file
diff --git a/packages/vec/src/index.ts b/packages/vec/src/index.ts
new file mode 100644
index 000000000..47960cc62
--- /dev/null
+++ b/packages/vec/src/index.ts
@@ -0,0 +1,499 @@
+export class Vec {
+ /**
+ * Clamp a value into a range.
+ * @param n
+ * @param min
+ */
+ static clamp(n: number, min: number): number
+ static clamp(n: number, min: number, max: number): number
+ static clamp(n: number, min: number, max?: number): number {
+ return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
+ }
+
+ /**
+ * Clamp a value into a range.
+ * @param n
+ * @param min
+ */
+ static clampV(A: number[], min: number): number[]
+ static clampV(A: number[], min: number, max: number): number[]
+ static clampV(A: number[], min: number, max?: number): number[] {
+ return A.map((n) => (max ? Vec.clamp(n, min, max) : Vec.clamp(n, min)))
+ }
+
+ /**
+ * Negate a vector.
+ * @param A
+ */
+ static neg = (A: number[]): number[] => {
+ return [-A[0], -A[1]]
+ }
+
+ /**
+ * Add vectors.
+ * @param A
+ * @param B
+ */
+ static add = (A: number[], B: number[]): number[] => {
+ return [A[0] + B[0], A[1] + B[1]]
+ }
+
+ /**
+ * Add scalar to vector.
+ * @param A
+ * @param B
+ */
+ static addScalar = (A: number[], n: number): number[] => {
+ return [A[0] + n, A[1] + n]
+ }
+
+ /**
+ * Subtract vectors.
+ * @param A
+ * @param B
+ */
+ static sub = (A: number[], B: number[]): number[] => {
+ return [A[0] - B[0], A[1] - B[1]]
+ }
+
+ /**
+ * Subtract scalar from vector.
+ * @param A
+ * @param B
+ */
+ static subScalar = (A: number[], n: number): number[] => {
+ return [A[0] - n, A[1] - n]
+ }
+
+ /**
+ * Get the vector from vectors A to B.
+ * @param A
+ * @param B
+ */
+ static vec = (A: number[], B: number[]): number[] => {
+ // A, B as vectors get the vector from A to B
+ return [B[0] - A[0], B[1] - A[1]]
+ }
+
+ /**
+ * Vector multiplication by scalar
+ * @param A
+ * @param n
+ */
+ static mul = (A: number[], n: number): number[] => {
+ return [A[0] * n, A[1] * n]
+ }
+
+ /**
+ * Multiple two vectors.
+ * @param A
+ * @param B
+ */
+ static mulV = (A: number[], B: number[]): number[] => {
+ return [A[0] * B[0], A[1] * B[1]]
+ }
+
+ /**
+ * Vector division by scalar.
+ * @param A
+ * @param n
+ */
+ static div = (A: number[], n: number): number[] => {
+ return [A[0] / n, A[1] / n]
+ }
+
+ /**
+ * Vector division by vector.
+ * @param A
+ * @param n
+ */
+ static divV = (A: number[], B: number[]): number[] => {
+ return [A[0] / B[0], A[1] / B[1]]
+ }
+
+ /**
+ * Perpendicular rotation of a vector A
+ * @param A
+ */
+ static per = (A: number[]): number[] => {
+ return [A[1], -A[0]]
+ }
+
+ /**
+ * Dot product
+ * @param A
+ * @param B
+ */
+ static dpr = (A: number[], B: number[]): number => {
+ return A[0] * B[0] + A[1] * B[1]
+ }
+
+ /**
+ * Cross product (outer product) | A X B |
+ * @param A
+ * @param B
+ */
+ static cpr = (A: number[], B: number[]): number => {
+ return A[0] * B[1] - B[0] * A[1]
+ }
+
+ /**
+ * Cross (for point in polygon)
+ *
+ */
+ static cross(x: number[], y: number[], z: number[]): number {
+ return (y[0] - x[0]) * (z[1] - x[1]) - (z[0] - x[0]) * (y[1] - x[1])
+ }
+
+ /**
+ * Length of the vector squared
+ * @param A
+ */
+ static len2 = (A: number[]): number => {
+ return A[0] * A[0] + A[1] * A[1]
+ }
+
+ /**
+ * Length of the vector
+ * @param A
+ */
+ static len = (A: number[]): number => {
+ return Math.hypot(A[0], A[1])
+ }
+
+ /**
+ * Project A over B
+ * @param A
+ * @param B
+ */
+ static pry = (A: number[], B: number[]): number => {
+ return Vec.dpr(A, B) / Vec.len(B)
+ }
+
+ /**
+ * Get normalized / unit vector.
+ * @param A
+ */
+ static uni = (A: number[]): number[] => {
+ return Vec.div(A, Vec.len(A))
+ }
+
+ /**
+ * Get normalized / unit vector.
+ * @param A
+ */
+ static normalize = (A: number[]): number[] => {
+ return Vec.uni(A)
+ }
+
+ /**
+ * Get the tangent between two vectors.
+ * @param A
+ * @param B
+ * @returns
+ */
+ static tangent = (A: number[], B: number[]): number[] => {
+ return Vec.uni(Vec.sub(A, B))
+ }
+
+ /**
+ * Dist length from A to B squared.
+ * @param A
+ * @param B
+ */
+ static dist2 = (A: number[], B: number[]): number => {
+ return Vec.len2(Vec.sub(A, B))
+ }
+
+ /**
+ * Dist length from A to B
+ * @param A
+ * @param B
+ */
+ static dist = (A: number[], B: number[]): number => {
+ return Math.hypot(A[1] - B[1], A[0] - B[0])
+ }
+
+ /**
+ * A faster, though less accurate method for testing distances. Maybe faster?
+ * @param A
+ * @param B
+ * @returns
+ */
+ static fastDist = (A: number[], B: number[]): number[] => {
+ const V = [B[0] - A[0], B[1] - A[1]]
+ const aV = [Math.abs(V[0]), Math.abs(V[1])]
+ let r = 1 / Math.max(aV[0], aV[1])
+ r = r * (1.29289 - (aV[0] + aV[1]) * r * 0.29289)
+ return [V[0] * r, V[1] * r]
+ }
+
+ /**
+ * Angle between vector A and vector B in radians
+ * @param A
+ * @param B
+ */
+ static ang = (A: number[], B: number[]): number => {
+ return Math.atan2(Vec.cpr(A, B), Vec.dpr(A, B))
+ }
+
+ /**
+ * Angle between vector A and vector B in radians
+ * @param A
+ * @param B
+ */
+ static angle = (A: number[], B: number[]): number => {
+ return Math.atan2(B[1] - A[1], B[0] - A[0])
+ }
+
+ /**
+ * Mean between two vectors or mid vector between two vectors
+ * @param A
+ * @param B
+ */
+ static med = (A: number[], B: number[]): number[] => {
+ return Vec.mul(Vec.add(A, B), 0.5)
+ }
+
+ /**
+ * Vector rotation by r (radians)
+ * @param A
+ * @param r rotation in radians
+ */
+ static rot = (A: number[], r = 0): number[] => {
+ return [A[0] * Math.cos(r) - A[1] * Math.sin(r), A[0] * Math.sin(r) + A[1] * Math.cos(r)]
+ }
+
+ /**
+ * Rotate a vector around another vector by r (radians)
+ * @param A vector
+ * @param C center
+ * @param r rotation in radians
+ */
+ static rotWith = (A: number[], C: number[], r = 0): number[] => {
+ if (r === 0) return A
+
+ const s = Math.sin(r)
+ const c = Math.cos(r)
+
+ const px = A[0] - C[0]
+ const py = A[1] - C[1]
+
+ const nx = px * c - py * s
+ const ny = px * s + py * c
+
+ return [nx + C[0], ny + C[1]]
+ }
+
+ /**
+ * Check of two vectors are identical.
+ * @param A
+ * @param B
+ */
+ static isEqual = (A: number[], B: number[]): boolean => {
+ return A[0] === B[0] && A[1] === B[1]
+ }
+
+ /**
+ * Interpolate vector A to B with a scalar t
+ * @param A
+ * @param B
+ * @param t scalar
+ */
+ static lrp = (A: number[], B: number[], t: number): number[] => {
+ return Vec.add(A, Vec.mul(Vec.sub(B, A), t))
+ }
+
+ /**
+ * Interpolate from A to B when curVAL goes fromVAL: number[] => to
+ * @param A
+ * @param B
+ * @param from Starting value
+ * @param to Ending value
+ * @param s Strength
+ */
+ static int = (A: number[], B: number[], from: number, to: number, s = 1): number[] => {
+ const t = (Vec.clamp(from, to) - from) / (to - from)
+ return Vec.add(Vec.mul(A, 1 - t), Vec.mul(B, s))
+ }
+
+ /**
+ * Get the angle between the three vectors A, B, and C.
+ * @param p1
+ * @param pc
+ * @param p2
+ */
+ static ang3 = (p1: number[], pc: number[], p2: number[]): number => {
+ // this,
+ const v1 = Vec.vec(pc, p1)
+ const v2 = Vec.vec(pc, p2)
+ return Vec.ang(v1, v2)
+ }
+
+ /**
+ * Absolute value of a vector.
+ * @param A
+ * @returns
+ */
+ static abs = (A: number[]): number[] => {
+ return [Math.abs(A[0]), Math.abs(A[1])]
+ }
+
+ static rescale = (a: number[], n: number): number[] => {
+ const l = Vec.len(a)
+ return [(n * a[0]) / l, (n * a[1]) / l]
+ }
+
+ /**
+ * Get whether p1 is left of p2, relative to pc.
+ * @param p1
+ * @param pc
+ * @param p2
+ */
+ static isLeft = (p1: number[], pc: number[], p2: number[]): number => {
+ // isLeft: >0 for counterclockwise
+ // =0 for none (degenerate)
+ // <0 for clockwise
+ return (pc[0] - p1[0]) * (p2[1] - p1[1]) - (p2[0] - p1[0]) * (pc[1] - p1[1])
+ }
+
+ /**
+ * Get whether p1 is left of p2, relative to pc.
+ * @param p1
+ * @param pc
+ * @param p2
+ */
+ static clockwise = (p1: number[], pc: number[], p2: number[]): boolean => {
+ return Vec.isLeft(p1, pc, p2) > 0
+ }
+
+ /**
+ * Round a vector to the a given precision.
+ * @param a
+ * @param d
+ */
+ static toFixed = (a: number[], d = 2): number[] => {
+ return a.map((v) => +v.toFixed(d))
+ }
+
+ /**
+ * Snap vector to nearest step.
+ * @param A
+ * @param step
+ * @example
+ * ```ts
+ * Vec.snap([10.5, 28], 10) // [10, 30]
+ * ```
+ */
+ static snap(a: number[], step = 1) {
+ return [Math.round(a[0] / step) * step, Math.round(a[1] / step) * step]
+ }
+
+ /**
+ * Get the nearest point on a line with a known unit vector that passes through point A
+ * @param A Any point on the line
+ * @param u The unit vector for the line.
+ * @param P A point not on the line to test.
+ * @returns
+ */
+ static nearestPointOnLineThroughPoint = (A: number[], u: number[], P: number[]): number[] => {
+ return Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u)))
+ }
+
+ /**
+ * Distance between a point and a line with a known unit vector that passes through a point.
+ * @param A Any point on the line
+ * @param u The unit vector for the line.
+ * @param P A point not on the line to test.
+ * @returns
+ */
+ static distanceToLineThroughPoint = (A: number[], u: number[], P: number[]): number => {
+ return Vec.dist(P, Vec.nearestPointOnLineThroughPoint(A, u, P))
+ }
+
+ /**
+ * Get the nearest point on a line segment between A and B
+ * @param A The start of the line segment
+ * @param B The end of the line segment
+ * @param P The off-line point
+ * @param clamp Whether to clamp the point between A and B.
+ * @returns
+ */
+ static nearestPointOnLineSegment = (
+ A: number[],
+ B: number[],
+ P: number[],
+ clamp = true
+ ): number[] => {
+ const u = Vec.uni(Vec.sub(B, A))
+ const C = Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u)))
+
+ if (clamp) {
+ if (C[0] < Math.min(A[0], B[0])) return A[0] < B[0] ? A : B
+ if (C[0] > Math.max(A[0], B[0])) return A[0] > B[0] ? A : B
+ if (C[1] < Math.min(A[1], B[1])) return A[1] < B[1] ? A : B
+ if (C[1] > Math.max(A[1], B[1])) return A[1] > B[1] ? A : B
+ }
+
+ return C
+ }
+
+ /**
+ * Distance between a point and the nearest point on a line segment between A and B
+ * @param A The start of the line segment
+ * @param B The end of the line segment
+ * @param P The off-line point
+ * @param clamp Whether to clamp the point between A and B.
+ * @returns
+ */
+ static distanceToLineSegment = (A: number[], B: number[], P: number[], clamp = true): number => {
+ return Vec.dist(P, Vec.nearestPointOnLineSegment(A, B, P, clamp))
+ }
+
+ /**
+ * Push a point A towards point B by a given distance.
+ * @param A
+ * @param B
+ * @param d
+ * @returns
+ */
+ static nudge = (A: number[], B: number[], d: number): number[] => {
+ return Vec.add(A, Vec.mul(Vec.uni(Vec.sub(B, A)), d))
+ }
+
+ /**
+ * Push a point in a given angle by a given distance.
+ * @param A
+ * @param B
+ * @param d
+ */
+ static nudgeAtAngle = (A: number[], a: number, d: number): number[] => {
+ return [Math.cos(a) * d + A[0], Math.sin(a) * d + A[1]]
+ }
+
+ /**
+ * Round a vector to a precision length.
+ * @param a
+ * @param n
+ */
+ static toPrecision = (a: number[], n = 4): number[] => {
+ return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
+ }
+
+ /**
+ * Get an array of points (with simulated pressure) between two points.
+ * @param A The first point.
+ * @param B The second point.
+ * @param steps The number of points to return.
+ * @param ease An easing function to apply to the simulated pressure.
+ */
+ static pointsBetween = (A: number[], B: number[], steps = 6): number[][] => {
+ return Array.from(Array(steps)).map((_, i) => {
+ const t = i / (steps - 1)
+ const k = Math.min(1, 0.5 + Math.abs(0.5 - t))
+ return [...Vec.lrp(A, B, t), k]
+ })
+ }
+}
+
+export default Vec
diff --git a/packages/vec/tsconfig.build.json b/packages/vec/tsconfig.build.json
new file mode 100644
index 000000000..2b108dffa
--- /dev/null
+++ b/packages/vec/tsconfig.build.json
@@ -0,0 +1,20 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": [
+ "node_modules",
+ "**/*.test.tsx",
+ "**/*.test.ts",
+ "**/*.spec.tsx",
+ "**/*.spec.ts",
+ "src/test",
+ "dist",
+ "docs"
+ ],
+ "compilerOptions": {
+ "composite": false,
+ "incremental": false,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ }
+}
diff --git a/packages/vec/tsconfig.json b/packages/vec/tsconfig.json
new file mode 100644
index 000000000..73d42bac1
--- /dev/null
+++ b/packages/vec/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src"],
+ "exclude": ["node_modules", "dist", "docs"],
+ "compilerOptions": {
+ "outDir": "./dist/types",
+ "rootDir": "src",
+ "baseUrl": "."
+ },
+ "typedocOptions": {
+ "entryPoints": ["src/index.ts"],
+ "out": "docs"
+ }
+}
diff --git a/replace-paths.ts b/replace-paths.ts
new file mode 100644
index 000000000..afb08434d
--- /dev/null
+++ b/replace-paths.ts
@@ -0,0 +1,338 @@
+#! /usr/bin/env node
+
+import * as program from 'commander'
+import { existsSync, readFileSync, writeFileSync } from 'fs'
+import { sync } from 'globby'
+import { dirname, relative, resolve } from 'path'
+import path from 'path'
+import fs from 'fs'
+import JSON5 from 'json5'
+
+/*
+"baseUrl": ".",
+"outDir": "lib",
+"paths": {
+ "src/*": ["src/*"]
+},
+*/
+
+export interface IRawTSConfig {
+ extends?: string
+ compilerOptions?: {
+ baseUrl?: string
+ outDir?: string
+ rootDir?: string
+ paths?: { [key: string]: string[] }
+ }
+}
+
+export interface ITSConfig {
+ baseUrl?: string
+ outDir?: string
+ rootDir?: string
+ compilerOptions?: Record
+ paths?: { [key: string]: string[] }
+}
+
+export const mapPaths = (
+ paths: { [key: string]: string[] },
+ mapper: (x: string) => string
+): { [key: string]: string[] } => {
+ const dest = {} as { [key: string]: string[] }
+ Object.keys(paths).forEach((key) => {
+ dest[key] = paths[key].map(mapper)
+ })
+ return dest
+}
+
+export const loadConfig = (file: string): ITSConfig => {
+ const fileToParse = fs.readFileSync(file)
+
+ const parsedJsonFile = JSON5.parse(fileToParse as unknown as string)
+
+ const {
+ extends: extendsPath,
+ compilerOptions: { baseUrl, outDir, rootDir, paths } = {
+ baseUrl: undefined,
+ outDir: undefined,
+ rootDir: undefined,
+ paths: undefined,
+ },
+ } = parsedJsonFile as IRawTSConfig
+
+ const config: ITSConfig = {}
+ if (baseUrl) {
+ config.baseUrl = baseUrl
+ }
+ if (outDir) {
+ config.outDir = outDir
+ }
+ if (rootDir) {
+ config.rootDir = rootDir
+ }
+ if (paths) {
+ config.paths = paths
+ }
+ if (extendsPath) {
+ const childConfigDirPath = path.dirname(file)
+ const parentConfigPath = path.resolve(childConfigDirPath, extendsPath)
+ const parentConfigDirPath = path.dirname(parentConfigPath)
+ const currentExtension = path.extname(parentConfigPath)
+
+ let parentExtendedConfigFile = path.format({
+ name: parentConfigPath,
+ ext: currentExtension === '' ? '.json' : '',
+ })
+
+ /* Ensure without a doubt there's no double extension */
+ if (/\.json\.json$/.test(parentExtendedConfigFile)) {
+ parentExtendedConfigFile = parentExtendedConfigFile.replace(/\.json\.json$/, '.json')
+ }
+
+ const parentConfig = loadConfig(parentExtendedConfigFile)
+
+ if (parentConfig.baseUrl) {
+ parentConfig.baseUrl = path.resolve(parentConfigDirPath, parentConfig.baseUrl)
+ }
+
+ return {
+ ...parentConfig,
+ ...config,
+ }
+ }
+
+ return config
+}
+
+program
+ .version('0.0.1')
+ .option('-p, --project ', 'path to tsconfig.json')
+ .option('-s, --src ', 'source root path')
+ .option('-o, --out ', 'output root path')
+ .option('-v, --verbose', 'output logs')
+
+program.on('--help', () => {
+ console.log(`
+ $ tscpath -p tsconfig.json
+`)
+})
+
+program.parse(process.argv)
+
+const {
+ out: flagOut,
+ project = 'tsconfig.json',
+ src: flagSrc,
+ verbose = false,
+} = program as {
+ out?: string | undefined
+ project?: string
+ src?: string | undefined
+ verbose?: boolean
+}
+
+const verboseLog = (...args: any[]): void => {
+ if (verbose) {
+ console.log(...args)
+ }
+}
+
+const configFile = resolve(process.cwd(), project)
+
+const rootDir = resolve(process.cwd())
+
+verboseLog(`Using tsconfig: ${configFile}`)
+
+const exitingErr = (): any => {
+ throw new Error('--- exiting tsconfig-replace-paths due to parameters missing ---')
+}
+
+const missingConfigErr = (property: string): any => {
+ console.error(`Whoops! Please set ${property} in your tsconfig or supply a flag`)
+ exitingErr()
+}
+
+const missingDirectoryErr = (directory: string, flag: string): any => {
+ console.error(
+ `Whoops! ${directory} must be specified in your project => --project ${project}, or flagged with directory => ${flag} './path'`
+ )
+ exitingErr()
+}
+
+// Imported the TS Config
+const returnedTsConfig = loadConfig(configFile)
+
+// Destructure only the necessary keys, and rename to give context
+const {
+ baseUrl,
+ paths,
+ outDir: tsConfigOutDir = '',
+ rootDir: tsConfigRootDir = rootDir,
+} = returnedTsConfig
+
+// If no flagSrc or tsConfigRootDir, error
+if (!flagSrc && tsConfigRootDir === '') {
+ missingConfigErr('compilerOptions.rootDir')
+}
+
+// If no flagOut or tsConfigOutDir, error
+if (!flagOut && tsConfigOutDir === '') {
+ missingConfigErr('compilerOptions.outDir')
+}
+
+// Are we going to use the flag or ts config for src?
+let usingSrcDir: string
+if (flagSrc) {
+ verboseLog('Using flag --src')
+ usingSrcDir = resolve(flagSrc)
+} else {
+ verboseLog('Using compilerOptions.rootDir from your tsconfig')
+ usingSrcDir = resolve(tsConfigRootDir)
+}
+if (!usingSrcDir) {
+ missingDirectoryErr('rootDir', '--src')
+}
+
+// Log which src is being used
+verboseLog(`Using src: ${usingSrcDir}`)
+
+// Are we going to use the flag or ts config for out?
+let usingOutDir: string
+if (flagOut) {
+ verboseLog('Using flag --out')
+ usingOutDir = resolve(flagOut)
+} else {
+ verboseLog('Using compilerOptions.outDir from your tsconfig')
+ usingOutDir = resolve(tsConfigOutDir)
+}
+if (!usingOutDir) {
+ missingDirectoryErr('outDir', '--out')
+}
+
+// Log which out is being used
+verboseLog(`Using out: ${usingOutDir}`)
+
+if (!baseUrl) {
+ throw new Error('compilerOptions.baseUrl is not set')
+}
+if (!paths) {
+ throw new Error('compilerOptions.paths is not set')
+}
+if (!usingOutDir) {
+ throw new Error('compilerOptions.outDir is not set')
+}
+if (!usingSrcDir) {
+ throw new Error('compilerOptions.rootDir is not set')
+}
+
+verboseLog(`baseUrl: ${baseUrl}`)
+verboseLog(`rootDir: ${usingSrcDir}`)
+verboseLog(`outDir: ${usingOutDir}`)
+verboseLog(`paths: ${JSON.stringify(paths, null, 2)}`)
+
+const configDir = dirname(configFile)
+
+const basePath = resolve(configDir, baseUrl)
+verboseLog(`basePath: ${basePath}`)
+
+const outPath = usingOutDir || resolve(basePath, usingOutDir)
+verboseLog(`outPath: ${outPath}`)
+
+const outFileToSrcFile = (x: string): string => resolve(usingSrcDir, relative(outPath, x))
+
+const aliases = Object.keys(paths)
+ .filter((path) => path.startsWith('~'))
+ .map((alias) => ({
+ prefix: alias.replace(/\*$/, ''),
+ aliasPaths: paths[alias as keyof typeof paths].map((p) =>
+ resolve(basePath, p.replace(/\*$/, ''))
+ ),
+ }))
+ .filter(({ prefix }) => prefix)
+verboseLog(`aliases: ${JSON.stringify(aliases, null, 2)}`)
+
+const toRelative = (from: string, x: string): string => {
+ const rel = relative(from, x)
+ return (rel.startsWith('.') ? rel : `./${rel}`).replace(/\\/g, '/')
+}
+
+const exts = ['.js', '.jsx', '.ts', '.tsx', '.d.ts', '.json']
+
+let replaceCount = 0
+
+const absToRel = (modulePath: string, outFile: string): string => {
+ const alen = aliases.length
+
+ for (let j = 0; j < alen; j += 1) {
+ const { prefix, aliasPaths } = aliases[j]
+
+ if (modulePath.startsWith(prefix)) {
+ const modulePathRel = modulePath.substring(prefix.length)
+ const srcFile = outFileToSrcFile(outFile)
+ const outRel = relative(basePath, outFile)
+
+ verboseLog(`${outRel} (source: ${relative(basePath, srcFile)}):`)
+ verboseLog(`\timport '${modulePath}'`)
+
+ const len = aliasPaths.length
+ for (let i = 0; i < len; i += 1) {
+ const apath = aliasPaths[i]
+ const moduleSrc = resolve(apath, modulePathRel)
+ if (existsSync(moduleSrc) || exts.some((ext) => existsSync(moduleSrc + ext))) {
+ const rel = toRelative(dirname(srcFile), moduleSrc)
+
+ replaceCount += 1
+
+ verboseLog(
+ `\treplacing '${modulePath}' -> '${rel}' referencing ${relative(basePath, moduleSrc)}`
+ )
+ return rel
+ }
+ }
+ verboseLog(`\tcould not replace ${modulePath}`)
+ }
+ }
+
+ return modulePath
+}
+
+const requireRegex = /(?:import|require)\(['"]([^'"]*)['"]\)/g
+const importRegex = /(?:import|from) ['"]([^'"]*)['"]/g
+
+const replaceImportStatement = (orig: string, matched: string, outFile: string): string => {
+ const index = orig.indexOf(matched)
+ return (
+ orig.substring(0, index) + absToRel(matched, outFile) + orig.substring(index + matched.length)
+ )
+}
+
+const replaceAlias = (text: string, outFile: string): string =>
+ text
+ .replace(requireRegex, (orig, matched) => replaceImportStatement(orig, matched, outFile))
+ .replace(importRegex, (orig, matched) => replaceImportStatement(orig, matched, outFile))
+
+// import relative to absolute path
+const files = sync(`${outPath}/**/*.{js,jsx,ts,tsx}`, {
+ dot: true,
+ noDir: true,
+} as any).map((x) => resolve(x))
+
+let changedFileCount = 0
+
+const flen = files.length
+let count = 0
+
+for (let i = 0; i < flen; i += 1) {
+ const file = files[i]
+ const text = readFileSync(file, 'utf8')
+ const prevReplaceCount = replaceCount
+ const newText = replaceAlias(text, file)
+ if (text !== newText) {
+ changedFileCount += 1
+ verboseLog(`${file}: replaced ${replaceCount - prevReplaceCount} paths`)
+ writeFileSync(file, newText, 'utf8')
+ count = count + 1
+ }
+}
+
+console.log(`Replaced ${replaceCount} paths in ${changedFileCount} files`)
diff --git a/tsconfig.base.json b/tsconfig.base.json
index c055d1da9..c3c0aa03d 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
- "declaration": true,
- "declarationMap": true,
"composite": true,
- "sourceMap": true,
+ "declaration": true,
+ "declarationMap": false,
+ "sourceMap": false,
"emitDeclarationOnly": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
diff --git a/tsconfig.json b/tsconfig.json
index 2c44f979d..764cb0212 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,14 +1,4 @@
{
- "composite": true,
"extends": "./tsconfig.base.json",
- "exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts"],
- "files": [],
- "references": [{ "path": "./packages/tldraw" }, { "path": "./packages/core" }],
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@tldraw/core": ["./packages/core"],
- "@tldraw/tldraw": ["./packages/tldraw"]
- }
- }
+ "exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts"]
}
diff --git a/yarn.lock b/yarn.lock
index e5f0ace5d..410236ae9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3135,23 +3135,6 @@
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^8.0.0"
-"@tldraw/intersect@latest":
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/@tldraw/intersect/-/intersect-0.1.4.tgz#390d76a0a24f625dfb9bb08f7f6a24e4a02007a1"
- integrity sha512-pxNMyUD9BMy0JqUSik4RXhf/meRA1TwJ3zESLebQ3EB3kThd9zICIUH5U8RmosvBx0c7dLMTvVzA1yVRbQBslQ==
- dependencies:
- "@tldraw/vec" "^0.1.3"
-
-"@tldraw/vec@^0.0.130":
- version "0.0.130"
- resolved "https://registry.yarnpkg.com/@tldraw/vec/-/vec-0.0.130.tgz#49be3dfabf9281c8361d0c3648e94c11f3a5ae83"
- integrity sha512-0vUa2asaUrxqDiu+1sseC1NSVN+IdSEK/gfrdyG7ZlrQPWECu8nhbanbKfbLkYafJuFsZnc6TZXnLs12T/+22g==
-
-"@tldraw/vec@^0.1.3", "@tldraw/vec@latest":
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/@tldraw/vec/-/vec-0.1.3.tgz#9c2ea9592058dd1458b49dbd817690a0c74e5473"
- integrity sha512-QIQu6xHWqYBarfCR2Cd55pb+HECCEFI3RlU0VEueztzGe2k5Mwcv1EuuIdPTZ1tg5o95Deo7mYgZKeZTcUCIxA==
-
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -3530,6 +3513,16 @@
resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
+"@uqt/ts-path-replace@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@uqt/ts-path-replace/-/ts-path-replace-1.1.1.tgz#c753fe92aa55575e7249c9defc908e311e37b93d"
+ integrity sha512-6OBpyAJRDAeneM1m8drI4e7P71kyoACdPVN5OMjsTmzT9becSZz95KVSRRQyljdDSCUPhdGzxOi5iBGp/eBYPQ==
+ dependencies:
+ lodash.merge "^4.6.2"
+ minimist "^1.2.5"
+ node-watch "^0.7.1"
+ replace-in-file "^6.1.0"
+
"@use-gesture/core@10.1.5":
version "10.1.5"
resolved "https://registry.yarnpkg.com/@use-gesture/core/-/core-10.1.5.tgz#4b956bc8aa6354c20c668930c5cacb16fe4415e4"
@@ -4752,7 +4745,7 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
-chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1:
+chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@@ -10856,6 +10849,11 @@ node-source-walk@^4.0.0, node-source-walk@^4.2.0:
dependencies:
"@babel/parser" "^7.0.0"
+node-watch@^0.7.1:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/node-watch/-/node-watch-0.7.2.tgz#545f057da8500487eb8287adcb4cb5a7338d7e21"
+ integrity sha512-g53VjSARRv1JdST0LZRIg8RiuLr1TaBbVPsVvxh0/0Ymvi0xYUjDuoqQQAWtHJQUXhiShowPT/aXKNeHBcyQsw==
+
nodemailer@^6.4.16:
version "6.7.1"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.1.tgz#09f72f8b375f7b259291757007bcd902c0174c6e"
@@ -12462,6 +12460,15 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
+replace-in-file@^6.1.0:
+ version "6.3.2"
+ resolved "https://registry.yarnpkg.com/replace-in-file/-/replace-in-file-6.3.2.tgz#0f19835137177c89932f45df319f3539a019484f"
+ integrity sha512-Dbt5pXKvFVPL3WAaEB3ZX+95yP0CeAtIPJDwYzHbPP5EAHn+0UoegH/Wg3HKflU9dYBH8UnBC2NvY3P+9EZtTg==
+ dependencies:
+ chalk "^4.1.2"
+ glob "^7.2.0"
+ yargs "^17.2.1"
+
request@^2.88.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
@@ -14020,6 +14027,15 @@ tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0:
minimist "^1.2.0"
strip-bom "^3.0.0"
+tsconfig-replace-paths@^0.0.11:
+ version "0.0.11"
+ resolved "https://registry.yarnpkg.com/tsconfig-replace-paths/-/tsconfig-replace-paths-0.0.11.tgz#0059a5ba5b6c156b00038ce46842e3d4d801a33b"
+ integrity sha512-BX10vOJL/kTZExQwj22FJgxCpneNYKfXvujBKGFpnvaKWl4/9Cpd678ZvKF+CX5NdxbKphscwtc8QzFA7zhVsA==
+ dependencies:
+ commander "^3.0.2"
+ globby "^10.0.1"
+ json5 "^2.2.0"
+
tsconfig-replace-paths@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/tsconfig-replace-paths/-/tsconfig-replace-paths-0.0.5.tgz#a9bf058604c50de5cab285c0eb0ed9b7d944d5ac"
@@ -14213,7 +14229,7 @@ typescript@^3.9.7:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
-typescript@^4.4.2, typescript@^4.4.3:
+typescript@^4.4.3, typescript@^4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
@@ -15145,7 +15161,7 @@ yargs@^14.2.2:
y18n "^4.0.0"
yargs-parser "^15.0.1"
-yargs@^17.0.1:
+yargs@^17.0.1, yargs@^17.2.1:
version "17.2.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea"
integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==