Removes core (off to its own repo)

pull/210/head
Steve Ruiz 2021-10-27 18:52:02 +01:00
rodzic 0e9e45734a
commit 599e6032a9
118 zmienionych plików z 65 dodań i 7023 usunięć

Wyświetl plik

@ -10,7 +10,6 @@
"license": "MIT",
"version": "0.0.74",
"workspaces": [
"packages/core",
"packages/tldraw",
"packages/dev",
"packages/vec",
@ -25,7 +24,7 @@
"start": "lerna run start:pre && lerna run start --stream --parallel",
"start:www": "yarn build:packages && lerna run start --parallel & cd packages/www && yarn dev",
"build": "yarn build:packages && cd packages/www && yarn build",
"build:packages": "cd packages/vec && yarn build && cd ../intersect && yarn build && cd ../core && yarn build && cd ../tldraw && yarn build",
"build:packages": "cd packages/vec && yarn build && cd ../intersect && yarn build && cd ../tldraw && yarn build",
"publish:patch": "yarn build:packages && lerna publish patch",
"docs": "lerna run docs",
"docs:watch": "lerna run docs:watch"
@ -36,8 +35,8 @@
"@testing-library/react": "^12.0.0",
"@types/jest": "^27.0.1",
"@types/node": "^15.0.1",
"@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9",
"@types/react": "^17.0.33",
"@types/react-dom": "^17.0.10",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"eslint": "^7.32.0",
@ -77,17 +76,15 @@
"modulePathIgnorePatterns": [
"<rootDir>/packages/vec/dist/",
"<rootDir>/packages/intersect/dist/",
"<rootDir>/packages/core/dist/",
"<rootDir>/packages/tldraw/dist/",
"<rootDir>/packages/tldraw/test-utils/"
],
"moduleNameMapper": {
"@tldraw/vec": "<rootDir>/packages/vec/src",
"@tldraw/intersect": "<rootDir>/packages/intersect/src",
"@tldraw/core": "<rootDir>/packages/core/src",
"@tldraw/tldraw": "<rootDir>/packages/tldraw/src",
"\\~(.*)": "<rootDir>/packages/tldraw/src/$1",
"\\+(.*)": "<rootDir>/packages/core/src/$1"
}
}
}
}

Wyświetl plik

@ -1,21 +0,0 @@
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.

Wyświetl plik

@ -1,263 +0,0 @@
# @tldraw/core
> `This library is not yet released and these docs are partially out of date!`
This package contains the core of the [tldraw](https://tldraw.com) library. It includes:
- [`Renderer`](#renderer) - a React component
- [`TLShapeUtility`](#tlshapeutility) - an abstract class for custom shape utilities
- the library's TypeScript [`types`](#types)
- several utility classes:
- [`Utils`](#utils)
- [`Vec`](#vec)
- [`Svg`](#svg)
- [`Intersect`](#intersect)
## Installation
```bash
yarn add @tldraw/core
```
## Usage
Import the `Renderer` React component and pass it the required props.
- [Example](#example)
- [Guide: Create a Custom Shape](#create-a-custom-shape)
## Documentation
### `Renderer`
| 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. |
| `theme` | `object` | (optional) an object with overrides for the Renderer's default colors. |
The theme object accepts valid CSS colors for the following properties:
| Property | Description |
| -------------- | --------------------------------------------------------------- |
| `foreground` | (optional) The primary (usually "text") color |
| `background` | (optional) The default page's background color |
| `brushFill` | (optional) The fill color of the brush selection box |
| `brushStroke` | (optional) The stroke color of the brush selection box |
| `selectFill` | (optional) The fill color of the selection bounds |
| `selectStroke` | (optional) The stroke color of the selection bounds and handles |
> Tip: If providing an object for the `theme` prop, either define the object outside of the parent component or memoize it with `React.useMemo`.
The renderer also accepts many (optional) event callbacks.
| Prop | Description |
| --------------------------- | --------------------------------------------------------- |
| `onPan` | The user panned with the mouse wheel |
| `onZoom` | The user zoomed with the mouse wheel |
| `onPinch` | The user moved their pointers during a pinch |
| `onPinchEnd` | The user stopped a two-pointer pinch |
| `onPinchStart` | The user began a two-pointer pinch |
| `onPointerMove` | The user moved their pointer |
| `onPointerUp` | The user ended a point |
| `onPointShape` | The user pointed a shape |
| `onDoubleClickShape` | The user double-pointed a shape |
| `onRightPointShape` | The user right-pointed a shape |
| `onMoveOverShape` | The user moved their pointer a shape |
| `onHoverShape` | The user moved their pointer onto a shape |
| `onUnhoverShape` | The user moved their pointer off of a shape |
| `onPointHandle` | The user pointed a shape handle |
| `onDoubleClickHandle` | The user double-pointed a shape handle |
| `onRightPointHandle`- | he user right-pointed a shape handle |
| `onMoveOverHandle` | The user moved their pointer over a shape handle |
| `onHoverHandle` | The user moved their pointer onto a shape handle |
| `onUnhoverHandle` | The user moved their pointer off of a shape handle |
| `onPointCanvas` | The user pointed the canvas |
| `onDoubleClickCanvas` | The user double-pointed the canvas |
| `onRightPointCanvas` | The user right-pointed the canvas |
| `onPointBounds` | The user pointed the selection bounds |
| `onDoubleClickBounds` | The user double-pointed the selection bounds |
| `onRightPointBounds` | The user right-pointed the selection bounds |
| `onPointBoundsHandle` | The user pointed a selection bounds edge or corner |
| `onDoubleClickBoundsHandle` | The user double-pointed a selection bounds edge or corner |
| `onBlurEditingShape` | The user blurred an editing (text) shape |
| `onError` | The renderer encountered an error |
> Tip: If providing callbacks, either define the functions outside of the parent component or memoize them first with `React.useMemo`.
### `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 |
| `currentParentId` | `string` | (optional) The current parent id for selection |
| `brush` | `TLBounds` | (optional) A `Bounds` for the current selection box |
| `pointedId` | `string` | (optional) The currently pointed shape id |
| `hoveredId` | `string` | (optional) The currently hovered shape id |
| `editingId` | `string` | (optional) The currently editing shape id |
| `editingBindingId` | `string` | (optional) The currently editing binding id |
### `TLShape`
An object that describes a shape on the page. The shapes in your document should extend this interface with other properties. See [Guide: Create a Custom Shape](#create-a-custom-shape).
| 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` | [`TLHandle{}`](#tlhandle) | (optional) A table of `TLHandle` objects |
| `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. |
### `TLBinding`
An object that describes a relationship between two shapes on the page.
| Property | Type | Description |
| -------- | -------- | --------------------------------------------- |
| `id` | `string` | A unique id for the binding. |
| `type` | `string` | The binding's type. |
| `fromId` | `string` | The id of the shape where the binding begins. |
| `toId` | `string` | The id of the shape where the binding begins. |
### `TLShapeUtil`
The `TLShapeUtil` is an abstract class that you can extend to create utilities for your custom shapes. See [Guide: Create a Custom Shape](#create-a-custom-shape).
## `inputs`
A class instance that stores the current pointer position and pressed keys.
### `Utils`
A general purpose utility class.
### `Vec`
A utility class for vector math and related methods.
### `Svg`
A utility class for creating SVG path data through imperative commands.
### `Intersect`
A utility class for finding intersections between various geometric shapes.
## Guides
### Create a Custom Shape
...
### Example
```tsx
import * as React from "react"
import { Renderer, TLShape, TLShapeUtil, Vec } from '@tldraw/core'
interface RectangleShape extends TLShape {
type: "rectangle",
size: number[]
}
class Rectangle extends TLShapeUtil<RectangleShape> {
// See the "Create a Custom Shape" guide
}
const myShapes = { rectangle: new Rectangle() }
function App() {
const [page, setPage] = React.useState({
id: "page1"
shapes: {
"rect1": {
id: 'rect1',
parentId: 'page1',
name: 'Rectangle',
childIndex: 0,
type: 'rectangle',
point: [0, 0],
rotation: 0,
size: [100, 100],
}
},
bindings: {}
})
const [pageState, setPageState] = React.useState({
id: "page1",
selectedIds: [],
camera: {
point: [0,0],
zoom: 1
}
})
const handlePan = React.useCallback((delta: number[]) => {
setPageState(pageState => {
...pageState,
camera: {
zoom,
point: Vec.sub(pageState.point, Vec.div(delta, pageState.zoom)),
},
})
}, [])
return (<Renderer
shapes={myShapes}
page={page}
pageState={pageState}
onPan={handlePan}
/>)
}
```
## Development
Run `yarn` to install dependencies.
Run `yarn start` to begin the monorepo's development server (`@tldraw/site`).
Run `nx test core` to execute unit tests via [Jest](https://jestjs.io).
## Contribution
To contribute, visit the [Discord channel](https://discord.gg/s4FXZ6fppJ).

Wyświetl plik

@ -1,65 +0,0 @@
{
"name": "@tldraw/core",
"version": "0.0.130",
"private": false,
"description": "A tiny little drawing app (core)",
"author": "@steveruizok",
"repository": {
"type": "git",
"url": "git+https://github.com/tldraw/tldraw.git",
"directory": "packages/core"
},
"license": "MIT",
"keywords": [],
"files": [
"dist/**/*"
],
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"typings": "./dist/types/index.d.ts",
"scripts": {
"start:pre": "node scripts/pre-dev && yarn types:pre",
"start": "node scripts/dev & yarn types:dev",
"build": "node scripts/build && yarn types:build",
"types:pre": "tsc",
"types:dev": "tsc -w",
"types:build": "tsc -p tsconfig.build.json && tsconfig-replace-paths -p tsconfig.build.json",
"lint": "eslint src/ --ext .ts,.tsx",
"clean": "rm -rf dist",
"ts-node": "ts-node",
"docs": "typedoc",
"docs:watch": "typedoc --watch"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/preset-env": "^7.15.4",
"@herbcaudill/tscpaths": "^0.0.17",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.10",
"@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9",
"@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^4.30.0",
"esbuild": "^0.13.8",
"eslint": "^7.32.0",
"lerna": "^4.0.0",
"react": ">=16.8",
"react-dom": "^16.8 || ^17.0",
"ts-node": "^10.2.1",
"tsconfig-replace-paths": "^0.0.5",
"tslib": "^2.3.1",
"typedoc": "^0.22.3",
"typescript": "^4.4.2"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": "^16.8 || ^17.0"
},
"dependencies": {
"@tldraw/intersect": "^0.0.130",
"@tldraw/vec": "^0.0.130",
"@use-gesture/react": "^10.0.2"
},
"gitHead": "083b36e167b6911927a6b58cbbb830b11b33f00a"
}

Wyświetl plik

@ -1,66 +0,0 @@
/* eslint-disable */
const fs = require('fs')
const esbuild = require('esbuild')
const { gzip } = require('zlib')
const name = process.env.npm_package_name || ''
async function main() {
if (fs.existsSync('./dist')) {
fs.rmSync('./dist', { recursive: true }, (e) => {
if (e) {
throw e
}
})
}
try {
esbuild.buildSync({
entryPoints: ['./src/index.ts'],
outdir: 'dist/cjs',
minify: true,
bundle: true,
format: 'cjs',
target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.build.json',
external: ['react', 'react-dom'],
metafile: true,
})
const esmResult = esbuild.buildSync({
entryPoints: ['./src/index.ts'],
outdir: 'dist/esm',
minify: true,
bundle: true,
format: 'esm',
target: 'es6',
tsconfig: './tsconfig.build.json',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
external: ['react', 'react-dom'],
metafile: true,
})
let esmSize = 0
Object.values(esmResult.metafile.outputs).forEach((output) => {
esmSize += output.bytes
})
fs.readFile('./dist/esm/index.js', (_err, data) => {
gzip(data, (_err, result) => {
console.log(
`${name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${(
result.length / 1000
).toFixed(2)}kb minified)`
)
})
})
} catch (e) {
console.log(`× ${name}: Build failed due to an error.`)
console.log(e)
}
}
main()

Wyświetl plik

@ -1,32 +0,0 @@
/* eslint-disable */
const esbuild = require('esbuild')
const name = process.env.npm_package_name || ''
async function main() {
esbuild.build({
entryPoints: ['./src/index.ts'],
outdir: 'dist/esm',
minify: false,
bundle: true,
format: 'esm',
target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.json',
external: ['react', 'react-dom'],
incremental: true,
sourcemap: true,
watch: {
onRebuild(error) {
if (error) {
console.log(`× ${name}: An error in prevented the rebuild.`)
return
}
console.log(`${name}: Rebuilt.`)
},
},
})
}
main()

Wyświetl plik

@ -1,28 +0,0 @@
/* eslint-disable */
const fs = require('fs')
const esbuild = require('esbuild')
async function main() {
if (fs.existsSync('./dist')) {
fs.rmSync('./dist', { recursive: true }, (e) => {
if (e) {
throw e
}
})
}
esbuild.build({
entryPoints: ['./src/index.ts'],
outdir: 'dist/esm',
minify: false,
bundle: true,
format: 'esm',
target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.json',
external: ['react', 'react-dom'],
})
}
main()

Wyświetl plik

@ -1,11 +0,0 @@
import { render } from '@testing-library/react'
import * as React from 'react'
import { Binding } from './binding'
jest.spyOn(console, 'error').mockImplementation(() => void null)
describe('binding', () => {
test('mounts component without crashing', () => {
render(<Binding point={[0, 0]} type={'anchor'} />)
})
})

Wyświetl plik

@ -1,16 +0,0 @@
import * as React from 'react'
import type { TLBinding } from '+types'
interface BindingProps {
point: number[]
type: TLBinding['type']
}
export function Binding({ point: [x, y], type }: BindingProps): JSX.Element {
return (
<g pointerEvents="none">
{type === 'center' && <circle className="tl-binding" cx={x} cy={y} r={8} />}
{type !== 'pin' && <use className="tl-binding" href="#cross" x={x} y={y} />}
</g>
)
}

Wyświetl plik

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

Wyświetl plik

@ -1,30 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import type { TLBounds } from '+types'
import { useBoundsEvents } from '+hooks'
import { Container } from '+components/container'
import { SVGContainer } from '+components/svg-container'
interface BoundsBgProps {
bounds: TLBounds
rotation: number
isHidden: boolean
}
export const BoundsBg = React.memo(({ bounds, rotation, isHidden }: BoundsBgProps): JSX.Element => {
const events = useBoundsEvents()
return (
<Container bounds={bounds} rotation={rotation}>
<SVGContainer>
<rect
className="tl-bounds-bg"
width={bounds.width}
height={bounds.height}
opacity={isHidden ? 0 : 1}
{...events}
/>
</SVGContainer>
</Container>
)
})

Wyświetl plik

@ -1,21 +0,0 @@
import { renderWithContext } from '+test'
import * as React from 'react'
import { Bounds } from './bounds'
describe('bounds', () => {
test('mounts component without crashing', () => {
renderWithContext(
<Bounds
zoom={1}
bounds={{ minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }}
rotation={0}
viewportWidth={1000}
isLocked={false}
isHidden={false}
hideBindingHandles={false}
hideCloneHandles={false}
hideRotateHandle={false}
/>
)
})
})

Wyświetl plik

@ -1,134 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import { TLBoundsEdge, TLBoundsCorner, TLBounds } from '+types'
import { CenterHandle } from './center-handle'
import { RotateHandle } from './rotate-handle'
import { CornerHandle } from './corner-handle'
import { EdgeHandle } from './edge-handle'
import { CloneButtons } from './clone-buttons'
import { Container } from '+components/container'
import { SVGContainer } from '+components/svg-container'
import { LinkHandle } from './link-handle'
interface BoundsProps {
zoom: number
bounds: TLBounds
rotation: number
isLocked: boolean
isHidden: boolean
hideCloneHandles: boolean
hideRotateHandle: boolean
hideBindingHandles: boolean
viewportWidth: number
children?: React.ReactNode
}
export const Bounds = React.memo(
({
zoom,
bounds,
viewportWidth,
rotation,
isHidden,
isLocked,
hideCloneHandles,
hideRotateHandle,
hideBindingHandles,
}: BoundsProps): JSX.Element => {
// Touch target size
const targetSize = (viewportWidth < 768 ? 16 : 8) / zoom
// Handle size
const size = 8 / zoom
const smallDimension = Math.min(bounds.width, bounds.height) * zoom
// If the bounds are small, don't show the rotate handle
const showRotateHandle = !hideRotateHandle && !isHidden && !isLocked && smallDimension > 32
// If the bounds are very small, don't show the edge handles
const showEdgeHandles = !isHidden && !isLocked && smallDimension > 24
// If the bounds are very very small, don't show the corner handles
const showCornerHandles = !isHidden && !isLocked && smallDimension > 20
// If the bounds are very small, don't show the clone handles
const showCloneHandles = !hideCloneHandles && smallDimension > 24
return (
<Container bounds={bounds} rotation={rotation}>
<SVGContainer>
<CenterHandle bounds={bounds} isLocked={isLocked} isHidden={isHidden} />
<EdgeHandle
targetSize={targetSize}
size={size}
bounds={bounds}
edge={TLBoundsEdge.Top}
isHidden={!showEdgeHandles}
/>
<EdgeHandle
targetSize={targetSize}
size={size}
bounds={bounds}
edge={TLBoundsEdge.Right}
isHidden={!showEdgeHandles}
/>
<EdgeHandle
targetSize={targetSize}
size={size}
bounds={bounds}
edge={TLBoundsEdge.Bottom}
isHidden={!showEdgeHandles}
/>
<EdgeHandle
targetSize={targetSize}
size={size}
bounds={bounds}
edge={TLBoundsEdge.Left}
isHidden={!showEdgeHandles}
/>
<CornerHandle
targetSize={targetSize}
size={size}
bounds={bounds}
isHidden={isHidden || !showCornerHandles}
corner={TLBoundsCorner.TopLeft}
/>
<CornerHandle
targetSize={targetSize}
size={size}
bounds={bounds}
isHidden={isHidden || !showCornerHandles}
corner={TLBoundsCorner.TopRight}
/>
<CornerHandle
targetSize={targetSize}
size={size}
bounds={bounds}
isHidden={isHidden || !showCornerHandles}
corner={TLBoundsCorner.BottomRight}
/>
<CornerHandle
targetSize={targetSize}
size={size}
bounds={bounds}
isHidden={isHidden || !showCornerHandles}
corner={TLBoundsCorner.BottomLeft}
/>
{showRotateHandle && (
<RotateHandle
targetSize={targetSize}
size={size}
bounds={bounds}
isHidden={!showEdgeHandles}
/>
)}
{showCloneHandles && <CloneButtons bounds={bounds} targetSize={targetSize} size={size} />}
{!hideBindingHandles && (
<LinkHandle
targetSize={targetSize}
size={size}
bounds={bounds}
isHidden={!showEdgeHandles}
/>
)}
</SVGContainer>
</Container>
)
}
)

Wyświetl plik

@ -1,24 +0,0 @@
import * as React from 'react'
import type { TLBounds } from '+types'
export interface CenterHandleProps {
bounds: TLBounds
isLocked: boolean
isHidden: boolean
}
export const CenterHandle = React.memo(
({ bounds, isLocked, isHidden }: CenterHandleProps): JSX.Element => {
return (
<rect
className={isLocked ? 'tl-bounds-center tl-dashed' : 'tl-bounds-center'}
x={-1}
y={-1}
width={bounds.width + 2}
height={bounds.height + 2}
opacity={isHidden ? 0 : 1}
pointerEvents="none"
/>
)
}
)

Wyświetl plik

@ -1,80 +0,0 @@
import * as React from 'react'
import { useTLContext } from '+hooks'
import type { TLBounds } from '+types'
const ROTATIONS = {
right: 0,
bottomRight: 45,
bottom: 90,
bottomLeft: 135,
left: 180,
topLeft: 225,
top: 270,
topRight: 315,
}
export interface CloneButtonProps {
bounds: TLBounds
targetSize: number
size: number
side: 'top' | 'right' | 'bottom' | 'left' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'
}
export function CloneButton({ bounds, side, targetSize, size }: CloneButtonProps) {
const x = {
left: -44,
topLeft: -44,
bottomLeft: -44,
right: bounds.width + 44,
topRight: bounds.width + 44,
bottomRight: bounds.width + 44,
top: bounds.width / 2,
bottom: bounds.width / 2,
}[side]
const y = {
left: bounds.height / 2,
right: bounds.height / 2,
top: -44,
topLeft: -44,
topRight: -44,
bottom: bounds.height + 44,
bottomLeft: bounds.height + 44,
bottomRight: bounds.height + 44,
}[side]
const { callbacks, inputs } = useTLContext()
const handleClick = React.useCallback(
(e: React.PointerEvent<SVGGElement>) => {
e.stopPropagation()
const info = inputs.pointerDown(e, side)
callbacks.onShapeClone?.(info, e)
},
[callbacks.onShapeClone]
)
return (
<g className="tl-clone-target" transform={`translate(${x}, ${y})`}>
<rect
className="tl-transparent"
width={targetSize * 4}
height={targetSize * 4}
x={-targetSize * 2}
y={-targetSize * 2}
/>
<g
className="tl-clone-button-target"
onPointerDown={handleClick}
transform={`rotate(${ROTATIONS[side]})`}
>
<circle className="tl-transparent " r={targetSize} />
<path
className="tl-clone-button"
d={`M -${size / 2},-${size / 2} L ${size / 2},0 -${size / 2},${size / 2} Z`}
strokeLinejoin="round"
/>
</g>
</g>
)
}

Wyświetl plik

@ -1,24 +0,0 @@
import * as React from 'react'
import type { TLBounds } from '+types'
import { CloneButton } from './clone-button'
export interface CloneButtonsProps {
bounds: TLBounds
targetSize: number
size: number
}
export function CloneButtons({ targetSize, size, bounds }: CloneButtonsProps) {
return (
<>
<CloneButton targetSize={targetSize} size={size} bounds={bounds} side="top" />
<CloneButton targetSize={targetSize} size={size} bounds={bounds} side="right" />
<CloneButton targetSize={targetSize} size={size} bounds={bounds} side="bottom" />
<CloneButton targetSize={targetSize} size={size} bounds={bounds} side="left" />
<CloneButton targetSize={targetSize} size={size} bounds={bounds} side="topLeft" />
<CloneButton targetSize={targetSize} size={size} bounds={bounds} side="topRight" />
<CloneButton targetSize={targetSize} size={size} bounds={bounds} side="bottomLeft" />
<CloneButton targetSize={targetSize} size={size} bounds={bounds} side="bottomRight" />
</>
)
}

Wyświetl plik

@ -1,49 +0,0 @@
import * as React from 'react'
import { useBoundsHandleEvents } from '+hooks'
import { TLBoundsCorner, TLBounds } from '+types'
const cornerBgClassnames = {
[TLBoundsCorner.TopLeft]: 'tl-cursor-nwse',
[TLBoundsCorner.TopRight]: 'tl-cursor-nesw',
[TLBoundsCorner.BottomRight]: 'tl-cursor-nwse',
[TLBoundsCorner.BottomLeft]: 'tl-cursor-nesw',
}
interface CornerHandleProps {
size: number
targetSize: number
bounds: TLBounds
corner: TLBoundsCorner
isHidden?: boolean
}
export const CornerHandle = React.memo(
({ size, targetSize, isHidden, corner, bounds }: CornerHandleProps): JSX.Element => {
const events = useBoundsHandleEvents(corner)
const isTop = corner === TLBoundsCorner.TopLeft || corner === TLBoundsCorner.TopRight
const isLeft = corner === TLBoundsCorner.TopLeft || corner === TLBoundsCorner.BottomLeft
return (
<g opacity={isHidden ? 0 : 1}>
<rect
className={'tl-transparent ' + (isHidden ? '' : cornerBgClassnames[corner])}
x={(isLeft ? -1 : bounds.width + 1) - targetSize}
y={(isTop ? -1 : bounds.height + 1) - targetSize}
width={targetSize * 2}
height={targetSize * 2}
pointerEvents={isHidden ? 'none' : 'all'}
{...events}
/>
<rect
className="tl-corner-handle"
x={(isLeft ? -1 : bounds.width + 1) - size / 2}
y={(isTop ? -1 : bounds.height + 1) - size / 2}
width={size}
height={size}
pointerEvents="none"
/>
</g>
)
}
)

Wyświetl plik

@ -1,42 +0,0 @@
import * as React from 'react'
import { useBoundsHandleEvents } from '+hooks'
import { TLBoundsEdge, TLBounds } from '+types'
const edgeClassnames = {
[TLBoundsEdge.Top]: 'tl-cursor-ns',
[TLBoundsEdge.Right]: 'tl-cursor-ew',
[TLBoundsEdge.Bottom]: 'tl-cursor-ns',
[TLBoundsEdge.Left]: 'tl-cursor-ew',
}
interface EdgeHandleProps {
targetSize: number
size: number
bounds: TLBounds
edge: TLBoundsEdge
isHidden: boolean
}
export const EdgeHandle = React.memo(
({ size, isHidden, bounds, edge }: EdgeHandleProps): JSX.Element => {
const events = useBoundsHandleEvents(edge)
const isHorizontal = edge === TLBoundsEdge.Top || edge === TLBoundsEdge.Bottom
const isFarEdge = edge === TLBoundsEdge.Right || edge === TLBoundsEdge.Bottom
const { height, width } = bounds
return (
<rect
pointerEvents={isHidden ? 'none' : 'all'}
className={'tl-transparent tl-edge-handle ' + (isHidden ? '' : edgeClassnames[edge])}
opacity={isHidden ? 0 : 1}
x={isHorizontal ? size / 2 : (isFarEdge ? width + 1 : -1) - size / 2}
y={isHorizontal ? (isFarEdge ? height + 1 : -1) - size / 2 : size / 2}
width={isHorizontal ? Math.max(0, width + 1 - size) : size}
height={isHorizontal ? size : Math.max(0, height + 1 - size)}
{...events}
/>
)
}
)

Wyświetl plik

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

Wyświetl plik

@ -1,48 +0,0 @@
import * as React from 'react'
import { useBoundsHandleEvents, useTLContext } from '+hooks'
import type { TLBounds } from '+types'
interface LinkHandleProps {
size: number
targetSize: number
isHidden: boolean
bounds: TLBounds
}
export function LinkHandle({ size, bounds, isHidden }: LinkHandleProps) {
const leftEvents = useBoundsHandleEvents('left')
const centerEvents = useBoundsHandleEvents('center')
const rightEvents = useBoundsHandleEvents('right')
return (
<g
cursor="grab"
transform={`translate(${bounds.width / 2 - size * 4}, ${bounds.height + size * 2})`}
>
<g className="tl-transparent" pointerEvents={isHidden ? 'none' : 'all'}>
<rect x={0} y={0} width={size * 2} height={size * 2} {...leftEvents} />
<rect x={size * 3} y={0} width={size * 2} height={size * 2} {...centerEvents} />
<rect x={size * 6} y={0} width={size * 2} height={size * 2} {...rightEvents} />
</g>
<g className="tl-rotate-handle" transform={`translate(${size / 2}, ${size / 2})`}>
<path
d={`M 0,${size / 2} L ${size},${size} ${size},0 Z`}
pointerEvents="none"
opacity={isHidden ? 0 : 1}
/>
<path
transform={`translate(${size * 3}, 0)`}
d={`M 0,0 L ${size},0 ${size / 2},${size} Z`}
pointerEvents="none"
opacity={isHidden ? 0 : 1}
/>
<path
transform={`translate(${size * 6}, 0)`}
d={`M ${size},${size / 2} L 0,0 0,${size} Z`}
pointerEvents="none"
opacity={isHidden ? 0 : 1}
/>
</g>
</g>
)
}

Wyświetl plik

@ -1,36 +0,0 @@
import * as React from 'react'
import { useBoundsHandleEvents } from '+hooks'
import type { TLBounds } from '+types'
interface RotateHandleProps {
bounds: TLBounds
size: number
targetSize: number
isHidden: boolean
}
export const RotateHandle = React.memo(
({ bounds, targetSize, size, isHidden }: RotateHandleProps): JSX.Element => {
const events = useBoundsHandleEvents('rotate')
return (
<g cursor="grab" opacity={isHidden ? 0 : 1}>
<circle
className="tl-transparent"
cx={bounds.width / 2}
cy={size * -2}
r={targetSize}
pointerEvents={isHidden ? 'none' : 'all'}
{...events}
/>
<circle
className="tl-rotate-handle"
cx={bounds.width / 2}
cy={size * -2}
r={size / 2}
pointerEvents="none"
/>
</g>
)
}
)

Wyświetl plik

@ -1,20 +0,0 @@
import * as React from 'react'
import { renderWithSvg } from '+test'
import { Brush } from './brush'
describe('brush', () => {
test('mounts component without crashing', () => {
renderWithSvg(
<Brush
brush={{
minX: 0,
maxX: 100,
minY: 0,
maxY: 100,
width: 100,
height: 100,
}}
/>
)
})
})

Wyświetl plik

@ -1,21 +0,0 @@
import { SVGContainer } from '+components'
import { Container } from '+components/container'
import type { TLBounds } from '+types'
import * as React from 'react'
export const Brush = React.memo(({ brush }: { brush: TLBounds }): JSX.Element | null => {
return (
<Container bounds={brush} rotation={0}>
<SVGContainer>
<rect
className="tl-brush"
opacity={1}
x={0}
y={0}
width={brush.width}
height={brush.height}
/>
</SVGContainer>
</Container>
)
})

Wyświetl plik

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

Wyświetl plik

@ -1,20 +0,0 @@
import * as React from 'react'
import { mockDocument, renderWithContext } from '+test'
import { Canvas } from './canvas'
describe('page', () => {
test('mounts component without crashing', () => {
renderWithContext(
<Canvas
page={mockDocument.page}
pageState={mockDocument.pageState}
hideBounds={false}
hideIndicators={false}
hideHandles={false}
hideBindingHandles={false}
hideCloneHandles={false}
hideRotateHandle={false}
/>
)
})
})

Wyświetl plik

@ -1,109 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react'
import {
usePreventNavigation,
useZoomEvents,
useSafariFocusOutFix,
useCanvasEvents,
useCameraCss,
useKeyEvents,
} from '+hooks'
import type { TLBinding, TLPage, TLPageState, TLShape, TLSnapLine, TLUsers } from '+types'
import { ErrorFallback } from '+components/error-fallback'
import { ErrorBoundary } from '+components/error-boundary'
import { Brush } from '+components/brush'
import { Page } from '+components/page'
import { Users } from '+components/users'
import { useResizeObserver } from '+hooks/useResizeObserver'
import { inputs } from '+inputs'
import { UsersIndicators } from '+components/users-indicators'
import { SnapLines } from '+components/snap-lines/snap-lines'
import { Overlay } from '+components/overlay'
function resetError() {
void null
}
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
page: TLPage<T, TLBinding>
pageState: TLPageState
snapLines?: TLSnapLine[]
users?: TLUsers<T>
userId?: string
hideBounds: boolean
hideHandles: boolean
hideIndicators: boolean
hideBindingHandles: boolean
hideCloneHandles: boolean
hideRotateHandle: boolean
externalContainerRef?: React.RefObject<HTMLElement>
meta?: M
id?: string
}
export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
id,
page,
pageState,
snapLines,
users,
userId,
meta,
externalContainerRef,
hideHandles,
hideBounds,
hideIndicators,
hideBindingHandles,
hideCloneHandles,
hideRotateHandle,
}: CanvasProps<T, M>): JSX.Element {
const rCanvas = React.useRef<HTMLDivElement>(null)
const rContainer = React.useRef<HTMLDivElement>(null)
const rLayer = React.useRef<HTMLDivElement>(null)
inputs.zoom = pageState.camera.zoom
useResizeObserver(rCanvas)
useZoomEvents(pageState.camera.zoom, externalContainerRef || rCanvas)
useSafariFocusOutFix()
usePreventNavigation(rCanvas, inputs.bounds.width)
useCameraCss(rLayer, rContainer, pageState)
useKeyEvents()
const events = useCanvasEvents()
return (
<div id={id} className="tl-container" ref={rContainer}>
<div id="canvas" className="tl-absolute tl-canvas" ref={rCanvas} {...events}>
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
<div ref={rLayer} className="tl-absolute tl-layer">
<Page
page={page}
pageState={pageState}
hideBounds={hideBounds}
hideIndicators={hideIndicators}
hideHandles={hideHandles}
hideBindingHandles={hideBindingHandles}
hideCloneHandles={hideCloneHandles}
hideRotateHandle={hideRotateHandle}
meta={meta}
/>
{users && userId && (
<UsersIndicators userId={userId} users={users} page={page} meta={meta} />
)}
{pageState.brush && <Brush brush={pageState.brush} />}
{users && <Users userId={userId} users={users} />}
</div>
</ErrorBoundary>
<Overlay camera={pageState.camera}>
{snapLines && <SnapLines snapLines={snapLines} />}
</Overlay>
</div>
</div>
)
}

Wyświetl plik

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

Wyświetl plik

@ -1,23 +0,0 @@
import * as React from 'react'
import type { TLBounds } from '+types'
import { usePosition } from '+hooks'
interface ContainerProps {
id?: string
bounds: TLBounds
rotation?: number
className?: string
children: React.ReactNode
}
export const Container = React.memo(
({ id, bounds, rotation = 0, className, children }: ContainerProps) => {
const rPositioned = usePosition(bounds, rotation)
return (
<div id={id} ref={rPositioned} className={['tl-positioned', className || ''].join(' ')}>
{children}
</div>
)
}
)

Wyświetl plik

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

Wyświetl plik

@ -1,155 +0,0 @@
import * as React from 'react'
// Copied from https://github.com/bvaughn/react-error-boundary/blob/master/src/index.tsx
// (There's an issue with esm builds of this library, so we can't use it directly.)
const changedArray = (a: Array<unknown> = [], b: Array<unknown> = []) =>
a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
interface FallbackProps {
error: Error
resetErrorBoundary: (...args: Array<unknown>) => void
}
interface ErrorBoundaryPropsWithComponent {
onResetKeysChange?: (
prevResetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined
) => void
onReset?: (...args: Array<unknown>) => void
onError?: (error: Error, info: { componentStack: string }) => void
resetKeys?: Array<unknown>
fallback?: never
FallbackComponent: React.ComponentType<FallbackProps>
fallbackRender?: never
}
declare function FallbackRender(
props: FallbackProps
): React.ReactElement<unknown, string | React.FunctionComponent | typeof React.Component> | null
interface ErrorBoundaryPropsWithRender {
onResetKeysChange?: (
prevResetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined
) => void
onReset?: (...args: Array<unknown>) => void
onError?: (error: Error, info: { componentStack: string }) => void
resetKeys?: Array<unknown>
fallback?: never
FallbackComponent?: never
fallbackRender: typeof FallbackRender
}
interface ErrorBoundaryPropsWithFallback {
onResetKeysChange?: (
prevResetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined
) => void
onReset?: (...args: Array<unknown>) => void
onError?: (error: Error, info: { componentStack: string }) => void
resetKeys?: Array<unknown>
fallback: React.ReactElement<
unknown,
string | React.FunctionComponent | typeof React.Component
> | null
FallbackComponent?: never
fallbackRender?: never
}
type ErrorBoundaryProps =
| ErrorBoundaryPropsWithFallback
| ErrorBoundaryPropsWithComponent
| ErrorBoundaryPropsWithRender
type ErrorBoundaryState = { error: Error | null }
const initialState: ErrorBoundaryState = { error: null }
class ErrorBoundary extends React.Component<
React.PropsWithRef<React.PropsWithChildren<ErrorBoundaryProps>>,
ErrorBoundaryState
> {
static getDerivedStateFromError(error: Error) {
return { error }
}
state = initialState
updatedWithError = false
resetErrorBoundary = (...args: Array<unknown>) => {
this.props.onReset?.(...args)
this.reset()
}
reset() {
this.updatedWithError = false
this.setState(initialState)
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
this.props.onError?.(error, info)
}
componentDidMount() {
const { error } = this.state
if (error !== null) {
this.updatedWithError = true
}
}
componentDidUpdate(prevProps: ErrorBoundaryProps) {
const { error } = this.state
const { resetKeys } = this.props
// There's an edge case where if the thing that triggered the error
// happens to *also* be in the resetKeys array, we'd end up resetting
// the error boundary immediately. This would likely trigger a second
// error to be thrown.
// So we make sure that we don't check the resetKeys on the first call
// of cDU after the error is set
if (error !== null && !this.updatedWithError) {
this.updatedWithError = true
return
}
if (error !== null && changedArray(prevProps.resetKeys, resetKeys)) {
this.props.onResetKeysChange?.(prevProps.resetKeys, resetKeys)
this.reset()
}
}
render() {
const { error } = this.state
const { fallbackRender, FallbackComponent, fallback } = this.props
if (error !== null) {
const props = {
error,
resetErrorBoundary: this.resetErrorBoundary,
}
if (React.isValidElement(fallback)) {
return fallback
} else if (typeof fallbackRender === 'function') {
return fallbackRender(props)
} else if (FallbackComponent) {
return <FallbackComponent {...props} />
} else {
throw new Error(
'react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop'
)
}
}
return this.props.children
}
}
export { ErrorBoundary }
export type {
FallbackProps,
ErrorBoundaryPropsWithComponent,
ErrorBoundaryPropsWithRender,
ErrorBoundaryPropsWithFallback,
ErrorBoundaryProps,
}

Wyświetl plik

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

Wyświetl plik

@ -1,9 +0,0 @@
import * as React from 'react'
import { renderWithContext } from '+test'
import { ErrorFallback } from './error-fallback'
describe('error fallback', () => {
test('mounts component without crashing', () => {
renderWithContext(<ErrorFallback error={new Error()} resetErrorBoundary={() => void null} />)
})
})

Wyświetl plik

@ -1,17 +0,0 @@
import * as React from 'react'
import { useTLContext } from '+hooks'
interface ErrorFallbackProps {
error: Error
resetErrorBoundary: () => void
}
export const ErrorFallback = React.memo(({ error, resetErrorBoundary }: ErrorFallbackProps) => {
const { callbacks } = useTLContext()
React.useEffect(() => {
callbacks.onError?.(error)
}, [error, resetErrorBoundary, callbacks])
return null
})

Wyświetl plik

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

Wyświetl plik

@ -1,37 +0,0 @@
import * as React from 'react'
import { useHandleEvents } from '+hooks'
import { Container } from '+components/container'
import Utils from '+utils'
import { SVGContainer } from '+components/svg-container'
interface HandleProps {
id: string
point: number[]
}
export const Handle = React.memo(({ id, point }: HandleProps) => {
const events = useHandleEvents(id)
return (
<Container
bounds={Utils.translateBounds(
{
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
width: 0,
height: 0,
},
point
)}
>
<SVGContainer>
<g className="tl-handle" {...events}>
<circle className="tl-handle-bg" pointerEvents="all" />
<circle className="tl-counter-scaled tl-handle" pointerEvents="none" r={4} />
</g>
</SVGContainer>
</Container>
)
})

Wyświetl plik

@ -1,10 +0,0 @@
import * as React from 'react'
import { renderWithContext } from '+test'
import { Handles } from './handles'
import { boxShape } from '+shape-utils/TLShapeUtil.spec'
describe('handles', () => {
test('mounts component without crashing', () => {
renderWithContext(<Handles shape={boxShape} zoom={1} />)
})
})

Wyświetl plik

@ -1,42 +0,0 @@
import * as React from 'react'
import { Vec } from '@tldraw/vec'
import type { TLHandle, TLShape } from '+types'
import { Handle } from './handle'
interface HandlesProps {
shape: TLShape
zoom: number
}
export const Handles = React.memo(({ shape, zoom }: HandlesProps): JSX.Element | null => {
if (shape.handles === undefined) {
return null
}
let prev: number[] | null = null
const handlesToShow = Object.values(shape.handles).reduce((acc, cur) => {
const point = Vec.add(cur.point, shape.point)
if (!prev || Vec.dist(point, prev) * zoom >= 32) {
acc.push(cur)
prev = point
}
return acc
}, [] as TLHandle[])
if (handlesToShow.length === 1) return null
return (
<>
{handlesToShow.map((handle) => (
<Handle
key={shape.id + '_' + handle.id}
id={handle.id}
point={Vec.add(handle.point, shape.point)}
/>
))}
</>
)
})

Wyświetl plik

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

Wyświetl plik

@ -1,15 +0,0 @@
import * as React from 'react'
interface HTMLContainerProps extends React.HTMLProps<HTMLDivElement> {
children: React.ReactNode
}
export const HTMLContainer = React.memo(
React.forwardRef<HTMLDivElement, HTMLContainerProps>(({ children, ...rest }, ref) => {
return (
<div ref={ref} className="tl-positioned-div" {...rest}>
{children}
</div>
)
})
)

Wyświetl plik

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

Wyświetl plik

@ -1,3 +0,0 @@
export * from './renderer'
export * from './svg-container'
export * from './html-container'

Wyświetl plik

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

Wyświetl plik

@ -1,24 +0,0 @@
import * as React from 'react'
export function Overlay({
camera,
children,
}: {
camera: { point: number[]; zoom: number }
children: React.ReactNode
}) {
const l = 2.5 / camera.zoom
return (
<svg className="tl-overlay">
<defs>
<g id="tl-snap-point">
<path
className="tl-snap-point"
d={`M ${-l},${-l} L ${l},${l} M ${-l},${l} L ${l},${-l}`}
/>
</g>
</defs>
<g transform={`scale(${camera.zoom}) translate(${camera.point})`}>{children}</g>
</svg>
)
}

Wyświetl plik

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

Wyświetl plik

@ -1,20 +0,0 @@
import * as React from 'react'
import { mockDocument, renderWithContext } from '+test'
import { Page } from './page'
describe('page', () => {
test('mounts component without crashing', () => {
renderWithContext(
<Page
page={mockDocument.page}
pageState={mockDocument.pageState}
hideBounds={false}
hideIndicators={false}
hideHandles={false}
hideBindingHandles={false}
hideCloneHandles={false}
hideRotateHandle={false}
/>
)
})
})

Wyświetl plik

@ -1,110 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react'
import type { TLBinding, TLPage, TLPageState, TLShape } from '+types'
import { useSelection, useShapeTree, useTLContext } from '+hooks'
import { Bounds } from '+components/bounds'
import { BoundsBg } from '+components/bounds/bounds-bg'
import { Handles } from '+components/handles'
import { ShapeNode } from '+components/shape'
import { ShapeIndicator } from '+components/shape-indicator'
import type { TLShapeUtil } from '+shape-utils'
interface PageProps<T extends TLShape, M extends Record<string, unknown>> {
page: TLPage<T, TLBinding>
pageState: TLPageState
hideBounds: boolean
hideHandles: boolean
hideIndicators: boolean
hideBindingHandles: boolean
hideCloneHandles: boolean
hideRotateHandle: boolean
meta?: M
}
/**
* The Page component renders the current page.
*/
export const Page = React.memo(function Page<T extends TLShape, M extends Record<string, unknown>>({
page,
pageState,
hideBounds,
hideHandles,
hideIndicators,
hideBindingHandles,
hideCloneHandles,
hideRotateHandle,
meta,
}: PageProps<T, M>): JSX.Element {
const { callbacks, shapeUtils, inputs } = useTLContext()
const shapeTree = useShapeTree(
page,
pageState,
shapeUtils,
[inputs.bounds.width, inputs.bounds.height],
meta,
callbacks.onRenderCountChange
)
const { bounds, isLinked, isLocked, rotation } = useSelection(page, pageState, shapeUtils)
const {
selectedIds,
hoveredId,
camera: { zoom },
} = pageState
let _hideCloneHandles = true
// Does the selected shape have handles?
let shapeWithHandles: TLShape | undefined = undefined
const selectedShapes = selectedIds.map((id) => page.shapes[id])
if (selectedShapes.length === 1) {
const shape = selectedShapes[0]
const utils = shapeUtils[shape.type] as TLShapeUtil<any, any>
_hideCloneHandles = hideCloneHandles || !utils.canClone
if (shape.handles !== undefined) {
shapeWithHandles = shape
}
}
return (
<>
{bounds && <BoundsBg bounds={bounds} rotation={rotation} isHidden={hideBounds} />}
{shapeTree.map((node) => (
<ShapeNode key={node.shape.id} utils={shapeUtils} {...node} />
))}
{!hideIndicators &&
selectedShapes.map((shape) => (
<ShapeIndicator key={'selected_' + shape.id} shape={shape} meta={meta} isSelected />
))}
{!hideIndicators && hoveredId && (
<ShapeIndicator
key={'hovered_' + hoveredId}
shape={page.shapes[hoveredId]}
meta={meta}
isHovered
/>
)}
{bounds && (
<Bounds
zoom={zoom}
bounds={bounds}
viewportWidth={inputs.bounds.width}
isLocked={isLocked}
rotation={rotation}
isHidden={hideBounds}
hideRotateHandle={hideRotateHandle}
hideBindingHandles={hideBindingHandles || !isLinked}
hideCloneHandles={_hideCloneHandles}
/>
)}
{!hideHandles && shapeWithHandles && <Handles shape={shapeWithHandles} zoom={zoom} />}
</>
)
})

Wyświetl plik

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

Wyświetl plik

@ -1,16 +0,0 @@
import * as React from 'react'
import { mockDocument, mockUtils } from '+test'
import { render } from '@testing-library/react'
import { Renderer } from './renderer'
describe('renderer', () => {
test('mounts component without crashing', () => {
render(
<Renderer
shapeUtils={mockUtils}
page={mockDocument.page}
pageState={mockDocument.pageState}
/>
)
})
})

Wyświetl plik

@ -1,169 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react'
import type {
TLShape,
TLPage,
TLPageState,
TLCallbacks,
TLTheme,
TLBounds,
TLBinding,
} from '../../types'
import { Canvas } from '../canvas'
import { Inputs } from '../../inputs'
import { useTLTheme, TLContext, TLContextType } from '../../hooks'
import type { TLSnapLine, TLUsers } from '+index'
import type { TLShapeUtilsMap } from '+shape-utils'
export interface RendererProps<T extends TLShape, M = any> extends Partial<TLCallbacks<T>> {
/**
* (optional) A unique id to be applied to the renderer element, used to scope styles.
*/
id?: string
/**
* (optional) A ref for the renderer's container element, used for scoping event handlers.
*/
containerRef?: React.RefObject<HTMLElement>
/**
* An object containing instances of your shape classes.
*/
shapeUtils: TLShapeUtilsMap<T>
/**
* The current page, containing shapes and bindings.
*/
page: TLPage<T, TLBinding>
/**
* The current page state.
*/
pageState: TLPageState
/**
* (optional) The current users to render.
*/
users?: TLUsers<T>
/**
* (optional) The current snap lines to render.
*/
snapLines?: TLSnapLine[]
/**
* (optional) The current user's id, used to identify the user.
*/
userId?: string
/**
* (optional) An object of custom theme colors.
*/
theme?: Partial<TLTheme>
/**
* (optional) When true, the renderer will not show the bounds for selected objects.
*/
hideBounds?: boolean
/**
* (optional) When true, the renderer will not show the handles of shapes with handles.
*/
hideHandles?: boolean
/**
* (optional) When true, the renderer will not show rotate handles for selected objects.
*/
hideRotateHandles?: boolean
/**
* (optional) When true, the renderer will not show buttons for cloning shapes.
*/
hideCloneHandles?: boolean
/**
* (optional) When true, the renderer will not show binding controls.
*/
hideBindingHandles?: boolean
/**
* (optional) When true, the renderer will not show indicators for selected or
* hovered objects,
*/
hideIndicators?: boolean
/**
* (optional) hen true, the renderer will ignore all inputs that were not made
* by a stylus or pen-type device.
*/
isPenMode?: boolean
/**
* (optional) An object of custom options that should be passed to rendered shapes.
*/
meta?: M
/**
* (optional) A callback that receives the renderer's inputs manager.
*/
onMount?: (inputs: Inputs) => void
/**
* (optional) A callback that is fired when the editor's client bounding box changes.
*/
onBoundsChange?: (bounds: TLBounds) => void
}
/**
* The Renderer component is the main component of the library. It
* accepts the current `page`, the `shapeUtils` needed to interpret
* and render the shapes and bindings on the `page`, and the current
* `pageState`.
* @param props
* @returns
*/
export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
id = 'tl',
shapeUtils,
page,
pageState,
users,
userId,
theme,
meta,
snapLines,
containerRef,
hideHandles = false,
hideIndicators = false,
hideCloneHandles = false,
hideBindingHandles = false,
hideRotateHandles = false,
hideBounds = false,
onMount,
...rest
}: RendererProps<T, M>): JSX.Element {
useTLTheme(theme, '#' + id)
const rSelectionBounds = React.useRef<TLBounds>(null)
const rPageState = React.useRef<TLPageState>(pageState)
React.useEffect(() => {
rPageState.current = pageState
}, [pageState])
const [context] = React.useState<TLContextType<T>>(() => ({
callbacks: rest,
shapeUtils,
rSelectionBounds,
rPageState,
inputs: new Inputs(),
}))
React.useEffect(() => {
onMount?.(context.inputs)
}, [context])
return (
<TLContext.Provider value={context as unknown as TLContextType<TLShape>}>
<Canvas
id={id}
page={page}
pageState={pageState}
snapLines={snapLines}
users={users}
userId={userId}
externalContainerRef={containerRef}
hideBounds={hideBounds}
hideIndicators={hideIndicators}
hideHandles={hideHandles}
hideCloneHandles={hideCloneHandles}
hideBindingHandles={hideBindingHandles}
hideRotateHandle={hideRotateHandles}
meta={meta}
/>
</TLContext.Provider>
)
}

Wyświetl plik

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

Wyświetl plik

@ -1,12 +0,0 @@
import * as React from 'react'
import { renderWithSvg } from '+test'
import { ShapeIndicator } from './shape-indicator'
import { boxShape } from '+shape-utils/TLShapeUtil.spec'
describe('shape indicator', () => {
test('mounts component without crashing', () => {
renderWithSvg(
<ShapeIndicator shape={boxShape} isSelected={true} isHovered={false} meta={undefined} />
)
})
})

Wyświetl plik

@ -1,47 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react'
import type { TLShape } from '+types'
import { usePosition, useTLContext } from '+hooks'
interface IndicatorProps<T extends TLShape, M = any> {
shape: T
meta: M extends any ? M : undefined
isSelected?: boolean
isHovered?: boolean
color?: string
}
export const ShapeIndicator = React.memo(
<T extends TLShape, M = any>({
isHovered = false,
isSelected = false,
shape,
color,
meta,
}: IndicatorProps<T, M>) => {
const { shapeUtils } = useTLContext()
const utils = shapeUtils[shape.type]
const bounds = utils.getBounds(shape)
const rPositioned = usePosition(bounds, shape.rotation)
return (
<div
ref={rPositioned}
className={
'tl-indicator tl-absolute ' + (color ? '' : isSelected ? 'tl-selected' : 'tl-hovered')
}
>
<svg width="100%" height="100%">
<g className="tl-centered-g" stroke={color}>
<utils.Indicator
shape={shape}
meta={meta}
isSelected={isSelected}
isHovered={isHovered}
/>
</g>
</svg>
</div>
)
}
)

Wyświetl plik

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

Wyświetl plik

@ -1,67 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import type { TLComponentProps, TLShape } from '+types'
import type { TLShapeUtil } from '+shape-utils'
interface RenderedShapeProps<T extends TLShape, E extends Element, M>
extends TLComponentProps<T, E, M> {
shape: T
utils: TLShapeUtil<T, E, M>
}
export const RenderedShape = React.memo(
<T extends TLShape, E extends Element, M>({
shape,
utils,
isEditing,
isBinding,
isHovered,
isSelected,
isCurrentParent,
onShapeChange,
onShapeBlur,
events,
meta,
}: RenderedShapeProps<T, E, M>) => {
const ref = utils.getRef(shape)
// consider using layout effect to update bounds cache if the ref is filled
return (
<utils.Component
ref={ref}
shape={shape}
isEditing={isEditing}
isBinding={isBinding}
isHovered={isHovered}
isSelected={isSelected}
isCurrentParent={isCurrentParent}
meta={meta}
events={events}
onShapeChange={onShapeChange}
onShapeBlur={onShapeBlur}
/>
)
},
(prev, next) => {
// If these have changed, then definitely render
if (
prev.isHovered !== next.isHovered ||
prev.isSelected !== next.isSelected ||
prev.isEditing !== next.isEditing ||
prev.isBinding !== next.isBinding ||
prev.meta !== next.meta ||
prev.isCurrentParent !== next.isCurrentParent
) {
return false
}
// If not, and if the shape has changed, ask the shape's class
// whether it should render
if (next.shape !== prev.shape) {
return !next.utils.shouldRender(next.shape, prev.shape)
}
return true
}
)

Wyświetl plik

@ -1,41 +0,0 @@
import * as React from 'react'
import type { IShapeTreeNode, TLShape } from '+types'
import { Shape } from './shape'
import type { TLShapeUtilsMap } from '+shape-utils'
interface ShapeNodeProps<T extends TLShape> extends IShapeTreeNode<T> {
utils: TLShapeUtilsMap<TLShape>
}
export const ShapeNode = React.memo(
<T extends TLShape>({
shape,
utils,
children,
isEditing,
isBinding,
isHovered,
isSelected,
isCurrentParent,
meta,
}: ShapeNodeProps<T>) => {
return (
<>
<Shape
shape={shape}
isEditing={isEditing}
isBinding={isBinding}
isHovered={isHovered}
isSelected={isSelected}
isCurrentParent={isCurrentParent}
utils={utils[shape.type as T['type']]}
meta={meta}
/>
{children &&
children.map((childNode) => (
<ShapeNode key={childNode.shape.id} utils={utils} {...childNode} />
))}
</>
)
}
)

Wyświetl plik

@ -1,25 +0,0 @@
import * as React from 'react'
import { renderWithContext } from '+test'
import { Shape } from './shape'
import { BoxUtil, boxShape } from '+shape-utils/TLShapeUtil.spec'
import type { TLShapeUtil } from '+shape-utils'
import type { TLShape } from '+types'
describe('shape', () => {
test('mounts component without crashing', () => {
renderWithContext(
<Shape
shape={boxShape}
utils={new BoxUtil() as unknown as TLShapeUtil<TLShape>}
isEditing={false}
isBinding={false}
isHovered={false}
isSelected={false}
isCurrentParent={false}
/>
)
})
})
// { shape: TLShape; ref: ForwardedRef<Element>; } & TLComponentProps<any, any> & RefAttributes<Element>
// { shape: BoxShape; ref: ForwardedRef<any>; } & TLComponentProps<any, any> & RefAttributes<any>'

Wyświetl plik

@ -1,51 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import { useShapeEvents } from '+hooks'
import type { IShapeTreeNode, TLShape } from '+types'
import { RenderedShape } from './rendered-shape'
import { Container } from '+components/container'
import { useTLContext } from '+hooks'
import { useForceUpdate } from '+hooks/useForceUpdate'
import type { TLShapeUtil } from '+shape-utils'
interface ShapeProps<T extends TLShape, M> extends IShapeTreeNode<T, M> {
utils: TLShapeUtil<T>
}
export const Shape = React.memo(
<T extends TLShape, M>({
shape,
utils,
isEditing,
isBinding,
isHovered,
isSelected,
isCurrentParent,
meta,
}: ShapeProps<T, M>) => {
const { callbacks } = useTLContext()
const bounds = utils.getBounds(shape)
const events = useShapeEvents(shape.id, isCurrentParent)
useForceUpdate()
return (
<Container id={shape.id} bounds={bounds} rotation={shape.rotation}>
<RenderedShape
shape={shape}
isBinding={isBinding}
isCurrentParent={isCurrentParent}
isEditing={isEditing}
isHovered={isHovered}
isSelected={isSelected}
utils={utils as any}
meta={meta}
events={events}
onShapeChange={callbacks.onShapeChange}
onShapeBlur={callbacks.onShapeBlur}
/>
</Container>
)
}
)

Wyświetl plik

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

Wyświetl plik

@ -1,32 +0,0 @@
import * as React from 'react'
import type { TLSnapLine } from '+types'
import Utils from '+utils'
export function SnapLines({ snapLines }: { snapLines: TLSnapLine[] }) {
return (
<>
{snapLines.map((snapLine, i) => (
<SnapLine key={i} snapLine={snapLine} />
))}
</>
)
}
export function SnapLine({ snapLine }: { snapLine: TLSnapLine }) {
const bounds = Utils.getBoundsFromPoints(snapLine)
return (
<>
<line
className="tl-snap-line"
x1={bounds.minX}
y1={bounds.minY}
x2={bounds.maxX}
y2={bounds.maxY}
/>
{snapLine.map(([x, y], i) => (
<use key={i} href="#tl-snap-point" x={x} y={y} />
))}
</>
)
}

Wyświetl plik

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

Wyświetl plik

@ -1,18 +0,0 @@
import * as React from 'react'
interface SvgContainerProps extends React.SVGProps<SVGSVGElement> {
id?: string
children: React.ReactNode
}
export const SVGContainer = React.memo(
React.forwardRef<SVGSVGElement, SvgContainerProps>(({ id, children, ...rest }, ref) => {
return (
<svg ref={ref} className="tl-positioned-svg" {...rest}>
<g id={id} className="tl-centered-g">
{children}
</g>
</svg>
)
})
)

Wyświetl plik

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

Wyświetl plik

@ -1,21 +0,0 @@
import * as React from 'react'
import type { TLShape, TLUser } from '+types'
interface UserProps {
user: TLUser<TLShape>
}
export function User({ user }: UserProps) {
const rUser = React.useRef<HTMLDivElement>(null)
return (
<div
ref={rUser}
className="tl-absolute tl-user"
style={{
backgroundColor: user.color,
transform: `translate(${user.point[0]}px, ${user.point[1]}px)`,
}}
/>
)
}

Wyświetl plik

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

Wyświetl plik

@ -1,64 +0,0 @@
import * as React from 'react'
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ShapeIndicator } from '+components/shape-indicator'
import type { TLPage, TLShape, TLUsers } from '+types'
import Utils from '+utils'
import { useTLContext } from '+hooks'
interface UserIndicatorProps<T extends TLShape> {
page: TLPage<any, any>
userId: string
users: TLUsers<T>
meta: any
}
export function UsersIndicators<T extends TLShape>({
userId,
users,
meta,
page,
}: UserIndicatorProps<T>) {
const { shapeUtils } = useTLContext()
return (
<>
{Object.values(users)
.filter(Boolean)
.filter((user) => user.id !== userId && user.selectedIds.length > 0)
.map((user) => {
const shapes = user.selectedIds.map((id) => page.shapes[id]).filter(Boolean)
if (shapes.length === 0) return null
const bounds = Utils.getCommonBounds(
shapes.map((shape) => shapeUtils[shape.type].getBounds(shape))
)
return (
<React.Fragment key={user.id + '_shapes'}>
<div
className="tl-absolute tl-user-indicator-bounds"
style={{
backgroundColor: user.color + '0d',
borderColor: user.color + '78',
transform: `translate(${bounds.minX}px, ${bounds.minY}px)`,
width: bounds.width,
height: bounds.height,
pointerEvents: 'none',
}}
/>
{shapes.map((shape) => (
<ShapeIndicator
key={`${user.id}_${shape.id}_indicator`}
shape={shape}
color={user.color}
meta={meta}
isHovered
/>
))}
</React.Fragment>
)
})}
</>
)
}

Wyświetl plik

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

Wyświetl plik

@ -1,20 +0,0 @@
import * as React from 'react'
import { User } from '+components/user/user'
import type { TLShape, TLUsers } from '+types'
export interface UserProps {
userId?: string
users: TLUsers<TLShape>
}
export function Users({ userId, users }: UserProps) {
return (
<>
{Object.values(users)
.filter((user) => user && user.id !== userId)
.map((user) => (
<User key={user.id} user={user} />
))}
</>
)
}

Wyświetl plik

@ -1,17 +0,0 @@
export * from './useTLContext'
export * from './useZoomEvents'
export * from './useSafariFocusOutFix'
export * from './useCanvasEvents'
export * from './useShapeEvents'
export * from './useShapeTree'
export * from './useStyle'
export * from './useCanvasEvents'
export * from './useBoundsHandleEvents'
export * from './useCameraCss'
export * from './useSelection'
export * from './useHandleEvents'
export * from './useHandles'
export * from './usePreventNavigation'
export * from './useBoundsEvents'
export * from './usePosition'
export * from './useKeyEvents'

Wyświetl plik

@ -1,78 +0,0 @@
import * as React from 'react'
import { useTLContext } from './useTLContext'
export function useBoundsEvents() {
const { callbacks, inputs } = useTLContext()
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
e.currentTarget?.setPointerCapture(e.pointerId)
const info = inputs.pointerDown(e, 'bounds')
callbacks.onPointBounds?.(info, e)
callbacks.onPointerDown?.(info, e)
},
[callbacks, inputs]
)
const onPointerUp = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
const isDoubleClick = inputs.isDoubleClick()
const info = inputs.pointerUp(e, 'bounds')
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget?.releasePointerCapture(e.pointerId)
}
if (isDoubleClick && !(info.altKey || info.metaKey)) {
callbacks.onDoubleClickBounds?.(info, e)
}
callbacks.onReleaseBounds?.(info, e)
callbacks.onPointerUp?.(info, e)
},
[callbacks, inputs]
)
const onPointerMove = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
callbacks.onDragBounds?.(inputs.pointerMove(e, 'bounds'), e)
}
const info = inputs.pointerMove(e, 'bounds')
callbacks.onPointerMove?.(info, e)
},
[callbacks, inputs]
)
const onPointerEnter = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
callbacks.onHoverBounds?.(inputs.pointerEnter(e, 'bounds'), e)
},
[callbacks, inputs]
)
const onPointerLeave = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
callbacks.onUnhoverBounds?.(inputs.pointerEnter(e, 'bounds'), e)
},
[callbacks, inputs]
)
return {
onPointerDown,
onPointerUp,
onPointerEnter,
onPointerMove,
onPointerLeave,
}
}

Wyświetl plik

@ -1,81 +0,0 @@
import * as React from 'react'
import type { TLBoundsEdge, TLBoundsCorner } from '+types'
import { useTLContext } from './useTLContext'
export function useBoundsHandleEvents(
id: TLBoundsCorner | TLBoundsEdge | 'rotate' | 'center' | 'left' | 'right'
) {
const { callbacks, inputs } = useTLContext()
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
e.currentTarget?.setPointerCapture(e.pointerId)
const info = inputs.pointerDown(e, id)
callbacks.onPointBoundsHandle?.(info, e)
callbacks.onPointerDown?.(info, e)
},
[inputs, callbacks, id]
)
const onPointerUp = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
const isDoubleClick = inputs.isDoubleClick()
const info = inputs.pointerUp(e, id)
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget?.releasePointerCapture(e.pointerId)
}
if (isDoubleClick && !(info.altKey || info.metaKey)) {
callbacks.onDoubleClickBoundsHandle?.(info, e)
}
callbacks.onReleaseBoundsHandle?.(info, e)
callbacks.onPointerUp?.(info, e)
},
[inputs, callbacks, id]
)
const onPointerMove = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
callbacks.onDragBoundsHandle?.(inputs.pointerMove(e, id), e)
}
const info = inputs.pointerMove(e, id)
callbacks.onPointerMove?.(info, e)
},
[inputs, callbacks, id]
)
const onPointerEnter = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
callbacks.onHoverBoundsHandle?.(inputs.pointerEnter(e, id), e)
},
[inputs, callbacks, id]
)
const onPointerLeave = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
callbacks.onUnhoverBoundsHandle?.(inputs.pointerEnter(e, id), e)
},
[inputs, callbacks, id]
)
return {
onPointerDown,
onPointerUp,
onPointerEnter,
onPointerMove,
onPointerLeave,
}
}

