[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>
This commit is contained in:
parent
3de6ef334a
commit
e2814943e9
105 changed files with 4795 additions and 300 deletions
13
.vscode/tasks.json
vendored
13
.vscode/tasks.json
vendored
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Check for type errors",
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"option": "watch",
|
||||
"problemMatcher": ["$tsc-watch"],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
|
||||
import * as gtag from '-utils/gtag'
|
||||
import React from 'react'
|
||||
import { useAccountHandlers } from '-hooks/useAccountHandlers'
|
||||
import * as gtag from 'utils/gtag'
|
||||
import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
|
||||
import { useAccountHandlers } from 'hooks/useAccountHandlers'
|
||||
|
||||
declare const window: Window & { app: TldrawApp }
|
||||
|
||||
|
@ -19,9 +19,9 @@ export default function Editor({ id = 'home', isUser = false, isSponsor = false
|
|||
// Send events to gtag as actions.
|
||||
const handlePersist = React.useCallback((_app: TldrawApp, reason?: string) => {
|
||||
gtag.event({
|
||||
action: reason,
|
||||
action: reason ?? '',
|
||||
category: 'editor',
|
||||
label: reason || 'persist',
|
||||
label: reason ?? 'persist',
|
||||
value: 0,
|
||||
})
|
||||
}, [])
|
||||
|
|
|
@ -3,9 +3,9 @@ import * as React from 'react'
|
|||
import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
|
||||
import { createClient } from '@liveblocks/client'
|
||||
import { LiveblocksProvider, RoomProvider } from '@liveblocks/react'
|
||||
import { useAccountHandlers } from '-hooks/useAccountHandlers'
|
||||
import { styled } from '-styles'
|
||||
import { useMultiplayerState } from '-hooks/useMultiplayerState'
|
||||
import { useAccountHandlers } from 'hooks/useAccountHandlers'
|
||||
import { styled } from 'styles'
|
||||
import { useMultiplayerState } from 'hooks/useMultiplayerState'
|
||||
|
||||
const client = createClient({
|
||||
publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_API_KEY || '',
|
||||
|
|
|
@ -129,10 +129,17 @@ export function useMultiplayerState(roomId: string) {
|
|||
page: { shapes, bindings },
|
||||
},
|
||||
},
|
||||
} = doc.toObject()
|
||||
} = doc.toObject() as { document: TDDocument }
|
||||
|
||||
Object.values(shapes).forEach((shape) => lShapes.set(shape.id, shape))
|
||||
Object.values(bindings).forEach((binding) => lBindings.set(binding.id, binding))
|
||||
for (const key in shapes) {
|
||||
const shape = shapes[key]
|
||||
lShapes.set(shape.id, shape)
|
||||
}
|
||||
|
||||
for (const key in bindings) {
|
||||
const binding = bindings[key]
|
||||
lBindings.set(binding.id, binding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,21 +182,23 @@ export function useMultiplayerState(roomId: string) {
|
|||
|
||||
if (!(lShapes && lBindings)) return
|
||||
|
||||
Object.entries(shapes).forEach(([id, shape]) => {
|
||||
for (const id in shapes) {
|
||||
const shape = shapes[id]
|
||||
if (!shape) {
|
||||
lShapes.delete(id)
|
||||
} else {
|
||||
lShapes.set(shape.id, shape)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Object.entries(bindings).forEach(([id, binding]) => {
|
||||
for (const id in bindings) {
|
||||
const binding = bindings[id]
|
||||
if (!binding) {
|
||||
lBindings.delete(id)
|
||||
} else {
|
||||
lBindings.set(binding.id, binding)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
rExpectingUpdate.current = true
|
||||
})
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"@sentry/react": "^6.13.2",
|
||||
"@sentry/tracing": "^6.13.2",
|
||||
"@stitches/react": "^1.2.5",
|
||||
"@tldraw/core": "^1.1.4",
|
||||
"@tldraw/tldraw": "^1.1.4",
|
||||
"@types/next-auth": "^3.15.0",
|
||||
"next": "^12.0.1",
|
||||
|
@ -42,7 +43,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-next": "11.1.2",
|
||||
"typescript": "^4.4.2"
|
||||
"typescript": "^4.5.2"
|
||||
},
|
||||
"gitHead": "838fabdbff1a66d4d7ee8aa5c5d117bc55acbff2"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import '../styles/globals.css'
|
||||
import { init } from '-utils/sentry'
|
||||
import Head from 'next/head'
|
||||
import useGtag from '-utils/useGtag'
|
||||
import useGtag from 'utils/useGtag'
|
||||
import { init } from 'utils/sentry'
|
||||
import type { AppProps } from 'next/app'
|
||||
import type React from 'react'
|
||||
|
||||
init()
|
||||
|
||||
|
@ -10,7 +12,7 @@ const APP_DESCRIPTION = 'A tiny little drawing app.'
|
|||
const APP_URL = 'https://tldraw.com'
|
||||
const IMAGE = 'https://tldraw.com/social-image.png'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
useGtag()
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,29 +1,19 @@
|
|||
import NextDocument, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
|
||||
import { getCssText } from '../styles'
|
||||
import { GA_TRACKING_ID } from '../utils/gtag'
|
||||
import { getCssText } from 'styles'
|
||||
import { GA_TRACKING_ID } from 'utils/gtag'
|
||||
|
||||
class MyDocument extends NextDocument {
|
||||
static async getInitialProps(ctx: DocumentContext): Promise<{
|
||||
styles: JSX.Element
|
||||
html: string
|
||||
head?: JSX.Element[]
|
||||
}> {
|
||||
try {
|
||||
const initialProps = await NextDocument.getInitialProps(ctx)
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const initialProps = await NextDocument.getInitialProps(ctx)
|
||||
|
||||
return {
|
||||
...initialProps,
|
||||
styles: (
|
||||
<>
|
||||
{initialProps.styles}
|
||||
<style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
|
||||
</>
|
||||
),
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
} finally {
|
||||
null
|
||||
return {
|
||||
...initialProps,
|
||||
styles: (
|
||||
<>
|
||||
{initialProps.styles}
|
||||
<style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
|
||||
</>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { isSponsoringMe } from '-utils/isSponsoringMe'
|
||||
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
||||
import { isSponsoringMe } from 'utils/isSponsoringMe'
|
||||
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
|
@ -20,13 +20,15 @@ export default function Auth(
|
|||
return baseUrl
|
||||
},
|
||||
async signIn(user, account, profile: { login?: string }) {
|
||||
const canLogin = await isSponsoringMe(profile?.login)
|
||||
if (profile?.login) {
|
||||
const canLogin = await isSponsoringMe(profile.login)
|
||||
|
||||
if (canLogin) {
|
||||
return canLogin
|
||||
} else {
|
||||
return '/sponsorware'
|
||||
if (canLogin) {
|
||||
return canLogin
|
||||
}
|
||||
}
|
||||
|
||||
return '/'
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
const AV_SIZE = 32
|
||||
const PADDING = 4
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import dynamic from 'next/dynamic'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import type { GetServerSideProps } from 'next'
|
||||
import { getSession } from 'next-auth/client'
|
||||
import Head from 'next/head'
|
||||
|
||||
const Editor = dynamic(() => import('-components/Editor'), { ssr: false })
|
||||
const Editor = dynamic(() => import('components/Editor'), { ssr: false })
|
||||
|
||||
interface PageProps {
|
||||
isUser: boolean
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react'
|
|||
import type { GetServerSideProps } from 'next'
|
||||
import { getSession } from 'next-auth/client'
|
||||
import dynamic from 'next/dynamic'
|
||||
const MultiplayerEditor = dynamic(() => import('-components/MultiplayerEditor'), { ssr: false })
|
||||
const MultiplayerEditor = dynamic(() => import('components/MultiplayerEditor'), { ssr: false })
|
||||
|
||||
interface RoomProps {
|
||||
id: string
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { styled } from 'styles'
|
||||
import { getSession, signin, signout, useSession } from 'next-auth/client'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import type { GetServerSideProps } from 'next'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import Head from 'next/head'
|
||||
|
|
|
@ -1,28 +1,37 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"composite": false,
|
||||
"incremental": false,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": ["dom", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"-*": ["./*"],
|
||||
"@tldraw/core": ["../../packages/core"],
|
||||
"@tldraw/tldraw": ["../../packages/tldraw"]
|
||||
},
|
||||
"incremental": true,
|
||||
"resolveJsonModule": true
|
||||
"*": ["./*"],
|
||||
"@tldraw/tldraw": ["./packages/tldraw"],
|
||||
"@tldraw/core": ["./packages/core"],
|
||||
"@tldraw/intersect": ["./packages/intersect"],
|
||||
"@tldraw/vec": ["./packages/vec"]
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../../packages/vec" },
|
||||
{ "path": "../../packages/intersect" },
|
||||
{ "path": "../../packages/core" },
|
||||
{ "path": "../../packages/tldraw" }
|
||||
],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
@ -1,6 +1 @@
|
|||
import { TDDocument } from '@tldraw/tldraw'
|
||||
import { LiveObject } from '@liveblocks/client'
|
||||
|
||||
export interface TldrawStorage {
|
||||
doc: LiveObject<{ uuid: string; document: TDDocument }>
|
||||
}
|
||||
export {}
|
||||
|
|
|
@ -13,11 +13,11 @@ export function init(): void {
|
|||
integrations.push(
|
||||
new RewriteFrames({
|
||||
iteratee: (frame) => {
|
||||
frame.filename = frame.filename.replace(
|
||||
process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR,
|
||||
frame.filename = frame?.filename?.replace(
|
||||
process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR as string,
|
||||
'app:///'
|
||||
)
|
||||
frame.filename = frame.filename.replace('.next', '_next')
|
||||
frame.filename = frame?.filename?.replace('.next', '_next')
|
||||
return frame
|
||||
},
|
||||
})
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
"devDependencies": {
|
||||
"@state-designer/react": "3.0.0",
|
||||
"@stitches/react": "^1.2.5",
|
||||
"@tldraw/core": "^1.1.3",
|
||||
"@tldraw/intersect": "latest",
|
||||
"@tldraw/vec": "latest",
|
||||
"@tldraw/core": "^1.1.4",
|
||||
"@tldraw/intersect": "^1.1.4",
|
||||
"@tldraw/vec": "^1.1.4",
|
||||
"@types/node": "^14.14.35",
|
||||
"@types/react": "^16.9.55",
|
||||
"@types/react-dom": "^16.9.9",
|
||||
|
@ -40,4 +40,4 @@
|
|||
"typescript": "4.2.3"
|
||||
},
|
||||
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
|
||||
}
|
||||
}
|
|
@ -8,6 +8,8 @@ export abstract class CustomShapeUtil<
|
|||
|
||||
canBind = false
|
||||
|
||||
hideBounds = false
|
||||
|
||||
abstract getCenter: (shape: T) => number[]
|
||||
|
||||
abstract getShape: (shape: Partial<T>) => T
|
||||
|
|
|
@ -8,6 +8,6 @@ export const pinchCamera: Action = (data, payload: TLPointerInfo) => {
|
|||
const nextPoint = Vec.sub(camera.point, Vec.div(payload.delta, camera.zoom))
|
||||
const p0 = Vec.sub(Vec.div(payload.point, camera.zoom), nextPoint)
|
||||
const p1 = Vec.sub(Vec.div(payload.point, nextZoom), nextPoint)
|
||||
data.pageState.camera.point = Vec.round(Vec.add(nextPoint, Vec.sub(p1, p0)))
|
||||
data.pageState.camera.point = Vec.toFixed(Vec.add(nextPoint, Vec.sub(p1, p0)))
|
||||
data.pageState.camera.zoom = nextZoom
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export const zoomIn: Action = (data) => {
|
|||
const center = [mutables.rendererBounds.width / 2, mutables.rendererBounds.height / 2]
|
||||
const p0 = Vec.sub(Vec.div(center, camera.zoom), center)
|
||||
const p1 = Vec.sub(Vec.div(center, zoom), center)
|
||||
const point = Vec.round(Vec.add(camera.point, Vec.sub(p1, p0)))
|
||||
const point = Vec.toFixed(Vec.add(camera.point, Vec.sub(p1, p0)))
|
||||
|
||||
data.pageState.camera.zoom = zoom
|
||||
data.pageState.camera.point = point
|
||||
|
|
|
@ -10,7 +10,7 @@ export const zoomOut: Action = (data) => {
|
|||
const center = [mutables.rendererBounds.width / 2, mutables.rendererBounds.height / 2]
|
||||
const p0 = Vec.sub(Vec.div(center, camera.zoom), center)
|
||||
const p1 = Vec.sub(Vec.div(center, zoom), center)
|
||||
const point = Vec.round(Vec.add(camera.point, Vec.sub(p1, p0)))
|
||||
const point = Vec.toFixed(Vec.add(camera.point, Vec.sub(p1, p0)))
|
||||
|
||||
data.pageState.camera.zoom = zoom
|
||||
data.pageState.camera.point = point
|
||||
|
|
|
@ -38,5 +38,5 @@ export function getZoomFitCamera(
|
|||
export function getZoomedCameraPoint(nextZoom: number, center: number[], pageState: TLPageState) {
|
||||
const p0 = Vec.sub(Vec.div(center, pageState.camera.zoom), pageState.camera.point)
|
||||
const p1 = Vec.sub(Vec.div(center, nextZoom), pageState.camera.point)
|
||||
return Vec.round(Vec.add(pageState.camera.point, Vec.sub(p1, p0)))
|
||||
return Vec.toFixed(Vec.add(pageState.camera.point, Vec.sub(p1, p0)))
|
||||
}
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
"exclude": ["node_modules", "dist", "docs"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/types",
|
||||
"baseUrl": ".",
|
||||
"rootDir": "src",
|
||||
"baseUrl": "src",
|
||||
"emitDeclarationOnly": false,
|
||||
"paths": {
|
||||
"*": ["src/*"],
|
||||
"@tldraw/core": ["../../packages/core"]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"name": "@tldraw/core-example-simple",
|
||||
"description": "A simple example project for @tldraw/core.",
|
||||
"author": "@steveruizok",
|
||||
|
@ -15,8 +15,8 @@
|
|||
},
|
||||
"files": [],
|
||||
"devDependencies": {
|
||||
"@tldraw/core": "^1.1.3",
|
||||
"@tldraw/vec": "^0.0.130",
|
||||
"@tldraw/core": "^1.1.4",
|
||||
"@tldraw/vec": "^1.1.4",
|
||||
"@types/node": "^14.14.35",
|
||||
"@types/react": "^16.9.55",
|
||||
"@types/react-dom": "^16.9.9",
|
||||
|
@ -30,4 +30,4 @@
|
|||
"typescript": "4.2.3"
|
||||
},
|
||||
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
|
||||
}
|
||||
}
|
|
@ -4,10 +4,11 @@
|
|||
"exclude": ["node_modules", "dist", "docs"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/types",
|
||||
"baseUrl": ".",
|
||||
"rootDir": "src",
|
||||
"baseUrl": "src",
|
||||
"emitDeclarationOnly": false,
|
||||
"paths": {
|
||||
"*": ["src/*"],
|
||||
"@tldraw/core": ["../packages/core"]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
"exclude": ["node_modules", "dist", "docs"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/types",
|
||||
"baseUrl": ".",
|
||||
"rootDir": "src",
|
||||
"baseUrl": "src",
|
||||
"emitDeclarationOnly": false,
|
||||
"paths": {
|
||||
"+*": ["./*"],
|
||||
"~*": ["./src/*"],
|
||||
"@tldraw/tldraw": ["../../packages/tldraw"]
|
||||
}
|
||||
},
|
||||
|
|
10
package.json
10
package.json
|
@ -9,8 +9,10 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/tldraw",
|
||||
"packages/vec",
|
||||
"packages/intersect",
|
||||
"packages/core",
|
||||
"packages/tldraw",
|
||||
"apps/www",
|
||||
"apps/electron",
|
||||
"apps/vscode/editor",
|
||||
|
@ -38,7 +40,7 @@
|
|||
"test:watch": "lerna run test:watch --stream",
|
||||
"docs": "lerna run typedoc",
|
||||
"docs:watch": "lerna run typedoc --watch",
|
||||
"postinstall": "husky install & yarn build:packages"
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc-node/jest": "^1.3.3",
|
||||
|
@ -62,7 +64,7 @@
|
|||
"resize-observer-polyfill": "^1.5.1",
|
||||
"tslib": "^2.3.0",
|
||||
"typedoc": "^0.22.3",
|
||||
"typescript": "^4.4.2"
|
||||
"typescript": "^4.5.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
@ -73,4 +75,4 @@
|
|||
"lint-staged": {
|
||||
"*": "fix:style && eslint"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"name": "@tldraw/core",
|
||||
"description": "The tldraw core renderer and utilities.",
|
||||
"author": "@steveruizok",
|
||||
|
@ -37,8 +37,8 @@
|
|||
"test:watch": "jest --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tldraw/intersect": "latest",
|
||||
"@tldraw/vec": "latest",
|
||||
"@tldraw/intersect": "^1.1.4",
|
||||
"@tldraw/vec": "^1.1.4",
|
||||
"@use-gesture/react": "^10.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -81,4 +81,4 @@
|
|||
"\\~(.*)": "<rootDir>/src/$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import * as React from 'react'
|
||||
import Utils from '../utils'
|
||||
import { intersectPolylineBounds } from '@tldraw/intersect'
|
||||
import type { TLBounds, TLComponentProps, TLForwardedRef, TLShape, TLUser } from '../types'
|
||||
import { intersectPolylineBounds } from '@tldraw/intersect'
|
||||
|
||||
export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M = any> {
|
||||
refMap = new Map<string, React.RefObject<E>>()
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useResizeObserver } from '~hooks/useResizeObserver'
|
|||
import { inputs } from '~inputs'
|
||||
import { UsersIndicators } from '~components/users-indicators'
|
||||
import { SnapLines } from '~components/snap-lines/snap-lines'
|
||||
import { Grid } from '~components/grid'
|
||||
import { Overlay } from '~components/overlay'
|
||||
|
||||
function resetError() {
|
||||
|
@ -28,6 +29,7 @@ interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
|||
page: TLPage<T, TLBinding>
|
||||
pageState: TLPageState
|
||||
snapLines?: TLSnapLine[]
|
||||
grid?: number
|
||||
users?: TLUsers<T>
|
||||
userId?: string
|
||||
hideBounds: boolean
|
||||
|
@ -37,6 +39,7 @@ interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
|||
hideCloneHandles: boolean
|
||||
hideResizeHandles: boolean
|
||||
hideRotateHandle: boolean
|
||||
hideGrid: boolean
|
||||
externalContainerRef?: React.RefObject<HTMLElement>
|
||||
meta?: M
|
||||
id?: string
|
||||
|
@ -48,6 +51,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
page,
|
||||
pageState,
|
||||
snapLines,
|
||||
grid,
|
||||
users,
|
||||
userId,
|
||||
meta,
|
||||
|
@ -59,6 +63,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
hideCloneHandles,
|
||||
hideResizeHandles,
|
||||
hideRotateHandle,
|
||||
hideGrid,
|
||||
onBoundsChange,
|
||||
}: CanvasProps<T, M>): JSX.Element {
|
||||
const rCanvas = React.useRef<HTMLDivElement>(null)
|
||||
|
@ -85,6 +90,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
<div id={id} className="tl-container" ref={rContainer}>
|
||||
<div id="canvas" className="tl-absolute tl-canvas" ref={rCanvas} {...events}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
|
||||
{!hideGrid && grid && <Grid grid={grid} camera={pageState.camera} />}
|
||||
<div ref={rLayer} className="tl-absolute tl-layer">
|
||||
<Page
|
||||
page={page}
|
||||
|
|
42
packages/core/src/components/grid/grid.tsx
Normal file
42
packages/core/src/components/grid/grid.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import * as React from 'react'
|
||||
import type { TLPageState } from '~types'
|
||||
import Utils from '~utils'
|
||||
|
||||
const STEPS = [
|
||||
[-1, 0.15, 64],
|
||||
[0.05, 0.375, 16],
|
||||
[0.15, 1, 4],
|
||||
[0.7, 2.5, 1],
|
||||
]
|
||||
|
||||
export function Grid({ grid, camera }: { camera: TLPageState['camera']; grid: number }) {
|
||||
return (
|
||||
<svg className="tl-grid" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
{STEPS.map(([min, mid, size], i) => {
|
||||
const s = size * grid * camera.zoom
|
||||
const xo = camera.point[0] * camera.zoom
|
||||
const yo = camera.point[1] * camera.zoom
|
||||
const gxo = xo > 0 ? xo % s : s + (xo % s)
|
||||
const gyo = yo > 0 ? yo % s : s + (yo % s)
|
||||
const opacity = camera.zoom < mid ? Utils.modulate(camera.zoom, [min, mid], [0, 1]) : 1
|
||||
|
||||
return (
|
||||
<pattern
|
||||
key={`grid-pattern-${i}`}
|
||||
id={`grid-${i}`}
|
||||
width={s}
|
||||
height={s}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<circle className={`tl-grid-dot`} cx={gxo} cy={gyo} r={1} opacity={opacity} />
|
||||
</pattern>
|
||||
)
|
||||
})}
|
||||
</defs>
|
||||
{STEPS.map((_, i) => (
|
||||
<rect key={`grid-rect-${i}`} width="100%" height="100%" fill={`url(#grid-${i})`} />
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
}
|
1
packages/core/src/components/grid/index.ts
Normal file
1
packages/core/src/components/grid/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './grid'
|
|
@ -86,6 +86,14 @@ export interface RendererProps<T extends TLShape, M = any> extends Partial<TLCal
|
|||
* hovered objects,
|
||||
*/
|
||||
hideIndicators?: boolean
|
||||
/**
|
||||
* (optional) When true, the renderer will not show the grid.
|
||||
*/
|
||||
hideGrid?: boolean
|
||||
/**
|
||||
* (optional) The size of the grid step.
|
||||
*/
|
||||
grid?: number
|
||||
/**
|
||||
* (optional) A callback that receives the renderer's inputs manager.
|
||||
*/
|
||||
|
@ -114,6 +122,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
|||
theme,
|
||||
meta,
|
||||
snapLines,
|
||||
grid,
|
||||
containerRef,
|
||||
hideHandles = false,
|
||||
hideIndicators = false,
|
||||
|
@ -122,6 +131,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
|||
hideResizeHandles = false,
|
||||
hideRotateHandles = false,
|
||||
hideBounds = false,
|
||||
hideGrid = true,
|
||||
...rest
|
||||
}: RendererProps<T, M>): JSX.Element {
|
||||
useTLTheme(theme, '#' + id)
|
||||
|
@ -164,6 +174,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
|||
page={page}
|
||||
pageState={pageState}
|
||||
snapLines={snapLines}
|
||||
grid={grid}
|
||||
users={users}
|
||||
userId={userId}
|
||||
externalContainerRef={containerRef}
|
||||
|
@ -174,6 +185,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
|||
hideBindingHandles={hideBindingHandles}
|
||||
hideRotateHandle={hideRotateHandles}
|
||||
hideResizeHandles={hideResizeHandles}
|
||||
hideGrid={hideGrid}
|
||||
onBoundsChange={onBoundsChange}
|
||||
meta={meta}
|
||||
/>
|
||||
|
|
|
@ -73,6 +73,7 @@ const defaultTheme: TLTheme = {
|
|||
selectFill: 'rgba(65, 132, 244, 0.05)',
|
||||
background: 'rgb(248, 249, 250)',
|
||||
foreground: 'rgb(51, 51, 51)',
|
||||
grid: 'rgba(144, 144, 144, 1)',
|
||||
}
|
||||
|
||||
const tlcss = css`
|
||||
|
@ -142,6 +143,15 @@ const tlcss = css`
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tl-grid {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tl-snap-line {
|
||||
stroke: var(--tl-accent);
|
||||
stroke-width: calc(1px * var(--tl-scale));
|
||||
|
@ -394,6 +404,10 @@ const tlcss = css`
|
|||
stroke: var(--tl-selectStroke);
|
||||
stroke-width: calc(2px * var(--tl-scale));
|
||||
}
|
||||
|
||||
.tl-grid-dot {
|
||||
fill: var(--tl-grid);
|
||||
}
|
||||
`
|
||||
|
||||
export function useTLTheme(theme?: Partial<TLTheme>, selector?: string) {
|
||||
|
|
|
@ -360,7 +360,7 @@ export class Inputs {
|
|||
target: 'pinch',
|
||||
origin,
|
||||
delta: delta,
|
||||
point: Vec.sub(Vec.round(point), [this.bounds.minX, this.bounds.minY]),
|
||||
point: Vec.sub(Vec.toFixed(point), [this.bounds.minX, this.bounds.minY]),
|
||||
pressure: 0.5,
|
||||
shiftKey,
|
||||
ctrlKey,
|
||||
|
|
|
@ -108,6 +108,7 @@ export interface TLTheme {
|
|||
selectStroke?: string
|
||||
background?: string
|
||||
foreground?: string
|
||||
grid?: string
|
||||
}
|
||||
|
||||
export type TLWheelEventHandler = (
|
||||
|
|
|
@ -1089,6 +1089,26 @@ export class Utils {
|
|||
return this.translateBounds(bounds, [dx, dy])
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap a bounding box to a grid size.
|
||||
* @param bounds
|
||||
* @param gridSize
|
||||
*/
|
||||
static snapBoundsToGrid(bounds: TLBounds, gridSize: number): TLBounds {
|
||||
const minX = Math.round(bounds.minX / gridSize) * gridSize
|
||||
const minY = Math.round(bounds.minY / gridSize) * gridSize
|
||||
const maxX = Math.round(bounds.maxX / gridSize) * gridSize
|
||||
const maxY = Math.round(bounds.maxY / gridSize) * gridSize
|
||||
return {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
width: Math.max(1, maxX - minX),
|
||||
height: Math.max(1, maxY - minY),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a bounding box without recalculating it.
|
||||
* @param bounds
|
||||
|
@ -1509,12 +1529,10 @@ left past the initial left edge) then swap points on that axis.
|
|||
(isFlippedX
|
||||
? initialBounds.maxX - initialShapeBounds.maxX
|
||||
: initialShapeBounds.minX - initialBounds.minX) / initialBounds.width
|
||||
|
||||
const ny =
|
||||
(isFlippedY
|
||||
? initialBounds.maxY - initialShapeBounds.maxY
|
||||
: initialShapeBounds.minY - initialBounds.minY) / initialBounds.height
|
||||
|
||||
const nw = initialShapeBounds.width / initialBounds.width
|
||||
const nh = initialShapeBounds.height / initialBounds.height
|
||||
|
||||
|
@ -1562,7 +1580,7 @@ left past the initial left edge) then swap points on that axis.
|
|||
* Get a bounding box with a midX and midY.
|
||||
* @param bounds
|
||||
*/
|
||||
static getBoundsWithCenter(bounds: TLBounds): TLBounds & { midX: number; midY: number } {
|
||||
static getBoundsWithCenter(bounds: TLBounds): TLBoundsWithCenter {
|
||||
const center = Utils.getBoundsCenter(bounds)
|
||||
return {
|
||||
...bounds,
|
||||
|
|
|
@ -13,8 +13,9 @@
|
|||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"incremental": false,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false,
|
||||
"emitDeclarationOnly": true
|
||||
}
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"references": [{ "path": "../vec" }, { "path": "../intersect" }]
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "docs"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/types",
|
||||
"rootDir": "src",
|
||||
"baseUrl": "src",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~*": ["./*"]
|
||||
"~*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"references": [{ "path": "../vec" }, { "path": "../intersect" }],
|
||||
"typedocOptions": {
|
||||
"entryPoints": ["src/index.ts"],
|
||||
"out": "docs"
|
||||
|
|
9
packages/intersect/CHANGELOG.md
Normal file
9
packages/intersect/CHANGELOG.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
## 0.1.4
|
||||
|
||||
- Fixes bug in `polyline`, adds `polygon` intersections.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Hello world.
|
21
packages/intersect/LICENSE.md
Normal file
21
packages/intersect/LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Stephen Ruiz Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
477
packages/intersect/README.md
Normal file
477
packages/intersect/README.md
Normal file
|
@ -0,0 +1,477 @@
|
|||
<div style="text-align: center; transform: scale(.5);">
|
||||
<img src="card-repo.png"/>
|
||||
</div>
|
||||
|
||||
# @tldraw/core
|
||||
|
||||
This package contains the `Renderer` and core utilities used by [tldraw](https://tldraw.com).
|
||||
|
||||
You can use this package to build projects like [tldraw](https://tldraw.com), where React components are rendered on a canvas user interface. Check out the [advanced example](https://core-steveruiz.vercel.app/).
|
||||
|
||||
💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
|
||||
|
||||
## Installation
|
||||
|
||||
Use your package manager of choice to install `@tldraw/core` and its peer dependencies.
|
||||
|
||||
```bash
|
||||
yarn add @tldraw/core
|
||||
# or
|
||||
npm i @tldraw/core
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
There are two examples in this repository.
|
||||
|
||||
The **simple** example in the `example` folder shows a minimal use of the library. It does not do much but this should be a good reference for the API without too much else built on top.
|
||||
|
||||
The **advanced** example in the `example-advanced` folder shows a more realistic use of the library. (Try it [here](https://core-steveruiz.vercel.app/)). While the fundamental patterns are the same, this example contains features such as: panning, pinching, and zooming the camera; creating, cloning, resizing, and deleting shapes; keyboard shortcuts, brush-selection; shape-snapping; undo, redo; and more. Much of the code in the advanced example comes from the [@tldraw/tldraw](https://tldraw.com) codebase.
|
||||
|
||||
If you're working on an app that uses this library, I recommend referring back to the advanced example for tips on how you might implement these features for your own project.
|
||||
|
||||
## Usage
|
||||
|
||||
Import the `Renderer` React component and pass it the required props.
|
||||
|
||||
```tsx
|
||||
import * as React from "react"
|
||||
import { Renderer, TLShape, TLShapeUtil, Vec } from '@tldraw/core'
|
||||
import { BoxShape, BoxUtil } from "./shapes/box"
|
||||
|
||||
const shapeUtils = { box: new BoxUtil() }
|
||||
|
||||
function App() {
|
||||
const [page, setPage] = React.useState({
|
||||
id: "page"
|
||||
shapes: {
|
||||
"box1": {
|
||||
id: 'box1',
|
||||
type: 'box',
|
||||
parentId: 'page',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
rotation: 0,
|
||||
}
|
||||
},
|
||||
bindings: {}
|
||||
})
|
||||
|
||||
const [pageState, setPageState] = React.useState({
|
||||
id: "page",
|
||||
selectedIds: [],
|
||||
camera: {
|
||||
point: [0,0],
|
||||
zoom: 1
|
||||
}
|
||||
})
|
||||
|
||||
return (<Renderer
|
||||
page={page}
|
||||
pageState={pageState}
|
||||
shapeUtils={shapeUtils}
|
||||
/>)
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### `Renderer`
|
||||
|
||||
To avoid unnecessary renders, be sure to pass "stable" values as props to the `Renderer`. Either define these values outside of the parent component, or place them in React state, or memoize them with `React.useMemo`.
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ------------ | ------------------------------- | ---------------------------------------------- |
|
||||
| `page` | [`TLPage`](#tlpage) | The current page object. |
|
||||
| `pageState` | [`TLPageState`](#tlpagestate) | The current page's state. |
|
||||
| `shapeUtils` | [`TLShapeUtils`](#tlshapeutils) | The shape utilities used to render the shapes. |
|
||||
|
||||
In addition to these required props, the Renderer accents many other **optional** props.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------------- | ----------------------------- | ----------------------------------------------------------------- |
|
||||
| `containerRef` | `React.MutableRefObject` | A React ref for the container, where CSS variables will be added. |
|
||||
| `theme` | `object` | An object with overrides for the Renderer's default colors. |
|
||||
| `hideBounds` | `boolean` | Do not show the bounding box for selected shapes. |
|
||||
| `hideHandles` | `boolean` | Do not show handles for shapes with handles. |
|
||||
| `hideBindingHandles` | `boolean` | Do not show binding controls for selected shapes with bindings. |
|
||||
| `hideResizeHandles` | `boolean` | Do not show resize handles for selected shapes. |
|
||||
| `hideRotateHandles` | `boolean` | Do not show rotate handles for selected shapes. |
|
||||
| `snapLines` | [`TLSnapLine`](#tlsnapline)[] | An array of "snap" lines. |
|
||||
| `users` | `object` | A table of [`TLUser`](#tluser)s. |
|
||||
| `userId` | `object` | The current user's [`TLUser`](#tluser) id. |
|
||||
|
||||
The theme object accepts valid CSS colors for the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------- |
|
||||
| `foreground` | The primary (usually "text") color |
|
||||
| `background` | The default page's background color |
|
||||
| `brushFill` | The fill color of the brush selection box |
|
||||
| `brushStroke` | The stroke color of the brush selection box |
|
||||
| `selectFill` | The fill color of the selection bounds |
|
||||
| `selectStroke` | The stroke color of the selection bounds and handles |
|
||||
|
||||
The Renderer also accepts many (optional) event callbacks.
|
||||
|
||||
| Prop | Description |
|
||||
| --------------------------- | ----------------------------------------------------------- |
|
||||
| `onPan` | Panned with the mouse wheel |
|
||||
| `onZoom` | Zoomed with the mouse wheel |
|
||||
| `onPinchStart` | Began a two-pointer pinch |
|
||||
| `onPinch` | Moved their pointers during a pinch |
|
||||
| `onPinchEnd` | Stopped a two-pointer pinch |
|
||||
| `onPointerDown` | Started pointing |
|
||||
| `onPointerMove` | Moved their pointer |
|
||||
| `onPointerUp` | Ended a point |
|
||||
| `onPointCanvas` | Pointed the canvas |
|
||||
| `onDoubleClickCanvas` | Double-pointed the canvas |
|
||||
| `onRightPointCanvas` | Right-pointed the canvas |
|
||||
| `onDragCanvas` | Dragged the canvas |
|
||||
| `onReleaseCanvas` | Stopped pointing the canvas |
|
||||
| `onHoverShape` | Moved their pointer onto a shape |
|
||||
| `onUnhoverShape` | Moved their pointer off of a shape |
|
||||
| `onPointShape` | Pointed a shape |
|
||||
| `onDoubleClickShape` | Double-pointed a shape |
|
||||
| `onRightPointShape` | Right-pointed a shape |
|
||||
| `onDragShape` | Dragged a shape |
|
||||
| `onReleaseShape` | Stopped pointing a shape |
|
||||
| `onHoverHandle` | Moved their pointer onto a shape handle |
|
||||
| `onUnhoverHandle` | Moved their pointer off of a shape handle |
|
||||
| `onPointHandle` | Pointed a shape handle |
|
||||
| `onDoubleClickHandle` | Double-pointed a shape handle |
|
||||
| `onRightPointHandle` | Right-pointed a shape handle |
|
||||
| `onDragHandle` | Dragged a shape handle |
|
||||
| `onReleaseHandle` | Stopped pointing shape handle |
|
||||
| `onHoverBounds` | Moved their pointer onto the selection bounds |
|
||||
| `onUnhoverBounds` | Moved their pointer off of the selection bounds |
|
||||
| `onPointBounds` | Pointed the selection bounds |
|
||||
| `onDoubleClickBounds` | Double-pointed the selection bounds |
|
||||
| `onRightPointBounds` | Right-pointed the selection bounds |
|
||||
| `onDragBounds` | Dragged the selection bounds |
|
||||
| `onReleaseBounds` | Stopped the selection bounds |
|
||||
| `onHoverBoundsHandle` | Moved their pointer onto a selection bounds handle |
|
||||
| `onUnhoverBoundsHandle` | Moved their pointer off of a selection bounds handle |
|
||||
| `onPointBoundsHandle` | Pointed a selection bounds handle |
|
||||
| `onDoubleClickBoundsHandle` | Double-pointed a selection bounds handle |
|
||||
| `onRightPointBoundsHandle` | Right-pointed a selection bounds handle |
|
||||
| `onDragBoundsHandle` | Dragged a selection bounds handle |
|
||||
| `onReleaseBoundsHandle` | Stopped a selection bounds handle |
|
||||
| `onShapeClone` | Clicked on a shape's clone handle |
|
||||
| `onShapeChange` | A shape's component prompted a change |
|
||||
| `onShapeBlur` | A shape's component was prompted a blur |
|
||||
| `onRenderCountChange` | The number of rendered shapes changed |
|
||||
| `onBoundsChange` | The Renderer's screen bounding box of the component changed |
|
||||
| `onError` | The Renderer encountered an error |
|
||||
|
||||
The `@tldraw/core` library provides types for most of the event handlers:
|
||||
|
||||
| Type |
|
||||
| ---------------------------- |
|
||||
| `TLPinchEventHandler` |
|
||||
| `TLPointerEventHandler` |
|
||||
| `TLCanvasEventHandler` |
|
||||
| `TLBoundsEventHandler` |
|
||||
| `TLBoundsHandleEventHandler` |
|
||||
| `TLShapeChangeHandler` |
|
||||
| `TLShapeBlurHandler` |
|
||||
| `TLShapeCloneHandler` |
|
||||
|
||||
### `TLPage`
|
||||
|
||||
An object describing the current page. It contains:
|
||||
|
||||
| Property | Type | Description |
|
||||
| ----------------- | --------------------------- | --------------------------------------------------------------------------- |
|
||||
| `id` | `string` | A unique id for the page. |
|
||||
| `shapes` | [`TLShape{}`](#tlshape) | A table of shapes. |
|
||||
| `bindings` | [`TLBinding{}`](#tlbinding) | A table of bindings. |
|
||||
| `backgroundColor` | `string` | (optional) The page's background fill color. Will also overwrite the theme. |
|
||||
|
||||
### `TLPageState`
|
||||
|
||||
An object describing the current page. It contains:
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------- | ---------- | --------------------------------------------------- |
|
||||
| `id` | `string` | The corresponding page's id |
|
||||
| `selectedIds` | `string[]` | An array of selected shape ids |
|
||||
| `camera` | `object` | An object describing the camera state |
|
||||
| `camera.point` | `number[]` | The camera's `[x, y]` coordinates |
|
||||
| `camera.zoom` | `number` | The camera's zoom level |
|
||||
| `pointedId` | `string` | (optional) The currently pointed shape id |
|
||||
| `hoveredId` | `string` | (optional) The currently hovered shape id |
|
||||
| `editingId` | `string` | (optional) The currently editing shape id |
|
||||
| `bindingId` | `string` | (optional) The currently editing binding. |
|
||||
| `brush` | `TLBounds` | (optional) A `Bounds` for the current selection box |
|
||||
|
||||
### `TLShape`
|
||||
|
||||
An object that describes a shape on the page. The shapes in your document should extend this interface with other properties. See [Shape Type](#shape-type).
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------------- | ---------- | ------------------------------------------------------------------------------------- |
|
||||
| `id` | `string` | The shape's id. |
|
||||
| `type` | `string` | The type of the shape, corresponding to the `type` of a [`TLShapeUtil`](#tlshapeutil) |
|
||||
| `parentId` | `string` | The id of the shape's parent (either the current page or another shape) |
|
||||
| `childIndex` | `number` | the order of the shape among its parent's children |
|
||||
| `name` | `string` | the name of the shape |
|
||||
| `point` | `number[]` | the shape's current `[x, y]` coordinates on the page |
|
||||
| `rotation` | `number` | (optiona) The shape's current rotation in radians |
|
||||
| `children` | `string[]` | (optional) An array containing the ids of this shape's children |
|
||||
| `handles` | `{}` | (optional) A table of [`TLHandle`](#tlhandle) objects |
|
||||
| `isGhost` | `boolean` | (optional) True if the shape is "ghosted", e.g. while deleting |
|
||||
| `isLocked` | `boolean` | (optional) True if the shape is locked |
|
||||
| `isHidden` | `boolean` | (optional) True if the shape is hidden |
|
||||
| `isEditing` | `boolean` | (optional) True if the shape is currently editing |
|
||||
| `isGenerated` | `boolean` | optional) True if the shape is generated programatically |
|
||||
| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked |
|
||||
|
||||
### `TLHandle`
|
||||
|
||||
An object that describes a relationship between two shapes on the page.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | ---------- | --------------------------------------------- |
|
||||
| `id` | `string` | An id for the handle |
|
||||
| `index` | `number` | The handle's order within the shape's handles |
|
||||
| `point` | `number[]` | The handle's `[x, y]` coordinates |
|
||||
|
||||
When a shape with handles is the only selected shape, the `Renderer` will display its handles. You can respond to interactions with these handles using the `on
|
||||
|
||||
### `TLBinding`
|
||||
|
||||
An object that describes a relationship between two shapes on the page.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | -------- | -------------------------------------------- |
|
||||
| `id` | `string` | A unique id for the binding |
|
||||
| `fromId` | `string` | The id of the shape where the binding begins |
|
||||
| `toId` | `string` | The id of the shape where the binding begins |
|
||||
|
||||
### `TLSnapLine`
|
||||
|
||||
A snapline is an array of points (formatted as `[x, y]`) that represent a "snapping" line.
|
||||
|
||||
### `TLShapeUtil`
|
||||
|
||||
The `TLShapeUtil` is an abstract class that you can extend to create utilities for your custom shapes. See the [Creating Shapes](#creating-shapes) guide to learn more.
|
||||
|
||||
### `TLUser`
|
||||
|
||||
A `TLUser` is the presence information for a multiplayer user. The user's pointer location and selections will be shown on the canvas. If the `TLUser`'s id matches the `Renderer`'s `userId` prop, then the user's cursor and selections will not be shown.
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | ---------- | --------------------------------------- |
|
||||
| `id` | `string` | A unique id for the user |
|
||||
| `color` | `string` | The user's color, used for indicators |
|
||||
| `point` | `number[]` | The user's pointer location on the page |
|
||||
| `selectedIds[]` | `string[]` | The user's selected shape ids |
|
||||
|
||||
### `Utils`
|
||||
|
||||
A general purpose utility class. See source for more.
|
||||
|
||||
## Guide: Creating Shapes
|
||||
|
||||
The `Renderer` component has no built-in shapes. It's up to you to define every shape that you want to see on the canvas. While these shapes are highly reusable between projects, you'll need to define them using the API described below.
|
||||
|
||||
> For several example shapes, see the folder `/example/src/shapes/`.
|
||||
|
||||
### Shape Type
|
||||
|
||||
Your first task is to define an interface for the shape that extends `TLShape`. It must have a `type` property.
|
||||
|
||||
```ts
|
||||
// BoxShape.ts
|
||||
import type { TLShape } from '@tldraw/core'
|
||||
|
||||
export interface BoxShape extends TLShape {
|
||||
type: 'box'
|
||||
size: number[]
|
||||
}
|
||||
```
|
||||
|
||||
### Component
|
||||
|
||||
Next, use `TLShapeUtil.Component` to create a second component for your shape's `Component`. The `Renderer` will use this component to display the shape on the canvas.
|
||||
|
||||
```tsx
|
||||
// BoxComponent.ts
|
||||
|
||||
import * as React from 'react'
|
||||
import { shapeComponent, SVGContainer } from '@tldraw/core'
|
||||
import type { BoxShape } from './BoxShape'
|
||||
|
||||
export const BoxComponent = TLShapeUtil.Component<BoxShape, SVGSVGElement>(
|
||||
({ shape, events, meta }, ref) => {
|
||||
const color = meta.isDarkMode ? 'white' : 'black'
|
||||
|
||||
return (
|
||||
<SVGContainer ref={ref} {...events}>
|
||||
<rect
|
||||
width={shape.size[0]}
|
||||
height={shape.size[1]}
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
pointerEvents="all"
|
||||
/>
|
||||
</SVGContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Your component can return HTML elements or SVG elements. If your shape is returning only SVG elements, wrap it in an `SVGContainer`. If your shape returns HTML elements, wrap it in an `HTMLContainer`. Not that you must set `pointerEvents` manually on the shapes you wish to receive pointer events.
|
||||
|
||||
The component will receive the following props:
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------- | ---------- | ------------------------------------------------------------------ |
|
||||
| `shape` | `TLShape` | The shape from `page.shapes` that is being rendered |
|
||||
| `meta` | `{}` | The value provided to the `Renderer`'s `meta` prop |
|
||||
| `events` | `{}` | Several pointer events that should be set on the container element |
|
||||
| `isSelected` | `boolean` | The shape is selected (its `id` is in `pageState.selectedIds`) |
|
||||
| `isHovered` | `boolean` | The shape is hovered (its `id` is `pageState.hoveredId`) |
|
||||
| `isEditing` | `boolean` | The shape is being edited (its `id` is `pageState.editingId`) |
|
||||
| `isGhost` | `boolean` | The shape is ghosted or is the child of a ghosted shape. |
|
||||
| `isChildOfSelected` | `boolean` | The shape is the child of a selected shape. |
|
||||
| `onShapeChange` | `Function` | The callback provided to the `Renderer`'s `onShapeChange` prop |
|
||||
| `onShapeBlur` | `Function` | The callback provided to the `Renderer`'s `onShapeBlur` prop |
|
||||
|
||||
### Indicator
|
||||
|
||||
Next, use `TLShapeUtil.Indicator` to create a second component for your shape's `Indicator`. This component is shown when the shape is hovered or selected. Your `Indicator` must return SVG elements only.
|
||||
|
||||
```tsx
|
||||
// BoxIndicator.ts
|
||||
|
||||
export const BoxIndicator = TLShapeUtil.Indicator<BoxShape>(({ shape }) => {
|
||||
return (
|
||||
<rect
|
||||
fill="none"
|
||||
stroke="dodgerblue"
|
||||
strokeWidth={1}
|
||||
width={shape.size[0]}
|
||||
height={shape.size[1]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
The indicator component will receive the following props:
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------ | --------- | -------------------------------------------------------------------------------------- |
|
||||
| `shape` | `TLShape` | The shape from `page.shapes` that is being rendered |
|
||||
| `meta` | {} | The value provided to the `Renderer`'s `meta` prop |
|
||||
| `user` | `TLUser` | The user when shown in a multiplayer session |
|
||||
| `isSelected` | `boolean` | Whether the current shape is selected (true if its `id` is in `pageState.selectedIds`) |
|
||||
| `isHovered` | `boolean` | Whether the current shape is hovered (true if its `id` is `pageState.hoveredId`) |
|
||||
|
||||
### ShapeUtil
|
||||
|
||||
Next, create a "shape util" for your shape. This is a class that extends `TLShapeUtil`. The `Renderer` will use an instance of this class to answer questions about the shape: what it should look like, where it is on screen, whether it can rotate, etc.
|
||||
|
||||
```ts
|
||||
// BoxUtil.ts
|
||||
|
||||
import { Utils, TLBounds, TLShapeUtil } from '@tldraw/core'
|
||||
import { BoxComponent } from './BoxComponent'
|
||||
import { BoxIndicator } from './BoxIndicator'
|
||||
import type { BoxShape } from './BoxShape'
|
||||
|
||||
export class BoxUtil extends TLShapeUtil<BoxShape, SVGSVGElement> {
|
||||
Component = BoxComponent
|
||||
|
||||
Indicator = BoxIndicator
|
||||
|
||||
getBounds = (shape: BoxShape): TLBounds => {
|
||||
const [width, height] = shape.size
|
||||
|
||||
const bounds = {
|
||||
minX: 0,
|
||||
maxX: width,
|
||||
minY: 0,
|
||||
maxY: height,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
|
||||
return Utils.translateBounds(bounds, shape.point)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set the `Component` field to your component and the `Indicator` field to your indicator component. Then define the `getBounds` method. This method will receive a shape and should return a `TLBounds` object.
|
||||
|
||||
You may also set the following fields:
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------------ | --------- | ------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `showCloneHandles` | `boolean` | `false` | Whether to display clone handles when the shape is the only selected shape |
|
||||
| `hideBounds` | `boolean` | `false` | Whether to hide the bounds when the shape is the only selected shape |
|
||||
| `isStateful` | `boolean` | `false` | Whether the shape has its own React state. When true, the shape will not be unmounted when off-screen |
|
||||
|
||||
### ShapeUtils Object
|
||||
|
||||
Finally, create a mapping of your project's shape utils and the `type` properties of their corresponding shapes. Pass this object to the `Renderer`'s `shapeUtils` prop.
|
||||
|
||||
```tsx
|
||||
// App.tsx
|
||||
|
||||
const shapeUtils = {
|
||||
box: new BoxUtil(),
|
||||
circle: new CircleUtil(),
|
||||
text: new TextUtil(),
|
||||
}
|
||||
|
||||
export function App() {
|
||||
// ...
|
||||
|
||||
return <Renderer page={page} pageState={pageState} {...etc} shapeUtils={shapeUtils} />
|
||||
}
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
To start the development servers for the package and the advanced example:
|
||||
|
||||
- Run `yarn` to install dependencies.
|
||||
- Run `yarn start`.
|
||||
- Open `localhost:5420`.
|
||||
|
||||
You can also run:
|
||||
|
||||
- `start:advanced` to start development servers for the package and the advanced example.
|
||||
- `start:simple` to start development servers for the package and the simple example.
|
||||
- `test` to execute unit tests via [Jest](https://jestjs.io).
|
||||
- `docs` to build the docs via [ts-doc](https://typedoc.org/).
|
||||
- `build` to build the package.
|
||||
|
||||
## Example
|
||||
|
||||
See the `example` folder or this [CodeSandbox](https://codesandbox.io/s/laughing-elion-gp0kx) example.
|
||||
|
||||
## Community
|
||||
|
||||
### Support
|
||||
|
||||
Need help? Please [open an issue](https://github.com/tldraw/tldraw/issues/new) for support.
|
||||
|
||||
### Discussion
|
||||
|
||||
Want to connect with other devs? Visit the [Discord channel](https://discord.gg/SBBEVCA4PG).
|
||||
|
||||
### License
|
||||
|
||||
This project is licensed under MIT.
|
||||
|
||||
If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
|
||||
|
||||
## Author
|
||||
|
||||
- [@steveruizok](https://twitter.com/steveruizok)
|
BIN
packages/intersect/card-repo.png
Normal file
BIN
packages/intersect/card-repo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
39
packages/intersect/package.json
Normal file
39
packages/intersect/package.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"version": "1.1.4",
|
||||
"name": "@tldraw/intersect",
|
||||
"description": "2D intersection utilities for TLDraw and maybe you, too.",
|
||||
"author": "@steveruizok",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tldraw/tldraw.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"2d",
|
||||
"vector",
|
||||
"intersection",
|
||||
"typescript",
|
||||
"javascript"
|
||||
],
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"scripts": {
|
||||
"start:packages": "yarn start",
|
||||
"start:core": "yarn start",
|
||||
"start": "node scripts/dev & yarn types:dev",
|
||||
"build:packages": "yarn build",
|
||||
"build:core": "yarn build",
|
||||
"build": "node scripts/build && yarn types:build",
|
||||
"types:dev": "tsc -w --p tsconfig.build.json",
|
||||
"types:build": "tsc -p tsconfig.build.json",
|
||||
"lint": "eslint src/ --ext .ts,.tsx",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tldraw/vec": "^1.1.4"
|
||||
}
|
||||
}
|
63
packages/intersect/scripts/build.js
Normal file
63
packages/intersect/scripts/build.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/* eslint-disable */
|
||||
const fs = require('fs')
|
||||
const esbuild = require('esbuild')
|
||||
const { gzip } = require('zlib')
|
||||
const pkg = require('../package.json')
|
||||
|
||||
async function main() {
|
||||
if (fs.existsSync('./dist')) {
|
||||
fs.rmSync('./dist', { recursive: true }, (e) => {
|
||||
if (e) {
|
||||
throw e
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
esbuild.buildSync({
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outdir: 'dist/cjs',
|
||||
minify: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
target: 'es6',
|
||||
tsconfig: './tsconfig.build.json',
|
||||
external: Object.keys(pkg.dependencies),
|
||||
metafile: true,
|
||||
sourcemap: true,
|
||||
})
|
||||
|
||||
const esmResult = esbuild.buildSync({
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outdir: 'dist/esm',
|
||||
minify: false,
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
target: 'es6',
|
||||
tsconfig: './tsconfig.build.json',
|
||||
external: Object.keys(pkg.dependencies),
|
||||
metafile: true,
|
||||
sourcemap: true,
|
||||
})
|
||||
|
||||
let esmSize = 0
|
||||
Object.values(esmResult.metafile.outputs).forEach((output) => {
|
||||
esmSize += output.bytes
|
||||
})
|
||||
|
||||
fs.readFile('./dist/esm/index.js', (_err, data) => {
|
||||
gzip(data, (_err, result) => {
|
||||
console.log(
|
||||
`✔ ${pkg.name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${(
|
||||
result.length / 1000
|
||||
).toFixed(2)}kb minified)`
|
||||
)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(`× ${pkg.name}: Build failed due to an error.`)
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
29
packages/intersect/scripts/dev.js
Normal file
29
packages/intersect/scripts/dev.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
/* eslint-disable */
|
||||
const esbuild = require('esbuild')
|
||||
const pkg = require('../package.json')
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await esbuild.build({
|
||||
entryPoints: ['src/index.tsx'],
|
||||
outfile: 'dist/index.js',
|
||||
bundle: true,
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
incremental: true,
|
||||
target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
},
|
||||
watch: {
|
||||
onRebuild(err) {
|
||||
err ? error('❌ Failed') : log('✅ Updated')
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
430
packages/intersect/src/index.d.ts
vendored
Normal file
430
packages/intersect/src/index.d.ts
vendored
Normal file
|
@ -0,0 +1,430 @@
|
|||
export declare type TLIntersection = {
|
||||
didIntersect: boolean;
|
||||
message: string;
|
||||
points: number[][];
|
||||
};
|
||||
export interface TLBounds {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation?: number;
|
||||
}
|
||||
/**
|
||||
* Find the intersection between a ray and a ray.
|
||||
* @param p0 The first ray's point
|
||||
* @param n0 The first ray's direction vector.
|
||||
* @param p1 The second ray's point.
|
||||
* @param n1 The second ray's direction vector.
|
||||
*/
|
||||
export declare function intersectRayRay(p0: number[], n0: number[], p1: number[], n1: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a ray and a line segment.
|
||||
* @param origin
|
||||
* @param direction
|
||||
* @param a1
|
||||
* @param a2
|
||||
*/
|
||||
export declare function intersectRayLineSegment(origin: number[], direction: number[], a1: number[], a2: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a ray and a rectangle.
|
||||
* @param origin
|
||||
* @param direction
|
||||
* @param point
|
||||
* @param size
|
||||
* @param rotation
|
||||
*/
|
||||
export declare function intersectRayRectangle(origin: number[], direction: number[], point: number[], size: number[], rotation?: number): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a ray and an ellipse.
|
||||
* @param origin
|
||||
* @param direction
|
||||
* @param center
|
||||
* @param rx
|
||||
* @param ry
|
||||
* @param rotation
|
||||
*/
|
||||
export declare function intersectRayEllipse(origin: number[], direction: number[], center: number[], rx: number, ry: number, rotation: number): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a ray and a bounding box.
|
||||
* @param origin
|
||||
* @param direction
|
||||
* @param bounds
|
||||
* @param rotation
|
||||
*/
|
||||
export declare function intersectRayBounds(origin: number[], direction: number[], bounds: TLBounds, rotation?: number): TLIntersection[];
|
||||
/**
|
||||
* Find the intersection between a line segment and a ray.
|
||||
* @param a1
|
||||
* @param a2
|
||||
* @param origin
|
||||
* @param direction
|
||||
*/
|
||||
export declare function intersectLineSegmentRay(a1: number[], a2: number[], origin: number[], direction: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersection between a line segment and a line segment.
|
||||
* @param a1
|
||||
* @param a2
|
||||
* @param b1
|
||||
* @param b2
|
||||
*/
|
||||
export declare function intersectLineSegmentLineSegment(a1: number[], a2: number[], b1: number[], b2: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a line segment and a rectangle.
|
||||
* @param a1
|
||||
* @param a2
|
||||
* @param point
|
||||
* @param size
|
||||
*/
|
||||
export declare function intersectLineSegmentRectangle(a1: number[], a2: number[], point: number[], size: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a line segment and an arc.
|
||||
* @param a1
|
||||
* @param a2
|
||||
* @param center
|
||||
* @param radius
|
||||
* @param start
|
||||
* @param end
|
||||
*/
|
||||
export declare function intersectLineSegmentArc(a1: number[], a2: number[], center: number[], radius: number, start: number[], end: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a line segment and a circle.
|
||||
* @param a1
|
||||
* @param a2
|
||||
* @param c
|
||||
* @param r
|
||||
*/
|
||||
export declare function intersectLineSegmentCircle(a1: number[], a2: number[], c: number[], r: number): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a line segment and an ellipse.
|
||||
* @param a1
|
||||
* @param a2
|
||||
* @param center
|
||||
* @param rx
|
||||
* @param ry
|
||||
* @param rotation
|
||||
*/
|
||||
export declare function intersectLineSegmentEllipse(a1: number[], a2: number[], center: number[], rx: number, ry: number, rotation?: number): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a line segment and a bounding box.
|
||||
* @param a1
|
||||
* @param a2
|
||||
* @param bounds
|
||||
*/
|
||||
export declare function intersectLineSegmentBounds(a1: number[], a2: number[], bounds: TLBounds): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a line segment and a polyline.
|
||||
* @param a1
|
||||
* @param a2
|
||||
* @param points
|
||||
*/
|
||||
export declare function intersectLineSegmentPolyline(a1: number[], a2: number[], points: number[][]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a line segment and a closed polygon.
|
||||
* @param a1
|
||||
* @param a2
|
||||
* @param points
|
||||
*/
|
||||
export declare function intersectLineSegmentPolygon(a1: number[], a2: number[], points: number[][]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a rectangle and a ray.
|
||||
* @param point
|
||||
* @param size
|
||||
* @param rotation
|
||||
* @param origin
|
||||
* @param direction
|
||||
*/
|
||||
export declare function intersectRectangleRay(point: number[], size: number[], rotation: number, origin: number[], direction: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a rectangle and a line segment.
|
||||
* @param point
|
||||
* @param size
|
||||
* @param a1
|
||||
* @param a2
|
||||
*/
|
||||
export declare function intersectRectangleLineSegment(point: number[], size: number[], a1: number[], a2: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a rectangle and a rectangle.
|
||||
* @param point1
|
||||
* @param size1
|
||||
* @param point2
|
||||
* @param size2
|
||||
*/
|
||||
export declare function intersectRectangleRectangle(point1: number[], size1: number[], point2: number[], size2: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a rectangle and an arc.
|
||||
* @param point
|
||||
* @param size
|
||||
* @param center
|
||||
* @param radius
|
||||
* @param start
|
||||
* @param end
|
||||
*/
|
||||
export declare function intersectRectangleArc(point: number[], size: number[], center: number[], radius: number, start: number[], end: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a rectangle and a circle.
|
||||
* @param point
|
||||
* @param size
|
||||
* @param c
|
||||
* @param r
|
||||
*/
|
||||
export declare function intersectRectangleCircle(point: number[], size: number[], c: number[], r: number): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a rectangle and an ellipse.
|
||||
* @param point
|
||||
* @param size
|
||||
* @param c
|
||||
* @param rx
|
||||
* @param ry
|
||||
* @param rotation
|
||||
*/
|
||||
export declare function intersectRectangleEllipse(point: number[], size: number[], c: number[], rx: number, ry: number, rotation?: number): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a rectangle and a bounding box.
|
||||
* @param point
|
||||
* @param size
|
||||
* @param bounds
|
||||
*/
|
||||
export declare function intersectRectangleBounds(point: number[], size: number[], bounds: TLBounds): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a rectangle and a polyline.
|
||||
* @param point
|
||||
* @param size
|
||||
* @param points
|
||||
*/
|
||||
export declare function intersectRectanglePolyline(point: number[], size: number[], points: number[][]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a rectangle and a polygon.
|
||||
* @param point
|
||||
* @param size
|
||||
* @param points
|
||||
*/
|
||||
export declare function intersectRectanglePolygon(point: number[], size: number[], points: number[][]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a arc and a line segment.
|
||||
* @param center
|
||||
* @param radius
|
||||
* @param start
|
||||
* @param end
|
||||
* @param a1
|
||||
* @param a2
|
||||
*/
|
||||
export declare function intersectArcLineSegment(center: number[], radius: number, start: number[], end: number[], a1: number[], a2: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a arc and a rectangle.
|
||||
* @param center
|
||||
* @param radius
|
||||
* @param start
|
||||
* @param end
|
||||
* @param point
|
||||
* @param size
|
||||
*/
|
||||
export declare function intersectArcRectangle(center: number[], radius: number, start: number[], end: number[], point: number[], size: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a arc and a bounding box.
|
||||
* @param center
|
||||
* @param radius
|
||||
* @param start
|
||||
* @param end
|
||||
* @param bounds
|
||||
*/
|
||||
export declare function intersectArcBounds(center: number[], radius: number, start: number[], end: number[], bounds: TLBounds): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a circle and a line segment.
|
||||
* @param c
|
||||
* @param r
|
||||
* @param a1
|
||||
* @param a2
|
||||
*/
|
||||
export declare function intersectCircleLineSegment(c: number[], r: number, a1: number[], a2: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a circle and a circle.
|
||||
* @param c1
|
||||
* @param r1
|
||||
* @param c2
|
||||
* @param r2
|
||||
*/
|
||||
export declare function intersectCircleCircle(c1: number[], r1: number, c2: number[], r2: number): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a circle and a rectangle.
|
||||
* @param c
|
||||
* @param r
|
||||
* @param point
|
||||
* @param size
|
||||
*/
|
||||
export declare function intersectCircleRectangle(c: number[], r: number, point: number[], size: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a circle and a bounding box.
|
||||
* @param c
|
||||
* @param r
|
||||
* @param bounds
|
||||
*/
|
||||
export declare function intersectCircleBounds(c: number[], r: number, bounds: TLBounds): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between an ellipse and a ray.
|
||||
* @param center
|
||||
* @param rx
|
||||
* @param ry
|
||||
* @param rotation
|
||||
* @param point
|
||||
* @param direction
|
||||
*/
|
||||
export declare function intersectEllipseRay(center: number[], rx: number, ry: number, rotation: number, point: number[], direction: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between an ellipse and a line segment.
|
||||
* @param center
|
||||
* @param rx
|
||||
* @param ry
|
||||
* @param rotation
|
||||
* @param a1
|
||||
* @param a2
|
||||
*/
|
||||
export declare function intersectEllipseLineSegment(center: number[], rx: number, ry: number, rotation: number | undefined, a1: number[], a2: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between an ellipse and a rectangle.
|
||||
* @param center
|
||||
* @param rx
|
||||
* @param ry
|
||||
* @param rotation
|
||||
* @param point
|
||||
* @param size
|
||||
*/
|
||||
export declare function intersectEllipseRectangle(center: number[], rx: number, ry: number, rotation: number | undefined, point: number[], size: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between an ellipse and an ellipse.
|
||||
* Adapted from https://gist.github.com/drawable/92792f59b6ff8869d8b1
|
||||
* @param _c1
|
||||
* @param _rx1
|
||||
* @param _ry1
|
||||
* @param _r1
|
||||
* @param _c2
|
||||
* @param _rx2
|
||||
* @param _ry2
|
||||
* @param _r2
|
||||
*/
|
||||
export declare function intersectEllipseEllipse(_c1: number[], _rx1: number, _ry1: number, _r1: number, _c2: number[], _rx2: number, _ry2: number, _r2: number): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between an ellipse and a circle.
|
||||
* @param c
|
||||
* @param rx
|
||||
* @param ry
|
||||
* @param rotation
|
||||
* @param c2
|
||||
* @param r2
|
||||
*/
|
||||
export declare function intersectEllipseCircle(c: number[], rx: number, ry: number, rotation: number, c2: number[], r2: number): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between an ellipse and a bounding box.
|
||||
* @param c
|
||||
* @param rx
|
||||
* @param ry
|
||||
* @param rotation
|
||||
* @param bounds
|
||||
*/
|
||||
export declare function intersectEllipseBounds(c: number[], rx: number, ry: number, rotation: number, bounds: TLBounds): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a bounding box and a ray.
|
||||
* @param bounds
|
||||
* @param origin
|
||||
* @param direction
|
||||
*/
|
||||
export declare function intersectBoundsRay(bounds: TLBounds, origin: number[], direction: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a bounding box and a line segment.
|
||||
* @param bounds
|
||||
* @param a1
|
||||
* @param a2
|
||||
*/
|
||||
export declare function intersectBoundsLineSegment(bounds: TLBounds, a1: number[], a2: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a bounding box and a rectangle.
|
||||
* @param bounds
|
||||
* @param point
|
||||
* @param size
|
||||
*/
|
||||
export declare function intersectBoundsRectangle(bounds: TLBounds, point: number[], size: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a bounding box and a bounding box.
|
||||
* @param bounds1
|
||||
* @param bounds2
|
||||
*/
|
||||
export declare function intersectBoundsBounds(bounds1: TLBounds, bounds2: TLBounds): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a bounding box and an arc.
|
||||
* @param bounds
|
||||
* @param center
|
||||
* @param radius
|
||||
* @param start
|
||||
* @param end
|
||||
*/
|
||||
export declare function intersectBoundsArc(bounds: TLBounds, center: number[], radius: number, start: number[], end: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a bounding box and a circle.
|
||||
* @param bounds
|
||||
* @param c
|
||||
* @param r
|
||||
*/
|
||||
export declare function intersectBoundsCircle(bounds: TLBounds, c: number[], r: number): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a bounding box and an ellipse.
|
||||
* @param bounds
|
||||
* @param c
|
||||
* @param rx
|
||||
* @param ry
|
||||
* @param rotation
|
||||
*/
|
||||
export declare function intersectBoundsEllipse(bounds: TLBounds, c: number[], rx: number, ry: number, rotation?: number): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a bounding box and a polyline.
|
||||
* @param bounds
|
||||
* @param points
|
||||
*/
|
||||
export declare function intersectBoundsPolyline(bounds: TLBounds, points: number[][]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a bounding box and a polygon.
|
||||
* @param bounds
|
||||
* @param points
|
||||
*/
|
||||
export declare function intersectBoundsPolygon(bounds: TLBounds, points: number[][]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a polyline and a line segment.
|
||||
* @param points
|
||||
* @param a1
|
||||
* @param a2
|
||||
*/
|
||||
export declare function intersectPolylineLineSegment(points: number[][], a1: number[], a2: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a polyline and a rectangle.
|
||||
* @param points
|
||||
* @param point
|
||||
* @param size
|
||||
*/
|
||||
export declare function intersectPolylineRectangle(points: number[][], point: number[], size: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a polyline and a bounding box.
|
||||
* @param points
|
||||
* @param bounds
|
||||
*/
|
||||
export declare function intersectPolylineBounds(points: number[][], bounds: TLBounds): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a polygon nd a line segment.
|
||||
* @param points
|
||||
* @param a1
|
||||
* @param a2
|
||||
*/
|
||||
export declare function intersectPolygonLineSegment(points: number[][], a1: number[], a2: number[]): TLIntersection;
|
||||
/**
|
||||
* Find the intersections between a polygon and a rectangle.
|
||||
* @param points
|
||||
* @param point
|
||||
* @param size
|
||||
*/
|
||||
export declare function intersectPolygonRectangle(points: number[][], point: number[], size: number[]): TLIntersection[];
|
||||
/**
|
||||
* Find the intersections between a polygon and a bounding box.
|
||||
* @param points
|
||||
* @param bounds
|
||||
*/
|
||||
export declare function intersectPolygonBounds(points: number[][], bounds: TLBounds): TLIntersection[];
|
1241
packages/intersect/src/index.ts
Normal file
1241
packages/intersect/src/index.ts
Normal file
File diff suppressed because it is too large
Load diff
21
packages/intersect/tsconfig.build.json
Normal file
21
packages/intersect/tsconfig.build.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.test.tsx",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.spec.ts",
|
||||
"src/test",
|
||||
"dist",
|
||||
"docs"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"incremental": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"references": [{ "path": "../vec" }]
|
||||
}
|
14
packages/intersect/tsconfig.json
Normal file
14
packages/intersect/tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"exclude": ["node_modules", "dist", "docs"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/types",
|
||||
"rootDir": "src",
|
||||
"baseUrl": "."
|
||||
},
|
||||
"references": [{ "path": "../vec" }],
|
||||
"typedocOptions": {
|
||||
"entryPoints": ["src/index.ts"],
|
||||
"out": "docs"
|
||||
}
|
||||
}
|
|
@ -49,9 +49,9 @@
|
|||
"@radix-ui/react-radio-group": "^0.1.1",
|
||||
"@radix-ui/react-tooltip": "^0.1.1",
|
||||
"@stitches/react": "^1.2.5",
|
||||
"@tldraw/core": "^1.1.3",
|
||||
"@tldraw/intersect": "latest",
|
||||
"@tldraw/vec": "latest",
|
||||
"@tldraw/core": "^1.1.4",
|
||||
"@tldraw/intersect": "^1.1.4",
|
||||
"@tldraw/vec": "^1.1.4",
|
||||
"idb-keyval": "^6.0.3",
|
||||
"perfect-freehand": "^1.0.16",
|
||||
"react-hotkeys-hook": "^3.4.0",
|
||||
|
@ -62,7 +62,7 @@
|
|||
"@swc-node/jest": "^1.3.3",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"tsconfig-replace-paths": "^0.0.5"
|
||||
"tsconfig-replace-paths": "^0.0.11"
|
||||
},
|
||||
"jest": {
|
||||
"setupFilesAfterEnv": [
|
||||
|
@ -91,4 +91,4 @@
|
|||
}
|
||||
},
|
||||
"gitHead": "325008ff82bd27b63d625ad1b760f8871fb71af9"
|
||||
}
|
||||
}
|
|
@ -8,9 +8,10 @@ import { TldrawContext, useStylesheet, useKeyboardShortcuts, useTldrawApp } from
|
|||
import { shapeUtils } from '~state/shapes'
|
||||
import { ToolsPanel } from '~components/ToolsPanel'
|
||||
import { TopPanel } from '~components/TopPanel'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import { ContextMenu } from '~components/ContextMenu'
|
||||
import { FocusButton } from '~components/FocusButton/FocusButton'
|
||||
import { FocusButton } from '~components/FocusButton'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import { GRID_SIZE } from '~constants'
|
||||
|
||||
export interface TldrawProps extends TDCallbacks {
|
||||
/**
|
||||
|
@ -425,6 +426,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
page={page}
|
||||
pageState={pageState}
|
||||
snapLines={appState.snapLines}
|
||||
grid={GRID_SIZE}
|
||||
users={room?.users}
|
||||
userId={room?.userId}
|
||||
theme={theme}
|
||||
|
@ -436,6 +438,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
hideBindingHandles={!settings.showBindingHandles}
|
||||
hideCloneHandles={!settings.showCloneHandles}
|
||||
hideRotateHandles={!settings.showRotateHandles}
|
||||
hideGrid={!settings.showGrid}
|
||||
onPinchStart={app.onPinchStart}
|
||||
onPinchEnd={app.onPinchEnd}
|
||||
onPinch={app.onPinch}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './FocusButton'
|
|
@ -26,6 +26,10 @@ export function PreferencesMenu() {
|
|||
app.setSetting('showRotateHandles', (v) => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleGrid = React.useCallback(() => {
|
||||
app.setSetting('showGrid', (v) => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleBoundShapesHandle = React.useCallback(() => {
|
||||
app.setSetting('showBindingHandles', (v) => !v)
|
||||
}, [app])
|
||||
|
@ -62,6 +66,9 @@ export function PreferencesMenu() {
|
|||
<DMCheckboxItem checked={settings.showCloneHandles} onCheckedChange={toggleCloneControls}>
|
||||
Clone Handles
|
||||
</DMCheckboxItem>
|
||||
<DMCheckboxItem checked={settings.showGrid} onCheckedChange={toggleGrid} kbd="#⇧G">
|
||||
Grid
|
||||
</DMCheckboxItem>
|
||||
<DMCheckboxItem checked={settings.isSnapping} onCheckedChange={toggleisSnapping}>
|
||||
Always Show Snaps
|
||||
</DMCheckboxItem>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export const GRID_SIZE = 8
|
||||
export const BINDING_DISTANCE = 24
|
||||
export const CLONING_DISTANCE = 32
|
||||
export const FIT_TO_SCREEN_PADDING = 128
|
||||
|
|
|
@ -129,6 +129,16 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift+g,⌘+shift+g',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.toggleGrid()
|
||||
},
|
||||
undefined,
|
||||
[app]
|
||||
)
|
||||
|
||||
// File System
|
||||
|
||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
||||
|
|
|
@ -672,7 +672,7 @@ export class TLDR {
|
|||
const rotatedCenter = Vec.rotWith(center, origin, delta)
|
||||
|
||||
// Get the top left point relative to the rotated center
|
||||
const nextPoint = Vec.round(Vec.sub(rotatedCenter, relativeCenter))
|
||||
const nextPoint = Vec.toFixed(Vec.sub(rotatedCenter, relativeCenter))
|
||||
|
||||
// If the shape has handles, we need to rotate the handles instead
|
||||
// of rotating the shape. Shapes with handles should never be rotated,
|
||||
|
@ -685,7 +685,7 @@ export class TLDR {
|
|||
Object.entries(shape.handles).map(([handleId, handle]) => {
|
||||
// Rotate each handle's point around the shape's center
|
||||
// (in relative shape space, as the handle's point will be).
|
||||
const point = Vec.round(Vec.rotWith(handle.point, relativeCenter, delta))
|
||||
const point = Vec.toFixed(Vec.rotWith(handle.point, relativeCenter, delta))
|
||||
return [handleId, { ...handle, point }]
|
||||
})
|
||||
) as T['handles'],
|
||||
|
|
|
@ -48,7 +48,7 @@ import { defaultStyle } from '~state/shapes/shared/shape-styles'
|
|||
import * as Commands from './commands'
|
||||
import { SessionArgsOfType, getSession, TldrawSession } from './sessions'
|
||||
import type { BaseTool } from './tools/BaseTool'
|
||||
import { USER_COLORS, FIT_TO_SCREEN_PADDING } from '~constants'
|
||||
import { USER_COLORS, FIT_TO_SCREEN_PADDING, GRID_SIZE } from '~constants'
|
||||
import { SelectTool } from './tools/SelectTool'
|
||||
import { EraseTool } from './tools/EraseTool'
|
||||
import { TextTool } from './tools/TextTool'
|
||||
|
@ -790,6 +790,16 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle grids.
|
||||
*/
|
||||
toggleGrid = (): this => {
|
||||
if (this.session) return this
|
||||
this.patchState({ settings: { showGrid: !this.settings.showGrid } }, 'settings:toggled_grid')
|
||||
this.persist()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a tool.
|
||||
* @param tool The tool to select, or "select".
|
||||
|
@ -1472,17 +1482,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
|
||||
|
||||
let center = Vec.round(this.getPagePoint(point || this.centerPoint))
|
||||
let center = Vec.toFixed(this.getPagePoint(point || this.centerPoint))
|
||||
|
||||
if (
|
||||
Vec.dist(center, this.pasteInfo.center) < 2 ||
|
||||
Vec.dist(center, Vec.round(Utils.getBoundsCenter(commonBounds))) < 2
|
||||
Vec.dist(center, Vec.toFixed(Utils.getBoundsCenter(commonBounds))) < 2
|
||||
) {
|
||||
center = Vec.add(center, this.pasteInfo.offset)
|
||||
this.pasteInfo.offset = Vec.add(this.pasteInfo.offset, [
|
||||
this.settings.nudgeDistanceLarge,
|
||||
this.settings.nudgeDistanceLarge,
|
||||
])
|
||||
this.pasteInfo.offset = Vec.add(this.pasteInfo.offset, [GRID_SIZE, GRID_SIZE])
|
||||
} else {
|
||||
this.pasteInfo.center = center
|
||||
this.pasteInfo.offset = [0, 0]
|
||||
|
@ -1499,7 +1506,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
shapesToPaste.map((shape) =>
|
||||
TLDR.getShapeUtil(shape.type).create({
|
||||
...shape,
|
||||
point: Vec.round(Vec.add(shape.point, delta)),
|
||||
point: Vec.toFixed(Vec.add(shape.point, delta)),
|
||||
parentId: shape.parentId || this.currentPageId,
|
||||
})
|
||||
),
|
||||
|
@ -1691,7 +1698,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
*/
|
||||
pan = (delta: number[]): this => {
|
||||
const { camera } = this.pageState
|
||||
return this.setCamera(Vec.round(Vec.sub(camera.point, delta)), camera.zoom, `panned`)
|
||||
return this.setCamera(Vec.toFixed(Vec.sub(camera.point, delta)), camera.zoom, `panned`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1706,7 +1713,11 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
const nextZoom = zoom
|
||||
const p0 = Vec.sub(Vec.div(point, camera.zoom), nextPoint)
|
||||
const p1 = Vec.sub(Vec.div(point, nextZoom), nextPoint)
|
||||
return this.setCamera(Vec.round(Vec.add(nextPoint, Vec.sub(p1, p0))), nextZoom, `pinch_zoomed`)
|
||||
return this.setCamera(
|
||||
Vec.toFixed(Vec.add(nextPoint, Vec.sub(p1, p0))),
|
||||
nextZoom,
|
||||
`pinch_zoomed`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1718,7 +1729,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
const { zoom, point } = this.pageState.camera
|
||||
const p0 = Vec.sub(Vec.div(center, zoom), point)
|
||||
const p1 = Vec.sub(Vec.div(center, next), point)
|
||||
return this.setCamera(Vec.round(Vec.add(point, Vec.sub(p1, p0))), next, `zoomed_camera`)
|
||||
return this.setCamera(Vec.toFixed(Vec.add(point, Vec.sub(p1, p0))), next, `zoomed_camera`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1767,7 +1778,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom
|
||||
|
||||
return this.setCamera(
|
||||
Vec.round(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
|
||||
Vec.toFixed(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
|
||||
zoom,
|
||||
`zoomed_to_fit`
|
||||
)
|
||||
|
@ -1798,7 +1809,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
const my = (rendererBounds.height - selectedBounds.height * zoom) / 2 / zoom
|
||||
|
||||
return this.setCamera(
|
||||
Vec.round(Vec.sub([mx, my], [selectedBounds.minX, selectedBounds.minY])),
|
||||
Vec.toFixed(Vec.sub([mx, my], [selectedBounds.minX, selectedBounds.minY])),
|
||||
zoom,
|
||||
`zoomed_to_selection`
|
||||
)
|
||||
|
@ -1821,7 +1832,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom
|
||||
|
||||
return this.setCamera(
|
||||
Vec.round(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
|
||||
Vec.toFixed(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
|
||||
this.pageState.camera.zoom,
|
||||
`zoomed_to_content`
|
||||
)
|
||||
|
@ -2119,6 +2130,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
status: TDStatus.Idle,
|
||||
},
|
||||
document: {
|
||||
...result.document,
|
||||
pageStates: {
|
||||
[this.currentPageId]: {
|
||||
...result.document?.pageStates?.[this.currentPageId],
|
||||
|
@ -2359,7 +2371,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
*/
|
||||
nudge = (delta: number[], isMajor = false, ids = this.selectedIds): this => {
|
||||
if (ids.length === 0) return this
|
||||
return this.setState(Commands.translateShapes(this, ids, Vec.mul(delta, isMajor ? 10 : 1)))
|
||||
const size = isMajor
|
||||
? this.settings.showGrid
|
||||
? this.currentGrid * 4
|
||||
: 10
|
||||
: this.settings.showGrid
|
||||
? this.currentGrid
|
||||
: 1
|
||||
|
||||
return this.setState(Commands.translateShapes(this, ids, Vec.mul(delta, size)))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2498,7 +2518,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
onKeyDown: TLKeyboardEventHandler = (key, info, e) => {
|
||||
switch (e.key) {
|
||||
case '.': {
|
||||
case '/': {
|
||||
if (this.status === 'idle') {
|
||||
const { shiftKey, metaKey, altKey, ctrlKey, spaceKey } = this
|
||||
|
||||
|
@ -2559,7 +2579,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
if (!info) return
|
||||
|
||||
switch (e.key) {
|
||||
case '.': {
|
||||
case '/': {
|
||||
const { currentPoint, shiftKey, metaKey, altKey, ctrlKey, spaceKey } = this
|
||||
|
||||
this.onPointerUp(
|
||||
|
@ -2950,7 +2970,18 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
// The center of the component (in screen space)
|
||||
get centerPoint() {
|
||||
const { width, height } = this.rendererBounds
|
||||
return Vec.round([width / 2, height / 2])
|
||||
return Vec.toFixed([width / 2, height / 2])
|
||||
}
|
||||
|
||||
get currentGrid() {
|
||||
const { zoom } = this.pageState.camera
|
||||
if (zoom < 0.15) {
|
||||
return GRID_SIZE * 16
|
||||
} else if (zoom < 1) {
|
||||
return GRID_SIZE * 4
|
||||
} else {
|
||||
return GRID_SIZE * 1
|
||||
}
|
||||
}
|
||||
|
||||
getShapeUtil = TLDR.getShapeUtil
|
||||
|
@ -2996,6 +3027,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
showRotateHandles: true,
|
||||
showBindingHandles: true,
|
||||
showCloneHandles: false,
|
||||
showGrid: false,
|
||||
},
|
||||
appState: {
|
||||
status: TDStatus.Idle,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { Utils } from '@tldraw/core'
|
||||
import { AlignType, TldrawCommand, TDShapeType } from '~types'
|
||||
import type { TDSnapshot } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import Vec from '@tldraw/vec'
|
||||
import type { TldrawApp } from '../../internal'
|
||||
|
||||
export function alignShapes(app: TldrawApp, ids: string[], type: AlignType): TldrawCommand {
|
||||
|
|
|
@ -170,7 +170,7 @@ export function moveShapesToPage(
|
|||
const mx = (viewportBounds.width - bounds.width * zoom) / 2 / zoom
|
||||
const my = (viewportBounds.height - bounds.height * zoom) / 2 / zoom
|
||||
|
||||
const point = Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my]))
|
||||
const point = Vec.toFixed(Vec.add([-bounds.minX, -bounds.minY], [mx, my]))
|
||||
|
||||
return {
|
||||
id: 'move_to_page',
|
||||
|
|
|
@ -35,7 +35,7 @@ export function styleShapes(
|
|||
|
||||
if (shape.type === TDShapeType.Text) {
|
||||
beforeShapes[shape.id].point = shape.point
|
||||
afterShapes[shape.id].point = Vec.round(
|
||||
afterShapes[shape.id].point = Vec.toFixed(
|
||||
Vec.add(
|
||||
shape.point,
|
||||
Vec.sub(
|
||||
|
|
|
@ -30,7 +30,7 @@ export function translateShapes(app: TldrawApp, ids: string[], delta: number[]):
|
|||
app.state,
|
||||
idsToMutate,
|
||||
(shape) => ({
|
||||
point: Vec.round(Vec.add(shape.point, delta)),
|
||||
point: Vec.toFixed(Vec.add(shape.point, delta)),
|
||||
}),
|
||||
currentPageId
|
||||
)
|
||||
|
|
|
@ -10,12 +10,12 @@ import {
|
|||
TldrawCommand,
|
||||
} from '~types'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { Utils } from '@tldraw/core'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import { BINDING_DISTANCE } from '~constants'
|
||||
import { shapeUtils } from '~state/shapes'
|
||||
import { BaseSession } from '../BaseSession'
|
||||
import type { TldrawApp } from '../../internal'
|
||||
import { Utils } from '@tldraw/core'
|
||||
|
||||
export class ArrowSession extends BaseSession {
|
||||
type = SessionType.Arrow
|
||||
|
@ -70,7 +70,14 @@ export class ArrowSession extends BaseSession {
|
|||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const { initialShape } = this
|
||||
const { currentPoint, shiftKey, altKey, metaKey } = this.app
|
||||
const {
|
||||
currentPoint,
|
||||
shiftKey,
|
||||
altKey,
|
||||
metaKey,
|
||||
currentGrid,
|
||||
settings: { showGrid },
|
||||
} = this.app
|
||||
|
||||
const shape = this.app.getShape<ArrowShape>(initialShape.id)
|
||||
|
||||
|
@ -90,15 +97,18 @@ export class ArrowSession extends BaseSession {
|
|||
if (shiftKey) {
|
||||
const A = handles[handleId === 'start' ? 'end' : 'start'].point
|
||||
const B = handles[handleId].point
|
||||
const C = Vec.round(Vec.sub(Vec.add(B, delta), shape.point))
|
||||
const C = Vec.toFixed(Vec.sub(Vec.add(B, delta), shape.point))
|
||||
const angle = Vec.angle(A, C)
|
||||
const adjusted = Vec.rotWith(C, A, Utils.snapAngleToSegments(angle, 24) - angle)
|
||||
delta = Vec.add(delta, Vec.sub(adjusted, C))
|
||||
}
|
||||
|
||||
const nextPoint = Vec.sub(Vec.add(handles[handleId].point, delta), shape.point)
|
||||
|
||||
const handle = {
|
||||
...handles[handleId],
|
||||
point: Vec.round(Vec.sub(Vec.add(handles[handleId].point, delta), shape.point)),
|
||||
point: showGrid ? Vec.snap(nextPoint, currentGrid) : Vec.toFixed(nextPoint),
|
||||
|
||||
bindingId: undefined,
|
||||
}
|
||||
|
||||
|
@ -340,14 +350,20 @@ export class ArrowSession extends BaseSession {
|
|||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const { initialShape, initialBinding, newStartBindingId, startBindingShapeId, handleId } = this
|
||||
|
||||
const currentShape = TLDR.onSessionComplete(this.app.page.shapes[initialShape.id]) as ArrowShape
|
||||
const currentBindingId = currentShape.handles[handleId].bindingId
|
||||
|
||||
if (
|
||||
!(currentBindingId || initialBinding) &&
|
||||
Vec.dist(currentShape.handles.start.point, currentShape.handles.end.point) > 2
|
||||
) {
|
||||
return this.cancel()
|
||||
}
|
||||
|
||||
const beforeBindings: Partial<Record<string, TDBinding>> = {}
|
||||
|
||||
const afterBindings: Partial<Record<string, TDBinding>> = {}
|
||||
|
||||
let afterShape = this.app.page.shapes[initialShape.id] as ArrowShape
|
||||
|
||||
const currentBindingId = afterShape.handles[handleId].bindingId
|
||||
|
||||
if (initialBinding) {
|
||||
beforeBindings[initialBinding.id] = this.isCreate ? undefined : initialBinding
|
||||
afterBindings[initialBinding.id] = undefined
|
||||
|
@ -363,8 +379,6 @@ export class ArrowSession extends BaseSession {
|
|||
afterBindings[newStartBindingId] = this.app.page.bindings[newStartBindingId]
|
||||
}
|
||||
|
||||
afterShape = TLDR.onSessionComplete(afterShape)
|
||||
|
||||
return {
|
||||
id: 'arrow',
|
||||
before: {
|
||||
|
@ -392,7 +406,7 @@ export class ArrowSession extends BaseSession {
|
|||
pages: {
|
||||
[this.app.currentPageId]: {
|
||||
shapes: {
|
||||
[initialShape.id]: afterShape,
|
||||
[initialShape.id]: currentShape,
|
||||
},
|
||||
bindings: afterBindings,
|
||||
},
|
||||
|
@ -441,7 +455,7 @@ export class ArrowSession extends BaseSession {
|
|||
fromId: shape.id,
|
||||
toId: target.id,
|
||||
handleId: handleId,
|
||||
point: Vec.round(bindingPoint.point),
|
||||
point: Vec.toFixed(bindingPoint.point),
|
||||
distance: bindingPoint.distance,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ export class DrawSession extends BaseSession {
|
|||
}
|
||||
|
||||
// The new adjusted point
|
||||
const newAdjustedPoint = Vec.round(Vec.sub(currentPoint, originPoint)).concat(currentPoint[2])
|
||||
const newAdjustedPoint = Vec.toFixed(Vec.sub(currentPoint, originPoint)).concat(currentPoint[2])
|
||||
|
||||
// Don't add duplicate points.
|
||||
if (Vec.isEqual(this.lastAdjustedPoint, newAdjustedPoint)) return
|
||||
|
@ -112,7 +112,7 @@ export class DrawSession extends BaseSession {
|
|||
// offset between the new top left and the original top left.
|
||||
|
||||
points = this.points.map((pt) => {
|
||||
return Vec.round(Vec.sub(pt, delta)).concat(pt[2])
|
||||
return Vec.toFixed(Vec.sub(pt, delta)).concat(pt[2])
|
||||
})
|
||||
} else {
|
||||
// If the new top left is the same as the previous top left,
|
||||
|
@ -197,8 +197,8 @@ export class DrawSession extends BaseSession {
|
|||
shapes: {
|
||||
[shapeId]: {
|
||||
...shape,
|
||||
point: Vec.round(shape.point),
|
||||
points: shape.points.map((pt) => Vec.round(pt)),
|
||||
point: Vec.toFixed(shape.point),
|
||||
points: shape.points.map((pt) => Vec.toFixed(pt)),
|
||||
isComplete: true,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -57,7 +57,7 @@ export class EraseSession extends BaseSession {
|
|||
}
|
||||
}
|
||||
|
||||
const newPoint = Vec.round(Vec.add(originPoint, Vec.sub(currentPoint, originPoint)))
|
||||
const newPoint = Vec.toFixed(Vec.add(originPoint, Vec.sub(currentPoint, originPoint)))
|
||||
|
||||
const deletedShapeIds = new Set<string>([])
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ describe('Rotate session', () => {
|
|||
it('keeps the center', () => {
|
||||
app.loadDocument(mockDocument).select('rect1', 'rect2')
|
||||
|
||||
const centerBefore = Vec.round(
|
||||
const centerBefore = Vec.toFixed(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
|
||||
)
|
||||
|
@ -114,7 +114,7 @@ describe('Rotate session', () => {
|
|||
|
||||
app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession()
|
||||
|
||||
const centerAfterA = Vec.round(
|
||||
const centerAfterA = Vec.toFixed(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
|
||||
)
|
||||
|
@ -122,7 +122,7 @@ describe('Rotate session', () => {
|
|||
|
||||
app.pointBoundsHandle('rotate', { x: 100, y: 0 }).movePointer([50, 0]).completeSession()
|
||||
|
||||
const centerAfterB = Vec.round(
|
||||
const centerAfterB = Vec.toFixed(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
|
||||
)
|
||||
|
@ -142,7 +142,7 @@ describe('Rotate session', () => {
|
|||
it('changes the center after nudging', () => {
|
||||
const app = new TldrawTestApp().loadDocument(mockDocument).select('rect1', 'rect2')
|
||||
|
||||
const centerBefore = Vec.round(
|
||||
const centerBefore = Vec.toFixed(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
|
||||
)
|
||||
|
@ -150,7 +150,7 @@ describe('Rotate session', () => {
|
|||
|
||||
app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession()
|
||||
|
||||
const centerAfterA = Vec.round(
|
||||
const centerAfterA = Vec.toFixed(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
|
||||
)
|
||||
|
@ -163,7 +163,7 @@ describe('Rotate session', () => {
|
|||
|
||||
app.pointBoundsHandle('rotate', { x: 50, y: 0 }).movePointer([100, 50]).completeSession()
|
||||
|
||||
const centerAfterB = Vec.round(
|
||||
const centerAfterB = Vec.toFixed(
|
||||
Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(app.selectedIds.map((id) => app.getShapeBounds(id)))
|
||||
)
|
||||
|
|
|
@ -113,7 +113,8 @@ export class TransformSession extends BaseSession {
|
|||
shiftKey,
|
||||
altKey,
|
||||
metaKey,
|
||||
settings: { isSnapping },
|
||||
currentGrid,
|
||||
settings: { isSnapping, showGrid },
|
||||
},
|
||||
} = this
|
||||
|
||||
|
@ -138,6 +139,13 @@ export class TransformSession extends BaseSession {
|
|||
}
|
||||
}
|
||||
|
||||
if (showGrid) {
|
||||
newBounds = {
|
||||
...newBounds,
|
||||
...Utils.snapBoundsToGrid(newBounds, currentGrid),
|
||||
}
|
||||
}
|
||||
|
||||
// Should we snap?
|
||||
|
||||
const speed = Vec.dist(currentPoint, previousPoint)
|
||||
|
@ -180,7 +188,7 @@ export class TransformSession extends BaseSession {
|
|||
this.scaleY = newBounds.scaleY
|
||||
|
||||
shapeBounds.forEach(({ initialShape, initialShapeBounds, transformOrigin }) => {
|
||||
const newShapeBounds = Utils.getRelativeTransformedBoundingBox(
|
||||
let newShapeBounds = Utils.getRelativeTransformedBoundingBox(
|
||||
newBounds,
|
||||
initialCommonBounds,
|
||||
initialShapeBounds,
|
||||
|
@ -188,13 +196,19 @@ export class TransformSession extends BaseSession {
|
|||
this.scaleY < 0
|
||||
)
|
||||
|
||||
shapes[initialShape.id] = TLDR.transform(this.app.getShape(initialShape.id), newShapeBounds, {
|
||||
if (showGrid) {
|
||||
newShapeBounds = Utils.snapBoundsToGrid(newShapeBounds, currentGrid)
|
||||
}
|
||||
|
||||
const afterShape = TLDR.transform(this.app.getShape(initialShape.id), newShapeBounds, {
|
||||
type: this.transformType,
|
||||
initialShape,
|
||||
scaleX: this.scaleX,
|
||||
scaleY: this.scaleY,
|
||||
transformOrigin,
|
||||
})
|
||||
|
||||
shapes[initialShape.id] = afterShape
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
@ -70,13 +70,14 @@ export class TransformSingleSession extends BaseSession {
|
|||
initialShape,
|
||||
initialShapeBounds,
|
||||
app: {
|
||||
settings: { isSnapping },
|
||||
settings: { isSnapping, showGrid },
|
||||
currentPageId,
|
||||
pageState: { camera },
|
||||
viewport,
|
||||
currentPoint,
|
||||
previousPoint,
|
||||
originPoint,
|
||||
currentGrid,
|
||||
shiftKey,
|
||||
altKey,
|
||||
metaKey,
|
||||
|
@ -85,12 +86,12 @@ export class TransformSingleSession extends BaseSession {
|
|||
|
||||
if (initialShape.isLocked) return void null
|
||||
|
||||
const shapes = {} as Record<string, Partial<TDShape>>
|
||||
|
||||
const delta = altKey
|
||||
? Vec.mul(Vec.sub(currentPoint, originPoint), 2)
|
||||
: Vec.sub(currentPoint, originPoint)
|
||||
|
||||
const shapes = {} as Record<string, Partial<TDShape>>
|
||||
|
||||
const shape = this.app.getShape(initialShape.id)
|
||||
|
||||
const utils = TLDR.getShapeUtil(shape)
|
||||
|
@ -110,6 +111,13 @@ export class TransformSingleSession extends BaseSession {
|
|||
}
|
||||
}
|
||||
|
||||
if (showGrid) {
|
||||
newBounds = {
|
||||
...newBounds,
|
||||
...Utils.snapBoundsToGrid(newBounds, currentGrid),
|
||||
}
|
||||
}
|
||||
|
||||
// Should we snap?
|
||||
|
||||
const speed = Vec.dist(currentPoint, previousPoint)
|
||||
|
@ -159,6 +167,10 @@ export class TransformSingleSession extends BaseSession {
|
|||
shapes[shape.id] = afterShape
|
||||
}
|
||||
|
||||
if (showGrid && afterShape && afterShape.point) {
|
||||
afterShape.point = Vec.snap(afterShape.point, currentGrid)
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Vec } from '@tldraw/vec'
|
||||
import { mockDocument, TldrawTestApp } from '~test'
|
||||
import { GroupShape, SessionType, TDShapeType, TDStatus } from '~types'
|
||||
|
||||
|
@ -114,15 +115,16 @@ describe('Translate session', () => {
|
|||
|
||||
expect(Object.keys(app.getPage().shapes).length).toBe(5)
|
||||
|
||||
app.movePointer({ x: 30, y: 30 })
|
||||
app.movePointer({ x: 20, y: 20, altKey: false })
|
||||
|
||||
expect(Object.keys(app.getPage().shapes).length).toBe(3)
|
||||
|
||||
app.completeSession()
|
||||
|
||||
// Original position + delta
|
||||
expect(app.getShape('rect1').point).toStrictEqual([30, 30])
|
||||
expect(app.getShape('rect2').point).toStrictEqual([130, 130])
|
||||
const rectPoint = app.getShape('rect1').point
|
||||
expect(app.getShape('rect1').point).toStrictEqual(rectPoint)
|
||||
expect(app.getShape('rect2').point).toStrictEqual([110, 110])
|
||||
|
||||
expect(Object.keys(app.page.shapes)).toStrictEqual(['rect1', 'rect2', 'rect3'])
|
||||
})
|
||||
|
@ -211,6 +213,7 @@ describe('Translate session', () => {
|
|||
.movePointer({ x: 20, y: 20, altKey: true })
|
||||
.completeSession()
|
||||
|
||||
const rectPoint = app.getShape('rect1').point
|
||||
const children = app.getShape<GroupShape>('groupA').children
|
||||
const newShapeId = children[children.length - 1]
|
||||
|
||||
|
@ -218,7 +221,7 @@ describe('Translate session', () => {
|
|||
expect(app.getShape<GroupShape>('groupA').children.length).toBe(3)
|
||||
expect(app.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
expect(app.getShape('rect2').point).toStrictEqual([100, 100])
|
||||
expect(app.getShape(newShapeId).point).toStrictEqual([20, 20])
|
||||
expect(app.getShape(newShapeId).point).toStrictEqual(Vec.add(rectPoint, [10, 10]))
|
||||
expect(app.getShape(newShapeId).parentId).toBe('groupA')
|
||||
|
||||
app.undo()
|
||||
|
@ -235,7 +238,7 @@ describe('Translate session', () => {
|
|||
expect(app.getShape<GroupShape>('groupA').children.length).toBe(3)
|
||||
expect(app.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
expect(app.getShape('rect2').point).toStrictEqual([100, 100])
|
||||
expect(app.getShape(newShapeId).point).toStrictEqual([20, 20])
|
||||
expect(app.getShape(newShapeId).point).toStrictEqual(Vec.add(rectPoint, [10, 10]))
|
||||
expect(app.getShape(newShapeId).parentId).toBe('groupA')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -24,6 +24,7 @@ type CloneInfo =
|
|||
}
|
||||
| {
|
||||
state: 'ready'
|
||||
cloneMap: Record<string, string>
|
||||
clones: TDShape[]
|
||||
clonedBindings: ArrowBinding[]
|
||||
}
|
||||
|
@ -172,7 +173,7 @@ export class TranslateSession extends BaseSession {
|
|||
bindingsToDelete,
|
||||
app: {
|
||||
pageState: { camera },
|
||||
settings: { isSnapping },
|
||||
settings: { isSnapping, showGrid },
|
||||
currentPageId,
|
||||
viewport,
|
||||
selectedIds,
|
||||
|
@ -182,13 +183,12 @@ export class TranslateSession extends BaseSession {
|
|||
altKey,
|
||||
shiftKey,
|
||||
metaKey,
|
||||
currentGrid,
|
||||
},
|
||||
} = this
|
||||
|
||||
const nextBindings: Patch<Record<string, TDBinding>> = {}
|
||||
|
||||
const nextShapes: Patch<Record<string, TDShape>> = {}
|
||||
|
||||
const nextPageState: Patch<TLPageState> = {}
|
||||
|
||||
let delta = Vec.sub(currentPoint, originPoint)
|
||||
|
@ -236,10 +236,12 @@ export class TranslateSession extends BaseSession {
|
|||
this.speed * camera.zoom < SLOW_SPEED &&
|
||||
this.snapInfo.state === 'ready'
|
||||
) {
|
||||
const bounds = Utils.getBoundsWithCenter(Utils.translateBounds(initialCommonBounds, delta))
|
||||
|
||||
const snapResult = Utils.getSnapPoints(
|
||||
bounds,
|
||||
Utils.getBoundsWithCenter(
|
||||
showGrid
|
||||
? Utils.snapBoundsToGrid(Utils.translateBounds(initialCommonBounds, delta), currentGrid)
|
||||
: Utils.translateBounds(initialCommonBounds, delta)
|
||||
),
|
||||
(this.isCloning ? this.snapInfo.bounds : this.snapInfo.others).filter(
|
||||
(bounds) => Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds)
|
||||
),
|
||||
|
@ -259,8 +261,6 @@ export class TranslateSession extends BaseSession {
|
|||
// The "movement" is the actual change of position between this
|
||||
// computed position and the previous computed position.
|
||||
|
||||
const movement = Vec.sub(delta, this.prev)
|
||||
|
||||
this.prev = delta
|
||||
|
||||
// If cloning...
|
||||
|
@ -287,7 +287,7 @@ export class TranslateSession extends BaseSession {
|
|||
|
||||
// Add the clones to the page
|
||||
clones.forEach((clone) => {
|
||||
nextShapes[clone.id] = { ...clone, point: Vec.round(Vec.add(clone.point, delta)) }
|
||||
nextShapes[clone.id] = { ...clone }
|
||||
|
||||
// Add clones to non-selected parents
|
||||
if (clone.parentId !== currentPageId && !selectedIds.includes(clone.parentId)) {
|
||||
|
@ -313,13 +313,11 @@ export class TranslateSession extends BaseSession {
|
|||
|
||||
// Either way, move the clones
|
||||
clones.forEach((clone) => {
|
||||
const current = (nextShapes[clone.id] || this.app.getShape(clone.id)) as TDShape
|
||||
|
||||
if (!current.point) throw Error('No point on that clone!')
|
||||
|
||||
nextShapes[clone.id] = {
|
||||
...clone,
|
||||
point: Vec.round(Vec.add(current.point, movement)),
|
||||
point: showGrid
|
||||
? Vec.snap(Vec.toFixed(Vec.add(clone.point, delta)), currentGrid)
|
||||
: Vec.toFixed(Vec.add(clone.point, delta)),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
@ -327,14 +325,11 @@ export class TranslateSession extends BaseSession {
|
|||
|
||||
const { clones } = this.cloneInfo
|
||||
|
||||
// Either way, move the clones
|
||||
clones.forEach((clone) => {
|
||||
const current = (nextShapes[clone.id] || this.app.getShape(clone.id)) as TDShape
|
||||
|
||||
if (!current.point) throw Error('No point on that clone!')
|
||||
|
||||
nextShapes[clone.id] = {
|
||||
point: Vec.round(Vec.add(current.point, movement)),
|
||||
point: showGrid
|
||||
? Vec.snap(Vec.toFixed(Vec.add(clone.point, delta)), currentGrid)
|
||||
: Vec.toFixed(Vec.add(clone.point, delta)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -350,7 +345,6 @@ export class TranslateSession extends BaseSession {
|
|||
this.isCloning = false
|
||||
|
||||
// Delete the bindings
|
||||
|
||||
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined))
|
||||
|
||||
// Remove the clones from parents
|
||||
|
@ -369,7 +363,9 @@ export class TranslateSession extends BaseSession {
|
|||
// Move the original shapes back to the cursor position
|
||||
initialShapes.forEach((shape) => {
|
||||
nextShapes[shape.id] = {
|
||||
point: Vec.round(Vec.add(shape.point, delta)),
|
||||
point: showGrid
|
||||
? Vec.snap(Vec.toFixed(Vec.add(shape.point, delta)), currentGrid)
|
||||
: Vec.toFixed(Vec.add(shape.point, delta)),
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -380,18 +376,18 @@ export class TranslateSession extends BaseSession {
|
|||
|
||||
// Set selected ids
|
||||
nextPageState.selectedIds = initialShapes.map((shape) => shape.id)
|
||||
} else {
|
||||
// Move the shapes by the delta
|
||||
initialShapes.forEach((shape) => {
|
||||
// const current = (nextShapes[shape.id] || this.app.getShape(shape.id)) as TDShape
|
||||
|
||||
nextShapes[shape.id] = {
|
||||
point: showGrid
|
||||
? Vec.snap(Vec.toFixed(Vec.add(shape.point, delta)), currentGrid)
|
||||
: Vec.toFixed(Vec.add(shape.point, delta)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Move the shapes by the delta
|
||||
initialShapes.forEach((shape) => {
|
||||
const current = (nextShapes[shape.id] || this.app.getShape(shape.id)) as TDShape
|
||||
|
||||
if (!current.point) throw Error('No point on that clone!')
|
||||
|
||||
nextShapes[shape.id] = {
|
||||
point: Vec.round(Vec.add(current.point, movement)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -696,6 +692,7 @@ export class TranslateSession extends BaseSession {
|
|||
this.cloneInfo = {
|
||||
state: 'ready',
|
||||
clones,
|
||||
cloneMap,
|
||||
clonedBindings,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
|||
|
||||
const isDraw = style.dash === DashStyle.Draw
|
||||
|
||||
const isStraightLine = Vec.dist(bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
|
||||
const isStraightLine = Vec.dist(bend.point, Vec.toFixed(Vec.med(start.point, end.point))) < 1
|
||||
|
||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||
|
||||
|
@ -122,7 +122,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
|||
if (isStraightLine) {
|
||||
const path = isDraw
|
||||
? renderFreehandArrowShaft(shape)
|
||||
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
|
||||
: 'M' + Vec.toFixed(start.point) + 'L' + Vec.toFixed(end.point)
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
|
||||
arrowDist,
|
||||
|
@ -398,11 +398,11 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
|||
|
||||
nextHandles['bend'] = {
|
||||
...bend,
|
||||
point: Vec.round(Math.abs(bendDist) < 10 ? midPoint : point),
|
||||
point: Vec.toFixed(Math.abs(bendDist) < 10 ? midPoint : point),
|
||||
}
|
||||
|
||||
return {
|
||||
point: Vec.round([bounds.minX, bounds.minY]),
|
||||
point: Vec.toFixed([bounds.minX, bounds.minY]),
|
||||
handles: nextHandles,
|
||||
}
|
||||
}
|
||||
|
@ -516,7 +516,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
|||
return this.onHandleChange(shape, {
|
||||
[handle.id]: {
|
||||
...handle,
|
||||
point: Vec.round(handlePoint),
|
||||
point: Vec.toFixed(handlePoint),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -529,11 +529,11 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
|||
...nextHandles,
|
||||
start: {
|
||||
...nextHandles.start,
|
||||
point: Vec.round(nextHandles.start.point),
|
||||
point: Vec.toFixed(nextHandles.start.point),
|
||||
},
|
||||
end: {
|
||||
...nextHandles.end,
|
||||
point: Vec.round(nextHandles.end.point),
|
||||
point: Vec.toFixed(nextHandles.end.point),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -601,10 +601,10 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
|||
|
||||
if (!Vec.isEqual(offset, [0, 0])) {
|
||||
Object.values(nextShape.handles).forEach((handle) => {
|
||||
handle.point = Vec.round(Vec.sub(handle.point, offset))
|
||||
handle.point = Vec.toFixed(Vec.sub(handle.point, offset))
|
||||
})
|
||||
|
||||
nextShape.point = Vec.round(Vec.add(nextShape.point, offset))
|
||||
nextShape.point = Vec.toFixed(Vec.add(nextShape.point, offset))
|
||||
}
|
||||
|
||||
return nextShape
|
||||
|
|
|
@ -38,7 +38,7 @@ export function getBendPoint(handles: ArrowShape['handles'], bend: number) {
|
|||
|
||||
const u = Vec.uni(Vec.vec(start.point, end.point))
|
||||
|
||||
const point = Vec.round(
|
||||
const point = Vec.toFixed(
|
||||
Math.abs(bendDist) < 10 ? midPoint : Vec.add(midPoint, Vec.mul(Vec.per(u), bendDist))
|
||||
)
|
||||
|
||||
|
@ -115,7 +115,7 @@ export function renderCurvedFreehandArrowShaft(
|
|||
|
||||
const angle = Utils.lerpAngles(startAngle, endAngle, t)
|
||||
|
||||
points.push(Vec.round(Vec.nudgeAtAngle(center, angle, radius)))
|
||||
points.push(Vec.toFixed(Vec.nudgeAtAngle(center, angle, radius)))
|
||||
}
|
||||
|
||||
const stroke = getStroke([startPoint, ...points, endPoint], {
|
||||
|
@ -221,7 +221,7 @@ export function getArrowPath(shape: ArrowShape) {
|
|||
|
||||
const path: (string | number)[] = []
|
||||
|
||||
const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
|
||||
const isStraightLine = Vec.dist(_bend.point, Vec.toFixed(Vec.med(start.point, end.point))) < 1
|
||||
|
||||
if (isStraightLine) {
|
||||
// Path (line segment)
|
||||
|
|
|
@ -331,7 +331,7 @@ export class EllipseUtil extends TDShapeUtil<T, E> {
|
|||
|
||||
transformSingle = (shape: T, bounds: TLBounds): Partial<T> => {
|
||||
return {
|
||||
point: Vec.round([bounds.minX, bounds.minY]),
|
||||
point: Vec.toFixed([bounds.minX, bounds.minY]),
|
||||
radius: Vec.div([bounds.width, bounds.height], 2),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -217,7 +217,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
|||
bounds: TLBounds,
|
||||
{ scaleX, scaleY, transformOrigin }: TransformInfo<T>
|
||||
): Partial<T> => {
|
||||
const point = Vec.round([
|
||||
const point = Vec.toFixed([
|
||||
bounds.minX +
|
||||
(bounds.width - shape.size[0]) * (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
|
||||
bounds.minY +
|
||||
|
|
|
@ -285,7 +285,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
} = initialShape
|
||||
|
||||
return {
|
||||
point: Vec.round([bounds.minX, bounds.minY]),
|
||||
point: Vec.toFixed([bounds.minX, bounds.minY]),
|
||||
style: {
|
||||
...initialShape.style,
|
||||
scale: scale * Math.max(Math.abs(scaleY), Math.abs(scaleX)),
|
||||
|
@ -309,7 +309,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
...shape.style,
|
||||
scale: 1,
|
||||
},
|
||||
point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))),
|
||||
point: Vec.toFixed(Vec.add(shape.point, Vec.sub(center, newCenter))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -141,7 +141,8 @@ export function getStickyShapeStyle(style: ShapeStyles, isDarkMode = false) {
|
|||
const { color } = style
|
||||
|
||||
const theme: Theme = isDarkMode ? 'dark' : 'light'
|
||||
const adjustedColor = color === ColorStyle.Black ? ColorStyle.Yellow : color
|
||||
const adjustedColor =
|
||||
color === ColorStyle.White || color === ColorStyle.Black ? ColorStyle.Yellow : color
|
||||
|
||||
return {
|
||||
fill: stickyFills[theme][adjustedColor],
|
||||
|
|
|
@ -13,9 +13,11 @@ export function transformRectangle<T extends TLShape & { size: number[] }>(
|
|||
{ initialShape, transformOrigin, scaleX, scaleY }: TLTransformInfo<T>
|
||||
) {
|
||||
if (shape.rotation || initialShape.isAspectRatioLocked) {
|
||||
const size = Vec.round(Vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY))))
|
||||
const size = Vec.toFixed(
|
||||
Vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY)))
|
||||
)
|
||||
|
||||
const point = Vec.round([
|
||||
const point = Vec.toFixed([
|
||||
bounds.minX +
|
||||
(bounds.width - shape.size[0]) * (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
|
||||
bounds.minY +
|
||||
|
@ -37,8 +39,8 @@ export function transformRectangle<T extends TLShape & { size: number[] }>(
|
|||
}
|
||||
} else {
|
||||
return {
|
||||
point: Vec.round([bounds.minX, bounds.minY]),
|
||||
size: Vec.round([bounds.width, bounds.height]),
|
||||
point: Vec.toFixed([bounds.minX, bounds.minY]),
|
||||
size: Vec.toFixed([bounds.width, bounds.height]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export function transformSingleRectangle<T extends TLShape & { size: number[] }>
|
|||
bounds: TLBounds
|
||||
) {
|
||||
return {
|
||||
size: Vec.round([bounds.width, bounds.height]),
|
||||
point: Vec.round([bounds.minX, bounds.minY]),
|
||||
size: Vec.toFixed([bounds.width, bounds.height]),
|
||||
point: Vec.toFixed([bounds.minX, bounds.minY]),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Utils, TLPointerEventHandler } from '@tldraw/core'
|
||||
import Vec from '@tldraw/vec'
|
||||
import { Arrow } from '~state/shapes'
|
||||
import { SessionType, TDShapeType } from '~types'
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
@ -13,6 +14,8 @@ export class ArrowTool extends BaseTool {
|
|||
|
||||
const {
|
||||
currentPoint,
|
||||
currentGrid,
|
||||
settings: { showGrid },
|
||||
appState: { currentPageId, currentStyle },
|
||||
} = this.app
|
||||
|
||||
|
@ -24,7 +27,7 @@ export class ArrowTool extends BaseTool {
|
|||
id,
|
||||
parentId: currentPageId,
|
||||
childIndex,
|
||||
point: currentPoint,
|
||||
point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
|
||||
style: { ...currentStyle },
|
||||
})
|
||||
|
||||
|
|
|
@ -89,7 +89,6 @@ export abstract class BaseTool<T extends string = any> extends TDEventHandler {
|
|||
return
|
||||
}
|
||||
|
||||
/* noop */
|
||||
if (key === 'Meta' || key === 'Control' || key === 'Alt') {
|
||||
this.app.updateSession()
|
||||
return
|
||||
|
@ -97,7 +96,6 @@ export abstract class BaseTool<T extends string = any> extends TDEventHandler {
|
|||
}
|
||||
|
||||
onKeyUp: TLKeyboardEventHandler = (key) => {
|
||||
/* noop */
|
||||
if (key === 'Meta' || key === 'Control' || key === 'Alt') {
|
||||
this.app.updateSession()
|
||||
return
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core'
|
||||
import Vec from '@tldraw/vec'
|
||||
import { Ellipse } from '~state/shapes'
|
||||
import { SessionType, TDShapeType } from '~types'
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
@ -13,6 +14,8 @@ export class EllipseTool extends BaseTool {
|
|||
|
||||
const {
|
||||
currentPoint,
|
||||
currentGrid,
|
||||
settings: { showGrid },
|
||||
appState: { currentPageId, currentStyle },
|
||||
} = this.app
|
||||
|
||||
|
@ -24,7 +27,7 @@ export class EllipseTool extends BaseTool {
|
|||
id,
|
||||
parentId: currentPageId,
|
||||
childIndex,
|
||||
point: currentPoint,
|
||||
point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
|
||||
style: { ...currentStyle },
|
||||
})
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Utils, TLPointerEventHandler } from '@tldraw/core'
|
||||
import Vec from '@tldraw/vec'
|
||||
import { Arrow } from '~state/shapes'
|
||||
import { SessionType, TDShapeType } from '~types'
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
@ -13,6 +14,8 @@ export class LineTool extends BaseTool {
|
|||
|
||||
const {
|
||||
currentPoint,
|
||||
currentGrid,
|
||||
settings: { showGrid },
|
||||
appState: { currentPageId, currentStyle },
|
||||
} = this.app
|
||||
|
||||
|
@ -24,7 +27,7 @@ export class LineTool extends BaseTool {
|
|||
id,
|
||||
parentId: currentPageId,
|
||||
childIndex,
|
||||
point: currentPoint,
|
||||
point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
|
||||
decorations: {
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core'
|
||||
import Vec from '@tldraw/vec'
|
||||
import { Rectangle } from '~state/shapes'
|
||||
import { SessionType, TDShapeType } from '~types'
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
@ -13,6 +14,8 @@ export class RectangleTool extends BaseTool {
|
|||
|
||||
const {
|
||||
currentPoint,
|
||||
currentGrid,
|
||||
settings: { showGrid },
|
||||
appState: { currentPageId, currentStyle },
|
||||
} = this.app
|
||||
|
||||
|
@ -24,7 +27,7 @@ export class RectangleTool extends BaseTool {
|
|||
id,
|
||||
parentId: currentPageId,
|
||||
childIndex,
|
||||
point: currentPoint,
|
||||
point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
|
||||
style: { ...currentStyle },
|
||||
})
|
||||
|
||||
|
|
|
@ -65,9 +65,14 @@ describe('When double clicking link controls', () => {
|
|||
const app = new TldrawTestApp()
|
||||
.loadDocument(doc)
|
||||
.select('rect2')
|
||||
.pointBoundsHandle('center', { x: 0, y: 0 })
|
||||
.pointBoundsHandle('center', [100, 100])
|
||||
.expectShapesToBeAtPoints({
|
||||
rect1: [0, 0],
|
||||
rect2: [100, 0],
|
||||
rect3: [200, 0],
|
||||
})
|
||||
|
||||
app.movePointer({ x: 100, y: 100 }).expectShapesToBeAtPoints({
|
||||
app.movePointer([200, 200]).expectShapesToBeAtPoints({
|
||||
rect1: [100, 100],
|
||||
rect2: [200, 100],
|
||||
rect3: [300, 100],
|
||||
|
|
|
@ -26,6 +26,8 @@ export class StickyTool extends BaseTool {
|
|||
if (this.status === Status.Idle) {
|
||||
const {
|
||||
currentPoint,
|
||||
currentGrid,
|
||||
settings: { showGrid },
|
||||
appState: { currentPageId, currentStyle },
|
||||
} = this.app
|
||||
|
||||
|
@ -39,7 +41,7 @@ export class StickyTool extends BaseTool {
|
|||
id,
|
||||
parentId: currentPageId,
|
||||
childIndex,
|
||||
point: currentPoint,
|
||||
point: showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
|
||||
style: { ...currentStyle },
|
||||
})
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { TLPointerEventHandler, TLKeyboardEventHandler } from '@tldraw/core'
|
||||
import Vec from '@tldraw/vec'
|
||||
import { TDShapeType } from '~types'
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
||||
|
@ -32,8 +33,13 @@ export class TextTool extends BaseTool {
|
|||
}
|
||||
|
||||
if (this.status === Status.Idle) {
|
||||
const { currentPoint } = this.app
|
||||
this.app.createTextShapeAtPoint(currentPoint)
|
||||
const {
|
||||
currentPoint,
|
||||
currentGrid,
|
||||
settings: { showGrid },
|
||||
} = this.app
|
||||
|
||||
this.app.createTextShapeAtPoint(showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint)
|
||||
this.setStatus(Status.Creating)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -162,4 +162,16 @@ export class TldrawTestApp extends TldrawApp {
|
|||
})
|
||||
return this
|
||||
}
|
||||
|
||||
pressKey = (key: string) => {
|
||||
const e = { key } as KeyboardEvent
|
||||
this.onKeyDown(key, inputs.keydown(e), e)
|
||||
return this
|
||||
}
|
||||
|
||||
releaseKey = (key: string) => {
|
||||
const e = { key } as KeyboardEvent
|
||||
this.onKeyUp(key, inputs.keyup(e), e)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import type { TLPage, TLUser, TLPageState } from '@tldraw/core'
|
||||
import type { FileSystemHandle } from '~state/data/browser-fs-access'
|
||||
import type {
|
||||
TLPage,
|
||||
TLUser,
|
||||
TLPageState,
|
||||
TLBinding,
|
||||
TLBoundsCorner,
|
||||
TLBoundsEdge,
|
||||
|
@ -89,6 +90,7 @@ export interface TDSnapshot {
|
|||
showRotateHandles: boolean
|
||||
showBindingHandles: boolean
|
||||
showCloneHandles: boolean
|
||||
showGrid: boolean
|
||||
}
|
||||
appState: {
|
||||
currentStyle: ShapeStyles
|
||||
|
@ -470,3 +472,29 @@ export interface Command<T extends { [key: string]: any }> {
|
|||
before: Patch<T>
|
||||
after: Patch<T>
|
||||
}
|
||||
|
||||
export interface FileWithHandle extends File {
|
||||
handle?: FileSystemHandle
|
||||
}
|
||||
|
||||
export interface FileWithDirectoryHandle extends File {
|
||||
directoryHandle?: FileSystemHandle
|
||||
}
|
||||
|
||||
// The following typings implement the relevant parts of the File System Access
|
||||
// API. This can be removed once the specification reaches the Candidate phase
|
||||
// and is implemented as part of microsoft/TSJS-lib-generator.
|
||||
|
||||
export interface FileSystemHandlePermissionDescriptor {
|
||||
mode?: 'read' | 'readwrite'
|
||||
}
|
||||
|
||||
export interface FileSystemHandle {
|
||||
readonly kind: 'file' | 'directory'
|
||||
readonly name: string
|
||||
|
||||
isSameEntry: (other: FileSystemHandle) => Promise<boolean>
|
||||
|
||||
queryPermission: (descriptor?: FileSystemHandlePermissionDescriptor) => Promise<PermissionState>
|
||||
requestPermission: (descriptor?: FileSystemHandlePermissionDescriptor) => Promise<PermissionState>
|
||||
}
|
||||
|
|
|
@ -11,12 +11,11 @@
|
|||
"docs"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"baseUrl": "src",
|
||||
"composite": false,
|
||||
"incremental": false,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false,
|
||||
"emitDeclarationOnly": true
|
||||
}
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"references": [{ "path": "../vec" }, { "path": "../intersect" }, { "path": "../core" }]
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src", "src/test/*.json"],
|
||||
"exclude": ["node_modules", "dist", "docs"],
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "./dist/types",
|
||||
"rootDir": "src",
|
||||
"baseUrl": "src",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~*": ["./*"],
|
||||
"@tldraw/core": ["../core"]
|
||||
"~*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"references": [{ "path": "../vec" }, { "path": "../intersect" }, { "path": "../core" }],
|
||||
"typedocOptions": {
|
||||
"entryPoints": ["src/index.ts"],
|
||||
"out": "docs"
|
||||
|
|
86
packages/vec/CHANGELOG.md
Normal file
86
packages/vec/CHANGELOG.md
Normal file
|
@ -0,0 +1,86 @@
|
|||
# Changelog
|
||||
|
||||
## 0.1.21
|
||||
|
||||
New:
|
||||
|
||||
- Adds the `isGhost` prop to `TLShape`. In `TLComponentProps`, the `isGhost` prop will be true if either a shape has its `isGhost` set to `true` OR if a shape is the descendant of a shape with `isGhost` set to `true`. A ghost shape will have the `tl-ghost` class name, though this is not used in the Renderer. You can set it yourself in your app.
|
||||
- Adds the `isChildOfSelected` prop for `TLComponentProps`. If a shape is the child of a selected shape, its `isChildOfSelected` prop will be true.
|
||||
|
||||
Improved:
|
||||
|
||||
- Fixes a bug that could occur with the order of grouped shapes.
|
||||
- Adds an Eraser tool to the advanced example.
|
||||
- Adds a Pencil tool to the advanced example.
|
||||
|
||||
## 0.1.20
|
||||
|
||||
- Update docs.
|
||||
- Adds `hideResizeHandles` prop.
|
||||
|
||||
## 0.1.19
|
||||
|
||||
- Remove stray `index.js` files.
|
||||
|
||||
## 0.1.18
|
||||
|
||||
- Even more dependency fixes.
|
||||
|
||||
## 0.1.17
|
||||
|
||||
- More dependency fixes.
|
||||
|
||||
## 0.1.16
|
||||
|
||||
- Fix dependencies, remove `@use-gesture/react` from bundle.
|
||||
|
||||
## 0.1.15
|
||||
|
||||
- Fix README.
|
||||
|
||||
## 0.1.14
|
||||
|
||||
- Add README to package.
|
||||
|
||||
## 0.1.13
|
||||
|
||||
- Remove `type` from `TLBinding`.
|
||||
|
||||
## 0.1.12
|
||||
|
||||
- Fix bug with initial bounds.
|
||||
|
||||
## 0.1.12
|
||||
|
||||
- Fix bug with initial bounds.
|
||||
|
||||
## 0.1.12
|
||||
|
||||
- Fix bug with bounds handle events.
|
||||
|
||||
## 0.1.11
|
||||
|
||||
- Fix bug with initial camera state.
|
||||
|
||||
## 0.1.10
|
||||
|
||||
- Improve example.
|
||||
- Improve types for `TLPage`.
|
||||
|
||||
## 0.1.9
|
||||
|
||||
- Bug fixes.
|
||||
|
||||
## 0.1.8
|
||||
|
||||
- Expands README.
|
||||
- Removes properties specific to the tldraw app.
|
||||
|
||||
## 0.1.7
|
||||
|
||||
- Fixes selection bug with SVGContainer.
|
||||
- Removes various properties specific to the tldraw app.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Re-writes API for ShapeUtils.
|
21
packages/vec/LICENSE.md
Normal file
21
packages/vec/LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Stephen Ruiz Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
477
packages/vec/README.md
Normal file
477
packages/vec/README.md
Normal file
|
@ -0,0 +1,477 @@
|
|||
<div style="text-align: center; transform: scale(.5);">
|
||||
<img src="card-repo.png"/>
|
||||
</div>
|
||||
|
||||
# @tldraw/core
|
||||
|
||||
This package contains the `Renderer` and core utilities used by [tldraw](https://tldraw.com).
|
||||
|
||||
You can use this package to build projects like [tldraw](https://tldraw.com), where React components are rendered on a canvas user interface. Check out the [advanced example](https://core-steveruiz.vercel.app/).
|
||||
|
||||
💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
|
||||
|
||||
## Installation
|
||||
|
||||
Use your package manager of choice to install `@tldraw/core` and its peer dependencies.
|
||||
|
||||
```bash
|
||||
yarn add @tldraw/core
|
||||
# or
|
||||
npm i @tldraw/core
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
There are two examples in this repository.
|
||||
|
||||
The **simple** example in the `example` folder shows a minimal use of the library. It does not do much but this should be a good reference for the API without too much else built on top.
|
||||
|
||||
The **advanced** example in the `example-advanced` folder shows a more realistic use of the library. (Try it [here](https://core-steveruiz.vercel.app/)). While the fundamental patterns are the same, this example contains features such as: panning, pinching, and zooming the camera; creating, cloning, resizing, and deleting shapes; keyboard shortcuts, brush-selection; shape-snapping; undo, redo; and more. Much of the code in the advanced example comes from the [@tldraw/tldraw](https://tldraw.com) codebase.
|
||||
|
||||
If you're working on an app that uses this library, I recommend referring back to the advanced example for tips on how you might implement these features for your own project.
|
||||
|
||||
## Usage
|
||||
|
||||
Import the `Renderer` React component and pass it the required props.
|
||||
|
||||
```tsx
|
||||
import * as React from "react"
|
||||
import { Renderer, TLShape, TLShapeUtil, Vec } from '@tldraw/core'
|
||||
import { BoxShape, BoxUtil } from "./shapes/box"
|
||||
|
||||
const shapeUtils = { box: new BoxUtil() }
|
||||
|
||||
function App() {
|
||||
const [page, setPage] = React.useState({
|
||||
id: "page"
|
||||
shapes: {
|
||||
"box1": {
|
||||
id: 'box1',
|
||||
type: 'box',
|
||||
parentId: 'page',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
rotation: 0,
|
||||
}
|
||||
},
|
||||
bindings: {}
|
||||
})
|
||||
|
||||
const [pageState, setPageState] = React.useState({
|
||||
id: "page",
|
||||
selectedIds: [],
|
||||
camera: {
|
||||
point: [0,0],
|
||||
zoom: 1
|
||||
}
|
||||
})
|
||||
|
||||
return (<Renderer
|
||||
page={page}
|
||||
pageState={pageState}
|
||||
shapeUtils={shapeUtils}
|
||||
/>)
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### `Renderer`
|
||||
|
||||
To avoid unnecessary renders, be sure to pass "stable" values as props to the `Renderer`. Either define these values outside of the parent component, or place them in React state, or memoize them with `React.useMemo`.
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ------------ | ------------------------------- | ---------------------------------------------- |
|
||||
| `page` | [`TLPage`](#tlpage) | The current page object. |
|
||||
| `pageState` | [`TLPageState`](#tlpagestate) | The current page's state. |
|
||||
| `shapeUtils` | [`TLShapeUtils`](#tlshapeutils) | The shape utilities used to render the shapes. |
|
||||
|
||||
In addition to these required props, the Renderer accents many other **optional** props.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------------- | ----------------------------- | ----------------------------------------------------------------- |
|
||||
| `containerRef` | `React.MutableRefObject` | A React ref for the container, where CSS variables will be added. |
|
||||
| `theme` | `object` | An object with overrides for the Renderer's default colors. |
|
||||
| `hideBounds` | `boolean` | Do not show the bounding box for selected shapes. |
|
||||
| `hideHandles` | `boolean` | Do not show handles for shapes with handles. |
|
||||
| `hideBindingHandles` | `boolean` | Do not show binding controls for selected shapes with bindings. |
|
||||
| `hideResizeHandles` | `boolean` | Do not show resize handles for selected shapes. |
|
||||
| `hideRotateHandles` | `boolean` | Do not show rotate handles for selected shapes. |
|
||||
| `snapLines` | [`TLSnapLine`](#tlsnapline)[] | An array of "snap" lines. |
|
||||
| `users` | `object` | A table of [`TLUser`](#tluser)s. |
|
||||
| `userId` | `object` | The current user's [`TLUser`](#tluser) id. |
|
||||
|
||||
The theme object accepts valid CSS colors for the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------- |
|
||||
| `foreground` | The primary (usually "text") color |
|
||||
| `background` | The default page's background color |
|
||||
| `brushFill` | The fill color of the brush selection box |
|
||||
| `brushStroke` | The stroke color of the brush selection box |
|
||||
| `selectFill` | The fill color of the selection bounds |
|
||||
| `selectStroke` | The stroke color of the selection bounds and handles |
|
||||
|
||||
The Renderer also accepts many (optional) event callbacks.
|
||||
|
||||
| Prop | Description |
|
||||
| --------------------------- | ----------------------------------------------------------- |
|
||||
| `onPan` | Panned with the mouse wheel |
|
||||
| `onZoom` | Zoomed with the mouse wheel |
|
||||
| `onPinchStart` | Began a two-pointer pinch |
|
||||
| `onPinch` | Moved their pointers during a pinch |
|
||||
| `onPinchEnd` | Stopped a two-pointer pinch |
|
||||
| `onPointerDown` | Started pointing |
|
||||
| `onPointerMove` | Moved their pointer |
|
||||
| `onPointerUp` | Ended a point |
|
||||
| `onPointCanvas` | Pointed the canvas |
|
||||
| `onDoubleClickCanvas` | Double-pointed the canvas |
|
||||
| `onRightPointCanvas` | Right-pointed the canvas |
|
||||
| `onDragCanvas` | Dragged the canvas |
|
||||
| `onReleaseCanvas` | Stopped pointing the canvas |
|
||||
| `onHoverShape` | Moved their pointer onto a shape |
|
||||
| `onUnhoverShape` | Moved their pointer off of a shape |
|
||||
| `onPointShape` | Pointed a shape |
|
||||
| `onDoubleClickShape` | Double-pointed a shape |
|
||||
| `onRightPointShape` | Right-pointed a shape |
|
||||
| `onDragShape` | Dragged a shape |
|
||||
| `onReleaseShape` | Stopped pointing a shape |
|
||||
| `onHoverHandle` | Moved their pointer onto a shape handle |
|
||||
| `onUnhoverHandle` | Moved their pointer off of a shape handle |
|
||||
| `onPointHandle` | Pointed a shape handle |
|
||||
| `onDoubleClickHandle` | Double-pointed a shape handle |
|
||||
| `onRightPointHandle` | Right-pointed a shape handle |
|
||||
| `onDragHandle` | Dragged a shape handle |
|
||||
| `onReleaseHandle` | Stopped pointing shape handle |
|
||||
| `onHoverBounds` | Moved their pointer onto the selection bounds |
|
||||
| `onUnhoverBounds` | Moved their pointer off of the selection bounds |
|
||||
| `onPointBounds` | Pointed the selection bounds |
|
||||
| `onDoubleClickBounds` | Double-pointed the selection bounds |
|
||||
| `onRightPointBounds` | Right-pointed the selection bounds |
|
||||
| `onDragBounds` | Dragged the selection bounds |
|
||||
| `onReleaseBounds` | Stopped the selection bounds |
|
||||
| `onHoverBoundsHandle` | Moved their pointer onto a selection bounds handle |
|
||||
| `onUnhoverBoundsHandle` | Moved their pointer off of a selection bounds handle |
|
||||
| `onPointBoundsHandle` | Pointed a selection bounds handle |
|
||||
| `onDoubleClickBoundsHandle` | Double-pointed a selection bounds handle |
|
||||
| `onRightPointBoundsHandle` | Right-pointed a selection bounds handle |
|
||||
| `onDragBoundsHandle` | Dragged a selection bounds handle |
|
||||
| `onReleaseBoundsHandle` | Stopped a selection bounds handle |
|
||||
| `onShapeClone` | Clicked on a shape's clone handle |
|
||||
| `onShapeChange` | A shape's component prompted a change |
|
||||
| `onShapeBlur` | A shape's component was prompted a blur |
|
||||
| `onRenderCountChange` | The number of rendered shapes changed |
|
||||
| `onBoundsChange` | The Renderer's screen bounding box of the component changed |
|
||||
| `onError` | The Renderer encountered an error |
|
||||
|
||||
The `@tldraw/core` library provides types for most of the event handlers:
|
||||
|
||||
| Type |
|
||||
| ---------------------------- |
|
||||
| `TLPinchEventHandler` |
|
||||
| `TLPointerEventHandler` |
|
||||
| `TLCanvasEventHandler` |
|
||||
| `TLBoundsEventHandler` |
|
||||
| `TLBoundsHandleEventHandler` |
|
||||
| `TLShapeChangeHandler` |
|
||||
| `TLShapeBlurHandler` |
|
||||
| `TLShapeCloneHandler` |
|
||||
|
||||
### `TLPage`
|
||||
|
||||
An object describing the current page. It contains:
|
||||
|
||||
| Property | Type | Description |
|
||||
| ----------------- | --------------------------- | --------------------------------------------------------------------------- |
|
||||
| `id` | `string` | A unique id for the page. |
|
||||
| `shapes` | [`TLShape{}`](#tlshape) | A table of shapes. |
|
||||
| `bindings` | [`TLBinding{}`](#tlbinding) | A table of bindings. |
|
||||
| `backgroundColor` | `string` | (optional) The page's background fill color. Will also overwrite the theme. |
|
||||
|
||||
### `TLPageState`
|
||||
|
||||
An object describing the current page. It contains:
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------- | ---------- | --------------------------------------------------- |
|
||||
| `id` | `string` | The corresponding page's id |
|
||||
| `selectedIds` | `string[]` | An array of selected shape ids |
|
||||
| `camera` | `object` | An object describing the camera state |
|
||||
| `camera.point` | `number[]` | The camera's `[x, y]` coordinates |
|
||||
| `camera.zoom` | `number` | The camera's zoom level |
|
||||
| `pointedId` | `string` | (optional) The currently pointed shape id |
|
||||
| `hoveredId` | `string` | (optional) The currently hovered shape id |
|
||||
| `editingId` | `string` | (optional) The currently editing shape id |
|
||||
| `bindingId` | `string` | (optional) The currently editing binding. |
|
||||
| `brush` | `TLBounds` | (optional) A `Bounds` for the current selection box |
|
||||
|
||||
### `TLShape`
|
||||
|
||||
An object that describes a shape on the page. The shapes in your document should extend this interface with other properties. See [Shape Type](#shape-type).
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------------- | ---------- | ------------------------------------------------------------------------------------- |
|
||||
| `id` | `string` | The shape's id. |
|
||||
| `type` | `string` | The type of the shape, corresponding to the `type` of a [`TLShapeUtil`](#tlshapeutil) |
|
||||
| `parentId` | `string` | The id of the shape's parent (either the current page or another shape) |
|
||||
| `childIndex` | `number` | the order of the shape among its parent's children |
|
||||
| `name` | `string` | the name of the shape |
|
||||
| `point` | `number[]` | the shape's current `[x, y]` coordinates on the page |
|
||||
| `rotation` | `number` | (optiona) The shape's current rotation in radians |
|
||||
| `children` | `string[]` | (optional) An array containing the ids of this shape's children |
|
||||
| `handles` | `{}` | (optional) A table of [`TLHandle`](#tlhandle) objects |
|
||||
| `isGhost` | `boolean` | (optional) True if the shape is "ghosted", e.g. while deleting |
|
||||
| `isLocked` | `boolean` | (optional) True if the shape is locked |
|
||||
| `isHidden` | `boolean` | (optional) True if the shape is hidden |
|
||||
| `isEditing` | `boolean` | (optional) True if the shape is currently editing |
|
||||
| `isGenerated` | `boolean` | optional) True if the shape is generated programatically |
|
||||
| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked |
|
||||
|
||||
### `TLHandle`
|
||||
|
||||
An object that describes a relationship between two shapes on the page.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | ---------- | --------------------------------------------- |
|
||||
| `id` | `string` | An id for the handle |
|
||||
| `index` | `number` | The handle's order within the shape's handles |
|
||||
| `point` | `number[]` | The handle's `[x, y]` coordinates |
|
||||
|
||||
When a shape with handles is the only selected shape, the `Renderer` will display its handles. You can respond to interactions with these handles using the `on
|
||||
|
||||
### `TLBinding`
|
||||
|
||||
An object that describes a relationship between two shapes on the page.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | -------- | -------------------------------------------- |
|
||||
| `id` | `string` | A unique id for the binding |
|
||||
| `fromId` | `string` | The id of the shape where the binding begins |
|
||||
| `toId` | `string` | The id of the shape where the binding begins |
|
||||
|
||||
### `TLSnapLine`
|
||||
|
||||
A snapline is an array of points (formatted as `[x, y]`) that represent a "snapping" line.
|
||||
|
||||
### `TLShapeUtil`
|
||||
|
||||
The `TLShapeUtil` is an abstract class that you can extend to create utilities for your custom shapes. See the [Creating Shapes](#creating-shapes) guide to learn more.
|
||||
|
||||
### `TLUser`
|
||||
|
||||
A `TLUser` is the presence information for a multiplayer user. The user's pointer location and selections will be shown on the canvas. If the `TLUser`'s id matches the `Renderer`'s `userId` prop, then the user's cursor and selections will not be shown.
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | ---------- | --------------------------------------- |
|
||||
| `id` | `string` | A unique id for the user |
|
||||
| `color` | `string` | The user's color, used for indicators |
|
||||
| `point` | `number[]` | The user's pointer location on the page |
|
||||
| `selectedIds[]` | `string[]` | The user's selected shape ids |
|
||||
|
||||
### `Utils`
|
||||
|
||||
A general purpose utility class. See source for more.
|
||||
|
||||
## Guide: Creating Shapes
|
||||
|
||||
The `Renderer` component has no built-in shapes. It's up to you to define every shape that you want to see on the canvas. While these shapes are highly reusable between projects, you'll need to define them using the API described below.
|
||||
|
||||
> For several example shapes, see the folder `/example/src/shapes/`.
|
||||
|
||||
### Shape Type
|
||||
|
||||
Your first task is to define an interface for the shape that extends `TLShape`. It must have a `type` property.
|
||||
|
||||
```ts
|
||||
// BoxShape.ts
|
||||
import type { TLShape } from '@tldraw/core'
|
||||
|
||||
export interface BoxShape extends TLShape {
|
||||
type: 'box'
|
||||
size: number[]
|
||||
}
|
||||
```
|
||||
|
||||
### Component
|
||||
|
||||
Next, use `TLShapeUtil.Component` to create a second component for your shape's `Component`. The `Renderer` will use this component to display the shape on the canvas.
|
||||
|
||||
```tsx
|
||||
// BoxComponent.ts
|
||||
|
||||
import * as React from 'react'
|
||||
import { shapeComponent, SVGContainer } from '@tldraw/core'
|
||||
import type { BoxShape } from './BoxShape'
|
||||
|
||||
export const BoxComponent = TLShapeUtil.Component<BoxShape, SVGSVGElement>(
|
||||
({ shape, events, meta }, ref) => {
|
||||
const color = meta.isDarkMode ? 'white' : 'black'
|
||||
|
||||
return (
|
||||
<SVGContainer ref={ref} {...events}>
|
||||
<rect
|
||||
width={shape.size[0]}
|
||||
height={shape.size[1]}
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
pointerEvents="all"
|
||||
/>
|
||||
</SVGContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Your component can return HTML elements or SVG elements. If your shape is returning only SVG elements, wrap it in an `SVGContainer`. If your shape returns HTML elements, wrap it in an `HTMLContainer`. Not that you must set `pointerEvents` manually on the shapes you wish to receive pointer events.
|
||||
|
||||
The component will receive the following props:
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------- | ---------- | ------------------------------------------------------------------ |
|
||||
| `shape` | `TLShape` | The shape from `page.shapes` that is being rendered |
|
||||
| `meta` | `{}` | The value provided to the `Renderer`'s `meta` prop |
|
||||
| `events` | `{}` | Several pointer events that should be set on the container element |
|
||||
| `isSelected` | `boolean` | The shape is selected (its `id` is in `pageState.selectedIds`) |
|
||||
| `isHovered` | `boolean` | The shape is hovered (its `id` is `pageState.hoveredId`) |
|
||||
| `isEditing` | `boolean` | The shape is being edited (its `id` is `pageState.editingId`) |
|
||||
| `isGhost` | `boolean` | The shape is ghosted or is the child of a ghosted shape. |
|
||||
| `isChildOfSelected` | `boolean` | The shape is the child of a selected shape. |
|
||||
| `onShapeChange` | `Function` | The callback provided to the `Renderer`'s `onShapeChange` prop |
|
||||
| `onShapeBlur` | `Function` | The callback provided to the `Renderer`'s `onShapeBlur` prop |
|
||||
|
||||
### Indicator
|
||||
|
||||
Next, use `TLShapeUtil.Indicator` to create a second component for your shape's `Indicator`. This component is shown when the shape is hovered or selected. Your `Indicator` must return SVG elements only.
|
||||
|
||||
```tsx
|
||||
// BoxIndicator.ts
|
||||
|
||||
export const BoxIndicator = TLShapeUtil.Indicator<BoxShape>(({ shape }) => {
|
||||
return (
|
||||
<rect
|
||||
fill="none"
|
||||
stroke="dodgerblue"
|
||||
strokeWidth={1}
|
||||
width={shape.size[0]}
|
||||
height={shape.size[1]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
The indicator component will receive the following props:
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------ | --------- | -------------------------------------------------------------------------------------- |
|
||||
| `shape` | `TLShape` | The shape from `page.shapes` that is being rendered |
|
||||
| `meta` | {} | The value provided to the `Renderer`'s `meta` prop |
|
||||
| `user` | `TLUser` | The user when shown in a multiplayer session |
|
||||
| `isSelected` | `boolean` | Whether the current shape is selected (true if its `id` is in `pageState.selectedIds`) |
|
||||
| `isHovered` | `boolean` | Whether the current shape is hovered (true if its `id` is `pageState.hoveredId`) |
|
||||
|
||||
### ShapeUtil
|
||||
|
||||
Next, create a "shape util" for your shape. This is a class that extends `TLShapeUtil`. The `Renderer` will use an instance of this class to answer questions about the shape: what it should look like, where it is on screen, whether it can rotate, etc.
|
||||
|
||||
```ts
|
||||
// BoxUtil.ts
|
||||
|
||||
import { Utils, TLBounds, TLShapeUtil } from '@tldraw/core'
|
||||
import { BoxComponent } from './BoxComponent'
|
||||
import { BoxIndicator } from './BoxIndicator'
|
||||
import type { BoxShape } from './BoxShape'
|
||||
|
||||
export class BoxUtil extends TLShapeUtil<BoxShape, SVGSVGElement> {
|
||||
Component = BoxComponent
|
||||
|
||||
Indicator = BoxIndicator
|
||||
|
||||
getBounds = (shape: BoxShape): TLBounds => {
|
||||
const [width, height] = shape.size
|
||||
|
||||
const bounds = {
|
||||
minX: 0,
|
||||
maxX: width,
|
||||
minY: 0,
|
||||
maxY: height,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
|
||||
return Utils.translateBounds(bounds, shape.point)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set the `Component` field to your component and the `Indicator` field to your indicator component. Then define the `getBounds` method. This method will receive a shape and should return a `TLBounds` object.
|
||||
|
||||
You may also set the following fields:
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------------ | --------- | ------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `showCloneHandles` | `boolean` | `false` | Whether to display clone handles when the shape is the only selected shape |
|
||||
| `hideBounds` | `boolean` | `false` | Whether to hide the bounds when the shape is the only selected shape |
|
||||
| `isStateful` | `boolean` | `false` | Whether the shape has its own React state. When true, the shape will not be unmounted when off-screen |
|
||||
|
||||
### ShapeUtils Object
|
||||
|
||||
Finally, create a mapping of your project's shape utils and the `type` properties of their corresponding shapes. Pass this object to the `Renderer`'s `shapeUtils` prop.
|
||||
|
||||
```tsx
|
||||
// App.tsx
|
||||
|
||||
const shapeUtils = {
|
||||
box: new BoxUtil(),
|
||||
circle: new CircleUtil(),
|
||||
text: new TextUtil(),
|
||||
}
|
||||
|
||||
export function App() {
|
||||
// ...
|
||||
|
||||
return <Renderer page={page} pageState={pageState} {...etc} shapeUtils={shapeUtils} />
|
||||
}
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
To start the development servers for the package and the advanced example:
|
||||
|
||||
- Run `yarn` to install dependencies.
|
||||
- Run `yarn start`.
|
||||
- Open `localhost:5420`.
|
||||
|
||||
You can also run:
|
||||
|
||||
- `start:advanced` to start development servers for the package and the advanced example.
|
||||
- `start:simple` to start development servers for the package and the simple example.
|
||||
- `test` to execute unit tests via [Jest](https://jestjs.io).
|
||||
- `docs` to build the docs via [ts-doc](https://typedoc.org/).
|
||||
- `build` to build the package.
|
||||
|
||||
## Example
|
||||
|
||||
See the `example` folder or this [CodeSandbox](https://codesandbox.io/s/laughing-elion-gp0kx) example.
|
||||
|
||||
## Community
|
||||
|
||||
### Support
|
||||
|
||||
Need help? Please [open an issue](https://github.com/tldraw/tldraw/issues/new) for support.
|
||||
|
||||
### Discussion
|
||||
|
||||
Want to connect with other devs? Visit the [Discord channel](https://discord.gg/SBBEVCA4PG).
|
||||
|
||||
### License
|
||||
|
||||
This project is licensed under MIT.
|
||||
|
||||
If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
|
||||
|
||||
## Author
|
||||
|
||||
- [@steveruizok](https://twitter.com/steveruizok)
|
BIN
packages/vec/card-repo.png
Normal file
BIN
packages/vec/card-repo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
35
packages/vec/package.json
Normal file
35
packages/vec/package.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"version": "1.1.4",
|
||||
"name": "@tldraw/vec",
|
||||
"description": "2D vector utilities for TLDraw and maybe you, too.",
|
||||
"author": "@steveruizok",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tldraw/tldraw.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"2d",
|
||||
"vector",
|
||||
"typescript",
|
||||
"javascript"
|
||||
],
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"scripts": {
|
||||
"start:packages": "yarn start",
|
||||
"start:core": "yarn start",
|
||||
"start": "node scripts/dev & yarn types:dev",
|
||||
"build:core": "yarn build",
|
||||
"build:packages": "yarn build",
|
||||
"build": "node scripts/build && yarn types:build",
|
||||
"types:dev": "tsc -w --p tsconfig.build.json",
|
||||
"types:build": "tsc -p tsconfig.build.json",
|
||||
"lint": "eslint src/ --ext .ts,.tsx",
|
||||
"clean": "rm -rf dist"
|
||||
}
|
||||
}
|
61
packages/vec/scripts/build.js
Normal file
61
packages/vec/scripts/build.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/* eslint-disable */
|
||||
const fs = require('fs')
|
||||
const esbuild = require('esbuild')
|
||||
const { gzip } = require('zlib')
|
||||
const pkg = require('../package.json')
|
||||
|
||||
async function main() {
|
||||
if (fs.existsSync('./dist')) {
|
||||
fs.rmSync('./dist', { recursive: true }, (e) => {
|
||||
if (e) {
|
||||
throw e
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
esbuild.buildSync({
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outdir: 'dist/cjs',
|
||||
minify: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
target: 'es6',
|
||||
tsconfig: './tsconfig.build.json',
|
||||
metafile: false,
|
||||
sourcemap: true,
|
||||
})
|
||||
|
||||
const esmResult = esbuild.buildSync({
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outdir: 'dist/esm',
|
||||
minify: false,
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
target: 'es6',
|
||||
tsconfig: './tsconfig.build.json',
|
||||
metafile: true,
|
||||
sourcemap: true,
|
||||
})
|
||||
|
||||
let esmSize = 0
|
||||
Object.values(esmResult.metafile.outputs).forEach((output) => {
|
||||
esmSize += output.bytes
|
||||
})
|
||||
|
||||
fs.readFile('./dist/esm/index.js', (_err, data) => {
|
||||
gzip(data, (_err, result) => {
|
||||
console.log(
|
||||
`✔ ${pkg.name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${(
|
||||
result.length / 1000
|
||||
).toFixed(2)}kb minified)`
|
||||
)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(`× ${pkg.name}: Build failed due to an error.`)
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
29
packages/vec/scripts/dev.js
Normal file
29
packages/vec/scripts/dev.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
/* eslint-disable */
|
||||
const esbuild = require('esbuild')
|
||||
const pkg = require('../package.json')
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await esbuild.build({
|
||||
entryPoints: ['src/index.tsx'],
|
||||
outfile: 'dist/index.js',
|
||||
bundle: true,
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
incremental: true,
|
||||
target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
},
|
||||
watch: {
|
||||
onRebuild(err) {
|
||||
err ? error('❌ Failed') : log('✅ Updated')
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
311
packages/vec/src/index.d.ts
vendored
Normal file
311
packages/vec/src/index.d.ts
vendored
Normal file
|
@ -0,0 +1,311 @@
|
|||
export declare class Vec {
|
||||
/**
|
||||
* Clamp a value into a range.
|
||||
* @param n
|
||||
* @param min
|
||||
*/
|
||||
static clamp(n: number, min: number): number;
|
||||
static clamp(n: number, min: number, max: number): number;
|
||||
/**
|
||||
* Clamp a value into a range.
|
||||
* @param n
|
||||
* @param min
|
||||
*/
|
||||
static clampV(A: number[], min: number): number[];
|
||||
static clampV(A: number[], min: number, max: number): number[];
|
||||
/**
|
||||
* Negate a vector.
|
||||
* @param A
|
||||
*/
|
||||
static neg: (A: number[]) => number[];
|
||||
/**
|
||||
* Add vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static add: (A: number[], B: number[]) => number[];
|
||||
/**
|
||||
* Add scalar to vector.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static addScalar: (A: number[], n: number) => number[];
|
||||
/**
|
||||
* Subtract vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static sub: (A: number[], B: number[]) => number[];
|
||||
/**
|
||||
* Subtract scalar from vector.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static subScalar: (A: number[], n: number) => number[];
|
||||
/**
|
||||
* Get the vector from vectors A to B.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static vec: (A: number[], B: number[]) => number[];
|
||||
/**
|
||||
* Vector multiplication by scalar
|
||||
* @param A
|
||||
* @param n
|
||||
*/
|
||||
static mul: (A: number[], n: number) => number[];
|
||||
/**
|
||||
* Multiple two vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static mulV: (A: number[], B: number[]) => number[];
|
||||
/**
|
||||
* Vector division by scalar.
|
||||
* @param A
|
||||
* @param n
|
||||
*/
|
||||
static div: (A: number[], n: number) => number[];
|
||||
/**
|
||||
* Vector division by vector.
|
||||
* @param A
|
||||
* @param n
|
||||
*/
|
||||
static divV: (A: number[], B: number[]) => number[];
|
||||
/**
|
||||
* Perpendicular rotation of a vector A
|
||||
* @param A
|
||||
*/
|
||||
static per: (A: number[]) => number[];
|
||||
/**
|
||||
* Dot product
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static dpr: (A: number[], B: number[]) => number;
|
||||
/**
|
||||
* Cross product (outer product) | A X B |
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static cpr: (A: number[], B: number[]) => number;
|
||||
/**
|
||||
* Cross (for point in polygon)
|
||||
*
|
||||
*/
|
||||
static cross(x: number[], y: number[], z: number[]): number;
|
||||
/**
|
||||
* Length of the vector squared
|
||||
* @param A
|
||||
*/
|
||||
static len2: (A: number[]) => number;
|
||||
/**
|
||||
* Length of the vector
|
||||
* @param A
|
||||
*/
|
||||
static len: (A: number[]) => number;
|
||||
/**
|
||||
* Project A over B
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static pry: (A: number[], B: number[]) => number;
|
||||
/**
|
||||
* Get normalized / unit vector.
|
||||
* @param A
|
||||
*/
|
||||
static uni: (A: number[]) => number[];
|
||||
/**
|
||||
* Get normalized / unit vector.
|
||||
* @param A
|
||||
*/
|
||||
static normalize: (A: number[]) => number[];
|
||||
/**
|
||||
* Get the tangent between two vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
* @returns
|
||||
*/
|
||||
static tangent: (A: number[], B: number[]) => number[];
|
||||
/**
|
||||
* Dist length from A to B squared.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static dist2: (A: number[], B: number[]) => number;
|
||||
/**
|
||||
* Dist length from A to B
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static dist: (A: number[], B: number[]) => number;
|
||||
/**
|
||||
* A faster, though less accurate method for testing distances. Maybe faster?
|
||||
* @param A
|
||||
* @param B
|
||||
* @returns
|
||||
*/
|
||||
static fastDist: (A: number[], B: number[]) => number[];
|
||||
/**
|
||||
* Angle between vector A and vector B in radians
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static ang: (A: number[], B: number[]) => number;
|
||||
/**
|
||||
* Angle between vector A and vector B in radians
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static angle: (A: number[], B: number[]) => number;
|
||||
/**
|
||||
* Mean between two vectors or mid vector between two vectors
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static med: (A: number[], B: number[]) => number[];
|
||||
/**
|
||||
* Vector rotation by r (radians)
|
||||
* @param A
|
||||
* @param r rotation in radians
|
||||
*/
|
||||
static rot: (A: number[], r?: number) => number[];
|
||||
/**
|
||||
* Rotate a vector around another vector by r (radians)
|
||||
* @param A vector
|
||||
* @param C center
|
||||
* @param r rotation in radians
|
||||
*/
|
||||
static rotWith: (A: number[], C: number[], r?: number) => number[];
|
||||
/**
|
||||
* Check of two vectors are identical.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static isEqual: (A: number[], B: number[]) => boolean;
|
||||
/**
|
||||
* Interpolate vector A to B with a scalar t
|
||||
* @param A
|
||||
* @param B
|
||||
* @param t scalar
|
||||
*/
|
||||
static lrp: (A: number[], B: number[], t: number) => number[];
|
||||
/**
|
||||
* Interpolate from A to B when curVAL goes fromVAL: number[] => to
|
||||
* @param A
|
||||
* @param B
|
||||
* @param from Starting value
|
||||
* @param to Ending value
|
||||
* @param s Strength
|
||||
*/
|
||||
static int: (A: number[], B: number[], from: number, to: number, s?: number) => number[];
|
||||
/**
|
||||
* Get the angle between the three vectors A, B, and C.
|
||||
* @param p1
|
||||
* @param pc
|
||||
* @param p2
|
||||
*/
|
||||
static ang3: (p1: number[], pc: number[], p2: number[]) => number;
|
||||
/**
|
||||
* Absolute value of a vector.
|
||||
* @param A
|
||||
* @returns
|
||||
*/
|
||||
static abs: (A: number[]) => number[];
|
||||
static rescale: (a: number[], n: number) => number[];
|
||||
/**
|
||||
* Get whether p1 is left of p2, relative to pc.
|
||||
* @param p1
|
||||
* @param pc
|
||||
* @param p2
|
||||
*/
|
||||
static isLeft: (p1: number[], pc: number[], p2: number[]) => number;
|
||||
/**
|
||||
* Get whether p1 is left of p2, relative to pc.
|
||||
* @param p1
|
||||
* @param pc
|
||||
* @param p2
|
||||
*/
|
||||
static clockwise: (p1: number[], pc: number[], p2: number[]) => boolean;
|
||||
/**
|
||||
* Round a vector to the a given precision.
|
||||
* @param a
|
||||
* @param d
|
||||
*/
|
||||
static toFixed: (a: number[], d?: number) => number[];
|
||||
/**
|
||||
* Snap vector to nearest step.
|
||||
* @param A
|
||||
* @param step
|
||||
* @example
|
||||
* ```ts
|
||||
* Vec.snap([10.5, 28], 10) // [10, 30]
|
||||
* ```
|
||||
*/
|
||||
static snap(a: number[], step?: number): number[];
|
||||
/**
|
||||
* Get the nearest point on a line with a known unit vector that passes through point A
|
||||
* @param A Any point on the line
|
||||
* @param u The unit vector for the line.
|
||||
* @param P A point not on the line to test.
|
||||
* @returns
|
||||
*/
|
||||
static nearestPointOnLineThroughPoint: (A: number[], u: number[], P: number[]) => number[];
|
||||
/**
|
||||
* Distance between a point and a line with a known unit vector that passes through a point.
|
||||
* @param A Any point on the line
|
||||
* @param u The unit vector for the line.
|
||||
* @param P A point not on the line to test.
|
||||
* @returns
|
||||
*/
|
||||
static distanceToLineThroughPoint: (A: number[], u: number[], P: number[]) => number;
|
||||
/**
|
||||
* Get the nearest point on a line segment between A and B
|
||||
* @param A The start of the line segment
|
||||
* @param B The end of the line segment
|
||||
* @param P The off-line point
|
||||
* @param clamp Whether to clamp the point between A and B.
|
||||
* @returns
|
||||
*/
|
||||
static nearestPointOnLineSegment: (A: number[], B: number[], P: number[], clamp?: boolean) => number[];
|
||||
/**
|
||||
* Distance between a point and the nearest point on a line segment between A and B
|
||||
* @param A The start of the line segment
|
||||
* @param B The end of the line segment
|
||||
* @param P The off-line point
|
||||
* @param clamp Whether to clamp the point between A and B.
|
||||
* @returns
|
||||
*/
|
||||
static distanceToLineSegment: (A: number[], B: number[], P: number[], clamp?: boolean) => number;
|
||||
/**
|
||||
* Push a point A towards point B by a given distance.
|
||||
* @param A
|
||||
* @param B
|
||||
* @param d
|
||||
* @returns
|
||||
*/
|
||||
static nudge: (A: number[], B: number[], d: number) => number[];
|
||||
/**
|
||||
* Push a point in a given angle by a given distance.
|
||||
* @param A
|
||||
* @param B
|
||||
* @param d
|
||||
*/
|
||||
static nudgeAtAngle: (A: number[], a: number, d: number) => number[];
|
||||
/**
|
||||
* Round a vector to a precision length.
|
||||
* @param a
|
||||
* @param n
|
||||
*/
|
||||
static toPrecision: (a: number[], n?: number) => number[];
|
||||
/**
|
||||
* Get an array of points (with simulated pressure) between two points.
|
||||
* @param A The first point.
|
||||
* @param B The second point.
|
||||
* @param steps The number of points to return.
|
||||
* @param ease An easing function to apply to the simulated pressure.
|
||||
*/
|
||||
static pointsBetween: (A: number[], B: number[], steps?: number) => number[][];
|
||||
}
|
||||
export default Vec;
|
||||
//# sourceMappingURL=index.d.ts.map
|
1
packages/vec/src/index.d.ts.map
Normal file
1
packages/vec/src/index.d.ts.map
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,qBAAa,GAAG;IACd;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;IAC5C,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;IAKzD;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE;IACjD,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE;IAK9D;;;OAGG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAG,MAAM,EAAE,CAEnC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEhD;IAED;;;;OAIG;IACH,MAAM,CAAC,SAAS,MAAO,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAEpD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEhD;IAED;;;;OAIG;IACH,MAAM,CAAC,SAAS,MAAO,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAEpD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAGhD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAE9C;IAED;;;;OAIG;IACH,MAAM,CAAC,IAAI,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEjD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAE9C;IAED;;;;OAIG;IACH,MAAM,CAAC,IAAI,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEjD;IAED;;;OAGG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAG,MAAM,EAAE,CAEnC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAE9C;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAE9C;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM;IAI3D;;;OAGG;IACH,MAAM,CAAC,IAAI,MAAO,MAAM,EAAE,KAAG,MAAM,CAElC;IAED;;;OAGG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAG,MAAM,CAEjC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAE9C;IAED;;;OAGG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAG,MAAM,EAAE,CAEnC;IAED;;;OAGG;IACH,MAAM,CAAC,SAAS,MAAO,MAAM,EAAE,KAAG,MAAM,EAAE,CAEzC;IAED;;;;;OAKG;IACH,MAAM,CAAC,OAAO,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEpD;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAEhD;IAED;;;;OAIG;IACH,MAAM,CAAC,IAAI,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAE/C;IAED;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAMrD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAE9C;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAEhD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAEhD;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,iBAAU,MAAM,EAAE,CAE1C;IAED;;;;;OAKG;IACH,MAAM,CAAC,OAAO,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,iBAAU,MAAM,EAAE,CAa3D;IAED;;;;OAIG;IACH,MAAM,CAAC,OAAO,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,OAAO,CAEnD;IAED;;;;;OAKG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAE3D;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,QAAQ,MAAM,MAAM,MAAM,iBAAU,MAAM,EAAE,CAGjF;IAED;;;;;OAKG;IACH,MAAM,CAAC,IAAI,OAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,MAAM,EAAE,KAAG,MAAM,CAK/D;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAG,MAAO,MAAM,EAAE,KAAG,MAAM,EAAE,CAEnC;IAED,MAAM,CAAC,OAAO,MAAO,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAGlD;IAED;;;;;OAKG;IACH,MAAM,CAAC,MAAM,OAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,MAAM,EAAE,KAAG,MAAM,CAKjE;IAED;;;;;OAKG;IACH,MAAM,CAAC,SAAS,OAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,MAAM,EAAE,KAAG,OAAO,CAErE;IAED;;;;OAIG;IACH,MAAM,CAAC,OAAO,MAAO,MAAM,EAAE,iBAAU,MAAM,EAAE,CAE9C;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,SAAI;IAIjC;;;;;;OAMG;IACH,MAAM,CAAC,8BAA8B,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,EAAE,CAExF;IAED;;;;;;OAMG;IACH,MAAM,CAAC,0BAA0B,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,KAAG,MAAM,CAElF;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,yBAAyB,MAC3B,MAAM,EAAE,KACR,MAAM,EAAE,KACR,MAAM,EAAE,sBAEV,MAAM,EAAE,CAYV;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,qBAAqB,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,sBAAiB,MAAM,CAE3F;IAED;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,CAE7D;IAED;;;;;OAKG;IACH,MAAM,CAAC,YAAY,MAAO,MAAM,EAAE,KAAK,MAAM,KAAK,MAAM,KAAG,MAAM,EAAE,CAElE;IAED;;;;OAIG;IACH,MAAM,CAAC,WAAW,MAAO,MAAM,EAAE,iBAAU,MAAM,EAAE,CAElD;IAED;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,MAAO,MAAM,EAAE,KAAK,MAAM,EAAE,qBAAc,MAAM,EAAE,EAAE,CAMvE;CACF;AAED,eAAe,GAAG,CAAA"}
|
499
packages/vec/src/index.ts
Normal file
499
packages/vec/src/index.ts
Normal file
|
@ -0,0 +1,499 @@
|
|||
export class Vec {
|
||||
/**
|
||||
* Clamp a value into a range.
|
||||
* @param n
|
||||
* @param min
|
||||
*/
|
||||
static clamp(n: number, min: number): number
|
||||
static clamp(n: number, min: number, max: number): number
|
||||
static clamp(n: number, min: number, max?: number): number {
|
||||
return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a value into a range.
|
||||
* @param n
|
||||
* @param min
|
||||
*/
|
||||
static clampV(A: number[], min: number): number[]
|
||||
static clampV(A: number[], min: number, max: number): number[]
|
||||
static clampV(A: number[], min: number, max?: number): number[] {
|
||||
return A.map((n) => (max ? Vec.clamp(n, min, max) : Vec.clamp(n, min)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Negate a vector.
|
||||
* @param A
|
||||
*/
|
||||
static neg = (A: number[]): number[] => {
|
||||
return [-A[0], -A[1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Add vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static add = (A: number[], B: number[]): number[] => {
|
||||
return [A[0] + B[0], A[1] + B[1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Add scalar to vector.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static addScalar = (A: number[], n: number): number[] => {
|
||||
return [A[0] + n, A[1] + n]
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static sub = (A: number[], B: number[]): number[] => {
|
||||
return [A[0] - B[0], A[1] - B[1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract scalar from vector.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static subScalar = (A: number[], n: number): number[] => {
|
||||
return [A[0] - n, A[1] - n]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the vector from vectors A to B.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static vec = (A: number[], B: number[]): number[] => {
|
||||
// A, B as vectors get the vector from A to B
|
||||
return [B[0] - A[0], B[1] - A[1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector multiplication by scalar
|
||||
* @param A
|
||||
* @param n
|
||||
*/
|
||||
static mul = (A: number[], n: number): number[] => {
|
||||
return [A[0] * n, A[1] * n]
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple two vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static mulV = (A: number[], B: number[]): number[] => {
|
||||
return [A[0] * B[0], A[1] * B[1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector division by scalar.
|
||||
* @param A
|
||||
* @param n
|
||||
*/
|
||||
static div = (A: number[], n: number): number[] => {
|
||||
return [A[0] / n, A[1] / n]
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector division by vector.
|
||||
* @param A
|
||||
* @param n
|
||||
*/
|
||||
static divV = (A: number[], B: number[]): number[] => {
|
||||
return [A[0] / B[0], A[1] / B[1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Perpendicular rotation of a vector A
|
||||
* @param A
|
||||
*/
|
||||
static per = (A: number[]): number[] => {
|
||||
return [A[1], -A[0]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Dot product
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static dpr = (A: number[], B: number[]): number => {
|
||||
return A[0] * B[0] + A[1] * B[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross product (outer product) | A X B |
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static cpr = (A: number[], B: number[]): number => {
|
||||
return A[0] * B[1] - B[0] * A[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross (for point in polygon)
|
||||
*
|
||||
*/
|
||||
static cross(x: number[], y: number[], z: number[]): number {
|
||||
return (y[0] - x[0]) * (z[1] - x[1]) - (z[0] - x[0]) * (y[1] - x[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the vector squared
|
||||
* @param A
|
||||
*/
|
||||
static len2 = (A: number[]): number => {
|
||||
return A[0] * A[0] + A[1] * A[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the vector
|
||||
* @param A
|
||||
*/
|
||||
static len = (A: number[]): number => {
|
||||
return Math.hypot(A[0], A[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Project A over B
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static pry = (A: number[], B: number[]): number => {
|
||||
return Vec.dpr(A, B) / Vec.len(B)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get normalized / unit vector.
|
||||
* @param A
|
||||
*/
|
||||
static uni = (A: number[]): number[] => {
|
||||
return Vec.div(A, Vec.len(A))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get normalized / unit vector.
|
||||
* @param A
|
||||
*/
|
||||
static normalize = (A: number[]): number[] => {
|
||||
return Vec.uni(A)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tangent between two vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
* @returns
|
||||
*/
|
||||
static tangent = (A: number[], B: number[]): number[] => {
|
||||
return Vec.uni(Vec.sub(A, B))
|
||||
}
|
||||
|
||||
/**
|
||||
* Dist length from A to B squared.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static dist2 = (A: number[], B: number[]): number => {
|
||||
return Vec.len2(Vec.sub(A, B))
|
||||
}
|
||||
|
||||
/**
|
||||
* Dist length from A to B
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static dist = (A: number[], B: number[]): number => {
|
||||
return Math.hypot(A[1] - B[1], A[0] - B[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* A faster, though less accurate method for testing distances. Maybe faster?
|
||||
* @param A
|
||||
* @param B
|
||||
* @returns
|
||||
*/
|
||||
static fastDist = (A: number[], B: number[]): number[] => {
|
||||
const V = [B[0] - A[0], B[1] - A[1]]
|
||||
const aV = [Math.abs(V[0]), Math.abs(V[1])]
|
||||
let r = 1 / Math.max(aV[0], aV[1])
|
||||
r = r * (1.29289 - (aV[0] + aV[1]) * r * 0.29289)
|
||||
return [V[0] * r, V[1] * r]
|
||||
}
|
||||
|
||||
/**
|
||||
* Angle between vector A and vector B in radians
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static ang = (A: number[], B: number[]): number => {
|
||||
return Math.atan2(Vec.cpr(A, B), Vec.dpr(A, B))
|
||||
}
|
||||
|
||||
/**
|
||||
* Angle between vector A and vector B in radians
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static angle = (A: number[], B: number[]): number => {
|
||||
return Math.atan2(B[1] - A[1], B[0] - A[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* Mean between two vectors or mid vector between two vectors
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static med = (A: number[], B: number[]): number[] => {
|
||||
return Vec.mul(Vec.add(A, B), 0.5)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector rotation by r (radians)
|
||||
* @param A
|
||||
* @param r rotation in radians
|
||||
*/
|
||||
static rot = (A: number[], r = 0): number[] => {
|
||||
return [A[0] * Math.cos(r) - A[1] * Math.sin(r), A[0] * Math.sin(r) + A[1] * Math.cos(r)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate a vector around another vector by r (radians)
|
||||
* @param A vector
|
||||
* @param C center
|
||||
* @param r rotation in radians
|
||||
*/
|
||||
static rotWith = (A: number[], C: number[], r = 0): number[] => {
|
||||
if (r === 0) return A
|
||||
|
||||
const s = Math.sin(r)
|
||||
const c = Math.cos(r)
|
||||
|
||||
const px = A[0] - C[0]
|
||||
const py = A[1] - C[1]
|
||||
|
||||
const nx = px * c - py * s
|
||||
const ny = px * s + py * c
|
||||
|
||||
return [nx + C[0], ny + C[1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check of two vectors are identical.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static isEqual = (A: number[], B: number[]): boolean => {
|
||||
return A[0] === B[0] && A[1] === B[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate vector A to B with a scalar t
|
||||
* @param A
|
||||
* @param B
|
||||
* @param t scalar
|
||||
*/
|
||||
static lrp = (A: number[], B: number[], t: number): number[] => {
|
||||
return Vec.add(A, Vec.mul(Vec.sub(B, A), t))
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate from A to B when curVAL goes fromVAL: number[] => to
|
||||
* @param A
|
||||
* @param B
|
||||
* @param from Starting value
|
||||
* @param to Ending value
|
||||
* @param s Strength
|
||||
*/
|
||||
static int = (A: number[], B: number[], from: number, to: number, s = 1): number[] => {
|
||||
const t = (Vec.clamp(from, to) - from) / (to - from)
|
||||
return Vec.add(Vec.mul(A, 1 - t), Vec.mul(B, s))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the angle between the three vectors A, B, and C.
|
||||
* @param p1
|
||||
* @param pc
|
||||
* @param p2
|
||||
*/
|
||||
static ang3 = (p1: number[], pc: number[], p2: number[]): number => {
|
||||
// this,
|
||||
const v1 = Vec.vec(pc, p1)
|
||||
const v2 = Vec.vec(pc, p2)
|
||||
return Vec.ang(v1, v2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute value of a vector.
|
||||
* @param A
|
||||
* @returns
|
||||
*/
|
||||
static abs = (A: number[]): number[] => {
|
||||
return [Math.abs(A[0]), Math.abs(A[1])]
|
||||
}
|
||||
|
||||
static rescale = (a: number[], n: number): number[] => {
|
||||
const l = Vec.len(a)
|
||||
return [(n * a[0]) / l, (n * a[1]) / l]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether p1 is left of p2, relative to pc.
|
||||
* @param p1
|
||||
* @param pc
|
||||
* @param p2
|
||||
*/
|
||||
static isLeft = (p1: number[], pc: number[], p2: number[]): number => {
|
||||
// isLeft: >0 for counterclockwise
|
||||
// =0 for none (degenerate)
|
||||
// <0 for clockwise
|
||||
return (pc[0] - p1[0]) * (p2[1] - p1[1]) - (p2[0] - p1[0]) * (pc[1] - p1[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether p1 is left of p2, relative to pc.
|
||||
* @param p1
|
||||
* @param pc
|
||||
* @param p2
|
||||
*/
|
||||
static clockwise = (p1: number[], pc: number[], p2: number[]): boolean => {
|
||||
return Vec.isLeft(p1, pc, p2) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Round a vector to the a given precision.
|
||||
* @param a
|
||||
* @param d
|
||||
*/
|
||||
static toFixed = (a: number[], d = 2): number[] => {
|
||||
return a.map((v) => +v.toFixed(d))
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap vector to nearest step.
|
||||
* @param A
|
||||
* @param step
|
||||
* @example
|
||||
* ```ts
|
||||
* Vec.snap([10.5, 28], 10) // [10, 30]
|
||||
* ```
|
||||
*/
|
||||
static snap(a: number[], step = 1) {
|
||||
return [Math.round(a[0] / step) * step, Math.round(a[1] / step) * step]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nearest point on a line with a known unit vector that passes through point A
|
||||
* @param A Any point on the line
|
||||
* @param u The unit vector for the line.
|
||||
* @param P A point not on the line to test.
|
||||
* @returns
|
||||
*/
|
||||
static nearestPointOnLineThroughPoint = (A: number[], u: number[], P: number[]): number[] => {
|
||||
return Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Distance between a point and a line with a known unit vector that passes through a point.
|
||||
* @param A Any point on the line
|
||||
* @param u The unit vector for the line.
|
||||
* @param P A point not on the line to test.
|
||||
* @returns
|
||||
*/
|
||||
static distanceToLineThroughPoint = (A: number[], u: number[], P: number[]): number => {
|
||||
return Vec.dist(P, Vec.nearestPointOnLineThroughPoint(A, u, P))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nearest point on a line segment between A and B
|
||||
* @param A The start of the line segment
|
||||
* @param B The end of the line segment
|
||||
* @param P The off-line point
|
||||
* @param clamp Whether to clamp the point between A and B.
|
||||
* @returns
|
||||
*/
|
||||
static nearestPointOnLineSegment = (
|
||||
A: number[],
|
||||
B: number[],
|
||||
P: number[],
|
||||
clamp = true
|
||||
): number[] => {
|
||||
const u = Vec.uni(Vec.sub(B, A))
|
||||
const C = Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u)))
|
||||
|
||||
if (clamp) {
|
||||
if (C[0] < Math.min(A[0], B[0])) return A[0] < B[0] ? A : B
|
||||
if (C[0] > Math.max(A[0], B[0])) return A[0] > B[0] ? A : B
|
||||
if (C[1] < Math.min(A[1], B[1])) return A[1] < B[1] ? A : B
|
||||
if (C[1] > Math.max(A[1], B[1])) return A[1] > B[1] ? A : B
|
||||
}
|
||||
|
||||
return C
|
||||
}
|
||||
|
||||
/**
|
||||
* Distance between a point and the nearest point on a line segment between A and B
|
||||
* @param A The start of the line segment
|
||||
* @param B The end of the line segment
|
||||
* @param P The off-line point
|
||||
* @param clamp Whether to clamp the point between A and B.
|
||||
* @returns
|
||||
*/
|
||||
static distanceToLineSegment = (A: number[], B: number[], P: number[], clamp = true): number => {
|
||||
return Vec.dist(P, Vec.nearestPointOnLineSegment(A, B, P, clamp))
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a point A towards point B by a given distance.
|
||||
* @param A
|
||||
* @param B
|
||||
* @param d
|
||||
* @returns
|
||||
*/
|
||||
static nudge = (A: number[], B: number[], d: number): number[] => {
|
||||
return Vec.add(A, Vec.mul(Vec.uni(Vec.sub(B, A)), d))
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a point in a given angle by a given distance.
|
||||
* @param A
|
||||
* @param B
|
||||
* @param d
|
||||
*/
|
||||
static nudgeAtAngle = (A: number[], a: number, d: number): number[] => {
|
||||
return [Math.cos(a) * d + A[0], Math.sin(a) * d + A[1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Round a vector to a precision length.
|
||||
* @param a
|
||||
* @param n
|
||||
*/
|
||||
static toPrecision = (a: number[], n = 4): number[] => {
|
||||
return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of points (with simulated pressure) between two points.
|
||||
* @param A The first point.
|
||||
* @param B The second point.
|
||||
* @param steps The number of points to return.
|
||||
* @param ease An easing function to apply to the simulated pressure.
|
||||
*/
|
||||
static pointsBetween = (A: number[], B: number[], steps = 6): number[][] => {
|
||||
return Array.from(Array(steps)).map((_, i) => {
|
||||
const t = i / (steps - 1)
|
||||
const k = Math.min(1, 0.5 + Math.abs(0.5 - t))
|
||||
return [...Vec.lrp(A, B, t), k]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Vec
|
20
packages/vec/tsconfig.build.json
Normal file
20
packages/vec/tsconfig.build.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.test.tsx",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.spec.ts",
|
||||
"src/test",
|
||||
"dist",
|
||||
"docs"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"incremental": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue