Browse Source

[feature] Add grids (#344)

* [feature] grids

* Shows relative grids at different zoom levels

* Update colors

* Restores vec and intersect to monorepo, changes vec.round to vec.toFixed, adds vec.snap

* Snapping in translate and transforms, fix shortcut

* fix bugs in build

* use grid size for nudge too

* update scripts

* Update grid.tsx

* Update grid.tsx

* Fixed!

* Update grid.tsx

* Fix package imports

* Update Editor.tsx

* Improve tsconfigs, imports

* Fix tiny arrow bugs, snap starting points to grid

* Update tsconfig.base.json

* Update shape-styles.ts

* Fix example tsconfig

* Fix translate type error

* Fix types, paths

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
main
Christian Petersen 2 years ago
parent
commit
e2814943e9
No account linked to committer's email address
100 changed files with 4383 additions and 266 deletions
  1. 0
    13
      .vscode/tasks.json
  2. 5
    5
      apps/www/components/Editor.tsx
  3. 3
    3
      apps/www/components/MultiplayerEditor.tsx
  4. 16
    7
      apps/www/hooks/useMultiplayerState.ts
  5. 2
    1
      apps/www/package.json
  6. 5
    3
      apps/www/pages/_app.tsx
  7. 12
    22
      apps/www/pages/_document.tsx
  8. 9
    7
      apps/www/pages/api/auth/[...nextauth].ts
  9. 1
    1
      apps/www/pages/api/sponsors.ts
  10. 2
    2
      apps/www/pages/index.tsx
  11. 1
    1
      apps/www/pages/r/[id].tsx
  12. 1
    1
      apps/www/pages/sponsorware.tsx
  13. 18
    9
      apps/www/tsconfig.json
  14. 1
    6
      apps/www/types.ts
  15. 3
    3
      apps/www/utils/sentry.ts
  16. 4
    4
      examples/core-example-advanced/package.json
  17. 2
    0
      examples/core-example-advanced/src/shapes/CustomShapeUtil.ts
  18. 1
    1
      examples/core-example-advanced/src/state/actions/camera/pinchCamera.ts
  19. 1
    1
      examples/core-example-advanced/src/state/actions/camera/zoomIn.ts
  20. 1
    1
      examples/core-example-advanced/src/state/actions/camera/zoomOut.ts
  21. 1
    1
      examples/core-example-advanced/src/state/helpers.ts
  22. 2
    1
      examples/core-example-advanced/tsconfig.json
  23. 4
    4
      examples/core-example/package.json
  24. 2
    1
      examples/core-example/tsconfig.json
  25. 2
    2
      examples/tldraw-example/tsconfig.json
  26. 6
    4
      package.json
  27. 4
    4
      packages/core/package.json
  28. 1
    1
      packages/core/src/TLShapeUtil/TLShapeUtil.tsx
  29. 6
    0
      packages/core/src/components/canvas/canvas.tsx
  30. 42
    0
      packages/core/src/components/grid/grid.tsx
  31. 1
    0
      packages/core/src/components/grid/index.ts
  32. 12
    0
      packages/core/src/components/renderer/renderer.tsx
  33. 14
    0
      packages/core/src/hooks/useStyle.tsx
  34. 1
    1
      packages/core/src/inputs.ts
  35. 1
    0
      packages/core/src/types.ts
  36. 21
    3
      packages/core/src/utils/utils.ts
  37. 5
    4
      packages/core/tsconfig.build.json
  38. 3
    3
      packages/core/tsconfig.json
  39. 9
    0
      packages/intersect/CHANGELOG.md
  40. 21
    0
      packages/intersect/LICENSE.md
  41. 477
    0
      packages/intersect/README.md
  42. BIN
      packages/intersect/card-repo.png
  43. 39
    0
      packages/intersect/package.json
  44. 63
    0
      packages/intersect/scripts/build.js
  45. 29
    0
      packages/intersect/scripts/dev.js
  46. 430
    0
      packages/intersect/src/index.d.ts
  47. 1241
    0
      packages/intersect/src/index.ts
  48. 21
    0
      packages/intersect/tsconfig.build.json
  49. 14
    0
      packages/intersect/tsconfig.json
  50. 5
    5
      packages/tldraw/package.json
  51. 5
    2
      packages/tldraw/src/Tldraw.tsx
  52. 1
    0
      packages/tldraw/src/components/FocusButton/index.ts
  53. 7
    0
      packages/tldraw/src/components/TopPanel/PreferencesMenu/PreferencesMenu.tsx
  54. 1
    0
      packages/tldraw/src/constants.ts
  55. 10
    0
      packages/tldraw/src/hooks/useKeyboardShortcuts.tsx
  56. 2
    2
      packages/tldraw/src/state/TLDR.ts
  57. 50
    18
      packages/tldraw/src/state/TldrawApp.ts
  58. 1
    2
      packages/tldraw/src/state/commands/alignShapes/alignShapes.ts
  59. 1
    1
      packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.ts
  60. 1
    1
      packages/tldraw/src/state/commands/styleShapes/styleShapes.ts
  61. 1
    1
      packages/tldraw/src/state/commands/translateShapes/translateShapes.ts
  62. 26
    12
      packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.ts
  63. 4
    4
      packages/tldraw/src/state/sessions/DrawSession/DrawSession.ts
  64. 1
    1
      packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts
  65. 6
    6
      packages/tldraw/src/state/sessions/RotateSession/RotateSession.spec.ts
  66. 17
    3
      packages/tldraw/src/state/sessions/TransformSession/TransformSession.ts
  67. 15
    3
      packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.ts
  68. 8
    5
      packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.spec.ts
  69. 30
    33
      packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.ts
  70. 9
    9
      packages/tldraw/src/state/shapes/ArrowUtil/ArrowUtil.tsx
  71. 3
    3
      packages/tldraw/src/state/shapes/ArrowUtil/arrowHelpers.ts
  72. 1
    1
      packages/tldraw/src/state/shapes/EllipseUtil/EllipseUtil.tsx
  73. 1
    1
      packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx
  74. 2
    2
      packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx
  75. 2
    1
      packages/tldraw/src/state/shapes/shared/shape-styles.ts
  76. 6
    4
      packages/tldraw/src/state/shapes/shared/transformRectangle.ts
  77. 2
    2
      packages/tldraw/src/state/shapes/shared/transformSingleRectangle.ts
  78. 4
    1
      packages/tldraw/src/state/tools/ArrowTool/ArrowTool.ts
  79. 0
    2
      packages/tldraw/src/state/tools/BaseTool.ts
  80. 4
    1
      packages/tldraw/src/state/tools/EllipseTool/EllipseTool.ts
  81. 4
    1
      packages/tldraw/src/state/tools/LineTool/LineTool.ts
  82. 4
    1
      packages/tldraw/src/state/tools/RectangleTool/RectangleTool.ts
  83. 7
    2
      packages/tldraw/src/state/tools/SelectTool/SelectTool.spec.ts
  84. 3
    1
      packages/tldraw/src/state/tools/StickyTool/StickyTool.ts
  85. 8
    2
      packages/tldraw/src/state/tools/TextTool/TextTool.ts
  86. 12
    0
      packages/tldraw/src/test/TldrawTestApp.tsx
  87. 30
    2
      packages/tldraw/src/types.ts
  88. 5
    6
      packages/tldraw/tsconfig.build.json
  89. 4
    5
      packages/tldraw/tsconfig.json
  90. 86
    0
      packages/vec/CHANGELOG.md
  91. 21
    0
      packages/vec/LICENSE.md
  92. 477
    0
      packages/vec/README.md
  93. BIN
      packages/vec/card-repo.png
  94. 35
    0
      packages/vec/package.json
  95. 61
    0
      packages/vec/scripts/build.js
  96. 29
    0
      packages/vec/scripts/dev.js
  97. 311
    0
      packages/vec/src/index.d.ts
  98. 1
    0
      packages/vec/src/index.d.ts.map
  99. 499
    0
      packages/vec/src/index.ts
  100. 0
    0
      packages/vec/tsconfig.build.json

+ 0
- 13
.vscode/tasks.json View File

@@ -1,13 +0,0 @@
1
-{
2
-  "version": "2.0.0",
3
-  "tasks": [
4
-    {
5
-      "label": "Check for type errors",
6
-      "type": "typescript",
7
-      "tsconfig": "tsconfig.json",
8
-      "option": "watch",
9
-      "problemMatcher": ["$tsc-watch"],
10
-      "group": "build"
11
-    }
12
-  ]
13
-}

+ 5
- 5
apps/www/components/Editor.tsx View File

@@ -1,7 +1,7 @@
1
-import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
2
-import * as gtag from '-utils/gtag'
3 1
 import React from 'react'
4
-import { useAccountHandlers } from '-hooks/useAccountHandlers'
2
+import * as gtag from 'utils/gtag'
3
+import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
4
+import { useAccountHandlers } from 'hooks/useAccountHandlers'
5 5
 
6 6
 declare const window: Window & { app: TldrawApp }
7 7
 
@@ -19,9 +19,9 @@ export default function Editor({ id = 'home', isUser = false, isSponsor = false
19 19
   // Send events to gtag as actions.
20 20
   const handlePersist = React.useCallback((_app: TldrawApp, reason?: string) => {
21 21
     gtag.event({
22
-      action: reason,
22
+      action: reason ?? '',
23 23
       category: 'editor',
24
-      label: reason || 'persist',
24
+      label: reason ?? 'persist',
25 25
       value: 0,
26 26
     })
27 27
   }, [])

+ 3
- 3
apps/www/components/MultiplayerEditor.tsx View File

@@ -3,9 +3,9 @@ import * as React from 'react'
3 3
 import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
4 4
 import { createClient } from '@liveblocks/client'
5 5
 import { LiveblocksProvider, RoomProvider } from '@liveblocks/react'
6
-import { useAccountHandlers } from '-hooks/useAccountHandlers'
7
-import { styled } from '-styles'
8
-import { useMultiplayerState } from '-hooks/useMultiplayerState'
6
+import { useAccountHandlers } from 'hooks/useAccountHandlers'
7
+import { styled } from 'styles'
8
+import { useMultiplayerState } from 'hooks/useMultiplayerState'
9 9
 
10 10
 const client = createClient({
11 11
   publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_API_KEY || '',

+ 16
- 7
apps/www/hooks/useMultiplayerState.ts View File

@@ -129,10 +129,17 @@ export function useMultiplayerState(roomId: string) {
129 129
                 page: { shapes, bindings },
130 130
               },
131 131
             },
132
-          } = doc.toObject()
132
+          } = doc.toObject() as { document: TDDocument }
133 133
 
134
-          Object.values(shapes).forEach((shape) => lShapes.set(shape.id, shape))
135
-          Object.values(bindings).forEach((binding) => lBindings.set(binding.id, binding))
134
+          for (const key in shapes) {
135
+            const shape = shapes[key]
136
+            lShapes.set(shape.id, shape)
137
+          }
138
+
139
+          for (const key in bindings) {
140
+            const binding = bindings[key]
141
+            lBindings.set(binding.id, binding)
142
+          }
136 143
         }
137 144
       }
138 145
 
@@ -175,21 +182,23 @@ export function useMultiplayerState(roomId: string) {
175 182
 
176 183
         if (!(lShapes && lBindings)) return
177 184
 
178
-        Object.entries(shapes).forEach(([id, shape]) => {
185
+        for (const id in shapes) {
186
+          const shape = shapes[id]
179 187
           if (!shape) {
180 188
             lShapes.delete(id)
181 189
           } else {
182 190
             lShapes.set(shape.id, shape)
183 191
           }
184
-        })
192
+        }
185 193
 
186
-        Object.entries(bindings).forEach(([id, binding]) => {
194
+        for (const id in bindings) {
195
+          const binding = bindings[id]
187 196
           if (!binding) {
188 197
             lBindings.delete(id)
189 198
           } else {
190 199
             lBindings.set(binding.id, binding)
191 200
           }
192
-        })
201
+        }
193 202
 
194 203
         rExpectingUpdate.current = true
195 204
       })

+ 2
- 1
apps/www/package.json View File

@@ -25,6 +25,7 @@
25 25
     "@sentry/react": "^6.13.2",
26 26
     "@sentry/tracing": "^6.13.2",
27 27
     "@stitches/react": "^1.2.5",
28
+    "@tldraw/core": "^1.1.4",
28 29
     "@tldraw/tldraw": "^1.1.4",
29 30
     "@types/next-auth": "^3.15.0",
30 31
     "next": "^12.0.1",
@@ -42,7 +43,7 @@
42 43
     "cors": "^2.8.5",
43 44
     "eslint": "7.32.0",
44 45
     "eslint-config-next": "11.1.2",
45
-    "typescript": "^4.4.2"
46
+    "typescript": "^4.5.2"
46 47
   },
47 48
   "gitHead": "838fabdbff1a66d4d7ee8aa5c5d117bc55acbff2"
48 49
 }

+ 5
- 3
apps/www/pages/_app.tsx View File

@@ -1,7 +1,9 @@
1 1
 import '../styles/globals.css'
2
-import { init } from '-utils/sentry'
3 2
 import Head from 'next/head'
4
-import useGtag from '-utils/useGtag'
3
+import useGtag from 'utils/useGtag'
4
+import { init } from 'utils/sentry'
5
+import type { AppProps } from 'next/app'
6
+import type React from 'react'
5 7
 
6 8
 init()
7 9
 
@@ -10,7 +12,7 @@ const APP_DESCRIPTION = 'A tiny little drawing app.'
10 12
 const APP_URL = 'https://tldraw.com'
11 13
 const IMAGE = 'https://tldraw.com/social-image.png'
12 14
 