Wyświetl plik

@ -1,43 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import type { TLPageState } from '+types'
export function useCameraCss(
layerRef: React.RefObject<HTMLDivElement>,
containerRef: React.RefObject<HTMLDivElement>,
pageState: TLPageState
) {
// Update the tl-zoom CSS variable when the zoom changes
const rZoom = React.useRef(pageState.camera.zoom)
const rPoint = React.useRef(pageState.camera.point)
React.useLayoutEffect(() => {
const { zoom, point } = pageState.camera
const didZoom = zoom !== rZoom.current
const didPan = point !== rPoint.current
rZoom.current = zoom
rPoint.current = point
if (didZoom || didPan) {
const layer = layerRef.current
const container = containerRef.current
// If we zoomed, set the CSS variable for the zoom
if (didZoom) {
if (container) {
container.style.setProperty('--tl-zoom', zoom.toString())
}
}
// Either way, position the layer
if (layer) {
layer.style.setProperty(
'transform',
`scale(${zoom}) translateX(${point[0]}px) translateY(${point[1]}px)`
)
}
}
}, [pageState.camera])
}

Wyświetl plik

@ -1,62 +0,0 @@
import * as React from 'react'
import { useTLContext } from './useTLContext'
export function useCanvasEvents() {
const { callbacks, inputs } = useTLContext()
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.currentTarget.setPointerCapture(e.pointerId)
const info = inputs.pointerDown(e, 'canvas')
if (e.button === 0) {
callbacks.onPointCanvas?.(info, e)
callbacks.onPointerDown?.(info, e)
}
},
[callbacks, inputs]
)
const onPointerMove = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
const info = inputs.pointerMove(e, 'canvas')
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
callbacks.onDragCanvas?.(info, e)
}
callbacks.onPointerMove?.(info, e)
},
[callbacks, inputs]
)
const onPointerUp = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
const isDoubleClick = inputs.isDoubleClick()
const info = inputs.pointerUp(e, 'canvas')
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget?.releasePointerCapture(e.pointerId)
}
if (isDoubleClick && !(info.altKey || info.metaKey)) {
callbacks.onDoubleClickCanvas?.(info, e)
}
callbacks.onReleaseCanvas?.(info, e)
callbacks.onPointerUp?.(info, e)
},
[callbacks, inputs]
)
return {
onPointerDown,
onPointerMove,
onPointerUp,
}
}

