kopia lustrzana https://github.com/Tldraw/Tldraw
Removes core (off to its own repo)
rodzic
0e9e45734a
commit
599e6032a9
11
package.json
11
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
|
@ -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).
|
|
@ -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"
|
||||
}
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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'} />)
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './binding'
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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" />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
export * from './bounds'
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
export * from './brush'
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './canvas'
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
export * from './container'
|
|
@ -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,
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './error-boundary'
|
|
@ -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} />)
|
||||
})
|
||||
})
|
|
@ -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
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
export * from './error-fallback'
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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} />)
|
||||
})
|
||||
})
|
|
@ -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)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
export * from './handles'
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
export * from './html-container'
|
|
@ -1,3 +0,0 @@
|
|||
export * from './renderer'
|
||||
export * from './svg-container'
|
||||
export * from './html-container'
|
|
@ -1 +0,0 @@
|
|||
export * from './overlay'
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './page'
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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} />}
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
export * from './renderer'
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './shape-indicator'
|
|
@ -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} />
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
export * from './shape-node'
|
|
@ -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
|
||||
}
|
||||
)
|
|
@ -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} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -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>'
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
export * from './snap-lines'
|
|
@ -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} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './svg-container'
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
export * from './user'
|
|
@ -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)`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './users-indicators'
|
|
@ -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>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './users'
|
|
@ -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} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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'
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function useForceUpdate() {
|
||||
const forceUpdate = React.useReducer((s) => s + 1, 0)
|
||||
React.useLayoutEffect(() => forceUpdate[1](), [])
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 }),
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export * from './components'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
export * from './inputs'
|
||||
export * from './shape-utils'
|
|
@ -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()
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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'
|
|
@ -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>
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export * from './mockDocument'
|
||||
export * from './mockUtils'
|
||||
export * from './renderWithContext'
|
||||
export * from './renderWithSvg'
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { BoxUtil } from '+shape-utils/TLShapeUtil.spec'
|
||||
|
||||
export const mockUtils = {
|
||||
box: new BoxUtil(),
|
||||
}
|
|
@ -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>)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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]
|
|
@ -1,5 +0,0 @@
|
|||
import { Utils } from './utils';
|
||||
export { Utils } from './utils';
|
||||
export { Svg } from './svg';
|
||||
export default Utils;
|
||||
//# sourceMappingURL=index.d.ts.map
|
|
@ -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
Ładowanie…
Reference in New Issue