13
-function MyApp({ Component, pageProps }) {
15
+function MyApp({ Component, pageProps }: AppProps) {
14 16
   useGtag()
15 17
 
16 18
   return (

+ 12
- 22
apps/www/pages/_document.tsx View File

@@ -1,29 +1,19 @@
1 1
 import NextDocument, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
2
-import { getCssText } from '../styles'
3
-import { GA_TRACKING_ID } from '../utils/gtag'
2
+import { getCssText } from 'styles'
3
+import { GA_TRACKING_ID } from 'utils/gtag'
4 4
 
5 5
 class MyDocument extends NextDocument {
6
-  static async getInitialProps(ctx: DocumentContext): Promise<{
7
-    styles: JSX.Element
8
-    html: string
9
-    head?: JSX.Element[]
10
-  }> {
11
-    try {
12
-      const initialProps = await NextDocument.getInitialProps(ctx)
6
+  static async getInitialProps(ctx: DocumentContext) {
7
+    const initialProps = await NextDocument.getInitialProps(ctx)
13 8
 
14
-      return {
15
-        ...initialProps,
16
-        styles: (
17
-          <>
18
-            {initialProps.styles}
19
-            <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
20
-          </>
21
-        ),
22
-      }
23
-    } catch (e) {
24
-      console.error(e.message)
25
-    } finally {
26
-      null
9
+    return {
10
+      ...initialProps,
11
+      styles: (
12
+        <>
13
+          {initialProps.styles}
14
+          <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
15
+        </>
16
+      ),
27 17
     }
28 18
   }
29 19
 

+ 9
- 7
apps/www/pages/api/auth/[...nextauth].ts View File

@@ -1,5 +1,5 @@
1
-import { isSponsoringMe } from '-utils/isSponsoringMe'
2
-import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
1
+import { isSponsoringMe } from 'utils/isSponsoringMe'
2
+import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
3 3
 import NextAuth from 'next-auth'
4 4
 import Providers from 'next-auth/providers'
5 5
 
@@ -20,13 +20,15 @@ export default function Auth(
20 20
         return baseUrl
21 21
       },
22 22
       async signIn(user, account, profile: { login?: string }) {
23
-        const canLogin = await isSponsoringMe(profile?.login)
23
+        if (profile?.login) {
24
+          const canLogin = await isSponsoringMe(profile.login)
24 25
 
25
-        if (canLogin) {
26
-          return canLogin
27
-        } else {
28
-          return '/sponsorware'
26
+          if (canLogin) {
27
+            return canLogin
28
+          }
29 29
         }
30
+
31
+        return '/'
30 32
       },
31 33
     },
32 34
   })

+ 1
- 1
apps/www/pages/api/sponsors.ts View File

@@ -1,4 +1,4 @@
1
-import { NextApiRequest, NextApiResponse } from 'next'
1
+import type { NextApiRequest, NextApiResponse } from 'next'
2 2
 
3 3
 const AV_SIZE = 32
4 4
 const PADDING = 4

+ 2
- 2
apps/www/pages/index.tsx View File

@@ -1,9 +1,9 @@
1 1
 import dynamic from 'next/dynamic'
2
-import { GetServerSideProps } from 'next'
2
+import type { GetServerSideProps } from 'next'
3 3
 import { getSession } from 'next-auth/client'
4 4
 import Head from 'next/head'
5 5
 
6
-const Editor = dynamic(() => import('-components/Editor'), { ssr: false })
6
+const Editor = dynamic(() => import('components/Editor'), { ssr: false })
7 7
 
8 8
 interface PageProps {
9 9
   isUser: boolean

+ 1
- 1
apps/www/pages/r/[id].tsx View File

@@ -2,7 +2,7 @@ import * as React from 'react'
2 2
 import type { GetServerSideProps } from 'next'
3 3
 import { getSession } from 'next-auth/client'
4 4
 import dynamic from 'next/dynamic'
5
-const MultiplayerEditor = dynamic(() => import('-components/MultiplayerEditor'), { ssr: false })
5
+const MultiplayerEditor = dynamic(() => import('components/MultiplayerEditor'), { ssr: false })
6 6
 
7 7
 interface RoomProps {
8 8
   id: string

+ 1
- 1
apps/www/pages/sponsorware.tsx View File

@@ -1,6 +1,6 @@
1 1
 import { styled } from 'styles'
2 2
 import { getSession, signin, signout, useSession } from 'next-auth/client'
3
-import { GetServerSideProps } from 'next'
3
+import type { GetServerSideProps } from 'next'
4 4
 import Link from 'next/link'
5 5
 import React from 'react'
6 6
 import Head from 'next/head'

+ 18
- 9
apps/www/tsconfig.json View File

@@ -1,28 +1,37 @@
1 1
 {
2 2
   "compilerOptions": {
3
-    "composite": true,
3
+    "composite": false,
4
+    "incremental": false,
5
+    "resolveJsonModule": true,
4 6
     "target": "es6",
5
-    "lib": ["dom", "dom.iterable", "esnext"],
7
+    "lib": ["dom", "esnext"],
6 8
     "allowJs": true,
7 9
     "skipLibCheck": true,
8 10
     "strict": false,
9 11
     "forceConsistentCasingInFileNames": true,
10 12
     "noEmit": true,
13
+    "emitDeclarationOnly": false,
11 14
     "esModuleInterop": true,
12 15
     "module": "esnext",
13 16
     "moduleResolution": "node",
14 17
     "isolatedModules": true,
15 18
     "jsx": "preserve",
16
-    "baseUrl": ".",
17 19
     "rootDir": ".",
20
+    "baseUrl": ".",
18 21
     "paths": {
19
-      "-*": ["./*"],
20
-      "@tldraw/core": ["../../packages/core"],
21
-      "@tldraw/tldraw": ["../../packages/tldraw"]
22
-    },
23
-    "incremental": true,
24
-    "resolveJsonModule": true
22
+      "*": ["./*"],
23
+      "@tldraw/tldraw": ["./packages/tldraw"],
24
+      "@tldraw/core": ["./packages/core"],
25
+      "@tldraw/intersect": ["./packages/intersect"],
26
+      "@tldraw/vec": ["./packages/vec"]
27
+    }
25 28
   },
29
+  "references": [
30
+    { "path": "../../packages/vec" },
31
+    { "path": "../../packages/intersect" },
32
+    { "path": "../../packages/core" },
33
+    { "path": "../../packages/tldraw" }
34
+  ],
26 35
   "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
27 36
   "exclude": ["node_modules"]
28 37
 }

+ 1
- 6
apps/www/types.ts View File

@@ -1,6 +1 @@
1
-import { TDDocument } from '@tldraw/tldraw'
2
-import { LiveObject } from '@liveblocks/client'
3
-
4
-export interface TldrawStorage {
5
-  doc: LiveObject<{ uuid: string; document: TDDocument }>
6
-}
1
+export {}

+ 3
- 3
apps/www/utils/sentry.ts View File

@@ -13,11 +13,11 @@ export function init(): void {
13 13
     integrations.push(
14 14
       new RewriteFrames({
15 15
         iteratee: (frame) => {
16
-          frame.filename = frame.filename.replace(
17
-            process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR,
16
+          frame.filename = frame?.filename?.replace(
17
+            process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR as string,
18 18
             'app:///'
19 19
           )
20
-          frame.filename = frame.filename.replace('.next', '_next')
20
+          frame.filename = frame?.filename?.replace('.next', '_next')
21 21
           return frame
22 22
         },
23 23
       })

+ 4
- 4
examples/core-example-advanced/package.json View File

@@ -18,9 +18,9 @@
18 18
   "devDependencies": {
19 19
     "@state-designer/react": "3.0.0",
20 20
     "@stitches/react": "^1.2.5",
21
-    "@tldraw/core": "^1.1.3",
22
-    "@tldraw/intersect": "latest",
23
-    "@tldraw/vec": "latest",
21
+    "@tldraw/core": "^1.1.4",
22
+    "@tldraw/intersect": "^1.1.4",
23
+    "@tldraw/vec": "^1.1.4",
24 24
     "@types/node": "^14.14.35",
25 25
     "@types/react": "^16.9.55",
26 26
     "@types/react-dom": "^16.9.9",
@@ -40,4 +40,4 @@
40 40
     "typescript": "4.2.3"
41 41
   },
42 42
   "gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
43
-}
43
+}

+ 2
- 0
examples/core-example-advanced/src/shapes/CustomShapeUtil.ts View File

@@ -8,6 +8,8 @@ export abstract class CustomShapeUtil<
8 8
 
9 9
   canBind = false
10 10
 
11
+  hideBounds = false
12
+
11 13
   abstract getCenter: (shape: T) => number[]
12 14
 
13 15
   abstract getShape: (shape: Partial<T>) => T

+ 1
- 1
examples/core-example-advanced/src/state/actions/camera/pinchCamera.ts View File

@@ -8,6 +8,6 @@ export const pinchCamera: Action = (data, payload: TLPointerInfo) => {
8 8
   const nextPoint = Vec.sub(camera.point, Vec.div(payload.delta, camera.zoom))
9 9
   const p0 = Vec.sub(Vec.div(payload.point, camera.zoom), nextPoint)
10 10
   const p1 = Vec.sub(Vec.div(payload.point, nextZoom), nextPoint)
11
-  data.pageState.camera.point = Vec.round(Vec.add(nextPoint, Vec.sub(p1, p0)))
11
+  data.pageState.camera.point = Vec.toFixed(Vec.add(nextPoint, Vec.sub(p1, p0)))
12 12
   data.pageState.camera.zoom = nextZoom
13 13
 }

+ 1
- 1
examples/core-example-advanced/src/state/actions/camera/zoomIn.ts View File

@@ -10,7 +10,7 @@ export const zoomIn: Action = (data) => {
10 10
   const center = [mutables.rendererBounds.width / 2, mutables.rendererBounds.height / 2]
11 11
   const p0 = Vec.sub(Vec.div(center, camera.zoom), center)
12 12
   const p1 = Vec.sub(Vec.div(center, zoom), center)
13
-  const point = Vec.round(Vec.add(camera.point, Vec.sub(p1, p0)))
13
+  const point = Vec.toFixed(Vec.add(camera.point, Vec.sub(p1, p0)))
14 14
 
15 15
   data.pageState.camera.zoom = zoom
16 16
   data.pageState.camera.point = point

+ 1
- 1
examples/core-example-advanced/src/state/actions/camera/zoomOut.ts View File

@@ -10,7 +10,7 @@ export const zoomOut: Action = (data) => {
10 10
   const center = [mutables.rendererBounds.width / 2, mutables.rendererBounds.height / 2]
11 11
   const p0 = Vec.sub(Vec.div(center, camera.zoom), center)
12 12
   const p1 = Vec.sub(Vec.div(center, zoom), center)
13
-  const point = Vec.round(Vec.add(camera.point, Vec.sub(p1, p0)))
13
+  const point = Vec.toFixed(Vec.add(camera.point, Vec.sub(p1, p0)))
14 14
 
15 15
   data.pageState.camera.zoom = zoom
16 16
   data.pageState.camera.point = point

+ 1
- 1
examples/core-example-advanced/src/state/helpers.ts View File

@@ -38,5 +38,5 @@ export function getZoomFitCamera(
38 38
 export function getZoomedCameraPoint(nextZoom: number, center: number[], pageState: TLPageState) {
39 39
   const p0 = Vec.sub(Vec.div(center, pageState.camera.zoom), pageState.camera.point)
40 40
   const p1 = Vec.sub(Vec.div(center, nextZoom), pageState.camera.point)
41
-  return Vec.round(Vec.add(pageState.camera.point, Vec.sub(p1, p0)))
41
+  return Vec.toFixed(Vec.add(pageState.camera.point, Vec.sub(p1, p0)))
42 42
 }

+ 2
- 1
examples/core-example-advanced/tsconfig.json View File

@@ -4,10 +4,11 @@
4 4
   "exclude": ["node_modules", "dist", "docs"],
5 5
   "compilerOptions": {
6 6
     "outDir": "./dist/types",
7
+    "baseUrl": ".",
7 8
     "rootDir": "src",
8
-    "baseUrl": "src",
9 9
     "emitDeclarationOnly": false,
10 10
     "paths": {
11
+      "*": ["src/*"],
11 12
       "@tldraw/core": ["../../packages/core"]
12 13
     }
13 14
   },

+ 4
- 4
examples/core-example/package.json View File

@@ -1,5 +1,5 @@
1 1
 {
2
-  "version": "1.1.3",
2
+  "version": "1.1.4",
3 3
   "name": "@tldraw/core-example-simple",
4 4
   "description": "A simple example project for @tldraw/core.",
5 5
   "author": "@steveruizok",
@@ -15,8 +15,8 @@
15 15
   },
16 16
   "files": [],
17 17
   "devDependencies": {
18
-    "@tldraw/core": "^1.1.3",
19
-    "@tldraw/vec": "^0.0.130",
18
+    "@tldraw/core": "^1.1.4",
19
+    "@tldraw/vec": "^1.1.4",
20 20
     "@types/node": "^14.14.35",
21 21
     "@types/react": "^16.9.55",
22 22
     "@types/react-dom": "^16.9.9",
@@ -30,4 +30,4 @@
30 30
     "typescript": "4.2.3"
31 31
   },
32 32
   "gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
33
-}
33
+}

+ 2
- 1
examples/core-example/tsconfig.json View File

@@ -4,10 +4,11 @@
4 4
   "exclude": ["node_modules", "dist", "docs"],
5 5
   "compilerOptions": {
6 6
     "outDir": "./dist/types",
7
+    "baseUrl": ".",
7 8
     "rootDir": "src",
8
-    "baseUrl": "src",
9 9
     "emitDeclarationOnly": false,
10 10
     "paths": {
11
+      "*": ["src/*"],
11 12
       "@tldraw/core": ["../packages/core"]
12 13
     }
13 14
   },

+ 2
- 2
examples/tldraw-example/tsconfig.json View File

@@ -4,11 +4,11 @@
4 4
   "exclude": ["node_modules", "dist", "docs"],
5 5
   "compilerOptions": {
6 6
     "outDir": "./dist/types",
7
+    "baseUrl": ".",
7 8
     "rootDir": "src",
8
-    "baseUrl": "src",
9 9
     "emitDeclarationOnly": false,
10 10
     "paths": {
11
-      "+*": ["./*"],
11
+      "~*": ["./src/*"],
12 12
       "@tldraw/tldraw": ["../../packages/tldraw"]
13 13
     }
14 14
   },

+ 6
- 4
package.json View File

@@ -9,8 +9,10 @@
9 9
   },
10 10
   "license": "MIT",
11 11
   "workspaces": [
12
-    "packages/tldraw",
12
+    "packages/vec",
13
+    "packages/intersect",
13 14
     "packages/core",
15
+    "packages/tldraw",
14 16
     "apps/www",
15 17
     "apps/electron",
16 18
     "apps/vscode/editor",
@@ -38,7 +40,7 @@
38 40
     "test:watch": "lerna run test:watch --stream",
39 41
     "docs": "lerna run typedoc",
40 42
     "docs:watch": "lerna run typedoc --watch",
41
-    "postinstall": "husky install & yarn build:packages"
43
+    "postinstall": "husky install"
42 44
   },
43 45
   "devDependencies": {
44 46
     "@swc-node/jest": "^1.3.3",
@@ -62,7 +64,7 @@
62 64
     "resize-observer-polyfill": "^1.5.1",
63 65
     "tslib": "^2.3.0",
64 66
     "typedoc": "^0.22.3",
65
-    "typescript": "^4.4.2"
67
+    "typescript": "^4.5.2"
66 68
   },
67 69
   "husky": {
68 70
     "hooks": {
@@ -73,4 +75,4 @@
73 75
   "lint-staged": {
74 76
     "*": "fix:style && eslint"
75 77
   }
76
-}
78
+}

+ 4
- 4
packages/core/package.json View File

@@ -1,5 +1,5 @@
1 1
 {
2
-  "version": "1.1.3",
2
+  "version": "1.1.4",
3 3
   "name": "@tldraw/core",
4 4
   "description": "The tldraw core renderer and utilities.",
5 5
   "author": "@steveruizok",
@@ -37,8 +37,8 @@
37 37
     "test:watch": "jest --watchAll"
38 38
   },
39 39
   "dependencies": {
40
-    "@tldraw/intersect": "latest",
41
-    "@tldraw/vec": "latest",
40
+    "@tldraw/intersect": "^1.1.4",
41
+    "@tldraw/vec": "^1.1.4",
42 42
     "@use-gesture/react": "^10.1.3"
43 43
   },
44 44
   "peerDependencies": {
@@ -81,4 +81,4 @@
81 81
       "\\~(.*)": "<rootDir>/src/$1"
82 82
     }
83 83
   }
84
-}
84
+}

+ 1
- 1
packages/core/src/TLShapeUtil/TLShapeUtil.tsx View File

@@ -1,8 +1,8 @@
1 1
 /* eslint-disable @typescript-eslint/no-explicit-any */
2 2
 import * as React from 'react'
3 3
 import Utils from '../utils'
4
-import { intersectPolylineBounds } from '@tldraw/intersect'
5 4
 import type { TLBounds, TLComponentProps, TLForwardedRef, TLShape, TLUser } from '../types'
5
+import { intersectPolylineBounds } from '@tldraw/intersect'
6 6
 
7 7
 export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M = any> {
8 8
   refMap = new Map<string, React.RefObject<E>>()

+ 6
- 0
packages/core/src/components/canvas/canvas.tsx View File

@@ -18,6 +18,7 @@ import { useResizeObserver } from '~hooks/useResizeObserver'
18 18
 import { inputs } from '~inputs'
19 19
 import { UsersIndicators } from '~components/users-indicators'
20 20
 import { SnapLines } from '~components/snap-lines/snap-lines'
21
+import { Grid } from '~components/grid'
21 22
 import { Overlay } from '~components/overlay'
22 23
 
23 24
 function resetError() {
@@ -28,6 +29,7 @@ interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
28 29
   page: TLPage<T, TLBinding>
29 30
   pageState: TLPageState
30 31
   snapLines?: TLSnapLine[]
32
+  grid?: number
31 33
   users?: TLUsers<T>
32 34
   userId?: string
33 35
   hideBounds: boolean
@@ -37,6 +39,7 @@ interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
37 39
   hideCloneHandles: boolean
38 40
   hideResizeHandles: boolean
39 41
   hideRotateHandle: boolean
42
+  hideGrid: boolean
40 43
   externalContainerRef?: React.RefObject<HTMLElement>
41 44
   meta?: M
42 45
   id?: string
@@ -48,6 +51,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
48 51
   page,
49 52
   pageState,
50 53
   snapLines,
54
+  grid,
51 55
   users,
52 56
   userId,
53 57
   meta,
@@ -59,6 +63,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
59 63
   hideCloneHandles,
60 64
   hideResizeHandles,
61 65
   hideRotateHandle,
66
+  hideGrid,
62 67
   onBoundsChange,
63 68
 }: CanvasProps<T, M>): JSX.Element {
64 69
   const rCanvas = React.useRef<HTMLDivElement>(null)
@@ -85,6 +90,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
85 90
     <div id={id} className="tl-container" ref={rContainer}>
86 91
       <div id="canvas" className="tl-absolute tl-canvas" ref={rCanvas} {...events}>
87 92
         <ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
93
+          {!hideGrid && grid && <Grid grid={grid} camera={pageState.camera} />}
88 94
           <div ref={rLayer} className="tl-absolute tl-layer">
89 95
             <Page
90 96
               page={page}

+ 42
- 0
packages/core/src/components/grid/grid.tsx View File

@@ -0,0 +1,42 @@
1
+import * as React from 'react'
2
+import type { TLPageState } from '~types'
3
+import Utils from '~utils'
4
+
5
+const STEPS = [
6
+  [-1, 0.15, 64],
7
+  [0.05, 0.375, 16],
8
+  [0.15, 1, 4],
9
+  [0.7, 2.5, 1],
10
+]
11
+
12
+export function Grid({ grid, camera }: { camera: TLPageState['camera']; grid: number }) {
13
+  return (
14
+    <svg className="tl-grid" version="1.1" xmlns="http://www.w3.org/2000/svg">
15
+      <defs>
16
+        {STEPS.map(([min, mid, size], i) => {
17
+          const s = size * grid * camera.zoom
18
+          const xo = camera.point[0] * camera.zoom
19
+          const yo = camera.point[1] * camera.zoom
20
+          const gxo = xo > 0 ? xo % s : s + (xo % s)
21
+          const gyo = yo > 0 ? yo % s : s + (yo % s)
22
+          const opacity = camera.zoom < mid ? Utils.modulate(camera.zoom, [min, mid], [0, 1]) : 1
23
+
24
+          return (
25
+            <pattern
26
+              key={`grid-pattern-${i}`}
27
+              id={`grid-${i}`}
28
+              width={s}
29
+              height={s}
30
+              patternUnits="userSpaceOnUse"
31
+            >
32
+              <circle className={`tl-grid-dot`} cx={gxo} cy={gyo} r={1} opacity={opacity} />
33
+            </pattern>
34
+          )
35
+        })}
36
+      </defs>
37
+      {STEPS.map((_, i) => (
38
+        <rect key={`grid-rect-${i}`} width="100%" height="100%" fill={`url(#grid-${i})`} />
39
+      ))}
40
+    </svg>
41
+  )
42
+}

+ 1
- 0
packages/core/src/components/grid/index.ts View File

@@ -0,0 +1 @@
1
+export * from './grid'

+ 12
- 0
packages/core/src/components/renderer/renderer.tsx View File

@@ -86,6 +86,14 @@ export interface RendererProps<T extends TLShape, M = any> extends Partial<TLCal
86 86
    * hovered objects,
87 87
    */
88 88
   hideIndicators?: boolean
89
+  /**
90
+   * (optional) When true, the renderer will not show the grid.
91
+   */
92
+  hideGrid?: boolean
93
+  /**
94
+   * (optional) The size of the grid step.
95
+   */
96
+  grid?: number
89 97
   /**
90 98
    * (optional) A callback that receives the renderer's inputs manager.
91 99
    */
@@ -114,6 +122,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
114 122
   theme,
115 123
   meta,
116 124
   snapLines,
125
+  grid,
117 126
   containerRef,
118 127
   hideHandles = false,
119 128
   hideIndicators = false,
@@ -122,6 +131,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
122 131
   hideResizeHandles = false,
123 132
   hideRotateHandles = false,
124 133
   hideBounds = false,
134
+  hideGrid = true,
125 135
   ...rest
126 136
 }: RendererProps<T, M>): JSX.Element {
127 137
   useTLTheme(theme, '#' + id)
@@ -164,6 +174,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
164 174
         page={page}
165 175
         pageState={pageState}
166 176
         snapLines={snapLines}
177
+        grid={grid}
167 178
         users={users}
168 179
         userId={userId}
169 180
         externalContainerRef={containerRef}
@@ -174,6 +185,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
174 185
         hideBindingHandles={hideBindingHandles}
175 186
         hideRotateHandle={hideRotateHandles}
176 187
         hideResizeHandles={hideResizeHandles}
188
+        hideGrid={hideGrid}
177 189
         onBoundsChange={onBoundsChange}
178 190
         meta={meta}
179 191
       />

+ 14
- 0
packages/core/src/hooks/useStyle.tsx View File

@@ -73,6 +73,7 @@ const defaultTheme: TLTheme = {
73 73
   selectFill: 'rgba(65, 132, 244, 0.05)',
74 74
   background: 'rgb(248, 249, 250)',
75 75
   foreground: 'rgb(51, 51, 51)',
76
+  grid: 'rgba(144, 144, 144, 1)',
76 77
 }
77 78
 
78 79
 const tlcss = css`
@@ -142,6 +143,15 @@ const tlcss = css`
142 143
     pointer-events: none;
143 144
   }
144 145
 
146
+  .tl-grid {
147
+    position: absolute;
148
+    width: 100%;
149
+    height: 100%;
150
+    touch-action: none;
151
+    pointer-events: none;
152
+    user-select: none;
153
+  }
154
+
145 155
   .tl-snap-line {
146 156
     stroke: var(--tl-accent);
147 157
     stroke-width: calc(1px * var(--tl-scale));
@@ -394,6 +404,10 @@ const tlcss = css`
394 404
     stroke: var(--tl-selectStroke);
395 405
     stroke-width: calc(2px * var(--tl-scale));
396 406
   }
407
+
408
+  .tl-grid-dot {
409
+    fill: var(--tl-grid);
410
+  }
397 411
 `
398 412
 
399 413
 export function useTLTheme(theme?: Partial<TLTheme>, selector?: string) {

+ 1
- 1
packages/core/src/inputs.ts View File

@@ -360,7 +360,7 @@ export class Inputs {
360 360
       target: 'pinch',
361 361
       origin,
362 362
       delta: delta,
363
-      point: Vec.sub(Vec.round(point), [this.bounds.minX, this.bounds.minY]),
363
+      point: Vec.sub(Vec.toFixed(point), [this.bounds.minX, this.bounds.minY]),
364 364
       pressure: 0.5,
365 365
       shiftKey,
366 366
       ctrlKey,

+ 1
- 0
packages/core/src/types.ts View File

@@ -108,6 +108,7 @@ export interface TLTheme {
108 108
   selectStroke?: string
109 109
   background?: string
110 110
   foreground?: string
111
+  grid?: string
111 112
 }
112 113
 
113 114
 export type TLWheelEventHandler = (

+ 21
- 3
packages/core/src/utils/utils.ts View File

@@ -1089,6 +1089,26 @@ export class Utils {
1089 1089
     return this.translateBounds(bounds, [dx, dy])
1090 1090
   }
1091 1091
 
1092
+  /**
1093
+   * Snap a bounding box to a grid size.
1094
+   * @param bounds
1095
+   * @param gridSize
1096
+   */
1097
+  static snapBoundsToGrid(bounds: TLBounds, gridSize: number): TLBounds {
1098
+    const minX = Math.round(bounds.minX / gridSize) * gridSize
1099
+    const minY = Math.round(bounds.minY / gridSize) * gridSize
1100
+    const maxX = Math.round(bounds.maxX / gridSize) * gridSize
1101
+    const maxY = Math.round(bounds.maxY / gridSize) * gridSize
1102
+    return {
1103
+      minX,
1104
+      minY,
1105
+      maxX,
1106
+      maxY,
1107
+      width: Math.max(1, maxX - minX),
1108
+      height: Math.max(1, maxY - minY),
1109
+    }
1110
+  }
1111
+
1092 1112
   /**
1093 1113
    * Move a bounding box without recalculating it.
1094 1114
    * @param bounds
@@ -1509,12 +1529,10 @@ left past the initial left edge) then swap points on that axis.
1509 1529
       (isFlippedX
1510 1530
         ? initialBounds.maxX - initialShapeBounds.maxX
1511 1531
         : initialShapeBounds.minX - initialBounds.minX) / initialBounds.width
1512
-
1513 1532
     const ny =
1514 1533
       (isFlippedY
1515 1534
         ? initialBounds.maxY - initialShapeBounds.maxY
1516 1535
         : initialShapeBounds.minY - initialBounds.minY) / initialBounds.height
1517
-
1518 1536
     const nw = initialShapeBounds.width / initialBounds.width
1519 1537
     const nh = initialShapeBounds.height / initialBounds.height
1520 1538
 
@@ -1562,7 +1580,7 @@ left past the initial left edge) then swap points on that axis.
1562 1580
    * Get a bounding box with a midX and midY.
1563 1581
    * @param bounds
1564 1582
    */
1565
-  static getBoundsWithCenter(bounds: TLBounds): TLBounds & { midX: number; midY: number } {
1583
+  static getBoundsWithCenter(bounds: TLBounds): TLBoundsWithCenter {
1566 1584
     const center = Utils.getBoundsCenter(bounds)
1567 1585
     return {
1568 1586
       ...bounds,

+ 5
- 4
packages/core/tsconfig.build.json View File

@@ -13,8 +13,9 @@
13 13
   "compilerOptions": {
14 14
     "composite": false,
15 15
     "incremental": false,
16
-    "declarationMap": false,
17
-    "sourceMap": false,
18
-    "emitDeclarationOnly": true
19
-  }
16
+    "declaration": true,
17
+    "declarationMap": true,
18
+    "sourceMap": true
19
+  },
20
+  "references": [{ "path": "../vec" }, { "path": "../intersect" }]
20 21
 }

+ 3
- 3
packages/core/tsconfig.json View File

@@ -1,15 +1,15 @@
1 1
 {
2 2
   "extends": "../../tsconfig.base.json",
3
-  "include": ["src"],
4 3
   "exclude": ["node_modules", "dist", "docs"],
5 4
   "compilerOptions": {
6 5
     "outDir": "./dist/types",
7 6
     "rootDir": "src",
8
-    "baseUrl": "src",
7
+    "baseUrl": ".",
9 8
     "paths": {
10
-      "~*": ["./*"]
9
+      "~*": ["./src/*"]
11 10
     }
12 11
   },
12
+  "references": [{ "path": "../vec" }, { "path": "../intersect" }],
13 13
   "typedocOptions": {
14 14
     "entryPoints": ["src/index.ts"],
15 15
     "out": "docs"

+ 9
- 0
packages/intersect/CHANGELOG.md View File

@@ -0,0 +1,9 @@
1
+# Changelog
2
+
3
+## 0.1.4
4
+
5
+- Fixes bug in `polyline`, adds `polygon` intersections.
6
+
7
+## 0.1.0
8
+
9
+- Hello world.

+ 21
- 0
packages/intersect/LICENSE.md View File

@@ -0,0 +1,21 @@
1
+MIT License
2
+
3
+Copyright (c) 2021 Stephen Ruiz Ltd
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.

+ 477
- 0
packages/intersect/README.md View File

@@ -0,0 +1,477 @@
1
+<div style="text-align: center; transform: scale(.5);">
2
+  <img src="card-repo.png"/>
3
+</div>
4
+
5
+# @tldraw/core
6
+
7
+This package contains the `Renderer` and core utilities used by [tldraw](https://tldraw.com).
8
+
9
+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/).
10
+
11
+💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
12
+
13
+## Installation
14
+
15
+Use your package manager of choice to install `@tldraw/core` and its peer dependencies.
16
+
17
+```bash
18
+yarn add @tldraw/core
19
+# or
20
+npm i @tldraw/core
21
+```
22
+
23
+## Examples
24
+
25
+There are two examples in this repository.
26
+
27
+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.
28
+
29
+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.
30
+
31
+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.
32
+
33
+## Usage
34
+
35
+Import the `Renderer` React component and pass it the required props.
36
+
37
+```tsx
38
+import * as React from "react"
39
+import { Renderer, TLShape, TLShapeUtil, Vec } from '@tldraw/core'
40
+import { BoxShape, BoxUtil } from "./shapes/box"
41
+
42
+const shapeUtils = { box: new BoxUtil() }
43
+
44
+function App() {
45
+  const [page, setPage] = React.useState({
46
+    id: "page"
47
+    shapes: {
48
+      "box1": {
49
+        id: 'box1',
50
+        type: 'box',
51
+        parentId: 'page',
52
+        childIndex: 0,
53
+        point: [0, 0],
54
+        size: [100, 100],
55
+        rotation: 0,
56
+      }
57
+    },
58
+    bindings: {}
59
+  })
60
+
61
+  const [pageState, setPageState] = React.useState({
62
+    id: "page",
63
+    selectedIds: [],
64
+    camera: {
65
+      point: [0,0],
66
+      zoom: 1
67
+    }
68
+  })
69
+
70
+  return (<Renderer
71
+    page={page}
72
+    pageState={pageState}
73
+    shapeUtils={shapeUtils}
74
+  />)
75
+}
76
+```
77
+
78
+## Documentation
79
+
80
+### `Renderer`
81
+
82
+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`.
83
+
84
+| Prop         | Type                            | Description                                    |
85
+| ------------ | ------------------------------- | ---------------------------------------------- |
86
+| `page`       | [`TLPage`](#tlpage)             | The current page object.                       |
87
+| `pageState`  | [`TLPageState`](#tlpagestate)   | The current page's state.                      |
88
+| `shapeUtils` | [`TLShapeUtils`](#tlshapeutils) | The shape utilities used to render the shapes. |
89
+
90
+In addition to these required props, the Renderer accents many other **optional** props.
91
+
92
+| Property             | Type                          | Description                                                       |
93
+| -------------------- | ----------------------------- | ----------------------------------------------------------------- |
94
+| `containerRef`       | `React.MutableRefObject`      | A React ref for the container, where CSS variables will be added. |
95
+| `theme`              | `object`                      | An object with overrides for the Renderer's default colors.       |
96
+| `hideBounds`         | `boolean`                     | Do not show the bounding box for selected shapes.                 |
97
+| `hideHandles`        | `boolean`                     | Do not show handles for shapes with handles.                      |
98
+| `hideBindingHandles` | `boolean`                     | Do not show binding controls for selected shapes with bindings.   |
99
+| `hideResizeHandles`  | `boolean`                     | Do not show resize handles for selected shapes.                   |
100
+| `hideRotateHandles`  | `boolean`                     | Do not show rotate handles for selected shapes.                   |
101
+| `snapLines`          | [`TLSnapLine`](#tlsnapline)[] | An array of "snap" lines.                                         |
102
+| `users`              | `object`                      | A table of [`TLUser`](#tluser)s.                                  |
103
+| `userId`             | `object`                      | The current user's [`TLUser`](#tluser) id.                        |
104
+
105
+The theme object accepts valid CSS colors for the following properties:
106
+
107
+| Property       | Description                                          |
108
+| -------------- | ---------------------------------------------------- |
109
+| `foreground`   | The primary (usually "text") color                   |
110
+| `background`   | The default page's background color                  |
111
+| `brushFill`    | The fill color of the brush selection box            |
112
+| `brushStroke`  | The stroke color of the brush selection box          |
113
+| `selectFill`   | The fill color of the selection bounds               |
114
+| `selectStroke` | The stroke color of the selection bounds and handles |
115
+
116
+The Renderer also accepts many (optional) event callbacks.
117
+
118
+| Prop                        | Description                                                 |
119
+| --------------------------- | ----------------------------------------------------------- |
120
+| `onPan`                     | Panned with the mouse wheel                                 |
121
+| `onZoom`                    | Zoomed with the mouse wheel                                 |
122
+| `onPinchStart`              | Began a two-pointer pinch                                   |
123
+| `onPinch`                   | Moved their pointers during a pinch                         |
124
+| `onPinchEnd`                | Stopped a two-pointer pinch                                 |
125
+| `onPointerDown`             | Started pointing                                            |
126
+| `onPointerMove`             | Moved their pointer                                         |
127
+| `onPointerUp`               | Ended a point                                               |
128
+| `onPointCanvas`             | Pointed the canvas                                          |
129
+| `onDoubleClickCanvas`       | Double-pointed the canvas                                   |
130
+| `onRightPointCanvas`        | Right-pointed the canvas                                    |
131
+| `onDragCanvas`              | Dragged the canvas                                          |
132
+| `onReleaseCanvas`           | Stopped pointing the canvas                                 |
133
+| `onHoverShape`              | Moved their pointer onto a shape                            |
134
+| `onUnhoverShape`            | Moved their pointer off of a shape                          |
135
+| `onPointShape`              | Pointed a shape                                             |
136
+| `onDoubleClickShape`        | Double-pointed a shape                                      |
137
+| `onRightPointShape`         | Right-pointed a shape                                       |
138
+| `onDragShape`               | Dragged a shape                                             |
139
+| `onReleaseShape`            | Stopped pointing a shape                                    |
140
+| `onHoverHandle`             | Moved their pointer onto a shape handle                     |
141
+| `onUnhoverHandle`           | Moved their pointer off of a shape handle                   |
142
+| `onPointHandle`             | Pointed a shape handle                                      |
143
+| `onDoubleClickHandle`       | Double-pointed a shape handle                               |
144
+| `onRightPointHandle`        | Right-pointed a shape handle                                |
145
+| `onDragHandle`              | Dragged a shape handle                                      |
146
+| `onReleaseHandle`           | Stopped pointing shape handle                               |
147
+| `onHoverBounds`             | Moved their pointer onto the selection bounds               |
148
+| `onUnhoverBounds`           | Moved their pointer off of the selection bounds             |
149
+| `onPointBounds`             | Pointed the selection bounds                                |
150
+| `onDoubleClickBounds`       | Double-pointed the selection bounds                         |
151
+| `onRightPointBounds`        | Right-pointed the selection bounds                          |
152
+| `onDragBounds`              | Dragged the selection bounds                                |
153
+| `onReleaseBounds`           | Stopped the selection bounds                                |
154
+| `onHoverBoundsHandle`       | Moved their pointer onto a selection bounds handle          |
155
+| `onUnhoverBoundsHandle`     | Moved their pointer off of a selection bounds handle        |
156
+| `onPointBoundsHandle`       | Pointed a selection bounds handle                           |
157
+| `onDoubleClickBoundsHandle` | Double-pointed a selection bounds handle                    |
158
+| `onRightPointBoundsHandle`  | Right-pointed a selection bounds handle                     |
159
+| `onDragBoundsHandle`        | Dragged a selection bounds handle                           |
160
+| `onReleaseBoundsHandle`     | Stopped a selection bounds handle                           |
161
+| `onShapeClone`              | Clicked on a shape's clone handle                           |
162
+| `onShapeChange`             | A shape's component prompted a change                       |
163
+| `onShapeBlur`               | A shape's component was prompted a blur                     |
164
+| `onRenderCountChange`       | The number of rendered shapes changed                       |
165
+| `onBoundsChange`            | The Renderer's screen bounding box of the component changed |
166
+| `onError`                   | The Renderer encountered an error                           |
167
+
168
+The `@tldraw/core` library provides types for most of the event handlers:
169
+
170
+| Type                         |
171
+| ---------------------------- |
172
+| `TLPinchEventHandler`        |
173
+| `TLPointerEventHandler`      |
174
+| `TLCanvasEventHandler`       |
175
+| `TLBoundsEventHandler`       |
176
+| `TLBoundsHandleEventHandler` |
177
+| `TLShapeChangeHandler`       |
178
+| `TLShapeBlurHandler`         |
179
+| `TLShapeCloneHandler`        |
180
+
181
+### `TLPage`
182
+
183
+An object describing the current page. It contains:
184
+
185
+| Property          | Type                        | Description                                                                 |
186
+| ----------------- | --------------------------- | --------------------------------------------------------------------------- |
187
+| `id`              | `string`                    | A unique id for the page.                                                   |
188
+| `shapes`          | [`TLShape{}`](#tlshape)     | A table of shapes.                                                          |
189
+| `bindings`        | [`TLBinding{}`](#tlbinding) | A table of bindings.                                                        |
190
+| `backgroundColor` | `string`                    | (optional) The page's background fill color. Will also overwrite the theme. |
191
+
192
+### `TLPageState`
193
+
194
+An object describing the current page. It contains:
195
+
196
+| Property       | Type       | Description                                         |
197
+| -------------- | ---------- | --------------------------------------------------- |
198
+| `id`           | `string`   | The corresponding page's id                         |
199
+| `selectedIds`  | `string[]` | An array of selected shape ids                      |
200
+| `camera`       | `object`   | An object describing the camera state               |
201
+| `camera.point` | `number[]` | The camera's `[x, y]` coordinates                   |
202
+| `camera.zoom`  | `number`   | The camera's zoom level                             |
203
+| `pointedId`    | `string`   | (optional) The currently pointed shape id           |
204
+| `hoveredId`    | `string`   | (optional) The currently hovered shape id           |
205
+| `editingId`    | `string`   | (optional) The currently editing shape id           |
206
+| `bindingId`    | `string`   | (optional) The currently editing binding.           |
207
+| `brush`        | `TLBounds` | (optional) A `Bounds` for the current selection box |
208
+
209
+### `TLShape`
210
+
211
+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).
212
+
213
+| Property              | Type       | Description                                                                           |
214
+| --------------------- | ---------- | ------------------------------------------------------------------------------------- |
215
+| `id`                  | `string`   | The shape's id.                                                                       |
216
+| `type`                | `string`   | The type of the shape, corresponding to the `type` of a [`TLShapeUtil`](#tlshapeutil) |
217
+| `parentId`            | `string`   | The id of the shape's parent (either the current page or another shape)               |
218
+| `childIndex`          | `number`   | the order of the shape among its parent's children                                    |
219
+| `name`                | `string`   | the name of the shape                                                                 |
220
+| `point`               | `number[]` | the shape's current `[x, y]` coordinates on the page                                  |
221
+| `rotation`            | `number`   | (optiona) The shape's current rotation in radians                                     |
222
+| `children`            | `string[]` | (optional) An array containing the ids of this shape's children                       |
223
+| `handles`             | `{}`       | (optional) A table of [`TLHandle`](#tlhandle) objects                                 |
224
+| `isGhost`             | `boolean`  | (optional) True if the shape is "ghosted", e.g. while deleting                        |
225
+| `isLocked`            | `boolean`  | (optional) True if the shape is locked                                                |
226
+| `isHidden`            | `boolean`  | (optional) True if the shape is hidden                                                |
227
+| `isEditing`           | `boolean`  | (optional) True if the shape is currently editing                                     |
228
+| `isGenerated`         | `boolean`  | optional) True if the shape is generated programatically                              |
229
+| `isAspectRatioLocked` | `boolean`  | (optional) True if the shape's aspect ratio is locked                                 |
230
+
231
+### `TLHandle`
232
+
233
+An object that describes a relationship between two shapes on the page.
234
+
235
+| Property | Type       | Description                                   |
236
+| -------- | ---------- | --------------------------------------------- |
237
+| `id`     | `string`   | An id for the handle                          |
238
+| `index`  | `number`   | The handle's order within the shape's handles |
239
+| `point`  | `number[]` | The handle's `[x, y]` coordinates             |
240
+
241
+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
242
+
243
+### `TLBinding`
244
+
245
+An object that describes a relationship between two shapes on the page.
246
+
247
+| Property | Type     | Description                                  |
248
+| -------- | -------- | -------------------------------------------- |
249
+| `id`     | `string` | A unique id for the binding                  |
250
+| `fromId` | `string` | The id of the shape where the binding begins |
251
+| `toId`   | `string` | The id of the shape where the binding begins |
252
+
253
+### `TLSnapLine`
254
+
255
+A snapline is an array of points (formatted as `[x, y]`) that represent a "snapping" line.
256
+
257
+### `TLShapeUtil`
258
+
259
+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.
260
+
261
+### `TLUser`
262
+
263
+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.
264
+
265
+| Property        | Type       | Description                             |
266
+| --------------- | ---------- | --------------------------------------- |
267
+| `id`            | `string`   | A unique id for the user                |
268
+| `color`         | `string`   | The user's color, used for indicators   |
269
+| `point`         | `number[]` | The user's pointer location on the page |
270
+| `selectedIds[]` | `string[]` | The user's selected shape ids           |
271
+
272
+### `Utils`
273
+
274
+A general purpose utility class. See source for more.
275
+
276
+## Guide: Creating Shapes
277
+
278
+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.
279
+
280
+> For several example shapes, see the folder `/example/src/shapes/`.
281
+
282
+### Shape Type
283
+
284
+Your first task is to define an interface for the shape that extends `TLShape`. It must have a `type` property.
285
+
286
+```ts
287
+// BoxShape.ts
288
+import type { TLShape } from '@tldraw/core'
289
+
290
+export interface BoxShape extends TLShape {
291
+  type: 'box'
292
+  size: number[]
293
+}
294
+```
295
+
296
+### Component
297
+
298
+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.
299
+
300
+```tsx
301
+// BoxComponent.ts
302
+
303
+import * as React from 'react'
304
+import { shapeComponent, SVGContainer } from '@tldraw/core'
305
+import type { BoxShape } from './BoxShape'
306
+
307
+export const BoxComponent = TLShapeUtil.Component<BoxShape, SVGSVGElement>(
308
+  ({ shape, events, meta }, ref) => {
309
+    const color = meta.isDarkMode ? 'white' : 'black'
310
+
311
+    return (
312
+      <SVGContainer ref={ref} {...events}>
313
+        <rect
314
+          width={shape.size[0]}
315
+          height={shape.size[1]}
316
+          stroke={color}
317
+          strokeWidth={2}
318
+          strokeLinejoin="round"
319
+          fill="none"
320
+          pointerEvents="all"
321
+        />
322
+      </SVGContainer>
323
+    )
324
+  }
325
+)
326
+```
327
+
328
+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.
329
+
330
+The component will receive the following props:
331
+
332
+| Name                | Type       | Description                                                        |
333
+| ------------------- | ---------- | ------------------------------------------------------------------ |
334
+| `shape`             | `TLShape`  | The shape from `page.shapes` that is being rendered                |
335
+| `meta`              | `{}`       | The value provided to the `Renderer`'s `meta` prop                 |
336
+| `events`            | `{}`       | Several pointer events that should be set on the container element |
337
+| `isSelected`        | `boolean`  | The shape is selected (its `id` is in `pageState.selectedIds`)     |
338
+| `isHovered`         | `boolean`  | The shape is hovered (its `id` is `pageState.hoveredId`)           |
339
+| `isEditing`         | `boolean`  | The shape is being edited (its `id` is `pageState.editingId`)      |
340
+| `isGhost`           | `boolean`  | The shape is ghosted or is the child of a ghosted shape.           |
341
+| `isChildOfSelected` | `boolean`  | The shape is the child of a selected shape.                        |
342
+| `onShapeChange`     | `Function` | The callback provided to the `Renderer`'s `onShapeChange` prop     |
343
+| `onShapeBlur`       | `Function` | The callback provided to the `Renderer`'s `onShapeBlur` prop       |
344
+
345
+### Indicator
346
+
347
+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.
348
+
349
+```tsx
350
+// BoxIndicator.ts
351
+
352
+export const BoxIndicator = TLShapeUtil.Indicator<BoxShape>(({ shape }) => {
353
+  return (
354
+    <rect
355
+      fill="none"
356
+      stroke="dodgerblue"
357
+      strokeWidth={1}
358
+      width={shape.size[0]}
359
+      height={shape.size[1]}
360
+    />
361
+  )
362
+})
363
+```
364
+
365
+The indicator component will receive the following props:
366
+
367
+| Name         | Type      | Description                                                                            |
368
+| ------------ | --------- | -------------------------------------------------------------------------------------- |
369
+| `shape`      | `TLShape` | The shape from `page.shapes` that is being rendered                                    |
370
+| `meta`       | {}        | The value provided to the `Renderer`'s `meta` prop                                     |
371
+| `user`       | `TLUser`  | The user when shown in a multiplayer session                                           |
372
+| `isSelected` | `boolean` | Whether the current shape is selected (true if its `id` is in `pageState.selectedIds`) |
373
+| `isHovered`  | `boolean` | Whether the current shape is hovered (true if its `id` is `pageState.hoveredId`)       |
374
+
375
+### ShapeUtil
376
+
377
+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.
378
+
379
+```ts
380
+// BoxUtil.ts
381
+
382
+import { Utils, TLBounds, TLShapeUtil } from '@tldraw/core'
383
+import { BoxComponent } from './BoxComponent'
384
+import { BoxIndicator } from './BoxIndicator'
385
+import type { BoxShape } from './BoxShape'
386
+
387
+export class BoxUtil extends TLShapeUtil<BoxShape, SVGSVGElement> {
388
+  Component = BoxComponent
389
+
390
+  Indicator = BoxIndicator
391
+
392
+  getBounds = (shape: BoxShape): TLBounds => {
393
+    const [width, height] = shape.size
394
+
395
+    const bounds = {
396
+      minX: 0,
397
+      maxX: width,
398
+      minY: 0,
399
+      maxY: height,
400
+      width,
401
+      height,
402
+    }
403
+
404
+    return Utils.translateBounds(bounds, shape.point)
405
+  }
406
+}
407
+```
408
+
409
+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.
410
+
411
+You may also set the following fields:
412
+
413
+| Name               | Type      | Default | Description                                                                                           |
414
+| ------------------ | --------- | ------- | ----------------------------------------------------------------------------------------------------- |
415
+| `showCloneHandles` | `boolean` | `false` | Whether to display clone handles when the shape is the only selected shape                            |
416
+| `hideBounds`       | `boolean` | `false` | Whether to hide the bounds when the shape is the only selected shape                                  |
417
+| `isStateful`       | `boolean` | `false` | Whether the shape has its own React state. When true, the shape will not be unmounted when off-screen |
418
+
419
+### ShapeUtils Object
420
+
421
+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.
422
+
423
+```tsx
424
+// App.tsx
425
+
426
+const shapeUtils = {
427
+  box: new BoxUtil(),
428
+  circle: new CircleUtil(),
429
+  text: new TextUtil(),
430
+}
431
+
432
+export function App() {
433
+  // ...
434
+
435
+  return <Renderer page={page} pageState={pageState} {...etc} shapeUtils={shapeUtils} />
436
+}
437
+```
438
+
439
+## Local Development
440
+
441
+To start the development servers for the package and the advanced example:
442
+
443
+- Run `yarn` to install dependencies.
444
+- Run `yarn start`.
445
+- Open `localhost:5420`.
446
+
447
+You can also run:
448
+
449
+- `start:advanced` to start development servers for the package and the advanced example.
450
+- `start:simple` to start development servers for the package and the simple example.
451
+- `test` to execute unit tests via [Jest](https://jestjs.io).
452
+- `docs` to build the docs via [ts-doc](https://typedoc.org/).
453
+- `build` to build the package.
454
+
455
+## Example
456
+
457
+See the `example` folder or this [CodeSandbox](https://codesandbox.io/s/laughing-elion-gp0kx) example.
458
+
459
+## Community
460
+
461
+### Support
462
+
463
+Need help? Please [open an issue](https://github.com/tldraw/tldraw/issues/new) for support.
464
+
465
+### Discussion
466
+
467
+Want to connect with other devs? Visit the [Discord channel](https://discord.gg/SBBEVCA4PG).
468
+
469
+### License
470
+
471
+This project is licensed under MIT.
472
+
473
+If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
474
+
475
+## Author
476
+
477
+- [@steveruizok](https://twitter.com/steveruizok)

BIN
packages/intersect/card-repo.png View File


+ 39
- 0
packages/intersect/package.json View File

@@ -0,0 +1,39 @@
1
+{
2
+  "version": "1.1.4",
3
+  "name": "@tldraw/intersect",
4
+  "description": "2D intersection utilities for TLDraw and maybe you, too.",
5
+  "author": "@steveruizok",
6
+  "repository": {
7
+    "type": "git",
8
+    "url": "git+https://github.com/tldraw/tldraw.git"
9
+  },
10
+  "license": "MIT",
11
+  "keywords": [
12
+    "2d",
13
+    "vector",
14
+    "intersection",
15
+    "typescript",
16
+    "javascript"
17
+  ],
18
+  "files": [
19
+    "dist/**/*"
20
+  ],
21
+  "main": "./dist/cjs/index.js",
22
+  "module": "./dist/esm/index.js",
23
+  "types": "./dist/types/index.d.ts",
24
+  "scripts": {
25
+    "start:packages": "yarn start",
26
+    "start:core": "yarn start",
27
+    "start": "node scripts/dev & yarn types:dev",
28
+    "build:packages": "yarn build",
29
+    "build:core": "yarn build",
30
+    "build": "node scripts/build && yarn types:build",
31
+    "types:dev": "tsc -w --p tsconfig.build.json",
32
+    "types:build": "tsc -p tsconfig.build.json",
33
+    "lint": "eslint src/ --ext .ts,.tsx",
34
+    "clean": "rm -rf dist"
35
+  },
36
+  "dependencies": {
37
+    "@tldraw/vec": "^1.1.4"
38
+  }
39
+}

+ 63
- 0
packages/intersect/scripts/build.js View File

@@ -0,0 +1,63 @@
1
+/* eslint-disable */
2
+const fs = require('fs')
3
+const esbuild = require('esbuild')
4
+const { gzip } = require('zlib')
5
+const pkg = require('../package.json')
6
+
7
+async function main() {
8
+  if (fs.existsSync('./dist')) {
9
+    fs.rmSync('./dist', { recursive: true }, (e) => {
10
+      if (e) {
11
+        throw e
12
+      }
13
+    })
14
+  }
15
+
16
+  try {
17
+    esbuild.buildSync({
18
+      entryPoints: ['./src/index.ts'],
19
+      outdir: 'dist/cjs',
20
+      minify: false,
21
+      bundle: true,
22
+      format: 'cjs',
23
+      target: 'es6',
24
+      tsconfig: './tsconfig.build.json',
25
+      external: Object.keys(pkg.dependencies),
26
+      metafile: true,
27
+      sourcemap: true,
28
+    })
29
+
30
+    const esmResult = esbuild.buildSync({
31
+      entryPoints: ['./src/index.ts'],
32
+      outdir: 'dist/esm',
33
+      minify: false,
34
+      bundle: true,
35
+      format: 'esm',
36
+      target: 'es6',
37
+      tsconfig: './tsconfig.build.json',
38
+      external: Object.keys(pkg.dependencies),
39
+      metafile: true,
40
+      sourcemap: true,
41
+    })
42
+
43
+    let esmSize = 0
44
+    Object.values(esmResult.metafile.outputs).forEach((output) => {
45
+      esmSize += output.bytes
46
+    })
47
+
48
+    fs.readFile('./dist/esm/index.js', (_err, data) => {
49
+      gzip(data, (_err, result) => {
50
+        console.log(
51
+          `✔ ${pkg.name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${(
52
+            result.length / 1000
53
+          ).toFixed(2)}kb minified)`
54
+        )
55
+      })
56
+    })
57
+  } catch (e) {
58
+    console.log(`× ${pkg.name}: Build failed due to an error.`)
59
+    console.log(e)
60
+  }
61
+}
62
+
63
+main()

+ 29
- 0
packages/intersect/scripts/dev.js View File

@@ -0,0 +1,29 @@
1
+/* eslint-disable */
2
+const esbuild = require('esbuild')
3
+const pkg = require('../package.json')
4
+
5
+async function main() {
6
+  try {
7
+    await esbuild.build({
8
+      entryPoints: ['src/index.tsx'],
9
+      outfile: 'dist/index.js',
10
+      bundle: true,
11
+      minify: false,
12
+      sourcemap: true,
13
+      incremental: true,
14
+      target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
15
+      define: {
16
+        'process.env.NODE_ENV': '"development"',
17
+      },
18
+      watch: {
19
+        onRebuild(err) {
20
+          err ? error('❌ Failed') : log('✅ Updated')
21
+        },
22
+      },
23
+    })
24
+  } catch (err) {
25
+    process.exit(1)
26
+  }
27
+}
28
+
29
+main()

+ 430
- 0
packages/intersect/src/index.d.ts View File

@@ -0,0 +1,430 @@
1
+export declare type TLIntersection = {
2
+    didIntersect: boolean;
3
+    message: string;
4
+    points: number[][];
5
+};
6
+export interface TLBounds {
7
+    minX: number;
8
+    minY: number;
9
+    maxX: number;
10
+    maxY: number;
11
+    width: number;
12
+    height: number;
13
+    rotation?: number;
14
+}
15
+/**
16
+ * Find the intersection between a ray and a ray.
17
+ * @param p0 The first ray's point
18
+ * @param n0 The first ray's direction vector.
19
+ * @param p1 The second ray's point.
20
+ * @param n1 The second ray's direction vector.
21
+ */
22
+export declare function intersectRayRay(p0: number[], n0: number[], p1: number[], n1: number[]): TLIntersection;
23
+/**
24
+ * Find the intersections between a ray and a line segment.
25
+ * @param origin
26
+ * @param direction
27
+ * @param a1
28
+ * @param a2
29
+ */
30
+export declare function intersectRayLineSegment(origin: number[], direction: number[], a1: number[], a2: number[]): TLIntersection;
31
+/**
32
+ * Find the intersections between a ray and a rectangle.
33
+ * @param origin
34
+ * @param direction
35
+ * @param point
36
+ * @param size
37
+ * @param rotation
38
+ */
39
+export declare function intersectRayRectangle(origin: number[], direction: number[], point: number[], size: number[], rotation?: number): TLIntersection[];
40
+/**
41
+ * Find the intersections between a ray and an ellipse.
42
+ * @param origin
43
+ * @param direction
44
+ * @param center
45
+ * @param rx
46
+ * @param ry
47
+ * @param rotation
48
+ */
49
+export declare function intersectRayEllipse(origin: number[], direction: number[], center: number[], rx: number, ry: number, rotation: number): TLIntersection;
50
+/**
51
+ * Find the intersections between a ray and a bounding box.
52
+ * @param origin
53
+ * @param direction
54
+ * @param bounds
55
+ * @param rotation
56
+ */
57
+export declare function intersectRayBounds(origin: number[], direction: number[], bounds: TLBounds, rotation?: number): TLIntersection[];
58
+/**
59
+ * Find the intersection between a line segment and a ray.
60
+ * @param a1
61
+ * @param a2
62
+ * @param origin
63
+ * @param direction
64
+ */
65
+export declare function intersectLineSegmentRay(a1: number[], a2: number[], origin: number[], direction: number[]): TLIntersection;
66
+/**
67
+ * Find the intersection between a line segment and a line segment.
68
+ * @param a1
69
+ * @param a2
70
+ * @param b1
71
+ * @param b2
72
+ */
73
+export declare function intersectLineSegmentLineSegment(a1: number[], a2: number[], b1: number[], b2: number[]): TLIntersection;
74
+/**
75
+ * Find the intersections between a line segment and a rectangle.
76
+ * @param a1
77
+ * @param a2
78
+ * @param point
79
+ * @param size
80
+ */
81
+export declare function intersectLineSegmentRectangle(a1: number[], a2: number[], point: number[], size: number[]): TLIntersection[];
82
+/**
83
+ * Find the intersections between a line segment and an arc.
84
+ * @param a1
85
+ * @param a2
86
+ * @param center
87
+ * @param radius
88
+ * @param start
89
+ * @param end
90
+ */
91
+export declare function intersectLineSegmentArc(a1: number[], a2: number[], center: number[], radius: number, start: number[], end: number[]): TLIntersection;
92
+/**
93
+ * Find the intersections between a line segment and a circle.
94
+ * @param a1
95
+ * @param a2
96
+ * @param c
97
+ * @param r
98
+ */
99
+export declare function intersectLineSegmentCircle(a1: number[], a2: number[], c: number[], r: number): TLIntersection;
100
+/**
101
+ * Find the intersections between a line segment and an ellipse.
102
+ * @param a1
103
+ * @param a2
104
+ * @param center
105
+ * @param rx
106
+ * @param ry
107
+ * @param rotation
108
+ */
109
+export declare function intersectLineSegmentEllipse(a1: number[], a2: number[], center: number[], rx: number, ry: number, rotation?: number): TLIntersection;
110
+/**
111
+ * Find the intersections between a line segment and a bounding box.
112
+ * @param a1
113
+ * @param a2
114
+ * @param bounds
115
+ */
116
+export declare function intersectLineSegmentBounds(a1: number[], a2: number[], bounds: TLBounds): TLIntersection[];
117
+/**
118
+ * Find the intersections between a line segment and a polyline.
119
+ * @param a1
120
+ * @param a2
121
+ * @param points
122
+ */
123
+export declare function intersectLineSegmentPolyline(a1: number[], a2: number[], points: number[][]): TLIntersection;
124
+/**
125
+ * Find the intersections between a line segment and a closed polygon.
126
+ * @param a1
127
+ * @param a2
128
+ * @param points
129
+ */
130
+export declare function intersectLineSegmentPolygon(a1: number[], a2: number[], points: number[][]): TLIntersection;
131
+/**
132
+ * Find the intersections between a rectangle and a ray.
133
+ * @param point
134
+ * @param size
135
+ * @param rotation
136
+ * @param origin
137
+ * @param direction
138
+ */
139
+export declare function intersectRectangleRay(point: number[], size: number[], rotation: number, origin: number[], direction: number[]): TLIntersection[];
140
+/**
141
+ * Find the intersections between a rectangle and a line segment.
142
+ * @param point
143
+ * @param size
144
+ * @param a1
145
+ * @param a2
146
+ */
147
+export declare function intersectRectangleLineSegment(point: number[], size: number[], a1: number[], a2: number[]): TLIntersection[];
148
+/**
149
+ * Find the intersections between a rectangle and a rectangle.
150
+ * @param point1
151
+ * @param size1
152
+ * @param point2
153
+ * @param size2
154
+ */
155
+export declare function intersectRectangleRectangle(point1: number[], size1: number[], point2: number[], size2: number[]): TLIntersection[];
156
+/**
157
+ * Find the intersections between a rectangle and an arc.
158
+ * @param point
159
+ * @param size
160
+ * @param center
161
+ * @param radius
162
+ * @param start
163
+ * @param end
164
+ */
165
+export declare function intersectRectangleArc(point: number[], size: number[], center: number[], radius: number, start: number[], end: number[]): TLIntersection[];
166
+/**
167
+ * Find the intersections between a rectangle and a circle.
168
+ * @param point
169
+ * @param size
170
+ * @param c
171
+ * @param r
172
+ */
173
+export declare function intersectRectangleCircle(point: number[], size: number[], c: number[], r: number): TLIntersection[];
174
+/**
175
+ * Find the intersections between a rectangle and an ellipse.
176
+ * @param point
177
+ * @param size
178
+ * @param c
179
+ * @param rx
180
+ * @param ry
181
+ * @param rotation
182
+ */
183
+export declare function intersectRectangleEllipse(point: number[], size: number[], c: number[], rx: number, ry: number, rotation?: number): TLIntersection[];
184
+/**
185
+ * Find the intersections between a rectangle and a bounding box.
186
+ * @param point
187
+ * @param size
188
+ * @param bounds
189
+ */
190
+export declare function intersectRectangleBounds(point: number[], size: number[], bounds: TLBounds): TLIntersection[];
191
+/**
192
+ * Find the intersections between a rectangle and a polyline.
193
+ * @param point
194
+ * @param size
195
+ * @param points
196
+ */
197
+export declare function intersectRectanglePolyline(point: number[], size: number[], points: number[][]): TLIntersection[];
198
+/**
199
+ * Find the intersections between a rectangle and a polygon.
200
+ * @param point
201
+ * @param size
202
+ * @param points
203
+ */
204
+export declare function intersectRectanglePolygon(point: number[], size: number[], points: number[][]): TLIntersection[];
205
+/**
206
+ * Find the intersections between a arc and a line segment.
207
+ * @param center
208
+ * @param radius
209
+ * @param start
210
+ * @param end
211
+ * @param a1
212
+ * @param a2
213
+ */
214
+export declare function intersectArcLineSegment(center: number[], radius: number, start: number[], end: number[], a1: number[], a2: number[]): TLIntersection;
215
+/**
216
+ * Find the intersections between a arc and a rectangle.
217
+ * @param center
218
+ * @param radius
219
+ * @param start
220
+ * @param end
221
+ * @param point
222
+ * @param size
223
+ */
224
+export declare function intersectArcRectangle(center: number[], radius: number, start: number[], end: number[], point: number[], size: number[]): TLIntersection[];
225
+/**
226
+ * Find the intersections between a arc and a bounding box.
227
+ * @param center
228
+ * @param radius
229
+ * @param start
230
+ * @param end
231
+ * @param bounds
232
+ */
233
+export declare function intersectArcBounds(center: number[], radius: number, start: number[], end: number[], bounds: TLBounds): TLIntersection[];
234
+/**
235
+ * Find the intersections between a circle and a line segment.
236
+ * @param c
237
+ * @param r
238
+ * @param a1
239
+ * @param a2
240
+ */
241
+export declare function intersectCircleLineSegment(c: number[], r: number, a1: number[], a2: number[]): TLIntersection;
242
+/**
243
+ * Find the intersections between a circle and a circle.
244
+ * @param c1
245
+ * @param r1
246
+ * @param c2
247
+ * @param r2
248
+ */
249
+export declare function intersectCircleCircle(c1: number[], r1: number, c2: number[], r2: number): TLIntersection;
250
+/**
251
+ * Find the intersections between a circle and a rectangle.
252
+ * @param c
253
+ * @param r
254
+ * @param point
255
+ * @param size
256
+ */
257
+export declare function intersectCircleRectangle(c: number[], r: number, point: number[], size: number[]): TLIntersection[];
258
+/**
259
+ * Find the intersections between a circle and a bounding box.
260
+ * @param c
261
+ * @param r
262
+ * @param bounds
263
+ */
264
+export declare function intersectCircleBounds(c: number[], r: number, bounds: TLBounds): TLIntersection[];
265
+/**
266
+ * Find the intersections between an ellipse and a ray.
267
+ * @param center
268
+ * @param rx
269
+ * @param ry
270
+ * @param rotation
271
+ * @param point
272
+ * @param direction
273
+ */
274
+export declare function intersectEllipseRay(center: number[], rx: number, ry: number, rotation: number, point: number[], direction: number[]): TLIntersection;
275
+/**
276
+ * Find the intersections between an ellipse and a line segment.
277
+ * @param center
278
+ * @param rx
279
+ * @param ry
280
+ * @param rotation
281
+ * @param a1
282
+ * @param a2
283
+ */
284
+export declare function intersectEllipseLineSegment(center: number[], rx: number, ry: number, rotation: number | undefined, a1: number[], a2: number[]): TLIntersection;
285
+/**
286
+ * Find the intersections between an ellipse and a rectangle.
287
+ * @param center
288
+ * @param rx
289
+ * @param ry
290
+ * @param rotation
291
+ * @param point
292
+ * @param size
293
+ */
294
+export declare function intersectEllipseRectangle(center: number[], rx: number, ry: number, rotation: number | undefined, point: number[], size: number[]): TLIntersection[];
295
+/**
296
+ * Find the intersections between an ellipse and an ellipse.
297
+ * Adapted from https://gist.github.com/drawable/92792f59b6ff8869d8b1
298
+ * @param _c1
299
+ * @param _rx1
300
+ * @param _ry1
301
+ * @param _r1
302
+ * @param _c2
303
+ * @param _rx2
304
+ * @param _ry2
305
+ * @param _r2
306
+ */
307
+export declare function intersectEllipseEllipse(_c1: number[], _rx1: number, _ry1: number, _r1: number, _c2: number[], _rx2: number, _ry2: number, _r2: number): TLIntersection;
308
+/**
309
+ * Find the intersections between an ellipse and a circle.
310
+ * @param c
311
+ * @param rx
312
+ * @param ry
313
+ * @param rotation
314
+ * @param c2
315
+ * @param r2
316
+ */
317
+export declare function intersectEllipseCircle(c: number[], rx: number, ry: number, rotation: number, c2: number[], r2: number): TLIntersection;
318
+/**
319
+ * Find the intersections between an ellipse and a bounding box.
320
+ * @param c
321
+ * @param rx
322
+ * @param ry
323
+ * @param rotation
324
+ * @param bounds
325
+ */
326
+export declare function intersectEllipseBounds(c: number[], rx: number, ry: number, rotation: number, bounds: TLBounds): TLIntersection[];
327
+/**
328
+ * Find the intersections between a bounding box and a ray.
329
+ * @param bounds
330
+ * @param origin
331
+ * @param direction
332
+ */
333
+export declare function intersectBoundsRay(bounds: TLBounds, origin: number[], direction: number[]): TLIntersection[];
334
+/**
335
+ * Find the intersections between a bounding box and a line segment.
336
+ * @param bounds
337
+ * @param a1
338
+ * @param a2
339
+ */
340
+export declare function intersectBoundsLineSegment(bounds: TLBounds, a1: number[], a2: number[]): TLIntersection[];
341
+/**
342
+ * Find the intersections between a bounding box and a rectangle.
343
+ * @param bounds
344
+ * @param point
345
+ * @param size
346
+ */
347
+export declare function intersectBoundsRectangle(bounds: TLBounds, point: number[], size: number[]): TLIntersection[];
348
+/**
349
+ * Find the intersections between a bounding box and a bounding box.
350
+ * @param bounds1
351
+ * @param bounds2
352
+ */
353
+export declare function intersectBoundsBounds(bounds1: TLBounds, bounds2: TLBounds): TLIntersection[];
354
+/**
355
+ * Find the intersections between a bounding box and an arc.
356
+ * @param bounds
357
+ * @param center
358
+ * @param radius
359
+ * @param start
360
+ * @param end
361
+ */
362
+export declare function intersectBoundsArc(bounds: TLBounds, center: number[], radius: number, start: number[], end: number[]): TLIntersection[];
363
+/**
364
+ * Find the intersections between a bounding box and a circle.
365
+ * @param bounds
366
+ * @param c
367
+ * @param r
368
+ */
369
+export declare function intersectBoundsCircle(bounds: TLBounds, c: number[], r: number): TLIntersection[];
370
+/**
371
+ * Find the intersections between a bounding box and an ellipse.
372
+ * @param bounds
373
+ * @param c
374
+ * @param rx
375
+ * @param ry
376
+ * @param rotation
377
+ */
378
+export declare function intersectBoundsEllipse(bounds: TLBounds, c: number[], rx: number, ry: number, rotation?: number): TLIntersection[];
379
+/**
380
+ * Find the intersections between a bounding box and a polyline.
381
+ * @param bounds
382
+ * @param points
383
+ */
384
+export declare function intersectBoundsPolyline(bounds: TLBounds, points: number[][]): TLIntersection[];
385
+/**
386
+ * Find the intersections between a bounding box and a polygon.
387
+ * @param bounds
388
+ * @param points
389
+ */
390
+export declare function intersectBoundsPolygon(bounds: TLBounds, points: number[][]): TLIntersection[];
391
+/**
392
+ * Find the intersections between a polyline and a line segment.
393
+ * @param points
394
+ * @param a1
395
+ * @param a2
396
+ */
397
+export declare function intersectPolylineLineSegment(points: number[][], a1: number[], a2: number[]): TLIntersection;
398
+/**
399
+ * Find the intersections between a polyline and a rectangle.
400
+ * @param points
401
+ * @param point
402
+ * @param size
403
+ */
404
+export declare function intersectPolylineRectangle(points: number[][], point: number[], size: number[]): TLIntersection[];
405
+/**
406
+ * Find the intersections between a polyline and a bounding box.
407
+ * @param points
408
+ * @param bounds
409
+ */
410
+export declare function intersectPolylineBounds(points: number[][], bounds: TLBounds): TLIntersection[];
411
+/**
412
+ * Find the intersections between a polygon nd a line segment.
413
+ * @param points
414
+ * @param a1
415
+ * @param a2
416
+ */
417
+export declare function intersectPolygonLineSegment(points: number[][], a1: number[], a2: number[]): TLIntersection;
418
+/**
419
+ * Find the intersections between a polygon and a rectangle.
420
+ * @param points
421
+ * @param point
422
+ * @param size
423
+ */
424
+export declare function intersectPolygonRectangle(points: number[][], point: number[], size: number[]): TLIntersection[];
425
+/**
426
+ * Find the intersections between a polygon and a bounding box.
427
+ * @param points
428
+ * @param bounds
429
+ */
430
+export declare function intersectPolygonBounds(points: number[][], bounds: TLBounds): TLIntersection[];

+ 1241
- 0
packages/intersect/src/index.ts
File diff suppressed because it is too large
View File


+ 21
- 0
packages/intersect/tsconfig.build.json View File

@@ -0,0 +1,21 @@
1
+{
2
+  "extends": "./tsconfig.json",
3
+  "exclude": [
4
+    "node_modules",
5
+    "**/*.test.tsx",
6
+    "**/*.test.ts",
7
+    "**/*.spec.tsx",
8
+    "**/*.spec.ts",
9
+    "src/test",
10
+    "dist",
11
+    "docs"
12
+  ],
13
+  "compilerOptions": {
14
+    "composite": false,
15
+    "incremental": false,
16
+    "declaration": true,
17
+    "declarationMap": true,
18
+    "sourceMap": true
19
+  },
20
+  "references": [{ "path": "../vec" }]
21
+}

+ 14
- 0
packages/intersect/tsconfig.json View File

@@ -0,0 +1,14 @@
1
+{
2
+  "extends": "../../tsconfig.base.json",
3
+  "exclude": ["node_modules", "dist", "docs"],
4
+  "compilerOptions": {
5
+    "outDir": "./dist/types",
6
+    "rootDir": "src",
7
+    "baseUrl": "."
8
+  },
9
+  "references": [{ "path": "../vec" }],
10
+  "typedocOptions": {
11
+    "entryPoints": ["src/index.ts"],
12
+    "out": "docs"
13
+  }
14
+}

+ 5
- 5
packages/tldraw/package.json View File

@@ -49,9 +49,9 @@
49 49
     "@radix-ui/react-radio-group": "^0.1.1",
50 50
     "@radix-ui/react-tooltip": "^0.1.1",
51 51
     "@stitches/react": "^1.2.5",
52
-    "@tldraw/core": "^1.1.3",
53
-    "@tldraw/intersect": "latest",
54
-    "@tldraw/vec": "latest",
52
+    "@tldraw/core": "^1.1.4",
53
+    "@tldraw/intersect": "^1.1.4",
54
+    "@tldraw/vec": "^1.1.4",
55 55
     "idb-keyval": "^6.0.3",
56 56
     "perfect-freehand": "^1.0.16",
57 57
     "react-hotkeys-hook": "^3.4.0",
@@ -62,7 +62,7 @@
62 62
     "@swc-node/jest": "^1.3.3",
63 63
     "@testing-library/jest-dom": "^5.14.1",
64 64
     "@testing-library/react": "^12.0.0",
65
-    "tsconfig-replace-paths": "^0.0.5"
65
+    "tsconfig-replace-paths": "^0.0.11"
66 66
   },
67 67
   "jest": {
68 68
     "setupFilesAfterEnv": [
@@ -91,4 +91,4 @@
91 91
     }
92 92
   },
93 93
   "gitHead": "325008ff82bd27b63d625ad1b760f8871fb71af9"
94
-}
94
+}

+ 5
- 2
packages/tldraw/src/Tldraw.tsx View File

@@ -8,9 +8,10 @@ import { TldrawContext, useStylesheet, useKeyboardShortcuts, useTldrawApp } from
8 8
 import { shapeUtils } from '~state/shapes'
9 9
 import { ToolsPanel } from '~components/ToolsPanel'
10 10
 import { TopPanel } from '~components/TopPanel'
11
-import { TLDR } from '~state/TLDR'
12 11
 import { ContextMenu } from '~components/ContextMenu'
13
-import { FocusButton } from '~components/FocusButton/FocusButton'
12
+import { FocusButton } from '~components/FocusButton'
13
+import { TLDR } from '~state/TLDR'
14
+import { GRID_SIZE } from '~constants'
14 15
 
15 16
 export interface TldrawProps extends TDCallbacks {
16 17
   /**
@@ -425,6 +426,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
425 426
           page={page}
426 427
           pageState={pageState}
427 428
           snapLines={appState.snapLines}
429
+          grid={GRID_SIZE}
428 430
           users={room?.users}
429 431
           userId={room?.userId}
430 432
           theme={theme}
@@ -436,6 +438,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
436 438
           hideBindingHandles={!settings.showBindingHandles}
437 439
           hideCloneHandles={!settings.showCloneHandles}
438 440
           hideRotateHandles={!settings.showRotateHandles}
441
+          hideGrid={!settings.showGrid}
439 442
           onPinchStart={app.onPinchStart}
440 443
           onPinchEnd={app.onPinchEnd}
441 444
           onPinch={app.onPinch}

+ 1
- 0
packages/tldraw/src/components/FocusButton/index.ts View File

@@ -0,0 +1 @@
1
+export * from './FocusButton'

+ 7
- 0
packages/tldraw/src/components/TopPanel/PreferencesMenu/PreferencesMenu.tsx View File

@@ -26,6 +26,10 @@ export function PreferencesMenu() {
26 26
     app.setSetting('showRotateHandles', (v) => !v)
27 27
   }, [app])
28 28
 
29
+  const toggleGrid = React.useCallback(() => {
30
+    app.setSetting('showGrid', (v) => !v)
31
+  }, [app])
32
+
29 33
   const toggleBoundShapesHandle = React.useCallback(() => {
30 34
     app.setSetting('showBindingHandles', (v) => !v)
31 35
   }, [app])
@@ -62,6 +66,9 @@ export function PreferencesMenu() {
62 66
       <DMCheckboxItem checked={settings.showCloneHandles} onCheckedChange={toggleCloneControls}>
63 67
         Clone Handles
64 68
       </DMCheckboxItem>
69
+      <DMCheckboxItem checked={settings.showGrid} onCheckedChange={toggleGrid} kbd="#⇧G">
70
+        Grid
71
+      </DMCheckboxItem>
65 72
       <DMCheckboxItem checked={settings.isSnapping} onCheckedChange={toggleisSnapping}>
66 73
         Always Show Snaps
67 74
       </DMCheckboxItem>

+ 1
- 0
packages/tldraw/src/constants.ts View File

@@ -1,4 +1,5 @@
1 1
 /* eslint-disable @typescript-eslint/no-explicit-any */
2
+export const GRID_SIZE = 8
2 3
 export const BINDING_DISTANCE = 24
3 4
 export const CLONING_DISTANCE = 32
4 5
 export const FIT_TO_SCREEN_PADDING = 128

+ 10
- 0
packages/tldraw/src/hooks/useKeyboardShortcuts.tsx View File

@@ -129,6 +129,16 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
129 129
     [app]
130 130
   )
131 131
 
132
+  useHotkeys(
133
+    'ctrl+shift+g,⌘+shift+g',
134
+    () => {
135
+      if (!canHandleEvent()) return
136
+      app.toggleGrid()
137
+    },
138
+    undefined,
139
+    [app]
140
+  )
141
+
132 142
   // File System
133 143
 
134 144
   const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()

+ 2
- 2
packages/tldraw/src/state/TLDR.ts View File

@@ -672,7 +672,7 @@ export class TLDR {
672 672
     const rotatedCenter = Vec.rotWith(center, origin, delta)
673 673
 
674 674
     // Get the top left point relative to the rotated center
675
-    const nextPoint = Vec.round(Vec.sub(rotatedCenter, relativeCenter))
675
+    const nextPoint = Vec.toFixed(Vec.sub(rotatedCenter, relativeCenter))
676 676
 
677 677
     // If the shape has handles, we need to rotate the handles instead
678 678
     // of rotating the shape. Shapes with handles should never be rotated,
@@ -685,7 +685,7 @@ export class TLDR {
685 685
           Object.entries(shape.handles).map(([handleId, handle]) => {
686 686
             // Rotate each handle's point around the shape's center
687 687
             // (in relative shape space, as the handle's point will be).
688
-            const point = Vec.round(Vec.rotWith(handle.point, relativeCenter, delta))
688
+            const point = Vec.toFixed(Vec.rotWith(handle.point, relativeCenter, delta))
689 689
             return [handleId, { ...handle, point }]
690 690
           })
691 691
         ) as T['handles'],

+ 50
- 18
packages/tldraw/src/state/TldrawApp.ts View File

@@ -48,7 +48,7 @@ import { defaultStyle } from '~state/shapes/shared/shape-styles'
48 48
 import * as Commands from './commands'
49 49
 import { SessionArgsOfType, getSession, TldrawSession } from './sessions'
50 50
 import type { BaseTool } from './tools/BaseTool'
51
-import { USER_COLORS, FIT_TO_SCREEN_PADDING } from '~constants'
51
+import { USER_COLORS, FIT_TO_SCREEN_PADDING, GRID_SIZE } from '~constants'
52 52
 import { SelectTool } from './tools/SelectTool'
53 53
 import { EraseTool } from './tools/EraseTool'
54 54
 import { TextTool } from './tools/TextTool'
@@ -790,6 +790,16 @@ export class TldrawApp extends StateManager<TDSnapshot> {
790 790
     return this
791 791
   }
792 792
 
793
+  /**
794
+   * Toggle grids.
795
+   */
796
+  toggleGrid = (): this => {
797
+    if (this.session) return this
798
+    this.patchState({ settings: { showGrid: !this.settings.showGrid } }, 'settings:toggled_grid')
799
+    this.persist()
800
+    return this
801
+  }
802
+
793 803
   /**
794 804
    * Select a tool.
795 805
    * @param tool The tool to select, or "select".
@@ -1472,17 +1482,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
1472 1482
 
1473 1483
       const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
1474 1484
 
1475
-      let center = Vec.round(this.getPagePoint(point || this.centerPoint))
1485
+      let center = Vec.toFixed(this.getPagePoint(point || this.centerPoint))
1476 1486
 
1477 1487
       if (
1478 1488
         Vec.dist(center, this.pasteInfo.center) < 2 ||
1479
-        Vec.dist(center, Vec.round(Utils.getBoundsCenter(commonBounds))) < 2
1489
+        Vec.dist(center, Vec.toFixed(Utils.getBoundsCenter(commonBounds))) < 2
1480 1490
       ) {
1481 1491
         center = Vec.add(center, this.pasteInfo.offset)
1482
-        this.pasteInfo.offset = Vec.add(this.pasteInfo.offset, [
1483
-          this.settings.nudgeDistanceLarge,
1484
-          this.settings.nudgeDistanceLarge,
1485
-        ])
1492
+        this.pasteInfo.offset = Vec.add(this.pasteInfo.offset, [GRID_SIZE, GRID_SIZE])
1486 1493
       } else {
1487 1494
         this.pasteInfo.center = center
1488 1495
         this.pasteInfo.offset = [0, 0]
@@ -1499,7 +1506,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
1499 1506
         shapesToPaste.map((shape) =>
1500 1507
           TLDR.getShapeUtil(shape.type).create({
1501 1508
             ...shape,
1502
-            point: Vec.round(Vec.add(shape.point, delta)),
1509
+            point: Vec.toFixed(Vec.add(shape.point, delta)),
1503 1510
             parentId: shape.parentId || this.currentPageId,
1504 1511
           })
1505 1512
         ),
@@ -1691,7 +1698,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
1691 1698
    */
1692 1699
   pan = (delta: number[]): this => {
1693 1700
     const { camera } = this.pageState
1694
-    return this.setCamera(Vec.round(Vec.sub(camera.point, delta)), camera.zoom, `panned`)
1701
+    return this.setCamera(Vec.toFixed(Vec.sub(camera.point, delta)), camera.zoom, `panned`)
1695 1702
   }
1696 1703
 
1697 1704
   /**
@@ -1706,7 +1713,11 @@ export class TldrawApp extends StateManager<TDSnapshot> {
1706 1713
     const nextZoom = zoom
1707 1714
     const p0 = Vec.sub(Vec.div(point, camera.zoom), nextPoint)
1708 1715
     const p1 = Vec.sub(Vec.div(point, nextZoom), nextPoint)
1709
-    return this.setCamera(Vec.round(Vec.add(nextPoint, Vec.sub(p1, p0))), nextZoom, `pinch_zoomed`)
1716
+    return this.setCamera(
1717
+      Vec.toFixed(Vec.add(nextPoint, Vec.sub(p1, p0))),
1718
+      nextZoom,
1719
+      `pinch_zoomed`
1720
+    )
1710 1721
   }
1711 1722
 
1712 1723
   /**
@@ -1718,7 +1729,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
1718 1729
     const { zoom, point } = this.pageState.camera
1719 1730
     const p0 = Vec.sub(Vec.div(center, zoom), point)
1720 1731
     const p1 = Vec.sub(Vec.div(center, next), point)
1721
-    return this.setCamera(Vec.round(Vec.add(point, Vec.sub(p1, p0))), next, `zoomed_camera`)
1732
+    return this.setCamera(Vec.toFixed(Vec.add(point, Vec.sub(p1, p0))), next, `zoomed_camera`)
1722 1733
   }
1723 1734
 
1724 1735
   /**
@@ -1767,7 +1778,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
1767 1778
     const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom
1768 1779
 
1769 1780
     return this.setCamera(
1770
-      Vec.round(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
1781
+      Vec.toFixed(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
1771 1782
       zoom,
1772 1783
       `zoomed_to_fit`
1773 1784
     )
@@ -1798,7 +1809,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
1798 1809
     const my = (rendererBounds.height - selectedBounds.height * zoom) / 2 / zoom
1799 1810
 
1800 1811
     return this.setCamera(
1801
-      Vec.round(Vec.sub([mx, my], [selectedBounds.minX, selectedBounds.minY])),
1812
+      Vec.toFixed(Vec.sub([mx, my], [selectedBounds.minX, selectedBounds.minY])),
1802 1813
       zoom,
1803 1814
       `zoomed_to_selection`
1804 1815
     )
@@ -1821,7 +1832,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
1821 1832
     const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom
1822 1833
 
1823 1834
     return this.setCamera(
1824
-      Vec.round(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
1835
+      Vec.toFixed(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
1825 1836
       this.pageState.camera.zoom,
1826 1837
       `zoomed_to_content`
1827 1838
     )
@@ -2119,6 +2130,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2119 2130
             status: TDStatus.Idle,
2120 2131
           },
2121 2132
           document: {
2133
+            ...result.document,
2122 2134
             pageStates: {
2123 2135
               [this.currentPageId]: {
2124 2136
                 ...result.document?.pageStates?.[this.currentPageId],
@@ -2359,7 +2371,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2359 2371
    */
2360 2372
   nudge = (delta: number[], isMajor = false, ids = this.selectedIds): this => {
2361 2373
     if (ids.length === 0) return this
2362
-    return this.setState(Commands.translateShapes(this, ids, Vec.mul(delta, isMajor ? 10 : 1)))
2374
+    const size = isMajor
2375
+      ? this.settings.showGrid
2376
+        ? this.currentGrid * 4
2377
+        : 10
2378
+      : this.settings.showGrid
2379
+      ? this.currentGrid
2380
+      : 1
2381
+
2382
+    return this.setState(Commands.translateShapes(this, ids, Vec.mul(delta, size)))
2363 2383
   }
2364 2384
 
2365 2385
   /**
@@ -2498,7 +2518,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2498 2518
 
2499 2519
   onKeyDown: TLKeyboardEventHandler = (key, info, e) => {
2500 2520
     switch (e.key) {
2501
-      case '.': {
2521
+      case '/': {
2502 2522
         if (this.status === 'idle') {
2503 2523
           const { shiftKey, metaKey, altKey, ctrlKey, spaceKey } = this
2504 2524
 
@@ -2559,7 +2579,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2559 2579
     if (!info) return
2560 2580
 
2561 2581
     switch (e.key) {
2562
-      case '.': {
2582
+      case '/': {
2563 2583
         const { currentPoint, shiftKey, metaKey, altKey, ctrlKey, spaceKey } = this
2564 2584
 
2565 2585
         this.onPointerUp(
@@ -2950,7 +2970,18 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2950 2970
   // The center of the component (in screen space)
2951 2971
   get centerPoint() {
2952 2972
     const { width, height } = this.rendererBounds
2953
-    return Vec.round([width / 2, height / 2])
2973
+    return Vec.toFixed([width / 2, height / 2])
2974
+  }
2975
+
2976
+  get currentGrid() {
2977
+    const { zoom } = this.pageState.camera
2978
+    if (zoom < 0.15) {
2979
+      return GRID_SIZE * 16
2980
+    } else if (zoom < 1) {
2981
+      return GRID_SIZE * 4
2982
+    } else {
2983
+      return GRID_SIZE * 1
2984
+    }
2954 2985
   }
2955 2986
 
2956 2987
   getShapeUtil = TLDR.getShapeUtil
@@ -2996,6 +3027,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2996 3027
       showRotateHandles: true,
2997 3028
       showBindingHandles: true,
2998 3029
       showCloneHandles: false,
3030
+      showGrid: false,
2999 3031
     },
3000 3032
     appState: {
3001 3033
       status: TDStatus.Idle,

+ 1
- 2
packages/tldraw/src/state/commands/alignShapes/alignShapes.ts View File

@@ -1,9 +1,8 @@
1 1
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+import { Vec } from '@tldraw/vec'
2 3
 import { Utils } from '@tldraw/core'
3 4
 import { AlignType, TldrawCommand, TDShapeType } from '~types'
4
-import type { TDSnapshot } from '~types'
5 5
 import { TLDR } from '~state/TLDR'
6
-import Vec from '@tldraw/vec'
7 6
 import type { TldrawApp } from '../../internal'
8 7
 
9 8
 export function alignShapes(app: TldrawApp, ids: string[], type: AlignType): TldrawCommand {

+ 1
- 1
packages/tldraw/src/state/commands/moveShapesToPage/moveShapesToPage.ts View File

@@ -170,7 +170,7 @@ export function moveShapesToPage(
170 170
   const mx = (viewportBounds.width - bounds.width * zoom) / 2 / zoom
171 171
   const my = (viewportBounds.height - bounds.height * zoom) / 2 / zoom
172 172
 
173
-  const point = Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my]))
173
+  const point = Vec.toFixed(Vec.add([-bounds.minX, -bounds.minY], [mx, my]))
174 174
 
175 175
   return {
176 176
     id: 'move_to_page',

+ 1
- 1
packages/tldraw/src/state/commands/styleShapes/styleShapes.ts View File

@@ -35,7 +35,7 @@ export function styleShapes(
35 35
 
36 36
       if (shape.type === TDShapeType.Text) {
37 37
         beforeShapes[shape.id].point = shape.point
38
-        afterShapes[shape.id].point = Vec.round(
38
+        afterShapes[shape.id].point = Vec.toFixed(
39 39
           Vec.add(
40 40
             shape.point,
41 41
             Vec.sub(

+ 1
- 1
packages/tldraw/src/state/commands/translateShapes/translateShapes.ts View File

@@ -30,7 +30,7 @@ export function translateShapes(app: TldrawApp, ids: string[], delta: number[]):
30 30
     app.state,
31 31
     idsToMutate,
32 32
     (shape) => ({
33
-      point: Vec.round(Vec.add(shape.point, delta)),
33
+      point: Vec.toFixed(Vec.add(shape.point, delta)),
34 34
     }),
35 35
     currentPageId
36 36
   )

+ 26
- 12
packages/tldraw/src/state/sessions/ArrowSession/ArrowSession.ts View File

@@ -10,12 +10,12 @@ import {
10 10
   TldrawCommand,
11 11
 } from '~types'
12 12
 import { Vec } from '@tldraw/vec'
13
-import { Utils } from '@tldraw/core'
14 13
 import { TLDR } from '~state/TLDR'
15 14
 import { BINDING_DISTANCE } from '~constants'
16 15
 import { shapeUtils } from '~state/shapes'
17 16
 import { BaseSession } from '../BaseSession'
18 17
 import type { TldrawApp } from '../../internal'
18
+import { Utils } from '@tldraw/core'
19 19
 
20 20
 export class ArrowSession extends BaseSession {
21 21
   type = SessionType.Arrow
@@ -70,7 +70,14 @@ export class ArrowSession extends BaseSession {
70 70
 
71 71
   update = (): TldrawPatch | undefined => {
72 72
     const { initialShape } = this
73
-    const { currentPoint, shiftKey, altKey, metaKey } = this.app
73
+    const {
74
+      currentPoint,
75
+      shiftKey,
76
+      altKey,
77
+      metaKey,
78
+      currentGrid,
79
+      settings: { showGrid },
80
+    } = this.app
74 81
 
75 82
     const shape = this.app.getShape<ArrowShape>(initialShape.id)
76 83
 
@@ -90,15 +97,18 @@ export class ArrowSession extends BaseSession {
90 97
     if (shiftKey) {
91 98
       const A = handles[handleId === 'start' ? 'end' : 'start'].point
92 99
       const B = handles[handleId].point
93
-      const C = Vec.round(Vec.sub(Vec.add(B, delta), shape.point))
100
+      const C = Vec.toFixed(Vec.sub(Vec.add(B, delta), shape.point))
94 101
       const angle = Vec.angle(A, C)
95 102
       const adjusted = Vec.rotWith(C, A, Utils.snapAngleToSegments(angle, 24) - angle)
96 103
       delta = Vec.add(delta, Vec.sub(adjusted, C))
97 104
     }
98 105
 
106
+    const nextPoint = Vec.sub(Vec.add(handles[handleId].point, delta), shape.point)
107
+
99 108
     const handle = {
100 109
       ...handles[handleId],
101
-      point: Vec.round(Vec.sub(Vec.add(handles[handleId].point, delta), shape.point)),
110
+      point: showGrid ? Vec.snap(nextPoint, currentGrid) : Vec.toFixed(nextPoint),
111
+
102 112
       bindingId: undefined,
103 113
     }
104 114
 
@@ -340,13 +350,19 @@ export class ArrowSession extends BaseSession {
340 350
   complete = (): TldrawPatch | TldrawCommand | undefined => {
341 351
     const { initialShape, initialBinding, newStartBindingId, startBindingShapeId, handleId } = this
342 352
 
343
-    const beforeBindings: Partial<Record<string, TDBinding>> = {}
353
+    const currentShape = TLDR.onSessionComplete(this.app.page.shapes[initialShape.id]) as ArrowShape
354
+    const currentBindingId = currentShape.handles[handleId].bindingId
344 355
 
345
-    const afterBindings: Partial<Record<string, TDBinding>> = {}
356
+    if (
357
+      !(currentBindingId || initialBinding) &&
358
+      Vec.dist(currentShape.handles.start.point, currentShape.handles.end.point) > 2
359
+    ) {
360
+      return this.cancel()
361
+    }
346 362
 
347
-    let afterShape = this.app.page.shapes[initialShape.id] as ArrowShape
363
+    const beforeBindings: Partial<Record<string, TDBinding>> = {}
348 364
 
349
-    const currentBindingId = afterShape.handles[handleId].bindingId
365
+    const afterBindings: Partial<Record<string, TDBinding>> = {}
350 366
 
351 367
     if (initialBinding) {
352 368
       beforeBindings[initialBinding.id] = this.isCreate ? undefined : initialBinding
@@ -363,8 +379,6 @@ export class ArrowSession extends BaseSession {
363 379
       afterBindings[newStartBindingId] = this.app.page.bindings[newStartBindingId]
364 380
     }
365 381
 
366
-    afterShape = TLDR.onSessionComplete(afterShape)
367
-
368 382
     return {
369 383
       id: 'arrow',
370 384
       before: {
@@ -392,7 +406,7 @@ export class ArrowSession extends BaseSession {
392 406
           pages: {
393 407
             [this.app.currentPageId]: {
394 408
               shapes: {
395
-                [initialShape.id]: afterShape,
409
+                [initialShape.id]: currentShape,
396 410
               },
397 411
               bindings: afterBindings,
398 412
             },
@@ -441,7 +455,7 @@ export class ArrowSession extends BaseSession {
441 455
       fromId: shape.id,
442 456
       toId: target.id,
443 457
       handleId: handleId,
444
-      point: Vec.round(bindingPoint.point),
458
+      point: Vec.toFixed(bindingPoint.point),
445 459
       distance: bindingPoint.distance,
446 460
     }
447 461
   }

+ 4
- 4
packages/tldraw/src/state/sessions/DrawSession/DrawSession.ts View File

@@ -81,7 +81,7 @@ export class DrawSession extends BaseSession {
81 81
     }
82 82
 
83 83
     // The new adjusted point
84
-    const newAdjustedPoint = Vec.round(Vec.sub(currentPoint, originPoint)).concat(currentPoint[2])
84
+    const newAdjustedPoint = Vec.toFixed(Vec.sub(currentPoint, originPoint)).concat(currentPoint[2])
85 85
 
86 86
     // Don't add duplicate points.
87 87
     if (Vec.isEqual(this.lastAdjustedPoint, newAdjustedPoint)) return
@@ -112,7 +112,7 @@ export class DrawSession extends BaseSession {
112 112
       // offset between the new top left and the original top left.
113 113
 
114 114
       points = this.points.map((pt) => {
115
-        return Vec.round(Vec.sub(pt, delta)).concat(pt[2])
115
+        return Vec.toFixed(Vec.sub(pt, delta)).concat(pt[2])
116 116
       })
117 117
     } else {
118 118
       // If the new top left is the same as the previous top left,
@@ -197,8 +197,8 @@ export class DrawSession extends BaseSession {
197 197
               shapes: {
198 198
                 [shapeId]: {
199 199
                   ...shape,
200
-                  point: Vec.round(shape.point),
201
-                  points: shape.points.map((pt) => Vec.round(pt)),
200
+                  point: Vec.toFixed(shape.point),
201
+                  points: shape.points.map((pt) => Vec.toFixed(pt)),
202 202
                   isComplete: true,
203 203
                 },
204 204
               },

+ 1
- 1
packages/tldraw/src/state/sessions/EraseSession/EraseSession.ts View File

@@ -57,7 +57,7 @@ export class EraseSession extends BaseSession {
57 57
       }
58 58
     }
59 59
 
60
-    const newPoint = Vec.round(Vec.add(originPoint, Vec.sub(currentPoint, originPoint)))
60
+    const newPoint = Vec.toFixed(Vec.add(originPoint, Vec.sub(currentPoint, originPoint)))
61 61
 
62 62
     const deletedShapeIds = new Set<string>([])
63 63
 

+ 6
- 6
packages/tldraw/src/state/sessions/RotateSession/RotateSession.spec.ts View File

@@ -106,7 +106,7 @@ describe('Rotate session', () => {
106 106
     it('keeps the center', () => {
107 107
       app.loadDocument(mockDocument).select('rect1', 'rect2')
108 108
 
109
-      const centerBefore = Vec.round(
109
+      const centerBefore = Vec.toFixed(
110 110
         Utils.getBoundsCenter(
111 111
           Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
112 112
         )
@@ -114,7 +114,7 @@ describe('Rotate session', () => {
114 114
 
115 115
       app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession()
116 116
 
117
-      const centerAfterA = Vec.round(
117
+      const centerAfterA = Vec.toFixed(
118 118
         Utils.getBoundsCenter(
119 119
           Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
120 120
         )
@@ -122,7 +122,7 @@ describe('Rotate session', () => {
122 122
 
123 123
       app.pointBoundsHandle('rotate', { x: 100, y: 0 }).movePointer([50, 0]).completeSession()
124 124
 
125
-      const centerAfterB = Vec.round(
125
+      const centerAfterB = Vec.toFixed(
126 126
         Utils.getBoundsCenter(
127 127
           Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
128 128
         )
@@ -142,7 +142,7 @@ describe('Rotate session', () => {
142 142
     it('changes the center after nudging', () => {
143 143
       const app = new TldrawTestApp().loadDocument(mockDocument).select('rect1', 'rect2')
144 144
 
145
-      const centerBefore = Vec.round(
145
+      const centerBefore = Vec.toFixed(
146 146
         Utils.getBoundsCenter(
147 147
           Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
148 148
         )
@@ -150,7 +150,7 @@ describe('Rotate session', () => {
150 150
 
151 151
       app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession()
152 152
 
153
-      const centerAfterA = Vec.round(
153
+      const centerAfterA = Vec.toFixed(
154 154
         Utils.getBoundsCenter(
155 155
           Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
156 156
         )
@@ -163,7 +163,7 @@ describe('Rotate session', () => {
163 163
 
164 164
       app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession()
165 165
 
166
-      const centerAfterB = Vec.round(
166
+      const centerAfterB = Vec.toFixed(
167 167
         Utils.getBoundsCenter(
168 168
           Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
169 169
         )

+ 17
- 3
packages/tldraw/src/state/sessions/TransformSession/TransformSession.ts View File

@@ -113,7 +113,8 @@ export class TransformSession extends BaseSession {
113 113
         shiftKey,
114 114
         altKey,
115 115
         metaKey,
116
-        settings: { isSnapping },
116
+        currentGrid,
117
+        settings: { isSnapping, showGrid },
117 118
       },
118 119
     } = this
119 120
 
@@ -138,6 +139,13 @@ export class TransformSession extends BaseSession {
138 139
       }
139 140
     }
140 141
 
142
+    if (showGrid) {
143
+      newBounds = {
144
+        ...newBounds,
145
+        ...Utils.snapBoundsToGrid(newBounds, currentGrid),
146
+      }
147
+    }
148
+
141 149
     // Should we snap?
142 150
 
143 151
     const speed = Vec.dist(currentPoint, previousPoint)
@@ -180,7 +188,7 @@ export class TransformSession extends BaseSession {
180 188
     this.scaleY = newBounds.scaleY
181 189
 
182 190
     shapeBounds.forEach(({ initialShape, initialShapeBounds, transformOrigin }) => {
183
-      const newShapeBounds = Utils.getRelativeTransformedBoundingBox(
191
+      let newShapeBounds = Utils.getRelativeTransformedBoundingBox(
184 192
         newBounds,
185 193
         initialCommonBounds,
186 194
         initialShapeBounds,
@@ -188,13 +196,19 @@ export class TransformSession extends BaseSession {
188 196
         this.scaleY < 0
189 197
       )
190 198
 
191
-      shapes[initialShape.id] = TLDR.transform(this.app.getShape(initialShape.id), newShapeBounds, {
199
+      if (showGrid) {
200
+        newShapeBounds = Utils.snapBoundsToGrid(newShapeBounds, currentGrid)
201
+      }
202
+
203
+      const afterShape = TLDR.transform(this.app.getShape(initialShape.id), newShapeBounds, {
192 204
         type: this.transformType,
193 205
         initialShape,
194 206
         scaleX: this.scaleX,
195 207
         scaleY: this.scaleY,
196 208
         transformOrigin,
197 209
       })
210
+
211
+      shapes[initialShape.id] = afterShape
198 212
     })
199 213
 
200 214
     return {

+ 15
- 3
packages/tldraw/src/state/sessions/TransformSingleSession/TransformSingleSession.ts View File

@@ -70,13 +70,14 @@ export class TransformSingleSession extends BaseSession {
70 70
       initialShape,
71 71
       initialShapeBounds,
72 72
       app: {
73
-        settings: { isSnapping },
73
+        settings: { isSnapping, showGrid },
74 74
         currentPageId,
75 75
         pageState: { camera },
76 76
         viewport,
77 77
         currentPoint,
78 78
         previousPoint,
79 79
         originPoint,
80
+        currentGrid,
80 81
         shiftKey,
81 82
         altKey,
82 83
         metaKey,
@@ -85,12 +86,12 @@ export class TransformSingleSession extends BaseSession {
85 86
 
86 87
     if (initialShape.isLocked) return void null
87 88
 
89
+    const shapes = {} as Record<string, Partial<TDShape>>
90
+
88 91
     const delta = altKey
89 92
       ? Vec.mul(Vec.sub(currentPoint, originPoint), 2)
90 93
       : Vec.sub(currentPoint, originPoint)
91 94
 
92
-    const shapes = {} as Record<string, Partial<TDShape>>
93
-
94 95
     const shape = this.app.getShape(initialShape.id)
95 96
 
96 97
     const utils = TLDR.getShapeUtil(shape)
@@ -110,6 +111,13 @@ export class TransformSingleSession extends BaseSession {
110 111
       }
111 112
     }
112 113
 
114
+    if (showGrid) {
115
+      newBounds = {
116
+        ...newBounds,
117
+        ...Utils.snapBoundsToGrid(newBounds, currentGrid),
118
+      }
119
+    }
120
+
113 121
     // Should we snap?
114 122
 
115 123
     const speed = Vec.dist(currentPoint, previousPoint)
@@ -159,6 +167,10 @@ export class TransformSingleSession extends BaseSession {
159 167
       shapes[shape.id] = afterShape
160 168
     }
161 169
 
170
+    if (showGrid && afterShape && afterShape.point) {
171
+      afterShape.point = Vec.snap(afterShape.point, currentGrid)
172
+    }
173
+
162 174
     return {
163 175
       appState: {
164 176
         snapLines,

+ 8
- 5
packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.spec.ts View File

@@ -1,3 +1,4 @@
1
+import { Vec } from '@tldraw/vec'
1 2
 import { mockDocument, TldrawTestApp } from '~test'
2 3
 import { GroupShape, SessionType, TDShapeType, TDStatus } from '~types'
3 4
 
@@ -114,15 +115,16 @@ describe('Translate session', () => {
114 115
 
115 116
     expect(Object.keys(app.getPage().shapes).length).toBe(5)
116 117
 
117
-    app.movePointer({ x: 30, y: 30 })
118
+    app.movePointer({ x: 20, y: 20, altKey: false })
118 119
 
119 120
     expect(Object.keys(app.getPage().shapes).length).toBe(3)
120 121
 
121 122
     app.completeSession()
122 123
 
123 124
     // Original position + delta
124
-    expect(app.getShape('rect1').point).toStrictEqual([30, 30])
125
-    expect(app.getShape('rect2').point).toStrictEqual([130, 130])
125
+    const rectPoint = app.getShape('rect1').point
126
+    expect(app.getShape('rect1').point).toStrictEqual(rectPoint)
127
+    expect(app.getShape('rect2').point).toStrictEqual([110, 110])
126 128
 
127 129
     expect(Object.keys(app.page.shapes)).toStrictEqual(['rect1', 'rect2', 'rect3'])
128 130
   })
@@ -211,6 +213,7 @@ describe('Translate session', () => {
211 213
         .movePointer({ x: 20, y: 20, altKey: true })
212 214
         .completeSession()
213 215
 
216
+      const rectPoint = app.getShape('rect1').point
214 217
       const children = app.getShape<GroupShape>('groupA').children
215 218
       const newShapeId = children[children.length - 1]
216 219
 
@@ -218,7 +221,7 @@ describe('Translate session', () => {
218 221
       expect(app.getShape<GroupShape>('groupA').children.length).toBe(3)
219 222
       expect(app.getShape('rect1').point).toStrictEqual([0, 0])
220 223
       expect(app.getShape('rect2').point).toStrictEqual([100, 100])
221
-      expect(app.getShape(newShapeId).point).toStrictEqual([20, 20])
224
+      expect(app.getShape(newShapeId).point).toStrictEqual(Vec.add(rectPoint, [10, 10]))
222 225
       expect(app.getShape(newShapeId).parentId).toBe('groupA')
223 226
 
224 227
       app.undo()
@@ -235,7 +238,7 @@ describe('Translate session', () => {
235 238
       expect(app.getShape<GroupShape>('groupA').children.length).toBe(3)
236 239
       expect(app.getShape('rect1').point).toStrictEqual([0, 0])
237 240
       expect(app.getShape('rect2').point).toStrictEqual([100, 100])
238
-      expect(app.getShape(newShapeId).point).toStrictEqual([20, 20])
241
+      expect(app.getShape(newShapeId).point).toStrictEqual(Vec.add(rectPoint, [10, 10]))
239 242
       expect(app.getShape(newShapeId).parentId).toBe('groupA')
240 243
     })
241 244
   })

+ 30
- 33
packages/tldraw/src/state/sessions/TranslateSession/TranslateSession.ts View File

@@ -24,6 +24,7 @@ type CloneInfo =
24 24
     }
25 25
   | {
26 26
       state: 'ready'
27
+      cloneMap: Record<string, string>
27 28
       clones: TDShape[]
28 29
       clonedBindings: ArrowBinding[]
29 30
     }
@@ -172,7 +173,7 @@ export class TranslateSession extends BaseSession {
172 173
       bindingsToDelete,
173 174
       app: {
174 175
         pageState: { camera },
175
-        settings: { isSnapping },
176
+        settings: { isSnapping, showGrid },
176 177
         currentPageId,
177 178
         viewport,
178 179
         selectedIds,
@@ -182,13 +183,12 @@ export class TranslateSession extends BaseSession {
182 183
         altKey,
183 184
         shiftKey,
184 185
         metaKey,
186
+        currentGrid,
185 187
       },
186 188
     } = this
187 189
 
188 190
     const nextBindings: Patch<Record<string, TDBinding>> = {}
189
-
190 191
     const nextShapes: Patch<Record<string, TDShape>> = {}
191
-
192 192
     const nextPageState: Patch<TLPageState> = {}
193 193
 
194 194
     let delta = Vec.sub(currentPoint, originPoint)
@@ -236,10 +236,12 @@ export class TranslateSession extends BaseSession {
236 236
       this.speed * camera.zoom < SLOW_SPEED &&
237 237
       this.snapInfo.state === 'ready'
238 238
     ) {
239
-      const bounds = Utils.getBoundsWithCenter(Utils.translateBounds(initialCommonBounds, delta))
240
-
241 239
       const snapResult = Utils.getSnapPoints(
242
-        bounds,
240
+        Utils.getBoundsWithCenter(
241
+          showGrid
242
+            ? Utils.snapBoundsToGrid(Utils.translateBounds(initialCommonBounds, delta), currentGrid)
243
+            : Utils.translateBounds(initialCommonBounds, delta)
244
+        ),
243 245
         (this.isCloning ? this.snapInfo.bounds : this.snapInfo.others).filter(
244 246
           (bounds) => Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds)
245 247
         ),
@@ -259,8 +261,6 @@ export class TranslateSession extends BaseSession {
259 261
     // The "movement" is the actual change of position between this
260 262
     // computed position and the previous computed position.
261 263
 
262
-    const movement = Vec.sub(delta, this.prev)
263
-
264 264
     this.prev = delta
265 265
 
266 266
     // If cloning...
@@ -287,7 +287,7 @@ export class TranslateSession extends BaseSession {
287 287
 
288 288
         // Add the clones to the page
289 289
         clones.forEach((clone) => {
290
-          nextShapes[clone.id] = { ...clone, point: Vec.round(Vec.add(clone.point, delta)) }
290
+          nextShapes[clone.id] = { ...clone }
291 291
 
292 292
           // Add clones to non-selected parents
293 293
           if (clone.parentId !== currentPageId && !selectedIds.includes(clone.parentId)) {
@@ -313,13 +313,11 @@ export class TranslateSession extends BaseSession {
313 313
 
314 314
         // Either way, move the clones
315 315
         clones.forEach((clone) => {
316
-          const current = (nextShapes[clone.id] || this.app.getShape(clone.id)) as TDShape
317
-
318
-          if (!current.point) throw Error('No point on that clone!')
319
-
320 316
           nextShapes[clone.id] = {
321 317
             ...clone,
322
-            point: Vec.round(Vec.add(current.point, movement)),
318
+            point: showGrid
319
+              ? Vec.snap(Vec.toFixed(Vec.add(clone.point, delta)), currentGrid)
320
+              : Vec.toFixed(Vec.add(clone.point, delta)),
323 321
           }
324 322
         })
325 323
       } else {
@@ -327,14 +325,11 @@ export class TranslateSession extends BaseSession {
327 325
 
328 326
         const { clones } = this.cloneInfo
329 327
 
330
-        // Either way, move the clones
331 328
         clones.forEach((clone) => {
332
-          const current = (nextShapes[clone.id] || this.app.getShape(clone.id)) as TDShape
333
-
334
-          if (!current.point) throw Error('No point on that clone!')
335
-
336 329
           nextShapes[clone.id] = {
337
-            point: Vec.round(Vec.add(current.point, movement)),
330
+            point: showGrid
331
+              ? Vec.snap(Vec.toFixed(Vec.add(clone.point, delta)), currentGrid)
332
+              : Vec.toFixed(Vec.add(clone.point, delta)),
338 333
           }
339 334
         })
340 335
       }
@@ -350,7 +345,6 @@ export class TranslateSession extends BaseSession {
350 345
         this.isCloning = false
351 346
 
352 347
         // Delete the bindings
353
-
354 348
         bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined))
355 349
 
356 350
         // Remove the clones from parents
@@ -369,7 +363,9 @@ export class TranslateSession extends BaseSession {
369 363
         // Move the original shapes back to the cursor position
370 364
         initialShapes.forEach((shape) => {
371 365
           nextShapes[shape.id] = {
372
-            point: Vec.round(Vec.add(shape.point, delta)),
366
+            point: showGrid
367
+              ? Vec.snap(Vec.toFixed(Vec.add(shape.point, delta)), currentGrid)
368
+              : Vec.toFixed(Vec.add(shape.point, delta)),
373 369
           }
374 370
         })
375 371
 
@@ -380,18 +376,18 @@ export class TranslateSession extends BaseSession {
380 376
 
381 377
         // Set selected ids
382 378
         nextPageState.selectedIds = initialShapes.map((shape) => shape.id)
383
-      }
384
-
385
-      // Move the shapes by the delta
386
-      initialShapes.forEach((shape) => {
387
-        const current = (nextShapes[shape.id] || this.app.getShape(shape.id)) as TDShape
388
-
389
-        if (!current.point) throw Error('No point on that clone!')
379
+      } else {
380
+        // Move the shapes by the delta
381
+        initialShapes.forEach((shape) => {
382
+          // const current = (nextShapes[shape.id] || this.app.getShape(shape.id)) as TDShape
390 383
 
391
-        nextShapes[shape.id] = {
392
-          point: Vec.round(Vec.add(current.point, movement)),
393
-        }
394
-      })
384
+          nextShapes[shape.id] = {
385
+            point: showGrid
386
+              ? Vec.snap(Vec.toFixed(Vec.add(shape.point, delta)), currentGrid)
387
+              : Vec.toFixed(Vec.add(shape.point, delta)),
388
+          }
389
+        })
390
+      }
395 391
     }
396 392
 
397 393
     return {
@@ -696,6 +692,7 @@ export class TranslateSession extends BaseSession {
696 692
     this.cloneInfo = {
697 693
       state: 'ready',
698 694
       clones,
695
+      cloneMap,
699 696
       clonedBindings,
700 697
     }
701 698
   }

+ 9
- 9
packages/tldraw/src/state/shapes/ArrowUtil/ArrowUtil.tsx View File

@@ -99,7 +99,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
99 99
 
100 100
     const isDraw = style.dash === DashStyle.Draw
101 101
 
102
-    const isStraightLine = Vec.dist(bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
102
+    const isStraightLine = Vec.dist(bend.point, Vec.toFixed(Vec.med(start.point, end.point))) < 1
103 103
 
104 104
     const styles = getShapeStyle(style, meta.isDarkMode)
105 105
 
@@ -122,7 +122,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
122 122
     if (isStraightLine) {
123 123
       const path = isDraw
124 124
         ? renderFreehandArrowShaft(shape)
125
-        : 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
125
+        : 'M' + Vec.toFixed(start.point) + 'L' + Vec.toFixed(end.point)
126 126
 
127 127
       const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
128 128
         arrowDist,
@@ -398,11 +398,11 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
398 398
 
399 399
     nextHandles['bend'] = {
400 400
       ...bend,
401
-      point: Vec.round(Math.abs(bendDist) < 10 ? midPoint : point),
401
+      point: Vec.toFixed(Math.abs(bendDist) < 10 ? midPoint : point),
402 402
     }
403 403
 
404 404
     return {
405
-      point: Vec.round([bounds.minX, bounds.minY]),
405
+      point: Vec.toFixed([bounds.minX, bounds.minY]),
406 406
       handles: nextHandles,
407 407
     }
408 408
   }
@@ -516,7 +516,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
516 516
     return this.onHandleChange(shape, {
517 517
       [handle.id]: {
518 518
         ...handle,
519
-        point: Vec.round(handlePoint),
519
+        point: Vec.toFixed(handlePoint),
520 520
       },
521 521
     })
522 522
   }
@@ -529,11 +529,11 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
529 529
       ...nextHandles,
530 530
       start: {
531 531
         ...nextHandles.start,
532
-        point: Vec.round(nextHandles.start.point),
532
+        point: Vec.toFixed(nextHandles.start.point),
533 533
       },
534 534
       end: {
535 535
         ...nextHandles.end,
536
-        point: Vec.round(nextHandles.end.point),
536
+        point: Vec.toFixed(nextHandles.end.point),
537 537
       },
538 538
     }
539 539
 
@@ -601,10 +601,10 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
601 601
 
602 602
     if (!Vec.isEqual(offset, [0, 0])) {
603 603
       Object.values(nextShape.handles).forEach((handle) => {
604
-        handle.point = Vec.round(Vec.sub(handle.point, offset))
604
+        handle.point = Vec.toFixed(Vec.sub(handle.point, offset))
605 605
       })
606 606
 
607
-      nextShape.point = Vec.round(Vec.add(nextShape.point, offset))
607
+      nextShape.point = Vec.toFixed(Vec.add(nextShape.point, offset))
608 608
     }
609 609
 
610 610
     return nextShape

+ 3
- 3
packages/tldraw/src/state/shapes/ArrowUtil/arrowHelpers.ts View File

@@ -38,7 +38,7 @@ export function getBendPoint(handles: ArrowShape['handles'], bend: number) {
38 38
 
39 39
   const u = Vec.uni(Vec.vec(start.point, end.point))
40 40
 
41
-  const point = Vec.round(
41
+  const point = Vec.toFixed(
42 42
     Math.abs(bendDist) < 10 ? midPoint : Vec.add(midPoint, Vec.mul(Vec.per(u), bendDist))
43 43
   )
44 44
 
@@ -115,7 +115,7 @@ export function renderCurvedFreehandArrowShaft(
115 115
 
116 116
     const angle = Utils.lerpAngles(startAngle, endAngle, t)
117 117
 
118
-    points.push(Vec.round(Vec.nudgeAtAngle(center, angle, radius)))
118
+    points.push(Vec.toFixed(Vec.nudgeAtAngle(center, angle, radius)))
119 119
   }
120 120
 
121 121
   const stroke = getStroke([startPoint, ...points, endPoint], {
@@ -221,7 +221,7 @@ export function getArrowPath(shape: ArrowShape) {
221 221
 
222 222
   const path: (string | number)[] = []
223 223
 
224
-  const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
224
+  const isStraightLine = Vec.dist(_bend.point, Vec.toFixed(Vec.med(start.point, end.point))) < 1
225 225
 
226 226
   if (isStraightLine) {
227 227
     // Path (line segment)

+ 1
- 1
packages/tldraw/src/state/shapes/EllipseUtil/EllipseUtil.tsx View File

@@ -331,7 +331,7 @@ export class EllipseUtil extends TDShapeUtil<T, E> {
331 331
 
332 332
   transformSingle = (shape: T, bounds: TLBounds): Partial<T> => {
333 333
     return {
334
-      point: Vec.round([bounds.minX, bounds.minY]),
334
+      point: Vec.toFixed([bounds.minX, bounds.minY]),
335 335
       radius: Vec.div([bounds.width, bounds.height], 2),
336 336
     }
337 337
   }

+ 1
- 1
packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx View File

@@ -217,7 +217,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
217 217
     bounds: TLBounds,
218 218
     { scaleX, scaleY, transformOrigin }: TransformInfo<T>
219 219
   ): Partial<T> => {
220
-    const point = Vec.round([
220
+    const point = Vec.toFixed([
221 221
       bounds.minX +
222 222
         (bounds.width - shape.size[0]) * (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
223 223
       bounds.minY +

+ 2
- 2
packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx View File

@@ -285,7 +285,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
285 285
     } = initialShape
286 286
 
287 287
     return {
288
-      point: Vec.round([bounds.minX, bounds.minY]),
288
+      point: Vec.toFixed([bounds.minX, bounds.minY]),
289 289
       style: {
290 290
         ...initialShape.style,
291 291
         scale: scale * Math.max(Math.abs(scaleY), Math.abs(scaleX)),
@@ -309,7 +309,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
309 309
         ...shape.style,
310 310
         scale: 1,
311 311
       },
312
-      point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))),
312
+      point: Vec.toFixed(Vec.add(shape.point, Vec.sub(center, newCenter))),
313 313
     }
314 314
   }
315 315
 

+ 2
- 1
packages/tldraw/src/state/shapes/shared/shape-styles.ts View File

@@ -141,7 +141,8 @@ export function getStickyShapeStyle(style: ShapeStyles, isDarkMode = false) {
141 141
   const { color } = style
142 142
 
143 143
   const theme: Theme = isDarkMode ? 'dark' : 'light'
144
-  const adjustedColor = color === ColorStyle.Black ? ColorStyle.Yellow : color
144
+  const adjustedColor =
145
+    color === ColorStyle.White || color === ColorStyle.Black ? ColorStyle.Yellow : color
145 146
 
146 147
   return {
147 148
     fill: stickyFills[theme][adjustedColor],

+ 6
- 4
packages/tldraw/src/state/shapes/shared/transformRectangle.ts View File

@@ -13,9 +13,11 @@ export function transformRectangle<T extends TLShape & { size: number[] }>(
13 13
   { initialShape, transformOrigin, scaleX, scaleY }: TLTransformInfo<T>
14 14
 ) {
15 15
   if (shape.rotation || initialShape.isAspectRatioLocked) {
16
-    const size = Vec.round(Vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY))))
16
+    const size = Vec.toFixed(
17
+      Vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY)))
18
+    )
17 19
 
18
-    const point = Vec.round([
20
+    const point = Vec.toFixed([
19 21
       bounds.minX +
20 22
         (bounds.width - shape.size[0]) * (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
21 23
       bounds.minY +
@@ -37,8 +39,8 @@ export function transformRectangle<T extends TLShape & { size: number[] }>(
37 39
     }
38 40
   } else {
39 41
     return {
40
-      point: Vec.round([bounds.minX, bounds.minY]),
41
-      size: Vec.round([bounds.width, bounds.height]),
42
+      point: Vec.toFixed([bounds.minX, bounds.minY]),
43
+      size: Vec.toFixed([bounds.width, bounds.height]),
42 44
     }
43 45
   }
44 46
 }

+ 2
- 2
packages/tldraw/src/state/shapes/shared/transformSingleRectangle.ts View File

@@ -11,7 +11,7 @@ export function transformSingleRectangle<T extends TLShape & { size: number[] }>
11 11
   bounds: TLBounds
12 12
 ) {
13 13
   return {
14
-    size: Vec.round([bounds.width, bounds.height]),
15
-    point: Vec.round([bounds.minX, bounds.minY]),
14
+    size: Vec.toFixed([bounds.width, bounds.height]),
15
+    point: Vec.toFixed([bounds.minX, bounds.minY]),
16 16
   }
17 17
 }

+ 4
- 1
packages/tldraw/src/state/tools/ArrowTool/ArrowTool.ts View File

@@ -1,4 +1,5 @@
1 1
 import { Utils, TLPointerEventHandler } from '@tldraw/core'
2
+import Vec from '@tldraw/vec'
2 3
 import { Arrow } from '~state/shapes'
3 4
 import { SessionType, TDShapeType } from '~types'
4 5
 import { BaseTool, Status } from '../BaseTool'
@@ -13,6 +14,8 @@ export class ArrowTool extends BaseTool {
13 14
 
14 15
     const {
15 16
       currentPoint,
17
+      currentGrid,
18
+      settings: { showGrid },
16 19
       appState: { currentPageId, currentStyle },
17 20
     } = this.app
18 21
 
@@ -24,7 +27,7 @@ export class ArrowTool extends BaseTool {
24 27
       id,
25 28
       parentId: currentPageId,
26 29
       childIndex,
27
-      point: currentPoint,
30
+      point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
28 31
       style: { ...currentStyle },
29 32
     })
30 33
 

+ 0
- 2
packages/tldraw/src/state/tools/BaseTool.ts View File

@@ -89,7 +89,6 @@ export abstract class BaseTool<T extends string = any> extends TDEventHandler {
89 89
       return
90 90
     }
91 91
 
92
-    /* noop */
93 92
     if (key === 'Meta' || key === 'Control' || key === 'Alt') {
94 93
       this.app.updateSession()
95 94
       return
@@ -97,7 +96,6 @@ export abstract class BaseTool<T extends string = any> extends TDEventHandler {
97 96
   }
98 97
 
99 98
   onKeyUp: TLKeyboardEventHandler = (key) => {
100
-    /* noop */
101 99
     if (key === 'Meta' || key === 'Control' || key === 'Alt') {
102 100
       this.app.updateSession()
103 101
       return

+ 4
- 1
packages/tldraw/src/state/tools/EllipseTool/EllipseTool.ts View File

@@ -1,4 +1,5 @@
1 1
 import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core'
2
+import Vec from '@tldraw/vec'
2 3
 import { Ellipse } from '~state/shapes'
3 4
 import { SessionType, TDShapeType } from '~types'
4 5
 import { BaseTool, Status } from '../BaseTool'
@@ -13,6 +14,8 @@ export class EllipseTool extends BaseTool {
13 14
 
14 15
     const {
15 16
       currentPoint,
17
+      currentGrid,
18
+      settings: { showGrid },
16 19
       appState: { currentPageId, currentStyle },
17 20
     } = this.app
18 21
 
@@ -24,7 +27,7 @@ export class EllipseTool extends BaseTool {
24 27
       id,
25 28
       parentId: currentPageId,
26 29
       childIndex,
27
-      point: currentPoint,
30
+      point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
28 31
       style: { ...currentStyle },
29 32
     })
30 33
 

+ 4
- 1
packages/tldraw/src/state/tools/LineTool/LineTool.ts View File

@@ -1,4 +1,5 @@
1 1
 import { Utils, TLPointerEventHandler } from '@tldraw/core'
2
+import Vec from '@tldraw/vec'
2 3
 import { Arrow } from '~state/shapes'
3 4
 import { SessionType, TDShapeType } from '~types'
4 5
 import { BaseTool, Status } from '../BaseTool'
@@ -13,6 +14,8 @@ export class LineTool extends BaseTool {
13 14
 
14 15
     const {
15 16
       currentPoint,
17
+      currentGrid,
18
+      settings: { showGrid },
16 19
       appState: { currentPageId, currentStyle },
17 20
     } = this.app
18 21
 
@@ -24,7 +27,7 @@ export class LineTool extends BaseTool {
24 27
       id,
25 28
       parentId: currentPageId,
26 29
       childIndex,
27
-      point: currentPoint,
30
+      point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
28 31
       decorations: {
29 32
         start: undefined,
30 33
         end: undefined,

+ 4
- 1
packages/tldraw/src/state/tools/RectangleTool/RectangleTool.ts View File

@@ -1,4 +1,5 @@
1 1
 import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core'
2
+import Vec from '@tldraw/vec'
2 3
 import { Rectangle } from '~state/shapes'
3 4
 import { SessionType, TDShapeType } from '~types'
4 5
 import { BaseTool, Status } from '../BaseTool'
@@ -13,6 +14,8 @@ export class RectangleTool extends BaseTool {
13 14
 
14 15
     const {
15 16
       currentPoint,
17
+      currentGrid,
18
+      settings: { showGrid },
16 19
       appState: { currentPageId, currentStyle },
17 20
     } = this.app
18 21
 
@@ -24,7 +27,7 @@ export class RectangleTool extends BaseTool {
24 27
       id,
25 28
       parentId: currentPageId,
26 29
       childIndex,
27
-      point: currentPoint,
30
+      point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
28 31
       style: { ...currentStyle },
29 32
     })
30 33
 

+ 7
- 2
packages/tldraw/src/state/tools/SelectTool/SelectTool.spec.ts View File

@@ -65,9 +65,14 @@ describe('When double clicking link controls', () => {
65 65
     const app = new TldrawTestApp()
66 66
       .loadDocument(doc)
67 67
       .select('rect2')
68
-      .pointBoundsHandle('center', { x: 0, y: 0 })
68
+      .pointBoundsHandle('center', [100, 100])
69
+      .expectShapesToBeAtPoints({
70
+        rect1: [0, 0],
71
+        rect2: [100, 0],
72
+        rect3: [200, 0],
73
+      })
69 74
 
70
-    app.movePointer({ x: 100, y: 100 }).expectShapesToBeAtPoints({
75
+    app.movePointer([200, 200]).expectShapesToBeAtPoints({
71 76
       rect1: [100, 100],
72 77
       rect2: [200, 100],
73 78
       rect3: [300, 100],

+ 3
- 1
packages/tldraw/src/state/tools/StickyTool/StickyTool.ts View File

@@ -26,6 +26,8 @@ export class StickyTool extends BaseTool {
26 26
     if (this.status === Status.Idle) {
27 27
       const {
28 28
         currentPoint,
29
+        currentGrid,
30
+        settings: { showGrid },
29 31
         appState: { currentPageId, currentStyle },
30 32
       } = this.app
31 33
 
@@ -39,7 +41,7 @@ export class StickyTool extends BaseTool {
39 41
         id,
40 42
         parentId: currentPageId,
41 43
         childIndex,
42
-        point: currentPoint,
44
+        point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
43 45
         style: { ...currentStyle },
44 46
       })
45 47
 

+ 8
- 2
packages/tldraw/src/state/tools/TextTool/TextTool.ts View File

@@ -1,4 +1,5 @@
1 1
 import type { TLPointerEventHandler, TLKeyboardEventHandler } from '@tldraw/core'
2
+import Vec from '@tldraw/vec'
2 3
 import { TDShapeType } from '~types'
3 4
 import { BaseTool, Status } from '../BaseTool'
4 5
 
@@ -32,8 +33,13 @@ export class TextTool extends BaseTool {
32 33
     }
33 34
 
34 35
     if (this.status === Status.Idle) {
35
-      const { currentPoint } = this.app
36
-      this.app.createTextShapeAtPoint(currentPoint)
36
+      const {
37
+        currentPoint,
38
+        currentGrid,
39
+        settings: { showGrid },
40
+      } = this.app
41
+
42
+      this.app.createTextShapeAtPoint(showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint)
37 43
       this.setStatus(Status.Creating)
38 44
       return
39 45
     }

+ 12
- 0
packages/tldraw/src/test/TldrawTestApp.tsx View File

@@ -162,4 +162,16 @@ export class TldrawTestApp extends TldrawApp {
162 162
     })
163 163
     return this
164 164
   }
165
+
166
+  pressKey = (key: string) => {
167
+    const e = { key } as KeyboardEvent
168
+    this.onKeyDown(key, inputs.keydown(e), e)
169
+    return this
170
+  }
171
+
172
+  releaseKey = (key: string) => {
173
+    const e = { key } as KeyboardEvent
174
+    this.onKeyUp(key, inputs.keyup(e), e)
175
+    return this
176
+  }
165 177
 }

+ 30
- 2
packages/tldraw/src/types.ts View File

@@ -1,8 +1,9 @@
1 1
 /* eslint-disable @typescript-eslint/no-explicit-any */
2 2
 /* eslint-disable @typescript-eslint/ban-types */
3
-import type { TLPage, TLUser, TLPageState } from '@tldraw/core'
4
-import type { FileSystemHandle } from '~state/data/browser-fs-access'
5 3
 import type {
4
+  TLPage,
5
+  TLUser,
6
+  TLPageState,
6 7
   TLBinding,
7 8
   TLBoundsCorner,
8 9
   TLBoundsEdge,
@@ -89,6 +90,7 @@ export interface TDSnapshot {
89 90
     showRotateHandles: boolean
90 91
     showBindingHandles: boolean
91 92
     showCloneHandles: boolean
93
+    showGrid: boolean
92 94
   }
93 95
   appState: {
94 96
     currentStyle: ShapeStyles
@@ -470,3 +472,29 @@ export interface Command<T extends { [key: string]: any }> {
470 472
   before: Patch<T>
471 473
   after: Patch<T>
472 474
 }
475
+
476
+export interface FileWithHandle extends File {
477
+  handle?: FileSystemHandle
478
+}
479
+
480
+export interface FileWithDirectoryHandle extends File {
481
+  directoryHandle?: FileSystemHandle
482
+}
483
+
484
+// The following typings implement the relevant parts of the File System Access
485
+// API. This can be removed once the specification reaches the Candidate phase
486
+// and is implemented as part of microsoft/TSJS-lib-generator.
487
+
488
+export interface FileSystemHandlePermissionDescriptor {
489
+  mode?: 'read' | 'readwrite'
490
+}
491
+
492
+export interface FileSystemHandle {
493
+  readonly kind: 'file' | 'directory'
494
+  readonly name: string
495
+
496
+  isSameEntry: (other: FileSystemHandle) => Promise<boolean>
497
+
498
+  queryPermission: (descriptor?: FileSystemHandlePermissionDescriptor) => Promise<PermissionState>
499
+  requestPermission: (descriptor?: FileSystemHandlePermissionDescriptor) => Promise<PermissionState>
500
+}

+ 5
- 6
packages/tldraw/tsconfig.build.json View File

@@ -11,12 +11,11 @@
11 11
     "docs"
12 12
   ],
13 13
   "compilerOptions": {
14
-    "rootDir": "src",
15
-    "baseUrl": "src",
16 14
     "composite": false,
17 15
     "incremental": false,
18
-    "declarationMap": false,
19
-    "sourceMap": false,
20
-    "emitDeclarationOnly": true
21
-  }
16
+    "declaration": true,
17
+    "declarationMap": true,
18
+    "sourceMap": true
19
+  },
20
+  "references": [{ "path": "../vec" }, { "path": "../intersect" }, { "path": "../core" }]
22 21
 }

+ 4
- 5
packages/tldraw/tsconfig.json View File

@@ -1,17 +1,16 @@
1 1
 {
2 2
   "extends": "../../tsconfig.base.json",
3
-  "include": ["src", "src/test/*.json"],
4 3
   "exclude": ["node_modules", "dist", "docs"],
4
+  "include": ["src"],
5 5
   "compilerOptions": {
6
-    "resolveJsonModule": true,
7 6
     "outDir": "./dist/types",
8 7
     "rootDir": "src",
9
-    "baseUrl": "src",
8
+    "baseUrl": ".",
10 9
     "paths": {
11
-      "~*": ["./*"],
12
-      "@tldraw/core": ["../core"]
10
+      "~*": ["./src/*"]
13 11
     }
14 12
   },
13
+  "references": [{ "path": "../vec" }, { "path": "../intersect" }, { "path": "../core" }],
15 14
   "typedocOptions": {
16 15
     "entryPoints": ["src/index.ts"],
17 16
     "out": "docs"

+ 86
- 0
packages/vec/CHANGELOG.md View File

@@ -0,0 +1,86 @@
1
+# Changelog
2
+
3
+## 0.1.21
4
+
5
+New:
6
+
7
+- 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.
8
+- Adds the `isChildOfSelected` prop for `TLComponentProps`. If a shape is the child of a selected shape, its `isChildOfSelected` prop will be true.
9
+
10
+Improved:
11
+
12
+- Fixes a bug that could occur with the order of grouped shapes.
13
+- Adds an Eraser tool to the advanced example.
14
+- Adds a Pencil tool to the advanced example.
15
+
16
+## 0.1.20
17
+
18
+- Update docs.
19
+- Adds `hideResizeHandles` prop.
20
+
21
+## 0.1.19
22
+
23
+- Remove stray `index.js` files.
24
+
25
+## 0.1.18
26
+
27
+- Even more dependency fixes.
28
+
29
+## 0.1.17
30
+
31
+- More dependency fixes.
32
+
33
+## 0.1.16
34
+
35
+- Fix dependencies, remove `@use-gesture/react` from bundle.
36
+
37
+## 0.1.15
38
+
39
+- Fix README.
40
+
41
+## 0.1.14
42
+
43
+- Add README to package.
44
+
45
+## 0.1.13
46
+
47
+- Remove `type` from `TLBinding`.
48
+
49
+## 0.1.12
50
+
51
+- Fix bug with initial bounds.
52
+
53
+## 0.1.12
54
+
55
+- Fix bug with initial bounds.
56
+
57
+## 0.1.12
58
+
59
+- Fix bug with bounds handle events.
60
+
61
+## 0.1.11
62
+
63
+- Fix bug with initial camera state.
64
+
65
+## 0.1.10
66
+
67
+- Improve example.
68
+- Improve types for `TLPage`.
69
+
70
+## 0.1.9
71
+
72
+- Bug fixes.
73
+
74
+## 0.1.8
75
+
76
+- Expands README.
77
+- Removes properties specific to the tldraw app.
78
+
79
+## 0.1.7
80
+
81
+- Fixes selection bug with SVGContainer.
82
+- Removes various properties specific to the tldraw app.
83
+
84
+## 0.1.0
85
+
86
+- Re-writes API for ShapeUtils.

+ 21
- 0
packages/vec/LICENSE.md View File

@@ -0,0 +1,21 @@
1
+MIT License
2
+
3
+Copyright (c) 2021 Stephen Ruiz Ltd
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.

+ 477
- 0
packages/vec/README.md View File

@@ -0,0 +1,477 @@
1
+<div style="text-align: center; transform: scale(.5);">
2
+  <img src="card-repo.png"/>
3
+</div>
4
+
5
+# @tldraw/core
6
+
7
+This package contains the `Renderer` and core utilities used by [tldraw](https://tldraw.com).
8
+
9
+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/).
10
+
11
+💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
12
+
13
+## Installation
14
+
15
+Use your package manager of choice to install `@tldraw/core` and its peer dependencies.
16
+
17
+```bash
18
+yarn add @tldraw/core
19
+# or
20
+npm i @tldraw/core
21
+```
22
+
23
+## Examples
24
+
25
+There are two examples in this repository.
26
+
27
+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.
28
+
29
+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.
30
+
31
+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.
32
+
33
+## Usage
34
+
35
+Import the `Renderer` React component and pass it the required props.
36
+
37
+```tsx
38
+import * as React from "react"
39
+import { Renderer, TLShape, TLShapeUtil, Vec } from '@tldraw/core'
40
+import { BoxShape, BoxUtil } from "./shapes/box"
41
+
42
+const shapeUtils = { box: new BoxUtil() }
43
+
44
+function App() {
45
+  const [page, setPage] = React.useState({
46
+    id: "page"
47
+    shapes: {
48
+      "box1": {
49
+        id: 'box1',
50
+        type: 'box',
51
+        parentId: 'page',
52
+        childIndex: 0,
53
+        point: [0, 0],
54
+        size: [100, 100],
55
+        rotation: 0,
56
+      }
57
+    },
58
+    bindings: {}
59
+  })
60
+
61
+  const [pageState, setPageState] = React.useState({
62
+    id: "page",
63
+    selectedIds: [],
64
+    camera: {
65
+      point: [0,0],
66
+      zoom: 1
67
+    }
68
+  })
69
+
70
+  return (<Renderer
71
+    page={page}
72
+    pageState={pageState}
73
+    shapeUtils={shapeUtils}
74
+  />)
75
+}
76
+```
77
+
78
+## Documentation
79
+
80
+### `Renderer`
81
+
82
+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`.
83
+
84
+| Prop         | Type                            | Description                                    |
85
+| ------------ | ------------------------------- | ---------------------------------------------- |
86
+| `page`       | [`TLPage`](#tlpage)             | The current page object.                       |
87
+| `pageState`  | [`TLPageState`](#tlpagestate)   | The current page's state.                      |
88
+| `shapeUtils` | [`TLShapeUtils`](#tlshapeutils) | The shape utilities used to render the shapes. |
89
+
90
+In addition to these required props, the Renderer accents many other **optional** props.
91
+
92
+| Property             | Type                          | Description                                                       |
93
+| -------------------- | ----------------------------- | ----------------------------------------------------------------- |
94
+| `containerRef`       | `React.MutableRefObject`      | A React ref for the container, where CSS variables will be added. |
95
+| `theme`              | `object`                      | An object with overrides for the Renderer's default colors.       |
96
+| `hideBounds`         | `boolean`                     | Do not show the bounding box for selected shapes.                 |
97
+| `hideHandles`        | `boolean`                     | Do not show handles for shapes with handles.                      |
98
+| `hideBindingHandles` | `boolean`                     | Do not show binding controls for selected shapes with bindings.   |
99
+| `hideResizeHandles`  | `boolean`                     | Do not show resize handles for selected shapes.                   |
100
+| `hideRotateHandles`  | `boolean`                     | Do not show rotate handles for selected shapes.                   |
101
+| `snapLines`          | [`TLSnapLine`](#tlsnapline)[] | An array of "snap" lines.                                         |
102
+| `users`              | `object`                      | A table of [`TLUser`](#tluser)s.                                  |
103
+| `userId`             | `object`                      | The current user's [`TLUser`](#tluser) id.                        |
104
+
105
+The theme object accepts valid CSS colors for the following properties:
106
+
107
+| Property       | Description                                          |
108
+| -------------- | ---------------------------------------------------- |
109
+| `foreground`   | The primary (usually "text") color                   |
110
+| `background`   | The default page's background color                  |
111
+| `brushFill`    | The fill color of the brush selection box            |
112
+| `brushStroke`  | The stroke color of the brush selection box          |
113
+| `selectFill`   | The fill color of the selection bounds               |
114
+| `selectStroke` | The stroke color of the selection bounds and handles |
115
+
116
+The Renderer also accepts many (optional) event callbacks.
117
+
118
+| Prop                        | Description                                                 |
119
+| --------------------------- | ----------------------------------------------------------- |
120
+| `onPan`                     | Panned with the mouse wheel                                 |
121
+| `onZoom`                    | Zoomed with the mouse wheel                                 |
122
+| `onPinchStart`              | Began a two-pointer pinch                                   |
123
+| `onPinch`                   | Moved their pointers during a pinch                         |
124
+| `onPinchEnd`                | Stopped a two-pointer pinch                                 |
125
+| `onPointerDown`             | Started pointing                                            |
126
+| `onPointerMove`             | Moved their pointer                                         |
127
+| `onPointerUp`               | Ended a point                                               |
128
+| `onPointCanvas`             | Pointed the canvas                                          |
129
+| `onDoubleClickCanvas`       | Double-pointed the canvas                                   |
130
+| `onRightPointCanvas`        | Right-pointed the canvas                                    |
131
+| `onDragCanvas`              | Dragged the canvas                                          |
132
+| `onReleaseCanvas`           | Stopped pointing the canvas                                 |
133
+| `onHoverShape`              | Moved their pointer onto a shape                            |
134
+| `onUnhoverShape`            | Moved their pointer off of a shape                          |
135
+| `onPointShape`              | Pointed a shape                                             |
136
+| `onDoubleClickShape`        | Double-pointed a shape                                      |
137
+| `onRightPointShape`         | Right-pointed a shape                                       |
138
+| `onDragShape`               | Dragged a shape                                             |
139
+| `onReleaseShape`            | Stopped pointing a shape                                    |
140
+| `onHoverHandle`             | Moved their pointer onto a shape handle                     |
141
+| `onUnhoverHandle`           | Moved their pointer off of a shape handle                   |
142
+| `onPointHandle`             | Pointed a shape handle                                      |
143
+| `onDoubleClickHandle`       | Double-pointed a shape handle                               |
144
+| `onRightPointHandle`        | Right-pointed a shape handle                                |
145
+| `onDragHandle`              | Dragged a shape handle                                      |
146
+| `onReleaseHandle`           | Stopped pointing shape handle                               |
147
+| `onHoverBounds`             | Moved their pointer onto the selection bounds               |
148
+| `onUnhoverBounds`           | Moved their pointer off of the selection bounds             |
149
+| `onPointBounds`             | Pointed the selection bounds                                |
150
+| `onDoubleClickBounds`       | Double-pointed the selection bounds                         |
151
+| `onRightPointBounds`        | Right-pointed the selection bounds                          |
152
+| `onDragBounds`              | Dragged the selection bounds                                |
153
+| `onReleaseBounds`           | Stopped the selection bounds                                |
154
+| `onHoverBoundsHandle`       | Moved their pointer onto a selection bounds handle          |
155
+| `onUnhoverBoundsHandle`     | Moved their pointer off of a selection bounds handle        |
156
+| `onPointBoundsHandle`       | Pointed a selection bounds handle                           |
157
+| `onDoubleClickBoundsHandle` | Double-pointed a selection bounds handle                    |
158
+| `onRightPointBoundsHandle`  | Right-pointed a selection bounds handle                     |
159
+| `onDragBoundsHandle`        | Dragged a selection bounds handle                           |
160
+| `onReleaseBoundsHandle`     | Stopped a selection bounds handle                           |
161
+| `onShapeClone`              | Clicked on a shape's clone handle                           |
162
+| `onShapeChange`             | A shape's component prompted a change                       |
163
+| `onShapeBlur`               | A shape's component was prompted a blur                     |
164
+| `onRenderCountChange`       | The number of rendered shapes changed                       |
165
+| `onBoundsChange`            | The Renderer's screen bounding box of the component changed |
166
+| `onError`                   | The Renderer encountered an error                           |
167
+
168
+The `@tldraw/core` library provides types for most of the event handlers:
169
+
170
+| Type                         |
171
+| ---------------------------- |
172
+| `TLPinchEventHandler`        |
173
+| `TLPointerEventHandler`      |
174
+| `TLCanvasEventHandler`       |
175
+| `TLBoundsEventHandler`       |
176
+| `TLBoundsHandleEventHandler` |
177
+| `TLShapeChangeHandler`       |
178
+| `TLShapeBlurHandler`         |
179
+| `TLShapeCloneHandler`        |
180
+
181
+### `TLPage`
182
+
183
+An object describing the current page. It contains:
184
+
185
+| Property          | Type                        | Description                                                                 |
186
+| ----------------- | --------------------------- | --------------------------------------------------------------------------- |
187
+| `id`              | `string`                    | A unique id for the page.                                                   |
188
+| `shapes`          | [`TLShape{}`](#tlshape)     | A table of shapes.                                                          |
189
+| `bindings`        | [`TLBinding{}`](#tlbinding) | A table of bindings.                                                        |
190
+| `backgroundColor` | `string`                    | (optional) The page's background fill color. Will also overwrite the theme. |
191
+
192
+### `TLPageState`
193
+
194
+An object describing the current page. It contains:
195
+
196
+| Property       | Type       | Description                                         |
197
+| -------------- | ---------- | --------------------------------------------------- |
198
+| `id`           | `string`   | The corresponding page's id                         |
199
+| `selectedIds`  | `string[]` | An array of selected shape ids                      |
200
+| `camera`       | `object`   | An object describing the camera state               |
201
+| `camera.point` | `number[]` | The camera's `[x, y]` coordinates                   |
202
+| `camera.zoom`  | `number`   | The camera's zoom level                             |
203
+| `pointedId`    | `string`   | (optional) The currently pointed shape id           |
204
+| `hoveredId`    | `string`   | (optional) The currently hovered shape id           |
205
+| `editingId`    | `string`   | (optional) The currently editing shape id           |
206
+| `bindingId`    | `string`   | (optional) The currently editing binding.           |
207
+| `brush`        | `TLBounds` | (optional) A `Bounds` for the current selection box |
208
+
209
+### `TLShape`
210
+
211
+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).
212
+
213
+| Property              | Type       | Description                                                                           |
214
+| --------------------- | ---------- | ------------------------------------------------------------------------------------- |
215
+| `id`                  | `string`   | The shape's id.                                                                       |
216
+| `type`                | `string`   | The type of the shape, corresponding to the `type` of a [`TLShapeUtil`](#tlshapeutil) |
217
+| `parentId`            | `string`   | The id of the shape's parent (either the current page or another shape)               |
218
+| `childIndex`          | `number`   | the order of the shape among its parent's children                                    |
219
+| `name`                | `string`   | the name of the shape                                                                 |
220
+| `point`               | `number[]` | the shape's current `[x, y]` coordinates on the page                                  |
221
+| `rotation`            | `number`   | (optiona) The shape's current rotation in radians                                     |
222
+| `children`            | `string[]` | (optional) An array containing the ids of this shape's children                       |
223
+| `handles`             | `{}`       | (optional) A table of [`TLHandle`](#tlhandle) objects                                 |
224
+| `isGhost`             | `boolean`  | (optional) True if the shape is "ghosted", e.g. while deleting                        |
225
+| `isLocked`            | `boolean`  | (optional) True if the shape is locked                                                |
226
+| `isHidden`            | `boolean`  | (optional) True if the shape is hidden                                                |
227
+| `isEditing`           | `boolean`  | (optional) True if the shape is currently editing                                     |
228
+| `isGenerated`         | `boolean`  | optional) True if the shape is generated programatically                              |
229
+| `isAspectRatioLocked` | `boolean`  | (optional) True if the shape's aspect ratio is locked                                 |
230
+
231
+### `TLHandle`
232
+
233
+An object that describes a relationship between two shapes on the page.
234
+
235
+| Property | Type       | Description                                   |
236
+| -------- | ---------- | --------------------------------------------- |
237
+| `id`     | `string`   | An id for the handle                          |
238
+| `index`  | `number`   | The handle's order within the shape's handles |
239
+| `point`  | `number[]` | The handle's `[x, y]` coordinates             |
240
+
241
+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
242
+
243
+### `TLBinding`
244
+
245
+An object that describes a relationship between two shapes on the page.
246
+
247
+| Property | Type     | Description                                  |
248
+| -------- | -------- | -------------------------------------------- |
249
+| `id`     | `string` | A unique id for the binding                  |
250
+| `fromId` | `string` | The id of the shape where the binding begins |
251
+| `toId`   | `string` | The id of the shape where the binding begins |
252
+
253
+### `TLSnapLine`
254
+
255
+A snapline is an array of points (formatted as `[x, y]`) that represent a "snapping" line.
256
+
257
+### `TLShapeUtil`
258
+
259
+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.
260
+
261
+### `TLUser`
262
+
263
+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.
264
+
265
+| Property        | Type       | Description                             |
266
+| --------------- | ---------- | --------------------------------------- |
267
+| `id`            | `string`   | A unique id for the user                |
268
+| `color`         | `string`   | The user's color, used for indicators   |
269
+| `point`         | `number[]` | The user's pointer location on the page |
270
+| `selectedIds[]` | `string[]` | The user's selected shape ids           |
271
+
272
+### `Utils`
273
+
274
+A general purpose utility class. See source for more.
275
+
276
+## Guide: Creating Shapes
277
+
278
+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.
279
+
280
+> For several example shapes, see the folder `/example/src/shapes/`.
281
+
282
+### Shape Type
283
+
284
+Your first task is to define an interface for the shape that extends `TLShape`. It must have a `type` property.
285
+
286
+```ts
287
+// BoxShape.ts
288
+import type { TLShape } from '@tldraw/core'
289
+
290
+export interface BoxShape extends TLShape {
291
+  type: 'box'
292
+  size: number[]
293
+}
294
+```
295
+
296
+### Component
297
+
298
+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.
299
+
300
+```tsx
301
+// BoxComponent.ts
302
+
303
+import * as React from 'react'
304
+import { shapeComponent, SVGContainer } from '@tldraw/core'
305
+import type { BoxShape } from './BoxShape'
306
+
307
+export const BoxComponent = TLShapeUtil.Component<BoxShape, SVGSVGElement>(
308
+  ({ shape, events, meta }, ref) => {
309
+    const color = meta.isDarkMode ? 'white' : 'black'
310
+
311
+    return (
312
+      <SVGContainer ref={ref} {...events}>
313
+        <rect
314
+          width={shape.size[0]}
315
+          height={shape.size[1]}
316
+          stroke={color}
317
+          strokeWidth={2}
318
+          strokeLinejoin="round"
319
+          fill="none"
320
+          pointerEvents="all"
321
+        />
322
+      </SVGContainer>
323
+    )
324
+  }
325
+)
326
+```
327
+
328
+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.
329
+
330
+The component will receive the following props:
331
+
332
+| Name                | Type       | Description                                                        |
333
+| ------------------- | ---------- | ------------------------------------------------------------------ |
334
+| `shape`             | `TLShape`  | The shape from `page.shapes` that is being rendered                |
335
+| `meta`              | `{}`       | The value provided to the `Renderer`'s `meta` prop                 |
336
+| `events`            | `{}`       | Several pointer events that should be set on the container element |
337
+| `isSelected`        | `boolean`  | The shape is selected (its `id` is in `pageState.selectedIds`)     |
338
+| `isHovered`         | `boolean`  | The shape is hovered (its `id` is `pageState.hoveredId`)           |
339
+| `isEditing`         | `boolean`  | The shape is being edited (its `id` is `pageState.editingId`)      |
340
+| `isGhost`           | `boolean`  | The shape is ghosted or is the child of a ghosted shape.           |
341
+| `isChildOfSelected` | `boolean`  | The shape is the child of a selected shape.                        |
342
+| `onShapeChange`     | `Function` | The callback provided to the `Renderer`'s `onShapeChange` prop     |
343
+| `onShapeBlur`       | `Function` | The callback provided to the `Renderer`'s `onShapeBlur` prop       |
344
+
345
+### Indicator
346
+
347
+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.
348
+
349
+```tsx
350
+// BoxIndicator.ts
351
+
352
+export const BoxIndicator = TLShapeUtil.Indicator<BoxShape>(({ shape }) => {
353
+  return (
354
+    <rect
355
+      fill="none"
356
+      stroke="dodgerblue"
357
+      strokeWidth={1}
358
+      width={shape.size[0]}
359
+      height={shape.size[1]}
360
+    />
361
+  )
362
+})
363
+```
364
+
365
+The indicator component will receive the following props:
366
+
367
+| Name         | Type      | Description                                                                            |
368
+| ------------ | --------- | -------------------------------------------------------------------------------------- |
369
+| `shape`      | `TLShape` | The shape from `page.shapes` that is being rendered                                    |
370
+| `meta`       | {}        | The value provided to the `Renderer`'s `meta` prop                                     |
371
+| `user`       | `TLUser`  | The user when shown in a multiplayer session                                           |
372
+| `isSelected` | `boolean` | Whether the current shape is selected (true if its `id` is in `pageState.selectedIds`) |
373
+| `isHovered`  | `boolean` | Whether the current shape is hovered (true if its `id` is `pageState.hoveredId`)       |
374
+
375
+### ShapeUtil
376
+
377
+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.
378
+
379
+```ts
380
+// BoxUtil.ts
381
+
382
+import { Utils, TLBounds, TLShapeUtil } from '@tldraw/core'
383
+import { BoxComponent } from './BoxComponent'
384
+import { BoxIndicator } from './BoxIndicator'
385
+import type { BoxShape } from './BoxShape'
386
+
387
+export class BoxUtil extends TLShapeUtil<BoxShape, SVGSVGElement> {
388
+  Component = BoxComponent
389
+
390
+  Indicator = BoxIndicator
391
+
392
+  getBounds = (shape: BoxShape): TLBounds => {
393
+    const [width, height] = shape.size
394
+
395
+    const bounds = {
396
+      minX: 0,
397
+      maxX: width,
398
+      minY: 0,
399
+      maxY: height,
400
+      width,
401
+      height,
402
+    }
403
+
404
+    return Utils.translateBounds(bounds, shape.point)
405
+  }
406
+}
407
+```
408
+
409
+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.
410
+
411
+You may also set the following fields:
412
+
413
+| Name               | Type      | Default | Description                                                                                           |
414
+| ------------------ | --------- | ------- | ----------------------------------------------------------------------------------------------------- |
415
+| `showCloneHandles` | `boolean` | `false` | Whether to display clone handles when the shape is the only selected shape                            |
416
+| `hideBounds`       | `boolean` | `false` | Whether to hide the bounds when the shape is the only selected shape                                  |
417
+| `isStateful`       | `boolean` | `false` | Whether the shape has its own React state. When true, the shape will not be unmounted when off-screen |
418
+
419
+### ShapeUtils Object
420
+
421
+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.
422
+
423
+```tsx
424
+// App.tsx
425
+
426
+const shapeUtils = {
427
+  box: new BoxUtil(),
428
+  circle: new CircleUtil(),
429
+  text: new TextUtil(),
430
+}
431
+
432
+export function App() {
433
+  // ...
434
+
435
+  return <Renderer page={page} pageState={pageState} {...etc} shapeUtils={shapeUtils} />
436
+}
437
+```
438
+
439
+## Local Development
440
+
441
+To start the development servers for the package and the advanced example:
442
+
443
+- Run `yarn` to install dependencies.
444
+- Run `yarn start`.
445
+- Open `localhost:5420`.
446
+
447
+You can also run:
448
+
449
+- `start:advanced` to start development servers for the package and the advanced example.
450
+- `start:simple` to start development servers for the package and the simple example.
451
+- `test` to execute unit tests via [Jest](https://jestjs.io).
452
+- `docs` to build the docs via [ts-doc](https://typedoc.org/).
453
+- `build` to build the package.
454
+
455
+## Example
456
+
457
+See the `example` folder or this [CodeSandbox](https://codesandbox.io/s/laughing-elion-gp0kx) example.
458
+
459
+## Community
460
+
461
+### Support
462
+
463
+Need help? Please [open an issue](https://github.com/tldraw/tldraw/issues/new) for support.
464
+
465
+### Discussion
466
+
467
+Want to connect with other devs? Visit the [Discord channel](https://discord.gg/SBBEVCA4PG).
468
+
469
+### License
470
+
471
+This project is licensed under MIT.
472
+
473
+If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
474
+
475
+## Author
476
+
477
+- [@steveruizok](https://twitter.com/steveruizok)

BIN
packages/vec/card-repo.png View File


+ 35
- 0
packages/vec/package.json View File

@@ -0,0 +1,35 @@
1
+{
2
+  "version": "1.1.4",
3
+  "name": "@tldraw/vec",
4
+  "description": "2D vector utilities for TLDraw and maybe you, too.",
5
+  "author": "@steveruizok",
6
+  "repository": {
7
+    "type": "git",
8
+    "url": "git+https://github.com/tldraw/tldraw.git"
9
+  },
10
+  "license": "MIT",
11
+  "keywords": [
12
+    "2d",
13
+    "vector",
14
+    "typescript",
15
+    "javascript"
16
+  ],
17
+  "files": [
18
+    "dist/**/*"
19
+  ],
20
+  "main": "./dist/cjs/index.js",
21
+  "module": "./dist/esm/index.js",
22
+  "types": "./dist/types/index.d.ts",
23
+  "scripts": {
24
+    "start:packages": "yarn start",
25
+    "start:core": "yarn start",
26
+    "start": "node scripts/dev & yarn types:dev",
27
+    "build:core": "yarn build",
28
+    "build:packages": "yarn build",
29
+    "build": "node scripts/build && yarn types:build",
30
+    "types:dev": "tsc -w --p tsconfig.build.json",
31
+    "types:build": "tsc -p tsconfig.build.json",
32
+    "lint": "eslint src/ --ext .ts,.tsx",
33
+    "clean": "rm -rf dist"
34
+  }
35
+}

+ 61
- 0
packages/vec/scripts/build.js View File

@@ -0,0 +1,61 @@
1
+/* eslint-disable */
2
+const fs = require('fs')
3
+const esbuild = require('esbuild')
4
+const { gzip } = require('zlib')
5
+const pkg = require('../package.json')
6
+
7
+async function main() {
8
+  if (fs.existsSync('./dist')) {
9
+    fs.rmSync('./dist', { recursive: true }, (e) => {
10
+      if (e) {
11
+        throw e
12
+      }
13
+    })
14
+  }
15
+
16
+  try {
17
+    esbuild.buildSync({
18
+      entryPoints: ['./src/index.ts'],
19
+      outdir: 'dist/cjs',
20
+      minify: false,
21
+      bundle: true,
22
+      format: 'cjs',
23
+      target: 'es6',
24
+      tsconfig: './tsconfig.build.json',
25
+      metafile: false,
26
+      sourcemap: true,
27
+    })
28
+
29
+    const esmResult = esbuild.buildSync({
30
+      entryPoints: ['./src/index.ts'],
31
+      outdir: 'dist/esm',
32
+      minify: false,
33
+      bundle: true,
34
+      format: 'esm',
35
+      target: 'es6',
36
+      tsconfig: './tsconfig.build.json',
37
+      metafile: true,
38
+      sourcemap: true,
39
+    })
40
+
41
+    let esmSize = 0
42
+    Object.values(esmResult.metafile.outputs).forEach((output) => {
43
+      esmSize += output.bytes
44
+    })
45
+
46
+    fs.readFile('./dist/esm/index.js', (_err, data) => {
47
+      gzip(data, (_err, result) => {
48
+        console.log(
49
+          `✔ ${pkg.name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${(
50
+            result.length / 1000
51
+          ).toFixed(2)}kb minified)`
52
+        )
53
+      })
54
+    })
55
+  } catch (e) {
56
+    console.log(`× ${pkg.name}: Build failed due to an error.`)
57
+    console.log(e)
58
+  }
59
+}
60
+
61
+main()

+ 29
- 0
packages/vec/scripts/dev.js View File

@@ -0,0 +1,29 @@
1
+/* eslint-disable */
2
+const esbuild = require('esbuild')
3
+const pkg = require('../package.json')
4
+
5
+async function main() {
6
+  try {
7
+    await esbuild.build({
8
+      entryPoints: ['src/index.tsx'],
9
+      outfile: 'dist/index.js',
10
+      bundle: true,
11
+      minify: false,
12
+      sourcemap: true,
13
+      incremental: true,
14
+      target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
15
+      define: {
16
+        'process.env.NODE_ENV': '"development"',
17
+      },
18
+      watch: {
19
+        onRebuild(err) {
20
+          err ? error('❌ Failed') : log('✅ Updated')
21
+        },
22
+      },
23
+    })
24
+  } catch (err) {
25
+    process.exit(1)
26
+  }
27
+}
28
+
29
+main()

+ 311
- 0
packages/vec/src/index.d.ts View File

@@ -0,0 +1,311 @@
1
+export declare class Vec {
2
+    /**
3
+     * Clamp a value into a range.
4
+     * @param n
5
+     * @param min
6
+     */
7
+    static clamp(n: number, min: number): number;
8
+    static clamp(n: number, min: number, max: number): number;
9
+    /**
10
+     * Clamp a value into a range.
11
+     * @param n
12
+     * @param min
13
+     */
14
+    static clampV(A: number[], min: number): number[];
15
+    static clampV(A: number[], min: number, max: number): number[];
16
+    /**
17
+     * Negate a vector.
18
+     * @param A
19
+     */
20
+    static neg: (A: number[]) => number[];
21
+    /**
22
+     * Add vectors.
23
+     * @param A
24
+     * @param B
25
+     */
26
+    static add: (A: number[], B: number[]) => number[];
27
+    /**
28
+     * Add scalar to vector.
29
+     * @param A
30
+     * @param B
31
+     */
32
+    static addScalar: (A: number[], n: number) => number[];
33
+    /**
34
+     * Subtract vectors.
35
+     * @param A
36
+     * @param B
37
+     */
38
+    static sub: (A: number[], B: number[]) => number[];
39
+    /**
40
+     * Subtract scalar from vector.
41
+     * @param A
42
+     * @param B
43
+     */
44
+    static subScalar: (A: number[], n: number) => number[];
45
+    /**
46
+     * Get the vector from vectors A to B.
47
+     * @param A
48
+     * @param B
49
+     */
50
+    static vec: (A: number[], B: number[]) => number[];
51
+    /**
52
+     * Vector multiplication by scalar
53
+     * @param A
54
+     * @param n
55
+     */
56
+    static mul: (A: number[], n: number) => number[];
57
+    /**
58
+     * Multiple two vectors.
59
+     * @param A
60
+     * @param B
61
+     */
62
+    static mulV: (A: number[], B: number[]) => number[];
63
+    /**
64
+     * Vector division by scalar.
65
+     * @param A
66
+     * @param n
67
+     */
68
+    static div: (A: number[], n: number) => number[];
69
+    /**
70
+     * Vector division by vector.
71
+     * @param A
72
+     * @param n
73
+     */
74
+    static divV: (A: number[], B: number[]) => number[];
75
+    /**
76
+     * Perpendicular rotation of a vector A
77
+     * @param A
78
+     */
79
+    static per: (A: number[]) => number[];
80
+    /**
81
+     * Dot product
82
+     * @param A
83
+     * @param B
84
+     */
85
+    static dpr: (A: number[], B: number[]) => number;
86
+    /**
87
+     * Cross product (outer product) | A X B |
88
+     * @param A
89
+     * @param B
90
+     */
91
+    static cpr: (A: number[], B: number[]) => number;
92
+    /**
93
+     * Cross (for point in polygon)
94
+     *
95
+     */
96
+    static cross(x: number[], y: number[], z: number[]): number;
97
+    /**
98
+     * Length of the vector squared
99
+     * @param A
100
+     */
101
+    static len2: (A: number[]) => number;
102
+    /**
103
+     * Length of the vector
104
+     * @param A
105
+     */
106
+    static len: (A: number[]) => number;
107
+    /**
108
+     * Project A over B
109
+     * @param A
110
+     * @param B
111
+     */
112
+    static pry: (A: number[], B: number[]) => number;
113
+    /**
114
+     * Get normalized / unit vector.
115
+     * @param A
116
+     */
117
+    static uni: (A: number[]) => number[];
118
+    /**
119
+     * Get normalized / unit vector.
120
+     * @param A
121
+     */
122
+    static normalize: (A: number[]) => number[];
123
+    /**
124
+     * Get the tangent between two vectors.
125
+     * @param A
126
+     * @param B
127
+     * @returns
128
+     */
129
+    static tangent: (A: number[], B: number[]) => number[];
130
+    /**
131
+     * Dist length from A to B squared.
132
+     * @param A
133
+     * @param B
134
+     */
135
+    static dist2: (A: number[], B: number[]) => number;
136
+    /**
137
+     * Dist length from A to B
138
+     * @param A
139
+     * @param B
140
+     */
141
+    static dist: (A: number[], B: number[]) => number;
142
+    /**
143
+     * A faster, though less accurate method for testing distances. Maybe faster?
144
+     * @param A
145
+     * @param B
146
+     * @returns
147
+     */
148
+    static fastDist: (A: number[], B: number[]) => number[];
149
+    /**
150
+     * Angle between vector A and vector B in radians
151
+     * @param A
152
+     * @param B
153
+     */
154
+    static ang: (A: number[], B: number[]) => number;
155
+    /**
156
+     * Angle between vector A and vector B in radians
157
+     * @param A
158
+     * @param B
159
+     */
160
+    static angle: (A: number[], B: number[]) => number;
161
+    /**
162
+     * Mean between two vectors or mid vector between two vectors
163
+     * @param A
164
+     * @param B
165
+     */
166
+    static med: (A: number[], B: number[]) => number[];
167
+    /**
168
+     * Vector rotation by r (radians)
169
+     * @param A
170
+     * @param r rotation in radians
171
+     */
172
+    static rot: (A: number[], r?: number) => number[];
173
+    /**
174
+     * Rotate a vector around another vector by r (radians)
175
+     * @param A vector
176
+     * @param C center
177
+     * @param r rotation in radians
178
+     */
179
+    static rotWith: (A: number[], C: number[], r?: number) => number[];
180
+    /**
181
+     * Check of two vectors are identical.
182
+     * @param A
183
+     * @param B
184
+     */
185
+    static isEqual: (A: number[], B: number[]) => boolean;
186
+    /**
187
+     * Interpolate vector A to B with a scalar t
188
+     * @param A
189
+     * @param B
190
+     * @param t scalar
191
+     */
192
+    static lrp: (A: number[], B: number[], t: number) => number[];
193
+    /**
194
+     * Interpolate from A to B when curVAL goes fromVAL: number[] => to
195
+     * @param A
196
+     * @param B
197
+     * @param from Starting value
198
+     * @param to Ending value
199
+     * @param s Strength
200
+     */
201
+    static int: (A: number[], B: number[], from: number, to: number, s?: number) => number[];
202
+    /**
203
+     * Get the angle between the three vectors A, B, and C.
204
+     * @param p1
205
+     * @param pc
206
+     * @param p2
207
+     */
208
+    static ang3: (p1: number[], pc: number[], p2: number[]) => number;
209
+    /**
210
+     * Absolute value of a vector.
211
+     * @param A
212
+     * @returns
213
+     */
214
+    static abs: (A: number[]) => number[];
215
+    static rescale: (a: number[], n: number) => number[];
216
+    /**
217
+     * Get whether p1 is left of p2, relative to pc.
218
+     * @param p1
219
+     * @param pc
220
+     * @param p2
221
+     */
222
+    static isLeft: (p1: number[], pc: number[], p2: number[]) => number;
223
+    /**
224
+     * Get whether p1 is left of p2, relative to pc.
225
+     * @param p1
226
+     * @param pc
227
+     * @param p2
228
+     */
229
+    static clockwise: (p1: number[], pc: number[], p2: number[]) => boolean;
230
+    /**
231
+     * Round a vector to the a given precision.
232
+     * @param a
233
+     * @param d
234
+     */
235
+    static toFixed: (a: number[], d?: number) => number[];
236
+    /**
237
+     * Snap vector to nearest step.
238
+     * @param A
239
+     * @param step
240
+     * @example
241
+     * ```ts
242
+     * Vec.snap([10.5, 28], 10) // [10, 30]
243
+     * ```
244
+     */
245
+    static snap(a: number[], step?: number): number[];
246
+    /**
247
+     * Get the nearest point on a line with a known unit vector that passes through point A
248
+     * @param A Any point on the line
249
+     * @param u The unit vector for the line.
250
+     * @param P A point not on the line to test.
251
+     * @returns
252
+     */
253
+    static nearestPointOnLineThroughPoint: (A: number[], u: number[], P: number[]) => number[];
254
+    /**
255
+     * Distance between a point and a line with a known unit vector that passes through a point.
256
+     * @param A Any point on the line
257
+     * @param u The unit vector for the line.
258
+     * @param P A point not on the line to test.
259
+     * @returns
260
+     */
261
+    static distanceToLineThroughPoint: (A: number[], u: number[], P: number[]) => number;
262
+    /**
263
+     * Get the nearest point on a line segment between A and B
264
+     * @param A The start of the line segment
265
+     * @param B The end of the line segment
266
+     * @param P The off-line point
267
+     * @param clamp Whether to clamp the point between A and B.
268
+     * @returns
269
+     */
270
+    static nearestPointOnLineSegment: (A: number[], B: number[], P: number[], clamp?: boolean) => number[];
271
+    /**
272
+     * Distance between a point and the nearest point on a line segment between A and B
273
+     * @param A The start of the line segment
274
+     * @param B The end of the line segment
275
+     * @param P The off-line point
276
+     * @param clamp Whether to clamp the point between A and B.
277
+     * @returns
278
+     */
279
+    static distanceToLineSegment: (A: number[], B: number[], P: number[], clamp?: boolean) => number;
280
+    /**
281
+     * Push a point A towards point B by a given distance.
282
+     * @param A
283
+     * @param B
284
+     * @param d
285
+     * @returns
286
+     */
287
+    static nudge: (A: number[], B: number[], d: number) => number[];
288
+    /**
289
+     * Push a point in a given angle by a given distance.
290
+     * @param A
291
+     * @param B
292
+     * @param d
293
+     */
294
+    static nudgeAtAngle: (A: number[], a: number, d: number) => number[];
295
+    /**
296
+     * Round a vector to a precision length.
297
+     * @param a
298
+     * @param n
299
+     */
300
+    static toPrecision: (a: number[], n?: number) => number[];
301
+    /**
302
+     * Get an array of points (with simulated pressure) between two points.
303
+     * @param A The first point.
304
+     * @param B The second point.
305
+     * @param steps The number of points to return.
306
+     * @param ease An easing function to apply to the simulated pressure.
307
+     */
308
+    static pointsBetween: (A: number[], B: number[], steps?: number) => number[][];
309
+}
310
+export default Vec;
311
+//# sourceMappingURL=index.d.ts.map

+ 1
- 0
packages/vec/src/index.d.ts.map View File

@@ -0,0 +1 @@
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"}

+ 499
- 0
packages/vec/src/index.ts View File

@@ -0,0 +1,499 @@
1
+export class Vec {
2
+  /**
3
+   * Clamp a value into a range.
4
+   * @param n
5
+   * @param min
6
+   */
7
+  static clamp(n: number, min: number): number
8
+  static clamp(n: number, min: number, max: number): number
9
+  static clamp(n: number, min: number, max?: number): number {
10
+    return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
11
+  }
12
+
13
+  /**
14
+   * Clamp a value into a range.
15
+   * @param n
16
+   * @param min
17
+   */
18
+  static clampV(A: number[], min: number): number[]
19
+  static clampV(A: number[], min: number, max: number): number[]
20
+  static clampV(A: number[], min: number, max?: number): number[] {
21
+    return A.map((n) => (max ? Vec.clamp(n, min, max) : Vec.clamp(n, min)))
22
+  }
23
+
24
+  /**
25
+   * Negate a vector.
26
+   * @param A
27
+   */
28
+  static neg = (A: number[]): number[] => {
29
+    return [-A[0], -A[1]]
30
+  }
31
+
32
+  /**
33
+   * Add vectors.
34
+   * @param A
35
+   * @param B
36
+   */
37
+  static add = (A: number[], B: number[]): number[] => {
38
+    return [A[0] + B[0], A[1] + B[1]]
39
+  }
40
+
41
+  /**
42
+   * Add scalar to vector.
43
+   * @param A
44
+   * @param B
45
+   */
46
+  static addScalar = (A: number[], n: number): number[] => {
47
+    return [A[0] + n, A[1] + n]
48
+  }
49
+
50
+  /**
51
+   * Subtract vectors.
52
+   * @param A
53
+   * @param B
54
+   */
55
+  static sub = (A: number[], B: number[]): number[] => {
56
+    return [A[0] - B[0], A[1] - B[1]]
57
+  }
58
+
59
+  /**
60
+   * Subtract scalar from vector.
61
+   * @param A
62
+   * @param B
63
+   */
64
+  static subScalar = (A: number[], n: number): number[] => {
65
+    return [A[0] - n, A[1] - n]
66
+  }
67
+
68
+  /**
69
+   * Get the vector from vectors A to B.
70
+   * @param A
71
+   * @param B
72
+   */
73
+  static vec = (A: number[], B: number[]): number[] => {
74
+    // A, B as vectors get the vector from A to B
75
+    return [B[0] - A[0], B[1] - A[1]]
76
+  }
77
+
78
+  /**
79
+   * Vector multiplication by scalar
80
+   * @param A
81
+   * @param n
82
+   */
83
+  static mul = (A: number[], n: number): number[] => {
84
+    return [A[0] * n, A[1] * n]
85
+  }
86
+
87
+  /**
88
+   * Multiple two vectors.
89
+   * @param A
90
+   * @param B
91
+   */
92
+  static mulV = (A: number[], B: number[]): number[] => {
93
+    return [A[0] * B[0], A[1] * B[1]]
94
+  }
95
+
96
+  /**
97
+   * Vector division by scalar.
98
+   * @param A
99
+   * @param n
100
+   */
101
+  static div = (A: number[], n: number): number[] => {
102
+    return [A[0] / n, A[1] / n]
103
+  }
104
+
105
+  /**
106
+   * Vector division by vector.
107
+   * @param A
108
+   * @param n
109
+   */
110
+  static divV = (A: number[], B: number[]): number[] => {
111
+    return [A[0] / B[0], A[1] / B[1]]
112
+  }
113
+
114
+  /**
115
+   * Perpendicular rotation of a vector A
116
+   * @param A
117
+   */
118
+  static per = (A: number[]): number[] => {
119
+    return [A[1], -A[0]]
120
+  }
121
+
122
+  /**
123
+   * Dot product
124
+   * @param A
125
+   * @param B
126
+   */
127
+  static dpr = (A: number[], B: number[]): number => {
128
+    return A[0] * B[0] + A[1] * B[1]
129
+  }
130
+
131
+  /**
132
+   * Cross product (outer product) | A X B |
133
+   * @param A
134
+   * @param B
135
+   */
136
+  static cpr = (A: number[], B: number[]): number => {
137
+    return A[0] * B[1] - B[0] * A[1]
138
+  }
139
+
140
+  /**
141
+   * Cross (for point in polygon)
142
+   *
143
+   */
144
+  static cross(x: number[], y: number[], z: number[]): number {
145
+    return (y[0] - x[0]) * (z[1] - x[1]) - (z[0] - x[0]) * (y[1] - x[1])
146
+  }
147
+
148
+  /**
149
+   * Length of the vector squared
150
+   * @param A
151
+   */
152
+  static len2 = (A: number[]): number => {
153
+    return A[0] * A[0] + A[1] * A[1]
154
+  }
155
+
156
+  /**
157
+   * Length of the vector
158
+   * @param A
159
+   */
160
+  static len = (A: number[]): number => {
161
+    return Math.hypot(A[0], A[1])
162
+  }
163
+
164
+  /**
165
+   * Project A over B
166
+   * @param A
167
+   * @param B
168
+   */
169
+  static pry = (A: number[], B: number[]): number => {
170
+    return Vec.dpr(A, B) / Vec.len(B)
171
+  }
172
+
173
+  /**
174
+   * Get normalized / unit vector.
175
+   * @param A
176
+   */
177
+  static uni = (A: number[]): number[] => {
178
+    return Vec.div(A, Vec.len(A))
179
+  }
180
+
181
+  /**
182
+   * Get normalized / unit vector.
183
+   * @param A
184
+   */
185
+  static normalize = (A: number[]): number[] => {
186
+    return Vec.uni(A)
187
+  }
188
+
189
+  /**
190
+   * Get the tangent between two vectors.
191
+   * @param A
192
+   * @param B
193
+   * @returns
194
+   */
195
+  static tangent = (A: number[], B: number[]): number[] => {
196
+    return Vec.uni(Vec.sub(A, B))
197
+  }
198
+
199
+  /**
200
+   * Dist length from A to B squared.
201
+   * @param A
202
+   * @param B
203
+   */
204
+  static dist2 = (A: number[], B: number[]): number => {
205
+    return Vec.len2(Vec.sub(A, B))
206
+  }
207
+
208
+  /**
209
+   * Dist length from A to B
210
+   * @param A
211
+   * @param B
212
+   */
213
+  static dist = (A: number[], B: number[]): number => {
214
+    return Math.hypot(A[1] - B[1], A[0] - B[0])
215
+  }
216
+
217
+  /**
218
+   * A faster, though less accurate method for testing distances. Maybe faster?
219
+   * @param A
220
+   * @param B
221
+   * @returns
222
+   */
223
+  static fastDist = (A: number[], B: number[]): number[] => {
224
+    const V = [B[0] - A[0], B[1] - A[1]]
225
+    const aV = [Math.abs(V[0]), Math.abs(V[1])]
226
+    let r = 1 / Math.max(aV[0], aV[1])
227
+    r = r * (1.29289 - (aV[0] + aV[1]) * r * 0.29289)
228
+    return [V[0] * r, V[1] * r]
229
+  }
230
+
231
+  /**
232
+   * Angle between vector A and vector B in radians
233
+   * @param A
234
+   * @param B
235
+   */
236
+  static ang = (A: number[], B: number[]): number => {
237
+    return Math.atan2(Vec.cpr(A, B), Vec.dpr(A, B))
238
+  }
239
+
240
+  /**
241
+   * Angle between vector A and vector B in radians
242
+   * @param A
243
+   * @param B
244
+   */
245
+  static angle = (A: number[], B: number[]): number => {
246
+    return Math.atan2(B[1] - A[1], B[0] - A[0])
247
+  }
248
+
249
+  /**
250
+   * Mean between two vectors or mid vector between two vectors
251
+   * @param A
252
+   * @param B
253
+   */
254
+  static med = (A: number[], B: number[]): number[] => {
255
+    return Vec.mul(Vec.add(A, B), 0.5)
256
+  }
257
+
258
+  /**
259
+   * Vector rotation by r (radians)
260
+   * @param A
261
+   * @param r rotation in radians
262
+   */
263
+  static rot = (A: number[], r = 0): number[] => {
264
+    return [A[0] * Math.cos(r) - A[1] * Math.sin(r), A[0] * Math.sin(r) + A[1] * Math.cos(r)]
265
+  }
266
+
267
+  /**
268
+   * Rotate a vector around another vector by r (radians)
269
+   * @param A vector
270
+   * @param C center
271
+   * @param r rotation in radians
272
+   */
273
+  static rotWith = (A: number[], C: number[], r = 0): number[] => {
274
+    if (r === 0) return A
275
+
276
+    const s = Math.sin(r)
277
+    const c = Math.cos(r)
278
+
279
+    const px = A[0] - C[0]
280
+    const py = A[1] - C[1]
281
+
282
+    const nx = px * c - py * s
283
+    const ny = px * s + py * c
284
+
285
+    return [nx + C[0], ny + C[1]]
286
+  }
287
+
288
+  /**
289
+   * Check of two vectors are identical.
290
+   * @param A
291
+   * @param B
292
+   */
293
+  static isEqual = (A: number[], B: number[]): boolean => {
294
+    return A[0] === B[0] && A[1] === B[1]
295
+  }
296
+
297
+  /**
298
+   * Interpolate vector A to B with a scalar t
299
+   * @param A
300
+   * @param B
301
+   * @param t scalar
302
+   */
303
+  static lrp = (A: number[], B: number[], t: number): number[] => {
304
+    return Vec.add(A, Vec.mul(Vec.sub(B, A), t))
305
+  }
306
+
307
+  /**
308
+   * Interpolate from A to B when curVAL goes fromVAL: number[] => to
309
+   * @param A
310
+   * @param B
311
+   * @param from Starting value
312
+   * @param to Ending value
313
+   * @param s Strength
314
+   */
315
+  static int = (A: number[], B: number[], from: number, to: number, s = 1): number[] => {
316
+    const t = (Vec.clamp(from, to) - from) / (to - from)
317
+    return Vec.add(Vec.mul(A, 1 - t), Vec.mul(B, s))
318
+  }
319
+
320
+  /**
321
+   * Get the angle between the three vectors A, B, and C.
322
+   * @param p1
323
+   * @param pc
324
+   * @param p2
325
+   */
326
+  static ang3 = (p1: number[], pc: number[], p2: number[]): number => {
327
+    // this,
328
+    const v1 = Vec.vec(pc, p1)
329
+    const v2 = Vec.vec(pc, p2)
330
+    return Vec.ang(v1, v2)
331
+  }
332
+
333
+  /**
334
+   * Absolute value of a vector.
335
+   * @param A
336
+   * @returns
337
+   */
338
+  static abs = (A: number[]): number[] => {
339
+    return [Math.abs(A[0]), Math.abs(A[1])]
340
+  }
341
+
342
+  static rescale = (a: number[], n: number): number[] => {
343
+    const l = Vec.len(a)
344
+    return [(n * a[0]) / l, (n * a[1]) / l]
345
+  }
346
+
347
+  /**
348
+   * Get whether p1 is left of p2, relative to pc.
349
+   * @param p1
350
+   * @param pc
351
+   * @param p2
352
+   */
353
+  static isLeft = (p1: number[], pc: number[], p2: number[]): number => {
354
+    //  isLeft: >0 for counterclockwise
355
+    //          =0 for none (degenerate)
356
+    //          <0 for clockwise
357
+    return (pc[0] - p1[0]) * (p2[1] - p1[1]) - (p2[0] - p1[0]) * (pc[1] - p1[1])
358
+  }
359
+
360
+  /**
361
+   * Get whether p1 is left of p2, relative to pc.
362
+   * @param p1
363
+   * @param pc
364
+   * @param p2
365
+   */
366
+  static clockwise = (p1: number[], pc: number[], p2: number[]): boolean => {
367
+    return Vec.isLeft(p1, pc, p2) > 0
368
+  }
369
+
370
+  /**
371
+   * Round a vector to the a given precision.
372
+   * @param a
373
+   * @param d
374
+   */
375
+  static toFixed = (a: number[], d = 2): number[] => {
376
+    return a.map((v) => +v.toFixed(d))
377
+  }
378
+
379
+  /**
380
+   * Snap vector to nearest step.
381
+   * @param A
382
+   * @param step
383
+   * @example
384
+   * ```ts
385
+   * Vec.snap([10.5, 28], 10) // [10, 30]
386
+   * ```
387
+   */
388
+  static snap(a: number[], step = 1) {
389
+    return [Math.round(a[0] / step) * step, Math.round(a[1] / step) * step]
390
+  }
391
+
392
+  /**
393
+   * Get the nearest point on a line with a known unit vector that passes through point A
394
+   * @param A Any point on the line
395
+   * @param u The unit vector for the line.
396
+   * @param P A point not on the line to test.
397
+   * @returns
398
+   */
399
+  static nearestPointOnLineThroughPoint = (A: number[], u: number[], P: number[]): number[] => {
400
+    return Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u)))
401
+  }
402
+
403
+  /**
404
+   * Distance between a point and a line with a known unit vector that passes through a point.
405
+   * @param A Any point on the line
406
+   * @param u The unit vector for the line.
407
+   * @param P A point not on the line to test.
408
+   * @returns
409
+   */
410
+  static distanceToLineThroughPoint = (A: number[], u: number[], P: number[]): number => {
411
+    return Vec.dist(P, Vec.nearestPointOnLineThroughPoint(A, u, P))
412
+  }
413
+
414
+  /**
415
+   * Get the nearest point on a line segment between A and B
416
+   * @param A The start of the line segment
417
+   * @param B The end of the line segment
418
+   * @param P The off-line point
419
+   * @param clamp Whether to clamp the point between A and B.
420
+   * @returns
421
+   */
422
+  static nearestPointOnLineSegment = (
423
+    A: number[],
424
+    B: number[],
425
+    P: number[],
426
+    clamp = true
427
+  ): number[] => {
428
+    const u = Vec.uni(Vec.sub(B, A))
429
+    const C = Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u)))
430
+
431
+    if (clamp) {
432
+      if (C[0] < Math.min(A[0], B[0])) return A[0] < B[0] ? A : B
433
+      if (C[0] > Math.max(A[0], B[0])) return A[0] > B[0] ? A : B
434
+      if (C[1] < Math.min(A[1], B[1])) return A[1] < B[1] ? A : B
435
+      if (C[1] > Math.max(A[1], B[1])) return A[1] > B[1] ? A : B
436
+    }
437
+
438
+    return C
439
+  }
440
+
441
+  /**
442
+   * Distance between a point and the nearest point on a line segment between A and B
443
+   * @param A The start of the line segment
444
+   * @param B The end of the line segment
445
+   * @param P The off-line point
446
+   * @param clamp Whether to clamp the point between A and B.
447
+   * @returns
448
+   */
449
+  static distanceToLineSegment = (A: number[], B: number[], P: number[], clamp = true): number => {
450
+    return Vec.dist(P, Vec.nearestPointOnLineSegment(A, B, P, clamp))
451
+  }
452
+
453
+  /**
454
+   * Push a point A towards point B by a given distance.
455
+   * @param A
456
+   * @param B
457
+   * @param d
458
+   * @returns
459
+   */
460
+  static nudge = (A: number[], B: number[], d: number): number[] => {
461
+    return Vec.add(A, Vec.mul(Vec.uni(Vec.sub(B, A)), d))
462
+  }
463
+
464
+  /**
465
+   * Push a point in a given angle by a given distance.
466
+   * @param A
467
+   * @param B
468
+   * @param d
469
+   */
470
+  static nudgeAtAngle = (A: number[], a: number, d: number): number[] => {
471
+    return [Math.cos(a) * d + A[0], Math.sin(a) * d + A[1]]
472
+  }
473
+
474
+  /**
475
+   * Round a vector to a precision length.
476
+   * @param a
477
+   * @param n
478
+   */
479
+  static toPrecision = (a: number[], n = 4): number[] => {
480
+    return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
481
+  }
482
+
483
+  /**
484
+   * Get an array of points (with simulated pressure) between two points.
485
+   * @param A The first point.
486
+   * @param B The second point.
487
+   * @param steps The number of points to return.
488
+   * @param ease An easing function to apply to the simulated pressure.
489
+   */
490
+  static pointsBetween = (A: number[], B: number[], steps = 6): number[][] => {
491
+    return Array.from(Array(steps)).map((_, i) => {
492
+      const t = i / (steps - 1)
493
+      const k = Math.min(1, 0.5 + Math.abs(0.5 - t))
494
+      return [...Vec.lrp(A, B, t), k]
495
+    })
496
+  }
497
+}
498
+
499
+export default Vec

+ 0
- 0
packages/vec/tsconfig.build.json View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save