Wyświetl plik

@ -1,6 +0,0 @@
import * as React from 'react'
export function useForceUpdate() {
const forceUpdate = React.useReducer((s) => s + 1, 0)
React.useLayoutEffect(() => forceUpdate[1](), [])
}

Wyświetl plik

@ -1,81 +0,0 @@
import * as React from 'react'
import { useTLContext } from './useTLContext'
export function useHandleEvents(id: string) {
const { inputs, callbacks } = useTLContext()
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
e.currentTarget?.setPointerCapture(e.pointerId)
const info = inputs.pointerDown(e, id)
callbacks.onPointHandle?.(info, e)
callbacks.onPointerDown?.(info, e)
},
[inputs, callbacks, id]
)
const onPointerUp = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
e.stopPropagation()
const isDoubleClick = inputs.isDoubleClick()
const info = inputs.pointerUp(e, id)
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget?.releasePointerCapture(e.pointerId)
if (isDoubleClick && !(info.altKey || info.metaKey)) {
callbacks.onDoubleClickHandle?.(info, e)
}
callbacks.onReleaseHandle?.(info, e)
}
callbacks.onPointerUp?.(info, e)
},
[inputs, callbacks]
)
const onPointerMove = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
const info = inputs.pointerMove(e, id)
callbacks.onDragHandle?.(info, e)
}
const info = inputs.pointerMove(e, id)
callbacks.onPointerMove?.(info, e)
},
[inputs, callbacks, id]
)
const onPointerEnter = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
const info = inputs.pointerEnter(e, id)
callbacks.onHoverHandle?.(info, e)
},
[inputs, callbacks, id]
)
const onPointerLeave = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
const info = inputs.pointerEnter(e, id)
callbacks.onUnhoverHandle?.(info, e)
},
[inputs, callbacks, id]
)
return {
onPointerDown,
onPointerUp,
onPointerEnter,
onPointerMove,
onPointerLeave,
}
}

Wyświetl plik

@ -1,19 +0,0 @@
import type { TLBinding, TLPage, TLPageState, TLShape } from '+types'
export function useHandles<T extends TLShape>(page: TLPage<T, TLBinding>, pageState: TLPageState) {
const { selectedIds } = pageState
let shapeWithHandles: TLShape | undefined = undefined
if (selectedIds.length === 1) {
const id = selectedIds[0]
const shape = page.shapes[id]
if (shape.handles !== undefined) {
shapeWithHandles = shape
}
}
return { shapeWithHandles }
}

Wyświetl plik

@ -1,25 +0,0 @@
import { useTLContext } from '+hooks'
import * as React from 'react'
export function useKeyEvents() {
const { inputs, callbacks } = useTLContext()
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
callbacks.onKeyDown?.(e.key, inputs.keydown(e), e)
}
const handleKeyUp = (e: KeyboardEvent) => {
inputs.keyup(e)
callbacks.onKeyUp?.(e.key, inputs.keyup(e), e)
}
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)
}
}, [inputs, callbacks])
}

Wyświetl plik

@ -1,32 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import type { TLBounds } from '+types'
export function usePosition(bounds: TLBounds, rotation = 0) {
const rBounds = React.useRef<HTMLDivElement>(null)
// Update the transform
React.useLayoutEffect(() => {
const elm = rBounds.current!
const transform = `
translate(
calc(${bounds.minX}px - var(--tl-padding)),
calc(${bounds.minY}px - var(--tl-padding))
)
rotate(${rotation + (bounds.rotation || 0)}rad)`
elm.style.setProperty('transform', transform)
elm.style.setProperty('width', `calc(${Math.floor(bounds.width)}px + (var(--tl-padding) * 2))`)
elm.style.setProperty(
'height',
`calc(${Math.floor(bounds.height)}px + (var(--tl-padding) * 2))`
)
// elm.style.setProperty('z-index', zIndex + '')
}, [bounds, rotation])
return rBounds
}

Wyświetl plik

@ -1,60 +0,0 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import * as React from 'react'
export function usePreventNavigation(
rCanvas: React.RefObject<HTMLDivElement>,
width: number
): void {
React.useEffect(() => {
const preventGestureNavigation = (event: TouchEvent) => {
event.preventDefault()
}
const preventNavigation = (event: TouchEvent) => {
// Center point of the touch area
const touchXPosition = event.touches[0].pageX
// Size of the touch area
const touchXRadius = event.touches[0].radiusX || 0
// We set a threshold (10px) on both sizes of the screen,
// if the touch area overlaps with the screen edges
// it's likely to trigger the navigation. We prevent the
// touchstart event in that case.
if (touchXPosition - touchXRadius < 10 || touchXPosition + touchXRadius > width - 10) {
event.preventDefault()
}
}
const elm = rCanvas.current
if (!elm) return () => void null
elm.addEventListener('touchstart', preventGestureNavigation)
// @ts-ignore
elm.addEventListener('gestureend', preventGestureNavigation)
// @ts-ignore
elm.addEventListener('gesturechange', preventGestureNavigation)
// @ts-ignore
elm.addEventListener('gesturestart', preventGestureNavigation)
// @ts-ignore
elm.addEventListener('touchstart', preventNavigation)
return () => {
if (elm) {
elm.removeEventListener('touchstart', preventGestureNavigation)
// @ts-ignore
elm.removeEventListener('gestureend', preventGestureNavigation)
// @ts-ignore
elm.removeEventListener('gesturechange', preventGestureNavigation)
// @ts-ignore
elm.removeEventListener('gesturestart', preventGestureNavigation)
// @ts-ignore
elm.removeEventListener('touchstart', preventNavigation)
}
}
}, [rCanvas, width])
}

Wyświetl plik

@ -1,72 +0,0 @@
import { useTLContext } from '+hooks'
import * as React from 'react'
import { Utils } from '+utils'
export function useResizeObserver<T extends Element>(ref: React.RefObject<T>) {
const { inputs, callbacks } = useTLContext()
const rIsMounted = React.useRef(false)
const forceUpdate = React.useReducer((x) => x + 1, 0)[1]
// When the element resizes, update the bounds (stored in inputs)
// and broadcast via the onBoundsChange callback prop.
const updateBounds = React.useCallback(() => {
if (rIsMounted.current) {
const rect = ref.current?.getBoundingClientRect()
if (rect) {
inputs.bounds = {
minX: rect.left,
maxX: rect.left + rect.width,
minY: rect.top,
maxY: rect.top + rect.height,
width: rect.width,
height: rect.height,
}
callbacks.onBoundsChange?.(inputs.bounds)
// Force an update for a second mount
forceUpdate()
}
} else {
// Skip the first mount
rIsMounted.current = true
}
}, [ref, forceUpdate, inputs, callbacks.onBoundsChange])
React.useEffect(() => {
const debouncedupdateBounds = Utils.debounce(updateBounds, 100)
window.addEventListener('scroll', debouncedupdateBounds)
window.addEventListener('resize', debouncedupdateBounds)
return () => {
window.removeEventListener('scroll', debouncedupdateBounds)
window.removeEventListener('resize', debouncedupdateBounds)
}
}, [])
React.useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
if (inputs.isPinching) {
return
}
if (entries[0].contentRect) {
updateBounds()
}
})
if (ref.current) {
resizeObserver.observe(ref.current)
}
return () => {
resizeObserver.disconnect()
}
}, [ref, inputs])
React.useEffect(() => {
updateBounds()
}, [ref])
}

Wyświetl plik

@ -1,22 +0,0 @@
import { useEffect } from 'react'
import Utils from '+utils'
import { useTLContext } from './useTLContext'
// Send event on iOS when a user presses the "Done" key while editing a text element.
export function useSafariFocusOutFix(): void {
const { callbacks } = useTLContext()
useEffect(() => {
function handleFocusOut() {
callbacks.onShapeBlur?.()
}
if (Utils.isMobileSafari()) {
document.addEventListener('focusout', handleFocusOut)
return () => document.removeEventListener('focusout', handleFocusOut)
}
return () => null
}, [callbacks])
}

Wyświetl plik

@ -1,90 +0,0 @@
import * as React from 'react'
import type { TLPage, TLPageState, TLShape, TLBounds, TLBinding } from '+types'
import Utils from '+utils'
import { useTLContext } from '+hooks'
import type { TLShapeUtil, TLShapeUtilsMap } from '+shape-utils'
function canvasToScreen(point: number[], camera: TLPageState['camera']): number[] {
return [(point[0] + camera.point[0]) * camera.zoom, (point[1] + camera.point[1]) * camera.zoom]
}
function getShapeUtils<T extends TLShape>(shapeUtils: TLShapeUtilsMap<T>, shape: T) {
return shapeUtils[shape.type as T['type']] as unknown as TLShapeUtil<T>
}
export function useSelection<T extends TLShape>(
page: TLPage<T, TLBinding>,
pageState: TLPageState,
shapeUtils: TLShapeUtilsMap<T>
) {
const { rSelectionBounds } = useTLContext()
const { selectedIds } = pageState
const rPrevBounds = React.useRef<TLBounds>()
let bounds: TLBounds | undefined = undefined
let rotation = 0
let isLocked = false
let isLinked = false
if (selectedIds.length === 1) {
const id = selectedIds[0]
const shape = page.shapes[id]
rotation = shape.rotation || 0
isLocked = shape.isLocked || false
bounds = getShapeUtils(shapeUtils, shape).getBounds(shape)
} else if (selectedIds.length > 1) {
const selectedShapes = selectedIds.map((id) => page.shapes[id])
rotation = 0
isLocked = selectedShapes.every((shape) => shape.isLocked)
bounds = selectedShapes.reduce((acc, shape, i) => {
if (i === 0) {
return getShapeUtils(shapeUtils, shape).getRotatedBounds(shape)
}
return Utils.getExpandedBounds(acc, getShapeUtils(shapeUtils, shape).getRotatedBounds(shape))
}, {} as TLBounds)
}
if (bounds) {
const [minX, minY] = canvasToScreen([bounds.minX, bounds.minY], pageState.camera)
const [maxX, maxY] = canvasToScreen([bounds.maxX, bounds.maxY], pageState.camera)
isLinked = !!Object.values(page.bindings).find(
(binding) => selectedIds.includes(binding.toId) || selectedIds.includes(binding.fromId)
)
rSelectionBounds.current = {
minX,
minY,
maxX,
maxY,
width: maxX - minX,
height: maxY - minY,
}
} else {
rSelectionBounds.current = null
}
const prevBounds = rPrevBounds.current
if (!prevBounds || !bounds) {
rPrevBounds.current = bounds
} else if (bounds) {
if (
prevBounds.minX === bounds.minX &&
prevBounds.minY === bounds.minY &&
prevBounds.maxX === bounds.maxX &&
prevBounds.maxY === bounds.maxY
) {
bounds = rPrevBounds.current
}
}
return { bounds, rotation, isLocked, isLinked }
}

Wyświetl plik

@ -1,114 +0,0 @@
import * as React from 'react'
import { Utils } from '+utils'
import { TLContext } from '+hooks'
export function useShapeEvents(id: string, disable = false) {
const { rPageState, rSelectionBounds, callbacks, inputs } = React.useContext(TLContext)
const onPointerDown = React.useCallback(
(e: React.PointerEvent) => {
if (disable) return
if (!inputs.pointerIsValid(e)) return
if (e.button === 2) {
callbacks.onRightPointShape?.(inputs.pointerDown(e, id), e)
return
}
if (e.button !== 0) return
const info = inputs.pointerDown(e, id)
e.stopPropagation()
e.currentTarget?.setPointerCapture(e.pointerId)
// If we click "through" the selection bounding box to hit a shape that isn't selected,
// treat the event as a bounding box click. Unfortunately there's no way I know to pipe
// the event to the actual bounds background element.
if (
rSelectionBounds.current &&
Utils.pointInBounds(info.point, rSelectionBounds.current) &&
!rPageState.current.selectedIds.includes(id)
) {
callbacks.onPointBounds?.(inputs.pointerDown(e, 'bounds'), e)
callbacks.onPointShape?.(info, e)
return
}
callbacks.onPointShape?.(info, e)
callbacks.onPointerDown?.(info, e)
},
[inputs, callbacks, id, disable]
)
const onPointerUp = React.useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return
if (!inputs.pointerIsValid(e)) return
if (disable) return
e.stopPropagation()
const isDoubleClick = inputs.isDoubleClick()
const info = inputs.pointerUp(e, id)
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget?.releasePointerCapture(e.pointerId)
}
if (isDoubleClick && !(info.altKey || info.metaKey)) {
callbacks.onDoubleClickShape?.(info, e)
}
callbacks.onReleaseShape?.(info, e)
callbacks.onPointerUp?.(info, e)
},
[inputs, callbacks, id, disable]
)
const onPointerMove = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
if (disable) return
e.stopPropagation()
if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
const info = inputs.pointerMove(e, id)
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
callbacks.onDragShape?.(info, e)
}
callbacks.onPointerMove?.(info, e)
},
[inputs, callbacks, id, disable]
)
const onPointerEnter = React.useCallback(
(e: React.PointerEvent) => {
if (!inputs.pointerIsValid(e)) return
if (disable) return
const info = inputs.pointerEnter(e, id)
callbacks.onHoverShape?.(info, e)
},
[inputs, callbacks, id, disable]
)
const onPointerLeave = React.useCallback(
(e: React.PointerEvent) => {
if (disable) return
if (!inputs.pointerIsValid(e)) return
const info = inputs.pointerEnter(e, id)
callbacks.onUnhoverShape?.(info, e)
},
[inputs, callbacks, id, disable]
)
return {
onPointerDown,
onPointerUp,
onPointerEnter,
onPointerMove,
onPointerLeave,
}
}

Wyświetl plik

@ -1,161 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import type {
IShapeTreeNode,
TLPage,
TLPageState,
TLShape,
TLCallbacks,
TLBinding,
TLBounds,
} from '+types'
import { Utils } from '+utils'
import { Vec } from '@tldraw/vec'
import type { TLShapeUtilsMap } from '+shape-utils'
function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
shape: T,
branch: IShapeTreeNode<T, M>[],
shapes: TLPage<T, TLBinding>['shapes'],
pageState: {
bindingTargetId?: string | null
bindingId?: string | null
hoveredId?: string | null
selectedIds: string[]
currentParentId?: string | null
editingId?: string | null
editingBindingId?: string | null
},
meta?: M
) {
// Create a node for this shape
const node: IShapeTreeNode<T, M> = {
shape,
meta: meta as any,
isCurrentParent: pageState.currentParentId === shape.id,
isEditing: pageState.editingId === shape.id,
isBinding: pageState.bindingTargetId === shape.id,
isSelected: pageState.selectedIds.includes(shape.id),
isHovered:
// The shape is hovered..
pageState.hoveredId === shape.id ||
// Or the shape has children and...
(shape.children !== undefined &&
// One of the children is hovered
((pageState.hoveredId && shape.children.includes(pageState.hoveredId)) ||
// Or one of the children is selected
shape.children.some((childId) => pageState.selectedIds.includes(childId)))),
}
// Add the node to the branch
branch.push(node)
// If the shape has children, add nodes for each child to the node's children array
if (shape.children) {
node.children = []
shape.children
.map((id) => shapes[id])
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((childShape) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
addToShapeTree(childShape, node.children!, shapes, pageState, meta)
)
}
}
function shapeIsInViewport(bounds: TLBounds, viewport: TLBounds) {
return Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds)
}
export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
page: TLPage<T, TLBinding>,
pageState: TLPageState,
shapeUtils: TLShapeUtilsMap<T>,
size: number[],
meta?: M,
onRenderCountChange?: TLCallbacks<T>['onRenderCountChange']
) {
const rTimeout = React.useRef<unknown>()
const rPreviousCount = React.useRef(0)
const rShapesIdsToRender = React.useRef(new Set<string>())
const rShapesToRender = React.useRef(new Set<TLShape>())
const { selectedIds, camera } = pageState
// Filter the page's shapes down to only those that:
// - are the direct child of the page
// - collide with or are contained by the viewport
// - OR are selected
const [minX, minY] = Vec.sub(Vec.div([0, 0], camera.zoom), camera.point)
const [maxX, maxY] = Vec.sub(Vec.div(size, camera.zoom), camera.point)
const viewport = {
minX,
minY,
maxX,
maxY,
height: maxX - minX,
width: maxY - minY,
}
const shapesToRender = rShapesToRender.current
const shapesIdsToRender = rShapesIdsToRender.current
shapesToRender.clear()
shapesIdsToRender.clear()
Object.values(page.shapes)
.filter(
(shape) =>
// Always render shapes that are flagged as stateful
shapeUtils[shape.type as T['type']].isStateful ||
// Always render selected shapes (this preserves certain drag interactions)
selectedIds.includes(shape.id) ||
// Otherwise, only render shapes that are in view
shapeIsInViewport(shapeUtils[shape.type as T['type']].getBounds(shape as any), viewport)
)
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((shape) => {
// If the shape's parent is the page, add it to our sets of shapes to render
if (shape.parentId === page.id) {
shapesIdsToRender.add(shape.id)
shapesToRender.add(shape)
return
}
// If the shape's parent is a different shape (e.g. a group),
// add the parent to the sets of shapes to render. The parent's
// children will all be rendered.
shapesIdsToRender.add(shape.parentId)
shapesToRender.add(page.shapes[shape.parentId])
})
// Call onChange callback when number of rendering shapes changes
if (shapesToRender.size !== rPreviousCount.current) {
// Use a timeout to clear call stack, in case the onChange handler
// produces a new state change, which could cause nested state
// changes, which is bad in React.
if (rTimeout.current) {
clearTimeout(rTimeout.current as number)
}
rTimeout.current = requestAnimationFrame(() => {
onRenderCountChange?.(Array.from(shapesIdsToRender.values()))
})
rPreviousCount.current = shapesToRender.size
}
const bindingTargetId = pageState.bindingId ? page.bindings[pageState.bindingId].toId : undefined
// Populate the shape tree
const tree: IShapeTreeNode<T, M>[] = []
const info = { ...pageState, bindingTargetId }
shapesToRender.forEach((shape) => addToShapeTree(shape, tree, page.shapes, info, meta))
return tree
}

Wyświetl plik

@ -1,411 +0,0 @@
import * as React from 'react'
import type { TLTheme } from '+types'
const styles = new Map<string, HTMLStyleElement>()
type AnyTheme = Record<string, string>
function makeCssTheme<T = AnyTheme>(prefix: string, theme: T) {
return Object.keys(theme).reduce((acc, key) => {
const value = theme[key as keyof T]
if (value) {
return acc + `${`--${prefix}-${key}`}: ${value};\n`
}
return acc
}, '')
}
function useTheme<T = AnyTheme>(prefix: string, theme: T, selector = ':root') {
React.useLayoutEffect(() => {
const style = document.createElement('style')
const cssTheme = makeCssTheme(prefix, theme)
style.setAttribute('id', `${prefix}-theme`)
style.setAttribute('data-selector', selector)
style.innerHTML = `
${selector} {
${cssTheme}
}
`
document.head.appendChild(style)
return () => {
if (style && document.head.contains(style)) {
document.head.removeChild(style)
}
}
}, [prefix, theme, selector])
}
function useStyle(uid: string, rules: string) {
React.useLayoutEffect(() => {
if (styles.get(uid)) {
return () => void null
}
const style = document.createElement('style')
style.innerHTML = rules
style.setAttribute('id', uid)
document.head.appendChild(style)
styles.set(uid, style)
return () => {
if (style && document.head.contains(style)) {
document.head.removeChild(style)
styles.delete(uid)
}
}
}, [uid, rules])
}
const css = (strings: TemplateStringsArray, ...args: unknown[]) =>
strings.reduce(
(acc, string, index) => acc + string + (index < args.length ? args[index] : ''),
''
)
const defaultTheme: TLTheme = {
accent: 'rgb(255, 0, 0)',
brushFill: 'rgba(0,0,0,.05)',
brushStroke: 'rgba(0,0,0,.25)',
selectStroke: 'rgb(66, 133, 244)',
selectFill: 'rgba(65, 132, 244, 0.05)',
background: 'rgb(248, 249, 250)',
foreground: 'rgb(51, 51, 51)',
}
const tlcss = css`
@font-face {
font-family: 'Recursive';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Recursive';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Recursive Mono';
font-style: normal;
font-weight: 420;
font-display: swap;
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
.tl-container {
--tl-zoom: 1;
--tl-scale: calc(1 / var(--tl-zoom));
--tl-padding: calc(64px * max(1, var(--tl-scale)));
position: relative;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
box-sizing: border-box;
padding: 0px;
margin: 0px;
z-index: 100;
overflow: hidden;
touch-action: none;
overscroll-behavior: none;
background-color: var(--tl-background);
}
.tl-container * {
box-sizing: border-box;
}
.tl-overlay {
position: absolute;
width: 100%;
height: 100%;
touch-action: none;
pointer-events: none;
}
.tl-snap-line {
stroke: var(--tl-accent);
stroke-width: calc(1px * var(--tl-scale));
}
.tl-snap-point {
stroke: var(--tl-accent);
stroke-width: calc(1px * var(--tl-scale));
}
.tl-canvas {
position: absolute;
width: 100%;
height: 100%;
touch-action: none;
pointer-events: all;
overflow: clip;
}
.tl-layer {
position: absolute;
top: 0px;
left: 0px;
height: 0px;
width: 0px;
contain: layout style size;
}
.tl-absolute {
position: absolute;
top: 0px;
left: 0px;
transform-origin: center center;
contain: layout style size;
}
.tl-positioned {
position: absolute;
top: 0px;
left: 0px;
transform-origin: center center;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
contain: layout style size;
}
.tl-positioned-svg {
width: 100%;
height: 100%;
overflow: hidden;
contain: layout style size;
}
.tl-positioned-div {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
padding: var(--tl-padding);
overflow: hidden;
contain: layout style size;
}
.tl-counter-scaled {
transform: scale(var(--tl-scale));
}
.tl-dashed {
stroke-dasharray: calc(2px * var(--tl-scale)), calc(2px * var(--tl-scale));
}
.tl-transparent {
fill: transparent;
stroke: transparent;
}
.tl-cursor-ns {
cursor: ns-resize;
}
.tl-cursor-ew {
cursor: ew-resize;
}
.tl-cursor-nesw {
cursor: nesw-resize;
}
.tl-cursor-nwse {
cursor: nwse-resize;
}
.tl-corner-handle {
stroke: var(--tl-selectStroke);
fill: var(--tl-background);
stroke-width: calc(1.5px * var(--tl-scale));
}
.tl-rotate-handle {
stroke: var(--tl-selectStroke);
fill: var(--tl-background);
stroke-width: calc(1.5px * var(--tl-scale));
cursor: grab;
}
.tl-binding {
fill: var(--tl-selectFill);
stroke: var(--tl-selectStroke);
stroke-width: calc(1px * var(--tl-scale));
pointer-events: none;
}
.tl-user {
left: -4px;
top: -4px;
height: 8px;
width: 8px;
border-radius: 100%;
pointer-events: none;
}
.tl-indicator {
fill: transparent;
stroke-width: calc(1.5px * var(--tl-scale));
pointer-events: none;
}
.tl-user-indicator-bounds {
border-style: solid;
border-width: calc(1px * var(--tl-scale));
}
.tl-selected {
stroke: var(--tl-selectStroke);
}
.tl-hovered {
stroke: var(--tl-selectStroke);
}
.tl-clone-target {
pointer-events: all;
}
.tl-clone-target:hover .tl-clone-button {
opacity: 1;
}
.tl-clone-button-target {
cursor: pointer;
pointer-events: all;
}
.tl-clone-button-target:hover .tl-clone-button {
fill: var(--tl-selectStroke);
}
.tl-clone-button {
opacity: 0;
r: calc(8px * var(--tl-scale));
stroke-width: calc(1.5px * var(--tl-scale));
stroke: var(--tl-selectStroke);
fill: var(--tl-background);
}
.tl-bounds {
pointer-events: none;
contain: layout style size;
}
.tl-bounds-bg {
stroke: none;
fill: var(--tl-selectFill);
pointer-events: all;
contain: layout style size;
}
.tl-bounds-center {
fill: transparent;
stroke: var(--tl-selectStroke);
stroke-width: calc(1.5px * var(--tl-scale));
}
.tl-brush {
fill: var(--tl-brushFill);
stroke: var(--tl-brushStroke);
stroke-width: calc(1px * var(--tl-scale));
pointer-events: none;
contain: layout style size;
}
.tl-dot {
fill: var(--tl-background);
stroke: var(--tl-foreground);
stroke-width: 2px;
}
.tl-handle {
pointer-events: all;
}
.tl-handle:hover .tl-handle-bg {
fill: var(--tl-selectFill);
}
.tl-handle:hover .tl-handle-bg > * {
stroke: var(--tl-selectFill);
}
.tl-handle:active .tl-handle-bg {
fill: var(--tl-selectFill);
}
.tl-handle:active .tl-handle-bg > * {
stroke: var(--tl-selectFill);
}
.tl-handle {
fill: var(--tl-background);
stroke: var(--tl-selectStroke);
stroke-width: 1.5px;
}
.tl-handle-bg {
fill: transparent;
stroke: none;
pointer-events: all;
r: calc(16px / max(1, var(--tl-zoom)));
}
.tl-binding-indicator {
stroke-width: calc(3px * var(--tl-scale));
fill: var(--tl-selectFill);
stroke: var(--tl-selected);
}
.tl-centered-g {
transform: translate(var(--tl-padding), var(--tl-padding));
}
.tl-current-parent > *[data-shy='true'] {
opacity: 1;
}
.tl-binding {
fill: none;
stroke: var(--tl-selectStroke);
stroke-width: calc(2px * var(--tl-scale));
}
`
export function useTLTheme(theme?: Partial<TLTheme>, selector?: string) {
const tltheme = React.useMemo<TLTheme>(
() => ({
...defaultTheme,
...theme,
}),
[theme]
)
useTheme('tl', tltheme, selector)
useStyle('tl-canvas', tlcss)
}

Wyświetl plik

@ -1,22 +0,0 @@
import * as React from 'react'
import type { Inputs } from '+inputs'
import type { TLCallbacks, TLShape, TLBounds, TLPageState } from '+types'
import type { TLShapeUtilsMap } from '+shape-utils'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface TLContextType<T extends TLShape> {
id?: string
callbacks: Partial<TLCallbacks<T>>
shapeUtils: TLShapeUtilsMap<T>
rPageState: React.MutableRefObject<TLPageState>
rSelectionBounds: React.MutableRefObject<TLBounds | null>
inputs: Inputs
}
export const TLContext = React.createContext({} as TLContextType<TLShape>)
export function useTLContext() {
const context = React.useContext(TLContext)
return context
}

Wyświetl plik

@ -1,112 +0,0 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import * as React from 'react'
import { useTLContext } from './useTLContext'
import { useGesture } from '@use-gesture/react'
import { Vec } from '@tldraw/vec'
// Capture zoom gestures (pinches, wheels and pans)
export function useZoomEvents<T extends HTMLElement>(zoom: number, ref: React.RefObject<T>) {
const rOriginPoint = React.useRef<number[] | undefined>(undefined)
const rPinchPoint = React.useRef<number[] | undefined>(undefined)
const rDelta = React.useRef<number[]>([0, 0])
const { inputs, callbacks } = useTLContext()
React.useEffect(() => {
const preventGesture = (event: TouchEvent) => {
event.preventDefault()
}
// @ts-ignore
document.addEventListener('gesturestart', preventGesture)
// @ts-ignore
document.addEventListener('gesturechange', preventGesture)
return () => {
// @ts-ignore
document.removeEventListener('gesturestart', preventGesture)
// @ts-ignore
document.removeEventListener('gesturechange', preventGesture)
}
}, [])
useGesture(
{
onWheel: ({ delta, event: e }) => {
if (e.altKey && e.buttons === 0) {
const point = inputs.pointer?.point ?? [inputs.bounds.width / 2, inputs.bounds.height / 2]
const info = inputs.pinch(point, point)
callbacks.onZoom?.({ ...info, delta: [...point, -e.deltaY] }, e)
return
}
e.preventDefault()
if (inputs.isPinching) return
if (Vec.isEqual(delta, [0, 0])) return
const info = inputs.pan(delta, e as WheelEvent)
callbacks.onPan?.(info, e)
},
onPinchStart: ({ origin, event }) => {
const elm = ref.current
if (!elm || !(event.target === elm || elm.contains(event.target as Node))) return
const info = inputs.pinch(origin, origin)
inputs.isPinching = true
callbacks.onPinchStart?.(info, event)
rPinchPoint.current = info.point
rOriginPoint.current = info.origin
rDelta.current = [0, 0]
},
onPinchEnd: ({ origin, event }) => {
const elm = ref.current
if (!(event.target === elm || elm?.contains(event.target as Node))) return
const info = inputs.pinch(origin, origin)
inputs.isPinching = false
callbacks.onPinchEnd?.(info, event)
rPinchPoint.current = undefined
rOriginPoint.current = undefined
rDelta.current = [0, 0]
},
onPinch: ({ origin, offset, event }) => {
const elm = ref.current
if (!(event.target === elm || elm?.contains(event.target as Node))) return
if (!rOriginPoint.current) return
const info = inputs.pinch(origin, rOriginPoint.current)
const trueDelta = Vec.sub(info.delta, rDelta.current)
rDelta.current = info.delta
callbacks.onPinch?.(
{
...info,
point: info.point,
origin: rOriginPoint.current,
delta: [...trueDelta, offset[0]],
},
event
)
rPinchPoint.current = origin
},
},
{
target: ref,
eventOptions: { passive: false },
pinch: {
from: zoom,
scaleBounds: () => ({ from: inputs.zoom, max: 5, min: 0.1 }),
},
}
)
}

Wyświetl plik

@ -1,5 +0,0 @@
export * from './components'
export * from './types'
export * from './utils'
export * from './inputs'
export * from './shape-utils'

Wyświetl plik

@ -1,392 +0,0 @@
import type React from 'react'
import type { TLKeyboardInfo, TLPointerInfo } from './types'
import { Utils } from './utils'
import { Vec } from '@tldraw/vec'
import type { TLBounds } from '+index'
const DOUBLE_CLICK_DURATION = 250
export class Inputs {
pointer?: TLPointerInfo<string>
keyboard?: TLKeyboardInfo
keys: Record<string, boolean> = {}
isPinching = false
bounds: TLBounds = {
minX: 0,
maxX: 640,
minY: 0,
maxY: 480,
width: 640,
height: 480,
}
zoom = 1
pointerUpTime = 0
activePointer?: number
pointerIsValid(e: TouchEvent | React.TouchEvent | PointerEvent | React.PointerEvent) {
if ('pointerId' in e) {
if (this.activePointer && this.activePointer !== e.pointerId) return false
}
if ('touches' in e) {
const touch = e.changedTouches[0]
if (this.activePointer && this.activePointer !== touch.identifier) return false
}
return true
}
touchStart<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const touch = e.changedTouches[0]
this.activePointer = touch.identifier
const info: TLPointerInfo<T> = {
target,
pointerId: touch.identifier,
origin: Inputs.getPoint(touch, this.bounds),
delta: [0, 0],
point: Inputs.getPoint(touch, this.bounds),
pressure: Inputs.getPressure(touch),
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
spaceKey: this.keys[' '],
}
this.pointer = info
return info
}
touchEnd<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const touch = e.changedTouches[0]
const info: TLPointerInfo<T> = {
target,
pointerId: touch.identifier,
origin: Inputs.getPoint(touch, this.bounds),
delta: [0, 0],
point: Inputs.getPoint(touch, this.bounds),
pressure: Inputs.getPressure(touch),
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
spaceKey: this.keys[' '],
}
this.pointer = info
this.activePointer = undefined
return info
}
touchMove<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const touch = e.changedTouches[0]
const prev = this.pointer
const point = Inputs.getPoint(touch, this.bounds)
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
const info: TLPointerInfo<T> = {
origin: point,
...prev,
target,
pointerId: touch.identifier,
point,
delta,
pressure: Inputs.getPressure(touch),
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
spaceKey: this.keys[' '],
}
this.pointer = info
return info
}
pointerDown<T extends string>(e: PointerEvent | React.PointerEvent, target: T): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const point = Inputs.getPoint(e, this.bounds)
this.activePointer = e.pointerId
const info: TLPointerInfo<T> = {
target,
pointerId: e.pointerId,
origin: point,
point: point,
delta: [0, 0],
pressure: Inputs.getPressure(e),
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
spaceKey: this.keys[' '],
}
this.pointer = info
return info
}
pointerEnter<T extends string>(
e: PointerEvent | React.PointerEvent,
target: T
): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const point = Inputs.getPoint(e, this.bounds)
const info: TLPointerInfo<T> = {
target,
pointerId: e.pointerId,
origin: point,
delta: [0, 0],
point: point,
pressure: Inputs.getPressure(e),
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
spaceKey: this.keys[' '],
}
this.pointer = info
return info
}
pointerMove<T extends string>(e: PointerEvent | React.PointerEvent, target: T): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const prev = this.pointer
const point = Inputs.getPoint(e, this.bounds)
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
const info: TLPointerInfo<T> = {
origin: point,
...prev,
target,
pointerId: e.pointerId,
point,
delta,
pressure: Inputs.getPressure(e),
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
spaceKey: this.keys[' '],
}
this.pointer = info
return info
}
pointerUp<T extends string>(e: PointerEvent | React.PointerEvent, target: T): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const prev = this.pointer
const point = Inputs.getPoint(e, this.bounds)
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
this.activePointer = undefined
const info: TLPointerInfo<T> = {
origin: point,
...prev,
target,
pointerId: e.pointerId,
point,
delta,
pressure: Inputs.getPressure(e),
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
spaceKey: this.keys[' '],
}
this.pointer = info
this.pointerUpTime = Date.now()
return info
}
panStart = (e: WheelEvent): TLPointerInfo<'wheel'> => {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const info: TLPointerInfo<'wheel'> = {
target: 'wheel',
pointerId: this.pointer?.pointerId || 0,
origin: this.pointer?.origin || [0, 0],
delta: [0, 0],
pressure: 0.5,
point: Inputs.getPoint(e, this.bounds),
shiftKey,
ctrlKey,
metaKey,
altKey,
spaceKey: this.keys[' '],
}
this.pointer = info
return info
}
pan = (delta: number[], e: WheelEvent): TLPointerInfo<'wheel'> => {
if (!this.pointer || this.pointer.target !== 'wheel') {
return this.panStart(e)
}
const { shiftKey, ctrlKey, metaKey, altKey } = e
const prev = this.pointer
const point = Inputs.getPoint(e, this.bounds)
const info: TLPointerInfo<'wheel'> = {
...prev,
target: 'wheel',
delta,
point,
shiftKey,
ctrlKey,
metaKey,
altKey,
spaceKey: this.keys[' '],
}
this.pointer = info
return info
}
isDoubleClick() {
if (!this.pointer) return false
const { origin, point } = this.pointer
return Date.now() - this.pointerUpTime < DOUBLE_CLICK_DURATION && Vec.dist(origin, point) < 4
}
clear() {
this.pointer = undefined
}
resetDoubleClick() {
this.pointerUpTime = 0
}
keydown = (e: KeyboardEvent | React.KeyboardEvent): TLKeyboardInfo => {
const { shiftKey, ctrlKey, metaKey, altKey } = e
this.keys[e.key] = true
return {
point: this.pointer?.point || [0, 0],
origin: this.pointer?.origin || [0, 0],
key: e.key,
keys: Object.keys(this.keys),
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
}
}
keyup = (e: KeyboardEvent | React.KeyboardEvent): TLKeyboardInfo => {
const { shiftKey, ctrlKey, metaKey, altKey } = e
delete this.keys[e.key]
return {
point: this.pointer?.point || [0, 0],
origin: this.pointer?.origin || [0, 0],
key: e.key,
keys: Object.keys(this.keys),
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
}
}
pinch(point: number[], origin: number[]) {
const { shiftKey, ctrlKey, metaKey, altKey } = this.keys
const delta = Vec.sub(origin, point)
const info: TLPointerInfo<'pinch'> = {
pointerId: 0,
target: 'pinch',
origin,
delta: delta,
point: Vec.sub(Vec.round(point), [this.bounds.minX, this.bounds.minY]),
pressure: 0.5,
shiftKey,
ctrlKey,
metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
altKey,
spaceKey: this.keys[' '],
}
this.pointer = info
return info
}
reset() {
this.pointerUpTime = 0
this.pointer = undefined
this.keyboard = undefined
this.activePointer = undefined
this.keys = {}
}
static getPoint(
e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent,
bounds: TLBounds
): number[] {
return [+e.clientX.toFixed(2) - bounds.minX, +e.clientY.toFixed(2) - bounds.minY]
}
static getPressure(e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent) {
return 'pressure' in e ? +e.pressure.toFixed(2) || 0.5 : 0.5
}
static commandKey(): string {
return Utils.isDarwin() ? '⌘' : 'Ctrl'
}
}
export const inputs = new Inputs()

Wyświetl plik

@ -1,234 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react'
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { TLShape, TLBounds, TLComponentProps } from '+types'
import { TLShapeUtil } from './TLShapeUtil'
import { render } from '@testing-library/react'
import { SVGContainer } from '+components'
import Utils from '+utils'
import type { TLIndicator } from '+shape-utils'
export interface BoxShape extends TLShape {
type: 'box'
size: number[]
}
const meta = { legs: 93 }
type Meta = typeof meta
export const boxShape: BoxShape = {
id: 'example1',
type: 'box',
parentId: 'page',
childIndex: 0,
name: 'Example Shape',
point: [0, 0],
size: [100, 100],
rotation: 0,
}
export class BoxUtil extends TLShapeUtil<BoxShape, SVGSVGElement, Meta> {
age = 100
Component = React.forwardRef<SVGSVGElement, TLComponentProps<BoxShape, SVGSVGElement>>(
({ shape, events, meta }, ref) => {
type T = typeof meta.legs
type C = T['toFixed']
return (
<SVGContainer ref={ref}>
<g {...events}>
<rect width={shape.size[0]} height={shape.size[1]} fill="none" stroke="black" />
</g>
</SVGContainer>
)
}
)
Indicator: TLIndicator<BoxShape, SVGSVGElement, Meta> = ({ shape }) => {
return (
<SVGContainer>
<rect width={shape.size[0]} height={shape.size[1]} fill="none" stroke="black" />
</SVGContainer>
)
}
getBounds = (shape: BoxShape) => {
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
const [width, height] = shape.size
return {
minX: 0,
maxX: width,
minY: 0,
maxY: height,
width,
height,
} as TLBounds
})
return Utils.translateBounds(bounds, shape.point)
}
}
describe('When creating a minimal ShapeUtil', () => {
const Box = new BoxUtil()
it('creates a shape utils', () => {
expect(Box).toBeTruthy()
})
test('accesses this in an override method', () => {
expect(Box.shouldRender(boxShape, { ...boxShape, point: [1, 1] })).toBe(true)
})
test('mounts component without crashing', () => {
const ref = React.createRef<SVGSVGElement>()
const ref2 = React.createRef<HTMLDivElement>()
const H = React.forwardRef<HTMLDivElement, { message: string }>((props, ref) => {
return <div ref={ref2}>{props.message}</div>
})
render(<H message="Hello" />)
render(
<Box.Component
ref={ref}
shape={boxShape}
isEditing={false}
isBinding={false}
isHovered={false}
isSelected={false}
isCurrentParent={false}
meta={{} as any}
events={{} as any}
/>
)
})
})
abstract class TLRealisticShapeUtil<
T extends TLShape,
E extends Element = any,
M = any
> extends TLShapeUtil<T, E, M> {
abstract type: T['type']
abstract getShape: (props: Partial<T>) => T
create = (props: { id: string } & Partial<T>) => {
this.refMap.set(props.id, React.createRef())
return this.getShape(props)
}
}
describe('When creating a realistic API around TLShapeUtil', () => {
class ExtendedBoxUtil extends TLRealisticShapeUtil<BoxShape, SVGSVGElement, Meta> {
type = 'box' as const
age = 100
Component = React.forwardRef<SVGSVGElement, TLComponentProps<BoxShape, SVGSVGElement>>(
({ shape, events, meta }, ref) => {
type T = typeof meta.legs
type C = T['toFixed']
return (
<SVGContainer ref={ref}>
<g {...events}>
<rect width={shape.size[0]} height={shape.size[1]} fill="none" stroke="black" />
</g>
</SVGContainer>
)
}
)
Indicator: TLIndicator<BoxShape, SVGSVGElement, Meta> = ({ shape }) => {
return (
<SVGContainer>
<rect width={shape.size[0]} height={shape.size[1]} fill="none" stroke="black" />
</SVGContainer>
)
}
getShape = (props: Partial<BoxShape>): BoxShape => ({
id: 'example1',
type: 'box',
parentId: 'page',
childIndex: 0,
name: 'Example Shape',
point: [0, 0],
size: [100, 100],
rotation: 0,
...props,
})
getBounds = (shape: BoxShape) => {
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
const [width, height] = shape.size
return {
minX: 0,
maxX: width,
minY: 0,
maxY: height,
width,
height,
} as TLBounds
})
return Utils.translateBounds(bounds, shape.point)
}
}
const Box = new ExtendedBoxUtil()
it('creates a shape utils', () => {
expect(Box).toBeTruthy()
})
it('creates a shape utils with extended properties', () => {
expect(Box.age).toBe(100)
})
it('creates a shape', () => {
expect(Box.create({ id: 'box1' })).toStrictEqual({
...boxShape,
id: 'box1',
})
})
test('accesses this in an override method', () => {
expect(Box.shouldRender(boxShape, { ...boxShape, point: [1, 1] })).toBe(true)
})
test('mounts component without crashing', () => {
const box = Box.create({ id: 'box1' })
const ref = React.createRef<SVGSVGElement>()
const ref2 = React.createRef<HTMLDivElement>()
const H = React.forwardRef<HTMLDivElement, { message: string }>((props, ref) => {
return <div ref={ref2}>{props.message}</div>
})
render(<H message="Hello" />)
render(
<Box.Component
ref={ref}
shape={box}
isEditing={false}
isBinding={false}
isHovered={false}
isSelected={false}
isCurrentParent={false}
meta={meta}
events={{} as any}
/>
)
})
})

Wyświetl plik

@ -1,76 +0,0 @@
/* 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, TLShape } from 'types'
export interface TLIndicator<T extends TLShape, E extends Element = any, M = any> {
(
this: TLShapeUtil<T, E, M>,
props: { shape: T; meta: M; isHovered: boolean; isSelected: boolean }
): React.ReactElement | null
}
export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M = any> {
refMap = new Map<string, React.RefObject<E>>()
boundsCache = new WeakMap<TLShape, TLBounds>()
canEdit = false
canBind = false
canClone = false
showBounds = true
isStateful = false
isAspectRatioLocked = false
abstract Component: React.ForwardRefExoticComponent<TLComponentProps<T, E, M>>
abstract Indicator: TLIndicator<T, E, M>
shouldRender: (prev: T, next: T) => boolean = () => true
getRef = (shape: T): React.RefObject<E> => {
if (!this.refMap.has(shape.id)) {
this.refMap.set(shape.id, React.createRef<E>())
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.refMap.get(shape.id)!
}
hitTest = (shape: T, point: number[]): boolean => {
const bounds = this.getBounds(shape)
return shape.rotation
? Utils.pointInPolygon(point, Utils.getRotatedCorners(bounds, shape.rotation))
: Utils.pointInBounds(point, bounds)
}
hitTestBounds = (shape: T, bounds: TLBounds) => {
const shapeBounds = this.getBounds(shape)
if (!shape.rotation) {
return (
Utils.boundsContain(bounds, shapeBounds) ||
Utils.boundsContain(shapeBounds, bounds) ||
Utils.boundsCollide(shapeBounds, bounds)
)
}
const corners = Utils.getRotatedCorners(shapeBounds, shape.rotation)
return (
corners.every((point) => Utils.pointInBounds(point, bounds)) ||
intersectPolylineBounds(corners, bounds).length > 0
)
}
abstract getBounds: (shape: T) => TLBounds
getRotatedBounds: (shape: T) => TLBounds = (shape) => {
return Utils.getBoundsFromPoints(Utils.getRotatedCorners(this.getBounds(shape), shape.rotation))
}
}

Wyświetl plik

@ -1,8 +0,0 @@
import type { TLShape } from '+types'
import type { TLShapeUtil } from './TLShapeUtil'
export type TLShapeUtilsMap<T extends TLShape> = {
[K in T['type']]: TLShapeUtil<Extract<T, { type: K }>>
}
export * from './TLShapeUtil'

Wyświetl plik

@ -1,23 +0,0 @@
import * as React from 'react'
import type { TLPageState, TLBounds } from '../types'
import { mockDocument } from './mockDocument'
import { mockUtils } from './mockUtils'
import { useTLTheme, TLContext } from '../hooks'
import { Inputs } from '+inputs'
export const ContextWrapper: React.FC = ({ children }) => {
useTLTheme()
const rSelectionBounds = React.useRef<TLBounds>(null)
const rPageState = React.useRef<TLPageState>(mockDocument.pageState)
const [context] = React.useState(() => ({
callbacks: {},
shapeUtils: mockUtils,
rSelectionBounds,
rPageState,
inputs: new Inputs(),
}))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return <TLContext.Provider value={context as any}>{children}</TLContext.Provider>
}

Wyświetl plik

@ -1,4 +0,0 @@
export * from './mockDocument'
export * from './mockUtils'
export * from './renderWithContext'
export * from './renderWithSvg'

Wyświetl plik

@ -1,19 +0,0 @@
import type { BoxShape } from '+shape-utils/TLShapeUtil.spec'
import type { TLBinding, TLPage, TLPageState } from '+types'
export const mockDocument: { page: TLPage<BoxShape, TLBinding>; pageState: TLPageState } = {
page: {
id: 'page1',
shapes: {},
bindings: {},
},
pageState: {
id: 'page1',
selectedIds: [],
currentParentId: 'page1',
camera: {
point: [0, 0],
zoom: 1,
},
},
}

Wyświetl plik

@ -1,5 +0,0 @@
import { BoxUtil } from '+shape-utils/TLShapeUtil.spec'
export const mockUtils = {
box: new BoxUtil(),
}

Wyświetl plik

@ -1,7 +0,0 @@
import * as React from 'react'
import { render } from '@testing-library/react'
import { ContextWrapper } from './context-wrapper'
export const renderWithContext = (children: JSX.Element) => {
return render(<ContextWrapper>{children}</ContextWrapper>)
}

Wyświetl plik

@ -1,11 +0,0 @@
import * as React from 'react'
import { render } from '@testing-library/react'
import { ContextWrapper } from './context-wrapper'
export const renderWithSvg = (children: JSX.Element) => {
return render(
<ContextWrapper>
<svg>{children}</svg>
</ContextWrapper>
)
}

Wyświetl plik

@ -1,361 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* --------------------- Primary -------------------- */
import type React from 'react'
export type Patch<T> = Partial<{ [P in keyof T]: T | Partial<T> | Patch<T[P]> }>
export type TLForwardedRef<T> =
| ((instance: T | null) => void)
| React.MutableRefObject<T | null>
| null
export interface TLPage<T extends TLShape, B extends TLBinding> {
id: string
name?: string
childIndex?: number
shapes: Record<string, T>
bindings: Record<string, B>
}
export interface TLPageState {
id: string
selectedIds: string[]
camera: {
point: number[]
zoom: number
}
brush?: TLBounds | null
pointedId?: string | null
hoveredId?: string | null
editingId?: string | null
bindingId?: string | null
currentParentId?: string | null
}
export interface TLUser<T extends TLShape> {
id: string
color: string
point: number[]
selectedIds: string[]
activeShapes: T[]
}
export type TLUsers<T extends TLShape, U extends TLUser<T> = TLUser<T>> = Record<string, U>
export type TLSnapLine = number[][]
export interface TLHandle {
id: string
index: number
point: number[]
canBind?: boolean
bindingId?: string
}
export interface TLShape {
id: string
type: string
parentId: string
childIndex: number
name: string
point: number[]
rotation?: number
children?: string[]
handles?: Record<string, TLHandle>
isLocked?: boolean
isHidden?: boolean
isEditing?: boolean
isGenerated?: boolean
isAspectRatioLocked?: boolean
}
export interface TLComponentProps<T extends TLShape, E = any, M = any> {
shape: T
isEditing: boolean
isBinding: boolean
isHovered: boolean
isSelected: boolean
isCurrentParent: boolean
meta: M extends any ? M : never
onShapeChange?: TLCallbacks<T>['onShapeChange']
onShapeBlur?: TLCallbacks<T>['onShapeBlur']
events: {
onPointerDown: (e: React.PointerEvent<E>) => void
onPointerUp: (e: React.PointerEvent<E>) => void
onPointerEnter: (e: React.PointerEvent<E>) => void
onPointerMove: (e: React.PointerEvent<E>) => void
onPointerLeave: (e: React.PointerEvent<E>) => void
}
ref?: React.Ref<E> | undefined
}
export interface TLShapeProps<T extends TLShape, E = any, M = any>
extends TLComponentProps<T, E, M> {
ref: TLForwardedRef<E>
shape: T
}
export interface TLTool {
id: string
name: string
}
export interface TLBinding<M = any> {
id: string
type: string
toId: string
fromId: string
meta: M
}
export interface TLTheme {
accent?: string
brushFill?: string
brushStroke?: string
selectFill?: string
selectStroke?: string
background?: string
foreground?: string
}
export type TLWheelEventHandler = (
info: TLPointerInfo<string>,
e: React.WheelEvent<Element> | WheelEvent
) => void
export type TLPinchEventHandler = (
info: TLPointerInfo<string>,
e:
| React.WheelEvent<Element>
| WheelEvent
| React.TouchEvent<Element>
| TouchEvent
| React.PointerEvent<Element>
| PointerEventInit
) => void
export type TLShapeChangeHandler<T, K = any> = (
shape: { id: string } & Partial<T>,
info?: K
) => void
export type TLShapeBlurHandler<K = any> = (info?: K) => void
export type TLKeyboardEventHandler = (key: string, info: TLKeyboardInfo, e: KeyboardEvent) => void
export type TLPointerEventHandler = (info: TLPointerInfo<string>, e: React.PointerEvent) => void
export type TLShapeCloneHandler = (
info: TLPointerInfo<
'top' | 'right' | 'bottom' | 'left' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'
>,
e: React.PointerEvent
) => void
export type TLShapeLinkHandler = (info: TLPointerInfo<'link'>, e: React.PointerEvent) => void
export type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.PointerEvent) => void
export type TLBoundsEventHandler = (info: TLPointerInfo<'bounds'>, e: React.PointerEvent) => void
export type TLBoundsHandleEventHandler = (
info: TLPointerInfo<TLBoundsHandle>,
e: React.PointerEvent
) => void
export interface TLCallbacks<T extends TLShape> {
// Camera events
onPinchStart: TLPinchEventHandler
onPinchEnd: TLPinchEventHandler
onPinch: TLPinchEventHandler
onPan: TLWheelEventHandler
onZoom: TLWheelEventHandler
// Pointer Events
onPointerMove: TLPointerEventHandler
onPointerUp: TLPointerEventHandler
onPointerDown: TLPointerEventHandler
// Canvas (background)
onPointCanvas: TLCanvasEventHandler
onDoubleClickCanvas: TLCanvasEventHandler
onRightPointCanvas: TLCanvasEventHandler
onDragCanvas: TLCanvasEventHandler
onReleaseCanvas: TLCanvasEventHandler
// Shape
onPointShape: TLPointerEventHandler
onDoubleClickShape: TLPointerEventHandler
onRightPointShape: TLPointerEventHandler
onDragShape: TLPointerEventHandler
onHoverShape: TLPointerEventHandler
onUnhoverShape: TLPointerEventHandler
onReleaseShape: TLPointerEventHandler
// Bounds (bounding box background)
onPointBounds: TLBoundsEventHandler
onDoubleClickBounds: TLBoundsEventHandler
onRightPointBounds: TLBoundsEventHandler
onDragBounds: TLBoundsEventHandler
onHoverBounds: TLBoundsEventHandler
onUnhoverBounds: TLBoundsEventHandler
onReleaseBounds: TLBoundsEventHandler
// Bounds handles (corners, edges)
onPointBoundsHandle: TLBoundsHandleEventHandler
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler
onRightPointBoundsHandle: TLBoundsHandleEventHandler
onDragBoundsHandle: TLBoundsHandleEventHandler
onHoverBoundsHandle: TLBoundsHandleEventHandler
onUnhoverBoundsHandle: TLBoundsHandleEventHandler
onReleaseBoundsHandle: TLBoundsHandleEventHandler
// Handles (ie the handles of a selected arrow)
onPointHandle: TLPointerEventHandler
onDoubleClickHandle: TLPointerEventHandler
onRightPointHandle: TLPointerEventHandler
onDragHandle: TLPointerEventHandler
onHoverHandle: TLPointerEventHandler
onUnhoverHandle: TLPointerEventHandler
onReleaseHandle: TLPointerEventHandler
// Misc
onShapeChange: TLShapeChangeHandler<T, any>
onShapeBlur: TLShapeBlurHandler<any>
onShapeClone: TLShapeCloneHandler
onRenderCountChange: (ids: string[]) => void
onError: (error: Error) => void
onBoundsChange: (bounds: TLBounds) => void
// Keyboard event handlers
onKeyDown: TLKeyboardEventHandler
onKeyUp: TLKeyboardEventHandler
}
export interface TLBounds {
minX: number
minY: number
maxX: number
maxY: number
width: number
height: number
rotation?: number
}
export interface TLBoundsWithCenter extends TLBounds {
midX: number
midY: number
}
export type TLIntersection = {
didIntersect: boolean
message: string
points: number[][]
}
export enum TLBoundsEdge {
Top = 'top_edge',
Right = 'right_edge',
Bottom = 'bottom_edge',
Left = 'left_edge',
}
export enum TLBoundsCorner {
TopLeft = 'top_left_corner',
TopRight = 'top_right_corner',
BottomRight = 'bottom_right_corner',
BottomLeft = 'bottom_left_corner',
}
export type TLBoundsHandle = TLBoundsCorner | TLBoundsEdge | 'rotate' | 'center' | 'left' | 'right'
export interface TLPointerInfo<T extends string = string> {
target: T
pointerId: number
origin: number[]
point: number[]
delta: number[]
pressure: number
shiftKey: boolean
ctrlKey: boolean
metaKey: boolean
altKey: boolean
spaceKey: boolean
}
export interface TLKeyboardInfo {
origin: number[]
point: number[]
key: string
keys: string[]
shiftKey: boolean
ctrlKey: boolean
metaKey: boolean
altKey: boolean
}
export interface TLTransformInfo<T extends TLShape> {
type: TLBoundsEdge | TLBoundsCorner
initialShape: T
scaleX: number
scaleY: number
transformOrigin: number[]
}
export interface TLBezierCurveSegment {
start: number[]
tangentStart: number[]
normalStart: number[]
pressureStart: number
end: number[]
tangentEnd: number[]
normalEnd: number[]
pressureEnd: number
}
export enum SnapPoints {
minX = 'minX',
midX = 'midX',
maxX = 'maxX',
minY = 'minY',
midY = 'midY',
maxY = 'maxY',
}
export type Snap =
| { id: SnapPoints; isSnapped: false }
| {
id: SnapPoints
isSnapped: true
to: number
B: TLBoundsWithCenter
distance: number
}
/* -------------------- Internal -------------------- */
export interface IShapeTreeNode<T extends TLShape, M = any> {
shape: T
children?: IShapeTreeNode<TLShape, M>[]
isEditing: boolean
isBinding: boolean
isHovered: boolean
isSelected: boolean
isCurrentParent: boolean
meta?: M extends any ? M : never
}
/* -------------------------------------------------- */
/* Utility Types */
/* -------------------------------------------------- */
export type MappedByType<K extends string, T extends { type: K }> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[P in T['type']]: T extends any ? (P extends T['type'] ? T : never) : never
}
export type RequiredKeys<T> = {
[K in keyof T]-?: Record<string, unknown> extends Pick<T, K> ? never : K
}[keyof T]

Wyświetl plik

@ -1,5 +0,0 @@
import { Utils } from './utils';
export { Utils } from './utils';
export { Svg } from './svg';
export default Utils;
//# sourceMappingURL=index.d.ts.map

Wyświetl plik

@ -1,4 +0,0 @@
import { Utils } from './utils'
export { Utils } from './utils'
export default Utils

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