rename app to editor (#1503)
This PR renames `App`, `app` and all appy names to `Editor`, `editor`, and editorry names. ### Change Type - [x] `major` — Breaking Change ### Release Notes - Rename `App` to `Editor` and many other things that reference `app` to `editor`.
This commit is contained in:
parent
640bc9de24
commit
735f1c41b7
311 changed files with 8365 additions and 8209 deletions
|
@ -13,7 +13,7 @@ keywords:
|
|||
|
||||
The `App` class is the main control class for tldraw's editor. You can use it to manage the editor's internal state, make changes to the document, or respond to changes that have occurred.
|
||||
|
||||
By design, the `App`'s surface area is [very large](/gen/editor/App-class). While that makes it difficult to fully document here, the general rule is that everything is available via the `App`. Need to create some shapes? Use `app.createShapes()`. Need to delete them? Use `app.deleteShapes()`. Need a sorted array of every shape on the current page? Use `app.sortedShapesArray`.
|
||||
By design, the `App`'s surface area is [very large](/gen/editor/App-class). While that makes it difficult to fully document here, the general rule is that everything is available via the `App`. Need to create some shapes? Use `editor.createShapes()`. Need to delete them? Use `editor.deleteShapes()`. Need a sorted array of every shape on the current page? Use `editor.sortedShapesArray`.
|
||||
|
||||
Rather than document everything, this page is intended to give a broad idea of how the `App` class is organized and some of the architectural concepts involved.
|
||||
|
||||
|
@ -23,100 +23,100 @@ The app holds the raw state of the document in its `store` property. Data is kep
|
|||
|
||||
For example, the store contains a `page` record for each page in the current document, as well as an `instancePageState` record for each page that stores information about the editor's state for that page, and a single `instanceState` for each editor instance which stores the id of the user's current page.
|
||||
|
||||
The app also exposes many _computed_ values which are derived from other records in the store. For example, `app.selectedIds` is a computed property that will return the editor's current selected shape ids for its current page.
|
||||
The app also exposes many _computed_ values which are derived from other records in the store. For example, `editor.selectedIds` is a computed property that will return the editor's current selected shape ids for its current page.
|
||||
|
||||
You can use these properties directly or you can use them in [signia](https://github.com/tldraw/signia) signals.
|
||||
|
||||
```tsx
|
||||
import { track } from "@tldraw/signia"
|
||||
import { useApp } from "@tldraw/tldraw"
|
||||
import { useEditor } from "@tldraw/tldraw"
|
||||
|
||||
export const SelectedIdsCount = track(() => {
|
||||
const app = useApp()
|
||||
const editor = useEditor()
|
||||
|
||||
return (
|
||||
<div>{app.selectedIds.length}</div>
|
||||
<div>{editor.selectedIds.length}</div>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
### Changing the state
|
||||
|
||||
The `App` class has many methods for updating its state. For example, you can change the current page's selection using `app.setSelectedIds`. You can also use other convenience methods, such as `app.select`, `app.deselect`, `app.selectAll`, or `app.selectNone`.
|
||||
The `App` class has many methods for updating its state. For example, you can change the current page's selection using `editor.setSelectedIds`. You can also use other convenience methods, such as `editor.select`, `editor.deselect`, `editor.selectAll`, or `editor.selectNone`.
|
||||
|
||||
```ts
|
||||
app.selectNone()
|
||||
app.select(myShapeId, myOtherShapeId)
|
||||
app.selectedIds // [myShapeId, myOtherShapeId]
|
||||
editor.selectNone()
|
||||
editor.select(myShapeId, myOtherShapeId)
|
||||
editor.selectedIds // [myShapeId, myOtherShapeId]
|
||||
```
|
||||
|
||||
Each change to the state happens within a transaction. You can batch changes into a single transaction using the `app.batch` method. It's a good idea to batch wherever possible, as this reduces the overhead for persisting or distributing those changes.
|
||||
Each change to the state happens within a transaction. You can batch changes into a single transaction using the `editor.batch` method. It's a good idea to batch wherever possible, as this reduces the overhead for persisting or distributing those changes.
|
||||
|
||||
### Listening for changes
|
||||
|
||||
You can subscribe to changes using `app.store.listen`. Each time a transaction completes, the app will call the callback with a history entry. This entry contains information about the records that were added, changed, or deleted, as well as whether the change was caused by the user or from a remote change.
|
||||
You can subscribe to changes using `editor.store.listen`. Each time a transaction completes, the app will call the callback with a history entry. This entry contains information about the records that were added, changed, or deleted, as well as whether the change was caused by the user or from a remote change.
|
||||
|
||||
```ts
|
||||
app.store.listen(entry => {
|
||||
editor.store.listen(entry => {
|
||||
entry // { changes, source }
|
||||
})
|
||||
```
|
||||
|
||||
### Remote changes
|
||||
|
||||
By default, changes to the editor's store are assumed to have come from the editor itself. You can use `app.store.mergeRemoteChanges` to make changes in the store that will be emitted via `store.listen` with the `source` property as `'remote'`.
|
||||
By default, changes to the editor's store are assumed to have come from the editor itself. You can use `editor.store.mergeRemoteChanges` to make changes in the store that will be emitted via `store.listen` with the `source` property as `'remote'`.
|
||||
|
||||
If you're setting up some kind of multiplayer backend, you would want to send only the `'user'` changes to the server and merge the changes from the server using `app.store.mergeRemoteChanges`. (We'll have more information about this soon.)
|
||||
If you're setting up some kind of multiplayer backend, you would want to send only the `'user'` changes to the server and merge the changes from the server using `editor.store.mergeRemoteChanges`. (We'll have more information about this soon.)
|
||||
|
||||
### Undo and redo
|
||||
|
||||
The history stack in tldraw contains two types of data: "marks" and "commands". Commands have their own `undo` and `redo` methods that describe how the state should change when the command is undone or redone.
|
||||
|
||||
You can call `app.mark(id)` to add a mark to the history stack with the given `id`.
|
||||
You can call `editor.mark(id)` to add a mark to the history stack with the given `id`.
|
||||
|
||||
When you call `app.undo()`, the app will undo each command until it finds either a mark or the start of the stack. When you call `app.redo()`, the app will redo each command until it finds either a mark or the end of the stack.
|
||||
When you call `editor.undo()`, the app will undo each command until it finds either a mark or the start of the stack. When you call `editor.redo()`, the app will redo each command until it finds either a mark or the end of the stack.
|
||||
|
||||
```ts
|
||||
// A
|
||||
app.mark("duplicate everything")
|
||||
app.selectAll()
|
||||
app.duplicateShapes(app.selectedIds)
|
||||
editor.mark("duplicate everything")
|
||||
editor.selectAll()
|
||||
editor.duplicateShapes(editor.selectedIds)
|
||||
// B
|
||||
|
||||
app.undo() // will return to A
|
||||
app.redo() // will return to B
|
||||
editor.undo() // will return to A
|
||||
editor.redo() // will return to B
|
||||
```
|
||||
|
||||
You can call `app.bail()` to undo and delete all commands in the stack until the first mark.
|
||||
You can call `editor.bail()` to undo and delete all commands in the stack until the first mark.
|
||||
|
||||
```ts
|
||||
// A
|
||||
app.mark("duplicate everything")
|
||||
app.selectAll()
|
||||
app.duplicateShapes(app.selectedIds)
|
||||
editor.mark("duplicate everything")
|
||||
editor.selectAll()
|
||||
editor.duplicateShapes(editor.selectedIds)
|
||||
// B
|
||||
|
||||
app.bail() // will return to A
|
||||
app.redo() // will do nothing
|
||||
editor.bail() // will return to A
|
||||
editor.redo() // will do nothing
|
||||
```
|
||||
|
||||
You can use `app.bailToMark(id)` to undo and delete all commands and marks until you reach a mark with the given `id`.
|
||||
You can use `editor.bailToMark(id)` to undo and delete all commands and marks until you reach a mark with the given `id`.
|
||||
|
||||
```ts
|
||||
// A
|
||||
app.mark("first")
|
||||
app.selectAll()
|
||||
editor.mark("first")
|
||||
editor.selectAll()
|
||||
// B
|
||||
app.mark("second")
|
||||
app.duplicateShapes(app.selectedIds)
|
||||
editor.mark("second")
|
||||
editor.duplicateShapes(editor.selectedIds)
|
||||
// C
|
||||
|
||||
app.bailToMark("first") // will to A
|
||||
editor.bailToMark("first") // will to A
|
||||
```
|
||||
|
||||
## Events and Tools
|
||||
|
||||
The `App` class receives events from the user interface via its `dispatch` method. When the `App` receives an event, it is first handled internally to update `app.inputs` and other state before, and then sent into to the app's state chart.
|
||||
The `App` class receives events from the user interface via its `dispatch` method. When the `App` receives an event, it is first handled internally to update `editor.inputs` and other state before, and then sent into to the app's state chart.
|
||||
|
||||
You shouldn't need to use the `dispatch` method directly, however you may write code in the state chart that responds to these events.
|
||||
|
||||
|
@ -126,39 +126,39 @@ The `App` class has a "state chart", or a tree of `StateNode` instances, that co
|
|||
|
||||
Each node be active or inactive. Each state node may also have zero or more children. When a state is active, and if the state has children, one (and only one) of its children must also be active. When a state node receives an event from its parent, it has the opportunity to handle the event before passing the event to its active child. The node can handle an event in any way: it can ignore the event, update records in the store, or run a _transition_ that changes which states nodes are active.
|
||||
|
||||
When a user interaction is sent to the app via its `dispatch` method, this event is sent to the app's root state node (`app.root`) and passed then down through the chart's active states until either it reaches a leaf node or until one of those nodes produces a transaction.
|
||||
When a user interaction is sent to the app via its `dispatch` method, this event is sent to the app's root state node (`editor.root`) and passed then down through the chart's active states until either it reaches a leaf node or until one of those nodes produces a transaction.
|
||||
|
||||
<Image title="Events" src="/images/api/events.png" alt="A diagram showing an event being sent to the app and handled in the state chart." title="The app passes an event into the state start where it is handled by each active state in order."/>
|
||||
|
||||
### Path
|
||||
|
||||
You can get the app's current "path" of active states via `app.root.path`. In the above example, the value would be `"root.select.idle"`.
|
||||
You can get the app's current "path" of active states via `editor.root.path`. In the above example, the value would be `"root.select.idle"`.
|
||||
|
||||
You can check whether a path is active via `app.isIn`, or else check whether multiple paths are active via `app.isInAny`.
|
||||
You can check whether a path is active via `editor.isIn`, or else check whether multiple paths are active via `editor.isInAny`.
|
||||
|
||||
```ts
|
||||
app.store.path // 'root.select.idle'
|
||||
editor.store.path // 'root.select.idle'
|
||||
|
||||
app.isIn('root.select') // true
|
||||
app.isIn('root.select.idle') // true
|
||||
app.isIn('root.select.pointing_shape') // false
|
||||
app.isInAny('app.select.idle', 'app.select.pointing_shape') // true
|
||||
editor.isIn('root.select') // true
|
||||
editor.isIn('root.select.idle') // true
|
||||
editor.isIn('root.select.pointing_shape') // false
|
||||
editor.isInAny('editor.select.idle', 'editor.select.pointing_shape') // true
|
||||
```
|
||||
|
||||
Note that the paths you pass to `isIn` or `isInAny` can be the full path or a partial of the start of the path. For example, if the full path is `root.select.idle`, then `isIn` would return true for the paths `root`, `root.select`, or `root.select.idle`.
|
||||
|
||||
> If all you're interested in is the state below `root`, there is a convenience property, `app.currentToolId`, that can help with the app's currently selected tool.
|
||||
> If all you're interested in is the state below `root`, there is a convenience property, `editor.currentToolId`, that can help with the app's currently selected tool.
|
||||
|
||||
```tsx
|
||||
import { track } from "@tldraw/signia"
|
||||
import { useApp } from "@tldraw/tldraw"
|
||||
import { useEditor } from "@tldraw/tldraw"
|
||||
|
||||
export const CreatingBubbleToolUi = track(() => {
|
||||
const app = useApp()
|
||||
const editor = useEditor()
|
||||
|
||||
const isSelected = app.isIn('root.bubble.creating')
|
||||
const isSelected = editor.isIn('root.bubble.creating')
|
||||
|
||||
if (!app.currentToolId === 'bubble') return
|
||||
if (!editor.currentToolId === 'bubble') return
|
||||
|
||||
return (
|
||||
<div data-isSelected={isSelected}>Creating Bubble</div>
|
||||
|
@ -170,7 +170,7 @@ export const CreatingBubbleToolUi = track(() => {
|
|||
|
||||
The app's `inputs` object holds information about the user's current input state, including their cursor position (in page space _and_ screen space), which keys are pressed, what their multi-click state is, and whether they are dragging, pointing, pinching, and so on.
|
||||
|
||||
Note that the modifier keys include a short delay after being released in order to prevent certain errors when modeling interactions. For example, when a user releases the "Shift" key, `app.inputs.shiftKey` will remain `true` for another 100 milliseconds or so.
|
||||
Note that the modifier keys include a short delay after being released in order to prevent certain errors when modeling interactions. For example, when a user releases the "Shift" key, `editor.inputs.shiftKey` will remain `true` for another 100 milliseconds or so.
|
||||
|
||||
This property is stored as regular data. It is not reactive.
|
||||
|
||||
|
@ -179,7 +179,7 @@ This property is stored as regular data. It is not reactive.
|
|||
### Create shapes
|
||||
|
||||
```ts
|
||||
app.createShapes([
|
||||
editor.createShapes([
|
||||
{
|
||||
id,
|
||||
type: 'geo',
|
||||
|
@ -200,9 +200,9 @@ app.createShapes([
|
|||
### Update shapes
|
||||
|
||||
```ts
|
||||
const shape = app.selectedShapes[0]
|
||||
const shape = editor.selectedShapes[0]
|
||||
|
||||
app.updateShapes([
|
||||
editor.updateShapes([
|
||||
{
|
||||
id: shape.id, // required
|
||||
type: shape.type, // required
|
||||
|
@ -218,21 +218,21 @@ app.updateShapes([
|
|||
### Delete shapes
|
||||
|
||||
```ts
|
||||
const shape = app.selectedShapes[0]
|
||||
const shape = editor.selectedShapes[0]
|
||||
|
||||
app.deleteShapes([shape.id])
|
||||
editor.deleteShapes([shape.id])
|
||||
```
|
||||
|
||||
### Get a shape by its id
|
||||
|
||||
```ts
|
||||
app.getShapeById(myShapeId)
|
||||
editor.getShapeById(myShapeId)
|
||||
```
|
||||
|
||||
### Move the camera
|
||||
|
||||
```ts
|
||||
app.setCamera(0, 0, 1)
|
||||
editor.setCamera(0, 0, 1)
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
@ -34,7 +34,7 @@ Next, copy the following folders: `icons`, `embed-icons`, `fonts`, and `translat
|
|||
|
||||
## Usage
|
||||
|
||||
You should be able to use the `<Tldraw/>` component in any React app.
|
||||
You should be able to use the `<Tldraw/>` component in any React editor.
|
||||
|
||||
To use the `<Tldraw/>` component, create a file like this one:
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ date: 3/22/2023
|
|||
order: 2
|
||||
---
|
||||
|
||||
You should be able to use the `<Tldraw/>` component in any React app.
|
||||
You should be able to use the `<Tldraw/>` component in any React editor.
|
||||
|
||||
To use the `<Tldraw/>` component, create a file like this one:
|
||||
|
||||
|
|
|
@ -33,6 +33,6 @@ function Example() {
|
|||
|
||||
The `onUiEvent` callback is called with the name of the event as a string and an object with information about the event's source (e.g. `menu` or `context-menu`) and possibly other data specific to each event, such as the direction in an `align-shapes` event.
|
||||
|
||||
Note that `onUiEvent` is only called when interacting with the user interface. It is not called when running commands manually against the app, e.g. `app.alignShapes()` will not call `onUiEvent`.
|
||||
Note that `onUiEvent` is only called when interacting with the user interface. It is not called when running commands manually against the app, e.g. `editor.alignShapes()` will not call `onUiEvent`.
|
||||
|
||||
See the [tldraw repository](https://github.com/tldraw/tldraw) for an example of how to customize tldraw's user interface.
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { PlaywrightTestArgs, PlaywrightWorkerArgs } from '@playwright/test'
|
||||
import { App } from '@tldraw/tldraw'
|
||||
import { Editor } from '@tldraw/tldraw'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// export async function expectPathToBe(page: Page, path: string) {
|
||||
// expect(await page.evaluate(() => app.root.path.value)).toBe(path)
|
||||
// expect(await page.evaluate(() => editor.root.path.value)).toBe(path)
|
||||
// }
|
||||
|
||||
// export async function expectToHaveNShapes(page: Page, numberOfShapes: number) {
|
||||
// expect(await page.evaluate(() => app.shapesArray.length)).toBe(numberOfShapes)
|
||||
// expect(await page.evaluate(() => editor.shapesArray.length)).toBe(numberOfShapes)
|
||||
// }
|
||||
|
||||
// export async function expectToHaveNSelectedShapes(page: Page, numberOfSelectedShapes: number) {
|
||||
// expect(await page.evaluate(() => app.selectedIds.length)).toBe(numberOfSelectedShapes)
|
||||
// expect(await page.evaluate(() => editor.selectedIds.length)).toBe(numberOfSelectedShapes)
|
||||
// }
|
||||
|
||||
declare const app: App
|
||||
declare const editor: Editor
|
||||
|
||||
export async function setup({ page }: PlaywrightTestArgs & PlaywrightWorkerArgs) {
|
||||
await setupPage(page)
|
||||
|
@ -35,7 +35,7 @@ export async function cleanup({ page }: PlaywrightTestArgs) {
|
|||
export async function setupPage(page: PlaywrightTestArgs['page']) {
|
||||
await page.goto('http://localhost:5420/end-to-end')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
await page.evaluate(() => (app.enableAnimations = false))
|
||||
await page.evaluate(() => (editor.enableAnimations = false))
|
||||
}
|
||||
|
||||
export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {
|
||||
|
@ -45,7 +45,7 @@ export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {
|
|||
await page.mouse.click(200, 250)
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.click(250, 300)
|
||||
await page.evaluate(() => app.selectNone())
|
||||
await page.evaluate(() => editor.selectNone())
|
||||
}
|
||||
|
||||
export async function cleanupPage(page: PlaywrightTestArgs['page']) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import test, { expect, Page } from '@playwright/test'
|
||||
import { App } from '@tldraw/tldraw'
|
||||
import { Editor } from '@tldraw/tldraw'
|
||||
import { setupPage } from '../shared-e2e'
|
||||
|
||||
declare const __tldraw_editor_events: any[]
|
||||
|
@ -8,7 +8,7 @@ declare const __tldraw_editor_events: any[]
|
|||
|
||||
let page: Page
|
||||
|
||||
declare const app: App
|
||||
declare const editor: Editor
|
||||
|
||||
test.describe('Canvas events', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
|
@ -103,7 +103,7 @@ test.describe('Canvas events', () => {
|
|||
})
|
||||
|
||||
test.fixme('complete', async () => {
|
||||
await page.evaluate(async () => app.complete())
|
||||
await page.evaluate(async () => editor.complete())
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
type: 'misc',
|
||||
name: 'complete',
|
||||
|
@ -111,7 +111,7 @@ test.describe('Canvas events', () => {
|
|||
})
|
||||
|
||||
test.fixme('cancel', async () => {
|
||||
await page.evaluate(async () => app.cancel())
|
||||
await page.evaluate(async () => editor.cancel())
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
type: 'misc',
|
||||
name: 'complete',
|
||||
|
@ -119,7 +119,7 @@ test.describe('Canvas events', () => {
|
|||
})
|
||||
|
||||
test.fixme('interrupt', async () => {
|
||||
await page.evaluate(async () => app.interrupt())
|
||||
await page.evaluate(async () => editor.interrupt())
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
type: 'misc',
|
||||
name: 'interrupt',
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import test, { expect } from '@playwright/test'
|
||||
import { App } from '@tldraw/tldraw'
|
||||
import { Editor } from '@tldraw/tldraw'
|
||||
import { setup } from '../shared-e2e'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
declare const app: App
|
||||
declare const editor: Editor
|
||||
|
||||
/**
|
||||
* These tests are skipped. They are here to show how to use the clipboard
|
||||
|
@ -23,8 +23,8 @@ test.describe.skip('clipboard tests', () => {
|
|||
await page.mouse.down()
|
||||
await page.mouse.up()
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(1)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
expect(await page.evaluate(() => editor.shapesArray.length)).toBe(1)
|
||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||
|
||||
await page.keyboard.down('Control')
|
||||
await page.keyboard.press('KeyC')
|
||||
|
@ -32,8 +32,8 @@ test.describe.skip('clipboard tests', () => {
|
|||
await page.keyboard.press('KeyV')
|
||||
await page.keyboard.up('Control')
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(2)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
expect(await page.evaluate(() => editor.shapesArray.length)).toBe(2)
|
||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||
})
|
||||
|
||||
test('copy and paste from main menu', async ({ page }) => {
|
||||
|
@ -42,8 +42,8 @@ test.describe.skip('clipboard tests', () => {
|
|||
await page.mouse.down()
|
||||
await page.mouse.up()
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(1)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
expect(await page.evaluate(() => editor.shapesArray.length)).toBe(1)
|
||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||
|
||||
await page.getByTestId('main.menu').click()
|
||||
await page.getByTestId('menu-item.edit').click()
|
||||
|
@ -53,8 +53,8 @@ test.describe.skip('clipboard tests', () => {
|
|||
await page.getByTestId('menu-item.edit').click()
|
||||
await page.getByTestId('menu-item.paste').click()
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(2)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
expect(await page.evaluate(() => editor.shapesArray.length)).toBe(2)
|
||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||
})
|
||||
|
||||
test('copy and paste from context menu', async ({ page }) => {
|
||||
|
@ -63,8 +63,8 @@ test.describe.skip('clipboard tests', () => {
|
|||
await page.mouse.down()
|
||||
await page.mouse.up()
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(1)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
expect(await page.evaluate(() => editor.shapesArray.length)).toBe(1)
|
||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||
|
||||
await page.mouse.click(100, 100, { button: 'right' })
|
||||
await page.getByTestId('menu-item.copy').click()
|
||||
|
@ -73,7 +73,7 @@ test.describe.skip('clipboard tests', () => {
|
|||
await page.mouse.click(100, 100, { button: 'right' })
|
||||
await page.getByTestId('menu-item.paste').click()
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(2)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
expect(await page.evaluate(() => editor.shapesArray.length)).toBe(2)
|
||||
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import test, { expect } from '@playwright/test'
|
||||
import { App, TLGeoShape } from '@tldraw/tldraw'
|
||||
import { Editor, TLGeoShape } from '@tldraw/tldraw'
|
||||
import { getAllShapeTypes, setup } from '../shared-e2e'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
declare const app: App
|
||||
declare const editor: Editor
|
||||
|
||||
test.describe('smoke tests', () => {
|
||||
test.beforeEach(setup)
|
||||
|
@ -65,7 +65,7 @@ test.describe('smoke tests', () => {
|
|||
expect(await getAllShapeTypes(page)).toEqual(['geo'])
|
||||
|
||||
const getSelectedShapeColor = async () =>
|
||||
await page.evaluate(() => (app.selectedShapes[0] as TLGeoShape).props.color)
|
||||
await page.evaluate(() => (editor.selectedShapes[0] as TLGeoShape).props.color)
|
||||
|
||||
// change style
|
||||
expect(await getSelectedShapeColor()).toBe('black')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import test, { Page, expect } from '@playwright/test'
|
||||
import { App, Box2dModel } from '@tldraw/tldraw'
|
||||
import { Box2dModel, Editor } from '@tldraw/tldraw'
|
||||
import { setupPage } from '../shared-e2e'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
|
@ -53,7 +53,7 @@ function formatLines(spans: { box: Box2dModel; text: string }[]) {
|
|||
return lines
|
||||
}
|
||||
|
||||
declare const app: App
|
||||
declare const editor: Editor
|
||||
let page: Page
|
||||
|
||||
test.describe('text measurement', () => {
|
||||
|
@ -64,7 +64,7 @@ test.describe('text measurement', () => {
|
|||
|
||||
test('measures text', async () => {
|
||||
const { w, h } = await page.evaluate<{ w: number; h: number }, typeof measureTextOptions>(
|
||||
async (options) => app.textMeasure.measureText('testing', options),
|
||||
async (options) => editor.textMeasure.measureText('testing', options),
|
||||
measureTextOptions
|
||||
)
|
||||
|
||||
|
@ -89,7 +89,7 @@ test.describe('text measurement', () => {
|
|||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans('testing', options),
|
||||
async (options) => editor.textMeasure.measureTextSpans('testing', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
|
@ -101,7 +101,7 @@ test.describe('text measurement', () => {
|
|||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans('testing', { ...options, width: 50 }),
|
||||
async (options) => editor.textMeasure.measureTextSpans('testing', { ...options, width: 50 }),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
|
@ -113,7 +113,7 @@ test.describe('text measurement', () => {
|
|||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans('testing testing', options),
|
||||
async (options) => editor.textMeasure.measureTextSpans('testing testing', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
|
@ -125,7 +125,7 @@ test.describe('text measurement', () => {
|
|||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans('testing testing ', options),
|
||||
async (options) => editor.textMeasure.measureTextSpans('testing testing ', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
|
@ -141,7 +141,7 @@ test.describe('text measurement', () => {
|
|||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) =>
|
||||
app.textMeasure.measureTextSpans('testing testing ', { ...options, width: 200 }),
|
||||
editor.textMeasure.measureTextSpans('testing testing ', { ...options, width: 200 }),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
|
@ -154,7 +154,7 @@ test.describe('text measurement', () => {
|
|||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) =>
|
||||
app.textMeasure.measureTextSpans(' testing testing', { ...options, width: 200 }),
|
||||
editor.textMeasure.measureTextSpans(' testing testing', { ...options, width: 200 }),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
|
@ -166,7 +166,7 @@ test.describe('text measurement', () => {
|
|||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans(' testing testing', options),
|
||||
async (options) => editor.textMeasure.measureTextSpans(' testing testing', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
|
@ -178,7 +178,8 @@ test.describe('text measurement', () => {
|
|||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans(' test\ning testing \n t', options),
|
||||
async (options) =>
|
||||
editor.textMeasure.measureTextSpans(' test\ning testing \n t', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
|
@ -196,7 +197,7 @@ test.describe('text measurement', () => {
|
|||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) =>
|
||||
app.textMeasure.measureTextSpans('testingtestingtestingtestingtestingtesting', options),
|
||||
editor.textMeasure.measureTextSpans('testingtestingtestingtestingtestingtesting', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
|
@ -214,7 +215,7 @@ test.describe('text measurement', () => {
|
|||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(async (options) => app.textMeasure.measureTextSpans('', options), measureTextSpansOptions)
|
||||
>(async (options) => editor.textMeasure.measureTextSpans('', options), measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([])
|
||||
})
|
||||
|
|
|
@ -14,20 +14,20 @@ export default function UserPresenceExample() {
|
|||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
persistenceKey="user-presence-example"
|
||||
onMount={(app) => {
|
||||
onMount={(editor) => {
|
||||
// For every connected peer you should put a TLInstancePresence record in the
|
||||
// store with their cursor position etc.
|
||||
|
||||
const peerPresence = InstancePresenceRecordType.create({
|
||||
id: InstancePresenceRecordType.createCustomId('peer-1-presence'),
|
||||
currentPageId: app.currentPageId,
|
||||
currentPageId: editor.currentPageId,
|
||||
userId: 'peer-1',
|
||||
instanceId: InstanceRecordType.createCustomId('peer-1-editor-instance'),
|
||||
userName: 'Peer 1',
|
||||
cursor: { x: 0, y: 0, type: 'default', rotation: 0 },
|
||||
})
|
||||
|
||||
app.store.put([peerPresence])
|
||||
editor.store.put([peerPresence])
|
||||
|
||||
// Make the fake user's cursor rotate in a circle
|
||||
if (rTimeout.current) {
|
||||
|
@ -40,7 +40,7 @@ export default function UserPresenceExample() {
|
|||
const now = Date.now()
|
||||
const t = (now % k) / k
|
||||
// rotate in a circle
|
||||
app.store.put([
|
||||
editor.store.put([
|
||||
{
|
||||
...peerPresence,
|
||||
cursor: {
|
||||
|
@ -53,10 +53,10 @@ export default function UserPresenceExample() {
|
|||
])
|
||||
}, 1000 / UPDATE_FPS)
|
||||
} else {
|
||||
app.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||
|
||||
rTimeout.current = setInterval(() => {
|
||||
app.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||
}, 1000)
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { App, TLEventMapHandler, Tldraw } from '@tldraw/tldraw'
|
||||
import { Editor, TLEventMapHandler, Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/editor.css'
|
||||
import '@tldraw/tldraw/ui.css'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export default function StoreEventsExample() {
|
||||
const [app, setApp] = useState<App>()
|
||||
const [editor, setEditor] = useState<Editor>()
|
||||
|
||||
const setAppToState = useCallback((app: App) => {
|
||||
setApp(app)
|
||||
const setAppToState = useCallback((editor: Editor) => {
|
||||
setEditor(editor)
|
||||
}, [])
|
||||
|
||||
const [storeEvents, setStoreEvents] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!app) return
|
||||
if (!editor) return
|
||||
|
||||
function logChangeEvent(eventName: string) {
|
||||
setStoreEvents((events) => [eventName, ...events])
|
||||
|
@ -49,12 +49,12 @@ export default function StoreEventsExample() {
|
|||
}
|
||||
}
|
||||
|
||||
app.on('change', handleChangeEvent)
|
||||
editor.on('change', handleChangeEvent)
|
||||
|
||||
return () => {
|
||||
app.off('change', handleChangeEvent)
|
||||
editor.off('change', handleChangeEvent)
|
||||
}
|
||||
}, [app])
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { App, Tldraw, TLGeoShape, useApp } from '@tldraw/tldraw'
|
||||
import { Editor, Tldraw, TLGeoShape, useEditor } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/editor.css'
|
||||
import '@tldraw/tldraw/ui.css'
|
||||
import { useEffect } from 'react'
|
||||
|
@ -11,14 +11,14 @@ import { useEffect } from 'react'
|
|||
// send events, observe changes, and perform actions.
|
||||
|
||||
export default function APIExample() {
|
||||
const handleMount = (app: App) => {
|
||||
const handleMount = (editor: Editor) => {
|
||||
// Create a shape id
|
||||
const id = app.createShapeId('hello')
|
||||
const id = editor.createShapeId('hello')
|
||||
|
||||
app.focus()
|
||||
editor.focus()
|
||||
|
||||
// Create a shape
|
||||
app.createShapes([
|
||||
editor.createShapes([
|
||||
{
|
||||
id,
|
||||
type: 'geo',
|
||||
|
@ -36,10 +36,10 @@ export default function APIExample() {
|
|||
])
|
||||
|
||||
// Get the created shape
|
||||
const shape = app.getShapeById<TLGeoShape>(id)!
|
||||
const shape = editor.getShapeById<TLGeoShape>(id)!
|
||||
|
||||
// Update the shape
|
||||
app.updateShapes([
|
||||
editor.updateShapes([
|
||||
{
|
||||
id,
|
||||
type: 'geo',
|
||||
|
@ -51,22 +51,22 @@ export default function APIExample() {
|
|||
])
|
||||
|
||||
// Select the shape
|
||||
app.select(id)
|
||||
editor.select(id)
|
||||
|
||||
// Rotate the shape around its center
|
||||
app.rotateShapesBy([id], Math.PI / 8)
|
||||
editor.rotateShapesBy([id], Math.PI / 8)
|
||||
|
||||
// Clear the selection
|
||||
app.selectNone()
|
||||
editor.selectNone()
|
||||
|
||||
// Zoom the camera to fit both shapes
|
||||
app.zoomToFit()
|
||||
editor.zoomToFit()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw persistenceKey="api-example" onMount={handleMount} autoFocus={false}>
|
||||
<InsideOfAppContext />
|
||||
<InsideOfEditorContext />
|
||||
</Tldraw>
|
||||
</div>
|
||||
)
|
||||
|
@ -74,26 +74,26 @@ export default function APIExample() {
|
|||
|
||||
// Another (sneakier) way to access the current app is through React context.
|
||||
// The Tldraw component provides the context, so you can add children to
|
||||
// the component and access the app through the useApp hook.
|
||||
// the component and access the app through the useEditor hook.
|
||||
|
||||
const InsideOfAppContext = () => {
|
||||
const app = useApp()
|
||||
const InsideOfEditorContext = () => {
|
||||
const editor = useEditor()
|
||||
|
||||
useEffect(() => {
|
||||
let i = 0
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const selection = [...app.selectedIds]
|
||||
app.selectAll()
|
||||
app.setProp('color', i % 2 ? 'blue' : 'light-blue')
|
||||
app.setSelectedIds(selection)
|
||||
const selection = [...editor.selectedIds]
|
||||
editor.selectAll()
|
||||
editor.setProp('color', i % 2 ? 'blue' : 'light-blue')
|
||||
editor.setSelectedIds(selection)
|
||||
i++
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [app])
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ export default function CustomConfigExample() {
|
|||
// We need to add it to the tools list. This "toolItem"
|
||||
// has information about its icon, label, keyboard shortcut,
|
||||
// and what to do when it's selected.
|
||||
tools(app, tools) {
|
||||
tools(editor, tools) {
|
||||
tools.card = {
|
||||
id: 'card',
|
||||
icon: 'color',
|
||||
|
@ -27,7 +27,7 @@ export default function CustomConfigExample() {
|
|||
kbd: 'c',
|
||||
readonlyOk: false,
|
||||
onSelect: () => {
|
||||
app.setSelectedTool('card')
|
||||
editor.setSelectedTool('card')
|
||||
},
|
||||
}
|
||||
return tools
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Canvas, TldrawEditor, useApp } from '@tldraw/tldraw'
|
||||
import { Canvas, TldrawEditor, useEditor } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/editor.css'
|
||||
import { useEffect } from 'react'
|
||||
import { track } from 'signia-react'
|
||||
|
@ -16,14 +16,14 @@ export default function CustomUiExample() {
|
|||
}
|
||||
|
||||
const CustomUi = track(() => {
|
||||
const app = useApp()
|
||||
const editor = useEditor()
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'Delete':
|
||||
case 'Backspace': {
|
||||
app.deleteShapes()
|
||||
editor.deleteShapes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,22 +39,22 @@ const CustomUi = track(() => {
|
|||
<div className="custom-toolbar">
|
||||
<button
|
||||
className="custom-button"
|
||||
data-isactive={app.currentToolId === 'select'}
|
||||
onClick={() => app.setSelectedTool('select')}
|
||||
data-isactive={editor.currentToolId === 'select'}
|
||||
onClick={() => editor.setSelectedTool('select')}
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
<button
|
||||
className="custom-button"
|
||||
data-isactive={app.currentToolId === 'draw'}
|
||||
onClick={() => app.setSelectedTool('draw')}
|
||||
data-isactive={editor.currentToolId === 'draw'}
|
||||
onClick={() => editor.setSelectedTool('draw')}
|
||||
>
|
||||
Pencil
|
||||
</button>
|
||||
<button
|
||||
className="custom-button"
|
||||
data-isactive={app.currentToolId === 'eraser'}
|
||||
onClick={() => app.setSelectedTool('eraser')}
|
||||
data-isactive={editor.currentToolId === 'eraser'}
|
||||
onClick={() => editor.setSelectedTool('eraser')}
|
||||
>
|
||||
Eraser
|
||||
</button>
|
||||
|
|
|
@ -18,9 +18,9 @@ export default function ErrorBoundaryExample() {
|
|||
ErrorFallback: null, // disable app-level error boundaries
|
||||
ShapeErrorFallback: ({ error }) => <div>Shape error! {String(error)}</div>, // use a custom error fallback for shapes
|
||||
}}
|
||||
onMount={(app) => {
|
||||
onMount={(editor) => {
|
||||
// When the app starts, create our error shape so we can see.
|
||||
app.createShapes([
|
||||
editor.createShapes([
|
||||
{
|
||||
type: 'error',
|
||||
id: createShapeId(),
|
||||
|
@ -31,8 +31,8 @@ export default function ErrorBoundaryExample() {
|
|||
])
|
||||
|
||||
// Center the camera on the error shape
|
||||
app.zoomToFit()
|
||||
app.resetZoom()
|
||||
editor.zoomToFit()
|
||||
editor.resetZoom()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -11,8 +11,8 @@ export default function EndToEnd() {
|
|||
onUiEvent={(name, data) => {
|
||||
;(window as any).__tldraw_ui_event = { name, data }
|
||||
}}
|
||||
onMount={(app) => {
|
||||
app.on('event', (info) => {
|
||||
onMount={(editor) => {
|
||||
editor.on('event', (info) => {
|
||||
;(window as any).__tldraw_editor_events.push(info)
|
||||
})
|
||||
}}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useApp } from '@tldraw/editor'
|
||||
import { useEditor } from '@tldraw/editor'
|
||||
import { parseAndLoadDocument, serializeTldrawJson } from '@tldraw/file-format'
|
||||
import { useDefaultHelpers } from '@tldraw/ui'
|
||||
import { debounce } from '@tldraw/utils'
|
||||
|
@ -10,7 +10,7 @@ import { vscode } from './utils/vscode'
|
|||
import type { VscodeMessage } from '../../messages'
|
||||
|
||||
export const ChangeResponder = () => {
|
||||
const app = useApp()
|
||||
const editor = useEditor()
|
||||
const { addToast, clearToasts, msg } = useDefaultHelpers()
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -18,15 +18,15 @@ export const ChangeResponder = () => {
|
|||
function handleMessage({ data: message }: MessageEvent<VscodeMessage>) {
|
||||
switch (message.type) {
|
||||
// case 'vscode:undo': {
|
||||
// app.undo()
|
||||
// editor.undo()
|
||||
// break
|
||||
// }
|
||||
// case 'vscode:redo': {
|
||||
// app.redo()
|
||||
// editor.redo()
|
||||
// break
|
||||
// }
|
||||
case 'vscode:revert': {
|
||||
parseAndLoadDocument(app, message.data.fileContents, msg, addToast)
|
||||
parseAndLoadDocument(editor, message.data.fileContents, msg, addToast)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ export const ChangeResponder = () => {
|
|||
clearToasts()
|
||||
window.removeEventListener('message', handleMessage)
|
||||
}
|
||||
}, [app, msg, addToast, clearToasts])
|
||||
}, [editor, msg, addToast, clearToasts])
|
||||
|
||||
React.useEffect(() => {
|
||||
// When the history changes, send the new file contents to VSCode
|
||||
|
@ -46,7 +46,7 @@ export const ChangeResponder = () => {
|
|||
vscode.postMessage({
|
||||
type: 'vscode:editor-updated',
|
||||
data: {
|
||||
fileContents: await serializeTldrawJson(app.store),
|
||||
fileContents: await serializeTldrawJson(editor.store),
|
||||
},
|
||||
})
|
||||
}, 250)
|
||||
|
@ -55,13 +55,13 @@ export const ChangeResponder = () => {
|
|||
type: 'vscode:editor-loaded',
|
||||
})
|
||||
|
||||
app.on('change-history', handleChange)
|
||||
editor.on('change-history', handleChange)
|
||||
|
||||
return () => {
|
||||
handleChange()
|
||||
app.off('change-history', handleChange)
|
||||
editor.off('change-history', handleChange)
|
||||
}
|
||||
}, [app])
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useApp } from '@tldraw/editor'
|
||||
import { useEditor } from '@tldraw/editor'
|
||||
import { parseAndLoadDocument } from '@tldraw/file-format'
|
||||
import { useDefaultHelpers } from '@tldraw/ui'
|
||||
import React from 'react'
|
||||
|
@ -11,7 +11,7 @@ export function FileOpen({
|
|||
fileContents: string
|
||||
forceDarkMode: boolean
|
||||
}) {
|
||||
const app = useApp()
|
||||
const editor = useEditor()
|
||||
const { msg, addToast, clearToasts } = useDefaultHelpers()
|
||||
const [isFileLoaded, setIsFileLoaded] = React.useState(false)
|
||||
|
||||
|
@ -32,7 +32,7 @@ export function FileOpen({
|
|||
}
|
||||
|
||||
async function loadFile() {
|
||||
await parseAndLoadDocument(app, fileContents, msg, addToast, onV1FileLoad, forceDarkMode)
|
||||
await parseAndLoadDocument(editor, fileContents, msg, addToast, onV1FileLoad, forceDarkMode)
|
||||
}
|
||||
|
||||
loadFile()
|
||||
|
@ -40,7 +40,7 @@ export function FileOpen({
|
|||
return () => {
|
||||
clearToasts()
|
||||
}
|
||||
}, [fileContents, app, addToast, msg, clearToasts, forceDarkMode, isFileLoaded])
|
||||
}, [fileContents, editor, addToast, msg, clearToasts, forceDarkMode, isFileLoaded])
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
App,
|
||||
Canvas,
|
||||
Editor,
|
||||
ErrorBoundary,
|
||||
TAB_ID,
|
||||
TldrawEditor,
|
||||
|
@ -64,7 +64,7 @@ export function WrappedTldrawEditor() {
|
|||
}
|
||||
|
||||
const menuOverrides = {
|
||||
menu: (_app: App, schema: MenuSchema, _helpers: any) => {
|
||||
menu: (_editor: Editor, schema: MenuSchema, _helpers: any) => {
|
||||
schema.forEach((item) => {
|
||||
if (item.id === 'menu' && item.type === 'group') {
|
||||
item.children = item.children.filter((menuItem) => {
|
||||
|
|
|
@ -44,11 +44,11 @@ const linksMenuGroup = menuGroup(
|
|||
)!
|
||||
|
||||
export const linksUiOverrides: TldrawUiOverrides = {
|
||||
helpMenu(app, schema) {
|
||||
helpMenu(editor, schema) {
|
||||
schema.push(linksMenuGroup)
|
||||
return schema
|
||||
},
|
||||
menu(app, schema, { isMobile }) {
|
||||
menu(editor, schema, { isMobile }) {
|
||||
if (isMobile) {
|
||||
schema.push(linksMenuGroup)
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ export class TldrawWebviewManager {
|
|||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<noscript>You need to enable JavaScript to run this editor.</noscript>
|
||||
<script>
|
||||
// Plenty of other extensions do this see <https://sourcegraph.com/search?q=context%3Aglobal+%22_defaultStyles%22&patternType=standard&sm=1&groupBy=repo>
|
||||
document.getElementById("_defaultStyles").remove();
|
||||
|
|
|
@ -119,8 +119,142 @@ export type AnimationOptions = Partial<{
|
|||
easing: typeof EASINGS.easeInOutCubic;
|
||||
}>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function applyRotationToSnapshotShapes({ delta, editor, snapshot, stage, }: {
|
||||
delta: number;
|
||||
snapshot: RotationSnapshot;
|
||||
editor: Editor;
|
||||
stage: 'end' | 'one-off' | 'start' | 'update';
|
||||
}): void;
|
||||
|
||||
// @public (undocumented)
|
||||
export class App extends EventEmitter<TLEventMap> {
|
||||
export interface AppOptions {
|
||||
getContainer: () => HTMLElement;
|
||||
shapes?: Record<string, ShapeInfo>;
|
||||
store: TLStore;
|
||||
tools?: StateNodeConstructor[];
|
||||
user?: TLUser;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const ARROW_LABEL_FONT_SIZES: Record<TLSizeType, number>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function blobAsString(blob: Blob): Promise<string>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const BOUND_ARROW_OFFSET = 10;
|
||||
|
||||
// @public (undocumented)
|
||||
export const Canvas: React_3.MemoExoticComponent<({ onDropOverride, }: {
|
||||
onDropOverride?: ((defaultOnDrop: (e: React_3.DragEvent<Element>) => Promise<void>) => (e: React_3.DragEvent<Element>) => Promise<void>) | undefined;
|
||||
}) => JSX.Element>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const checkFlag: (flag: (() => boolean) | boolean | undefined) => boolean | undefined;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ClipboardPayload = {
|
||||
data: string;
|
||||
kind: 'file';
|
||||
type: 'application/tldraw';
|
||||
} | {
|
||||
data: string;
|
||||
kind: 'text';
|
||||
type: 'application/tldraw';
|
||||
} | {
|
||||
data: TLClipboardModel;
|
||||
kind: 'content';
|
||||
type: 'application/tldraw';
|
||||
};
|
||||
|
||||
// @public
|
||||
export function containBoxSize(originalSize: BoxWidthHeight, containBoxSize: BoxWidthHeight): BoxWidthHeight;
|
||||
|
||||
// @public (undocumented)
|
||||
export function correctSpacesToNbsp(input: string): string;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createAssetShapeAtPoint(editor: Editor, svgString: string, point: Vec2dModel): Promise<void>;
|
||||
|
||||
// @public
|
||||
export function createBookmarkShapeAtPoint(editor: Editor, url: string, point: Vec2dModel): Promise<void>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createEmbedShapeAtPoint(editor: Editor, url: string, point: Vec2dModel, props: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
doesResize?: boolean;
|
||||
}): void;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createShapesFromFiles(editor: Editor, files: File[], position: VecLike, _ignoreParent?: boolean): Promise<void>;
|
||||
|
||||
// @public
|
||||
export function createTLStore(opts?: StoreOptions): TLStore;
|
||||
|
||||
// @public (undocumented)
|
||||
export function dataTransferItemAsString(item: DataTransferItem): Promise<string>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function dataUrlToFile(url: string, filename: string, mimeType: string): Promise<File>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export type DebugFlag<T> = DebugFlagDef<T> & Atom<T>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const debugFlags: {
|
||||
preventDefaultLogging: DebugFlag<boolean>;
|
||||
pointerCaptureLogging: DebugFlag<boolean>;
|
||||
pointerCaptureTracking: DebugFlag<boolean>;
|
||||
pointerCaptureTrackingObject: DebugFlag<Map<Element, number>>;
|
||||
elementRemovalLogging: DebugFlag<boolean>;
|
||||
debugSvg: DebugFlag<boolean>;
|
||||
throwToBlob: DebugFlag<boolean>;
|
||||
logMessages: DebugFlag<never[]>;
|
||||
resetConnectionEveryPing: DebugFlag<boolean>;
|
||||
debugCursors: DebugFlag<boolean>;
|
||||
forceSrgb: DebugFlag<boolean>;
|
||||
};
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DEFAULT_ANIMATION_OPTIONS: {
|
||||
duration: number;
|
||||
easing: (t: number) => number;
|
||||
};
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DEFAULT_BOOKMARK_HEIGHT = 320;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DEFAULT_BOOKMARK_WIDTH = 300;
|
||||
|
||||
// @public (undocumented)
|
||||
export let defaultEditorAssetUrls: EditorAssetUrls;
|
||||
|
||||
// @public (undocumented)
|
||||
export function defaultEmptyAs(str: string, dflt: string): string;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DefaultErrorFallback: TLErrorFallback;
|
||||
|
||||
// @public (undocumented)
|
||||
export const defaultShapes: Record<string, ShapeInfo>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const defaultTools: StateNodeConstructor[];
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DOUBLE_CLICK_DURATION = 450;
|
||||
|
||||
// @public (undocumented)
|
||||
export function downloadDataURLAsFile(dataUrl: string, filename: string): void;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DRAG_DISTANCE = 4;
|
||||
|
||||
// @public (undocumented)
|
||||
export class Editor extends EventEmitter<TLEventMap> {
|
||||
constructor({ store, user, tools, shapes, getContainer, }: AppOptions);
|
||||
addOpenMenu: (id: string) => this;
|
||||
alignShapes(operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top', ids?: TLShapeId[]): this;
|
||||
|
@ -543,140 +677,6 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
zoomToSelection(opts?: AnimationOptions): this;
|
||||
}
|
||||
|
||||
// @internal (undocumented)
|
||||
export function applyRotationToSnapshotShapes({ delta, app, snapshot, stage, }: {
|
||||
delta: number;
|
||||
snapshot: RotationSnapshot;
|
||||
app: App;
|
||||
stage: 'end' | 'one-off' | 'start' | 'update';
|
||||
}): void;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface AppOptions {
|
||||
getContainer: () => HTMLElement;
|
||||
shapes?: Record<string, ShapeInfo>;
|
||||
store: TLStore;
|
||||
tools?: StateNodeConstructor[];
|
||||
user?: TLUser;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const ARROW_LABEL_FONT_SIZES: Record<TLSizeType, number>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function blobAsString(blob: Blob): Promise<string>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const BOUND_ARROW_OFFSET = 10;
|
||||
|
||||
// @public (undocumented)
|
||||
export const Canvas: React_3.MemoExoticComponent<({ onDropOverride, }: {
|
||||
onDropOverride?: ((defaultOnDrop: (e: React_3.DragEvent<Element>) => Promise<void>) => (e: React_3.DragEvent<Element>) => Promise<void>) | undefined;
|
||||
}) => JSX.Element>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const checkFlag: (flag: (() => boolean) | boolean | undefined) => boolean | undefined;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ClipboardPayload = {
|
||||
data: string;
|
||||
kind: 'file';
|
||||
type: 'application/tldraw';
|
||||
} | {
|
||||
data: string;
|
||||
kind: 'text';
|
||||
type: 'application/tldraw';
|
||||
} | {
|
||||
data: TLClipboardModel;
|
||||
kind: 'content';
|
||||
type: 'application/tldraw';
|
||||
};
|
||||
|
||||
// @public
|
||||
export function containBoxSize(originalSize: BoxWidthHeight, containBoxSize: BoxWidthHeight): BoxWidthHeight;
|
||||
|
||||
// @public (undocumented)
|
||||
export function correctSpacesToNbsp(input: string): string;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createAssetShapeAtPoint(app: App, svgString: string, point: Vec2dModel): Promise<void>;
|
||||
|
||||
// @public
|
||||
export function createBookmarkShapeAtPoint(app: App, url: string, point: Vec2dModel): Promise<void>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createEmbedShapeAtPoint(app: App, url: string, point: Vec2dModel, props: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
doesResize?: boolean;
|
||||
}): void;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createShapesFromFiles(app: App, files: File[], position: VecLike, _ignoreParent?: boolean): Promise<void>;
|
||||
|
||||
// @public
|
||||
export function createTLStore(opts?: StoreOptions): TLStore;
|
||||
|
||||
// @public (undocumented)
|
||||
export function dataTransferItemAsString(item: DataTransferItem): Promise<string>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function dataUrlToFile(url: string, filename: string, mimeType: string): Promise<File>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export type DebugFlag<T> = DebugFlagDef<T> & Atom<T>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const debugFlags: {
|
||||
preventDefaultLogging: DebugFlag<boolean>;
|
||||
pointerCaptureLogging: DebugFlag<boolean>;
|
||||
pointerCaptureTracking: DebugFlag<boolean>;
|
||||
pointerCaptureTrackingObject: DebugFlag<Map<Element, number>>;
|
||||
elementRemovalLogging: DebugFlag<boolean>;
|
||||
debugSvg: DebugFlag<boolean>;
|
||||
throwToBlob: DebugFlag<boolean>;
|
||||
logMessages: DebugFlag<never[]>;
|
||||
resetConnectionEveryPing: DebugFlag<boolean>;
|
||||
debugCursors: DebugFlag<boolean>;
|
||||
forceSrgb: DebugFlag<boolean>;
|
||||
};
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DEFAULT_ANIMATION_OPTIONS: {
|
||||
duration: number;
|
||||
easing: (t: number) => number;
|
||||
};
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DEFAULT_BOOKMARK_HEIGHT = 320;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DEFAULT_BOOKMARK_WIDTH = 300;
|
||||
|
||||
// @public (undocumented)
|
||||
export let defaultEditorAssetUrls: EditorAssetUrls;
|
||||
|
||||
// @public (undocumented)
|
||||
export function defaultEmptyAs(str: string, dflt: string): string;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DefaultErrorFallback: TLErrorFallback;
|
||||
|
||||
// @public (undocumented)
|
||||
export const defaultShapes: Record<string, ShapeInfo>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const defaultTools: StateNodeConstructor[];
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DOUBLE_CLICK_DURATION = 450;
|
||||
|
||||
// @public (undocumented)
|
||||
export function downloadDataURLAsFile(dataUrl: string, filename: string): void;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DRAG_DISTANCE = 4;
|
||||
|
||||
// @public (undocumented)
|
||||
export type EditorAssetUrls = {
|
||||
fonts: {
|
||||
|
@ -802,8 +802,8 @@ export function getPointerInfo(e: PointerEvent | React.PointerEvent, container:
|
|||
export function getResizedImageDataUrl(dataURLForImage: string, width: number, height: number): Promise<string>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function getRotationSnapshot({ app }: {
|
||||
app: App;
|
||||
export function getRotationSnapshot({ editor }: {
|
||||
editor: Editor;
|
||||
}): {
|
||||
selectionPageCenter: Vec2d;
|
||||
initialCursorAngle: number;
|
||||
|
@ -873,7 +873,7 @@ export function hardReset({ shouldReload }?: {
|
|||
}): Promise<void>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function hardResetApp(): void;
|
||||
export function hardResetEditor(): void;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const HASH_PATERN_ZOOM_NAMES: Record<string, string>;
|
||||
|
@ -1445,9 +1445,7 @@ export { sortByIndex }
|
|||
|
||||
// @public (undocumented)
|
||||
export abstract class StateNode implements Partial<TLEventHandlers> {
|
||||
constructor(app: App, parent?: StateNode);
|
||||
// (undocumented)
|
||||
app: App;
|
||||
constructor(editor: Editor, parent?: StateNode);
|
||||
// (undocumented)
|
||||
static children?: () => StateNodeConstructor[];
|
||||
// (undocumented)
|
||||
|
@ -1455,6 +1453,8 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|||
// (undocumented)
|
||||
current: Atom<StateNode | undefined>;
|
||||
// (undocumented)
|
||||
editor: Editor;
|
||||
// (undocumented)
|
||||
enter(info: any, from: string): void;
|
||||
// (undocumented)
|
||||
exit(info: any, from: string): void;
|
||||
|
@ -1523,7 +1523,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|||
// @public (undocumented)
|
||||
export interface StateNodeConstructor {
|
||||
// (undocumented)
|
||||
new (app: App, parent?: StateNode): StateNode;
|
||||
new (editor: Editor, parent?: StateNode): StateNode;
|
||||
// (undocumented)
|
||||
children?: () => StateNodeConstructor[];
|
||||
// (undocumented)
|
||||
|
@ -1802,7 +1802,7 @@ export type TldrawEditorProps = {
|
|||
assetUrls?: EditorAssetUrls;
|
||||
autoFocus?: boolean;
|
||||
components?: Partial<TLEditorComponents>;
|
||||
onMount?: (app: App) => void;
|
||||
onMount?: (editor: Editor) => void;
|
||||
onCreateAssetFromFile?: (file: File) => Promise<TLAsset>;
|
||||
onCreateBookmarkFromUrl?: (url: string) => Promise<{
|
||||
image: string;
|
||||
|
@ -2474,9 +2474,7 @@ export type TLSelectionHandle = RotateCorner | SelectionCorner | SelectionEdge;
|
|||
|
||||
// @public (undocumented)
|
||||
export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
|
||||
constructor(app: App, type: T['type']);
|
||||
// (undocumented)
|
||||
app: App;
|
||||
constructor(editor: Editor, type: T['type']);
|
||||
bounds(shape: T): Box2d;
|
||||
canBind: <K>(_shape: T, _otherShape?: K | undefined) => boolean;
|
||||
canCrop: TLShapeUtilFlag<T>;
|
||||
|
@ -2488,6 +2486,8 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
|
|||
canUnmount: TLShapeUtilFlag<T>;
|
||||
center(shape: T): Vec2dModel;
|
||||
abstract defaultProps(): T['props'];
|
||||
// (undocumented)
|
||||
editor: Editor;
|
||||
// @internal (undocumented)
|
||||
expandSelectionOutlinePx(shape: T): number;
|
||||
protected abstract getBounds(shape: T): Box2d;
|
||||
|
@ -2551,7 +2551,7 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
|
|||
// @public (undocumented)
|
||||
export interface TLShapeUtilConstructor<T extends TLUnknownShape, ShapeUtil extends TLShapeUtil<T> = TLShapeUtil<T>> {
|
||||
// (undocumented)
|
||||
new (app: App, type: T['type']): ShapeUtil;
|
||||
new (editor: Editor, type: T['type']): ShapeUtil;
|
||||
// (undocumented)
|
||||
type: T['type'];
|
||||
}
|
||||
|
@ -2710,10 +2710,10 @@ export type UiExitHandler = (info: any, to: string) => void;
|
|||
export function uniqueId(): string;
|
||||
|
||||
// @public (undocumented)
|
||||
export const useApp: () => App;
|
||||
export function useContainer(): HTMLDivElement;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useContainer(): HTMLDivElement;
|
||||
export const useEditor: () => Editor;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function useLocalStore(opts?: {
|
||||
|
|
|
@ -21,12 +21,12 @@ export {
|
|||
type TldrawEditorProps,
|
||||
} from './lib/TldrawEditor'
|
||||
export {
|
||||
App,
|
||||
Editor,
|
||||
isShapeWithHandles,
|
||||
type AnimationOptions,
|
||||
type AppOptions,
|
||||
type TLChange,
|
||||
} from './lib/app/App'
|
||||
} from './lib/app/Editor'
|
||||
export { TLArrowUtil } from './lib/app/shapeutils/TLArrowUtil/TLArrowUtil'
|
||||
export { TLBookmarkUtil } from './lib/app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
|
||||
export { TLBoxUtil } from './lib/app/shapeutils/TLBoxUtil'
|
||||
|
@ -174,8 +174,8 @@ export {
|
|||
ZOOMS,
|
||||
} from './lib/constants'
|
||||
export { normalizeWheel } from './lib/hooks/shared'
|
||||
export { useApp } from './lib/hooks/useApp'
|
||||
export { useContainer } from './lib/hooks/useContainer'
|
||||
export { useEditor } from './lib/hooks/useEditor'
|
||||
export type { TLEditorComponents } from './lib/hooks/useEditorComponents'
|
||||
export { useLocalStore } from './lib/hooks/useLocalStore'
|
||||
export { usePeerIds } from './lib/hooks/usePeerIds'
|
||||
|
@ -241,7 +241,7 @@ export {
|
|||
type TLCopyType,
|
||||
type TLExportType,
|
||||
} from './lib/utils/export'
|
||||
export { hardResetApp } from './lib/utils/hard-reset'
|
||||
export { hardResetEditor } from './lib/utils/hard-reset'
|
||||
export { isAnimated, isGIF } from './lib/utils/is-gif-animated'
|
||||
export { setPropsForNextShape } from './lib/utils/props-for-next-shape'
|
||||
export { refreshPage } from './lib/utils/refresh-page'
|
||||
|
|
|
@ -2,16 +2,16 @@ import { TLAsset, TLInstanceId, TLRecord, TLStore } from '@tldraw/tlschema'
|
|||
import { Store, StoreSnapshot } from '@tldraw/tlstore'
|
||||
import { annotateError } from '@tldraw/utils'
|
||||
import React, { memo, useCallback, useLayoutEffect, useState, useSyncExternalStore } from 'react'
|
||||
import { App } from './app/App'
|
||||
import { Editor } from './app/Editor'
|
||||
import { StateNodeConstructor } from './app/statechart/StateNode'
|
||||
import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls'
|
||||
import { DefaultErrorFallback } from './components/DefaultErrorFallback'
|
||||
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
||||
import { ShapeInfo } from './config/createTLStore'
|
||||
import { AppContext } from './hooks/useApp'
|
||||
import { ContainerProvider, useContainer } from './hooks/useContainer'
|
||||
import { useCursor } from './hooks/useCursor'
|
||||
import { useDarkMode } from './hooks/useDarkMode'
|
||||
import { EditorContext } from './hooks/useEditor'
|
||||
import {
|
||||
EditorComponentsProvider,
|
||||
TLEditorComponents,
|
||||
|
@ -56,13 +56,13 @@ export type TldrawEditorProps = {
|
|||
*
|
||||
* ```ts
|
||||
* function TldrawEditor() {
|
||||
* return <Editor onMount={(app) => app.selectAll()} />
|
||||
* return <Editor onMount={(editor) => editor.selectAll()} />
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param app - The app instance.
|
||||
* @param editor - The editor instance.
|
||||
*/
|
||||
onMount?: (app: App) => void
|
||||
onMount?: (editor: Editor) => void
|
||||
/**
|
||||
* Called when the editor generates a new asset from a file, such as when an image is dropped into
|
||||
* the canvas.
|
||||
|
@ -70,7 +70,7 @@ export type TldrawEditorProps = {
|
|||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* const app = new App({
|
||||
* const editor = new App({
|
||||
* onCreateAssetFromFile: (file) => uploadFileAndCreateAsset(file),
|
||||
* })
|
||||
* ```
|
||||
|
@ -87,7 +87,7 @@ export type TldrawEditorProps = {
|
|||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* app.onCreateBookmarkFromUrl(url, id)
|
||||
* editor.onCreateBookmarkFromUrl(url, id)
|
||||
* ```
|
||||
*
|
||||
* @param url - The url that was created.
|
||||
|
@ -241,66 +241,67 @@ function TldrawEditorWithReadyStore({
|
|||
}) {
|
||||
const { ErrorFallback } = useEditorComponents()
|
||||
const container = useContainer()
|
||||
const [app, setApp] = useState<App | null>(null)
|
||||
const [editor, setEditor] = useState<Editor | null>(null)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const app = new App({
|
||||
const editor = new Editor({
|
||||
store,
|
||||
shapes,
|
||||
tools,
|
||||
getContainer: () => container,
|
||||
})
|
||||
;(window as any).app = app
|
||||
setApp(app)
|
||||
;(window as any).app = editor
|
||||
;(window as any).editor = editor
|
||||
setEditor(editor)
|
||||
return () => {
|
||||
app.dispose()
|
||||
editor.dispose()
|
||||
}
|
||||
}, [container, shapes, tools, store])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!app) return
|
||||
if (!editor) return
|
||||
|
||||
// Overwrite the default onCreateAssetFromFile handler.
|
||||
if (onCreateAssetFromFile) {
|
||||
app.onCreateAssetFromFile = onCreateAssetFromFile
|
||||
editor.onCreateAssetFromFile = onCreateAssetFromFile
|
||||
}
|
||||
|
||||
if (onCreateBookmarkFromUrl) {
|
||||
app.onCreateBookmarkFromUrl = onCreateBookmarkFromUrl
|
||||
editor.onCreateBookmarkFromUrl = onCreateBookmarkFromUrl
|
||||
}
|
||||
}, [app, onCreateAssetFromFile, onCreateBookmarkFromUrl])
|
||||
}, [editor, onCreateAssetFromFile, onCreateBookmarkFromUrl])
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (app && autoFocus) app.focus()
|
||||
}, [app, autoFocus])
|
||||
if (editor && autoFocus) editor.focus()
|
||||
}, [editor, autoFocus])
|
||||
|
||||
const onMountEvent = useEvent((app: App) => {
|
||||
onMount?.(app)
|
||||
app.emit('mount')
|
||||
const onMountEvent = useEvent((editor: Editor) => {
|
||||
onMount?.(editor)
|
||||
editor.emit('mount')
|
||||
window.tldrawReady = true
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (app) onMountEvent(app)
|
||||
}, [app, onMountEvent])
|
||||
if (editor) onMountEvent(editor)
|
||||
}, [editor, onMountEvent])
|
||||
|
||||
const crashingError = useSyncExternalStore(
|
||||
useCallback(
|
||||
(onStoreChange) => {
|
||||
if (app) {
|
||||
app.on('crash', onStoreChange)
|
||||
return () => app.off('crash', onStoreChange)
|
||||
if (editor) {
|
||||
editor.on('crash', onStoreChange)
|
||||
return () => editor.off('crash', onStoreChange)
|
||||
}
|
||||
return () => {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
[app]
|
||||
[editor]
|
||||
),
|
||||
() => app?.crashingError ?? null
|
||||
() => editor?.crashingError ?? null
|
||||
)
|
||||
|
||||
if (!app) {
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -312,15 +313,17 @@ function TldrawEditorWithReadyStore({
|
|||
// document in the event of an error to reassure them that their work is
|
||||
// not lost.
|
||||
<OptionalErrorBoundary
|
||||
fallback={ErrorFallback ? (error) => <ErrorFallback error={error} app={app} /> : null}
|
||||
onError={(error) => app.annotateError(error, { origin: 'react.tldraw', willCrashApp: true })}
|
||||
fallback={ErrorFallback ? (error) => <ErrorFallback error={error} editor={editor} /> : null}
|
||||
onError={(error) =>
|
||||
editor.annotateError(error, { origin: 'react.tldraw', willCrashApp: true })
|
||||
}
|
||||
>
|
||||
{crashingError ? (
|
||||
<Crash crashingError={crashingError} />
|
||||
) : (
|
||||
<AppContext.Provider value={app}>
|
||||
<EditorContext.Provider value={editor}>
|
||||
<Layout>{children}</Layout>
|
||||
</AppContext.Provider>
|
||||
</EditorContext.Provider>
|
||||
)}
|
||||
</OptionalErrorBoundary>
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,72 +1,72 @@
|
|||
import { TLShapeId } from '@tldraw/tlschema'
|
||||
import { TestApp } from '../../test/TestApp'
|
||||
import { TestEditor } from '../../test/TestEditor'
|
||||
import { TL } from '../../test/jsx'
|
||||
|
||||
let app: TestApp
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
app = new TestApp()
|
||||
editor = new TestEditor()
|
||||
})
|
||||
|
||||
describe('arrowBindingsIndex', () => {
|
||||
it('keeps a mapping from bound shapes to the arrows that bind to them', () => {
|
||||
const ids = app.createShapesFromJsx([
|
||||
const ids = editor.createShapesFromJsx([
|
||||
<TL.geo ref="box1" x={0} y={0} w={100} h={100} fill="solid" />,
|
||||
<TL.geo ref="box2" x={200} y={0} w={100} h={100} fill="solid" />,
|
||||
])
|
||||
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50)
|
||||
const arrow = app.onlySelectedShape!
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50)
|
||||
const arrow = editor.onlySelectedShape!
|
||||
expect(arrow.type).toBe('arrow')
|
||||
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toEqual([{ arrowId: arrow.id, handleId: 'start' }])
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toEqual([{ arrowId: arrow.id, handleId: 'end' }])
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([{ arrowId: arrow.id, handleId: 'start' }])
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toEqual([{ arrowId: arrow.id, handleId: 'end' }])
|
||||
})
|
||||
|
||||
it('works if there are many arrows', () => {
|
||||
const ids = app.createShapesFromJsx([
|
||||
const ids = editor.createShapesFromJsx([
|
||||
<TL.geo ref="box1" x={0} y={0} w={100} h={100} />,
|
||||
<TL.geo ref="box2" x={200} y={0} w={100} h={100} />,
|
||||
])
|
||||
|
||||
app.setSelectedTool('arrow')
|
||||
editor.setSelectedTool('arrow')
|
||||
// span both boxes
|
||||
app.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50)
|
||||
const arrow1 = app.onlySelectedShape!
|
||||
editor.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50)
|
||||
const arrow1 = editor.onlySelectedShape!
|
||||
expect(arrow1.type).toBe('arrow')
|
||||
|
||||
// start at box 1 and leave
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50)
|
||||
const arrow2 = app.onlySelectedShape!
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50)
|
||||
const arrow2 = editor.onlySelectedShape!
|
||||
expect(arrow2.type).toBe('arrow')
|
||||
|
||||
// start outside box 1 and enter
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50)
|
||||
const arrow3 = app.onlySelectedShape!
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50)
|
||||
const arrow3 = editor.onlySelectedShape!
|
||||
expect(arrow3.type).toBe('arrow')
|
||||
|
||||
// start at box 2 and leave
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50)
|
||||
const arrow4 = app.onlySelectedShape!
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50)
|
||||
const arrow4 = editor.onlySelectedShape!
|
||||
expect(arrow4.type).toBe('arrow')
|
||||
|
||||
// start outside box 2 and enter
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50)
|
||||
const arrow5 = app.onlySelectedShape!
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50)
|
||||
const arrow5 = editor.onlySelectedShape!
|
||||
expect(arrow5.type).toBe('arrow')
|
||||
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toEqual([
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([
|
||||
{ arrowId: arrow1.id, handleId: 'start' },
|
||||
{ arrowId: arrow2.id, handleId: 'start' },
|
||||
{ arrowId: arrow3.id, handleId: 'end' },
|
||||
])
|
||||
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toEqual([
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toEqual([
|
||||
{ arrowId: arrow1.id, handleId: 'end' },
|
||||
{ arrowId: arrow4.id, handleId: 'start' },
|
||||
{ arrowId: arrow5.id, handleId: 'end' },
|
||||
|
@ -89,126 +89,130 @@ describe('arrowBindingsIndex', () => {
|
|||
let arrowEId: TLShapeId
|
||||
let ids: Record<string, TLShapeId>
|
||||
beforeEach(() => {
|
||||
ids = app.createShapesFromJsx([
|
||||
ids = editor.createShapesFromJsx([
|
||||
<TL.geo ref="box1" x={0} y={0} w={100} h={100} />,
|
||||
<TL.geo ref="box2" x={200} y={0} w={100} h={100} />,
|
||||
])
|
||||
|
||||
// span both boxes
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50)
|
||||
arrowAId = app.onlySelectedShape!.id
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50)
|
||||
arrowAId = editor.onlySelectedShape!.id
|
||||
// start at box 1 and leave
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50)
|
||||
arrowBId = app.onlySelectedShape!.id
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50)
|
||||
arrowBId = editor.onlySelectedShape!.id
|
||||
// start outside box 1 and enter
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50)
|
||||
arrowCId = app.onlySelectedShape!.id
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50)
|
||||
arrowCId = editor.onlySelectedShape!.id
|
||||
// start at box 2 and leave
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50)
|
||||
arrowDId = app.onlySelectedShape!.id
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50)
|
||||
arrowDId = editor.onlySelectedShape!.id
|
||||
// start outside box 2 and enter
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50)
|
||||
arrowEId = app.onlySelectedShape!.id
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50)
|
||||
arrowEId = editor.onlySelectedShape!.id
|
||||
})
|
||||
it('deletes the entry if you delete the bound shapes', () => {
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
app.deleteShapes([ids.box2])
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toEqual([])
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
editor.deleteShapes([ids.box2])
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toEqual([])
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
})
|
||||
it('deletes the entry if you delete an arrow', () => {
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
app.deleteShapes([arrowEId])
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(2)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
editor.deleteShapes([arrowEId])
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(2)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
|
||||
app.deleteShapes([arrowDId])
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(1)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
editor.deleteShapes([arrowDId])
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
|
||||
app.deleteShapes([arrowCId])
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(1)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(2)
|
||||
editor.deleteShapes([arrowCId])
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(2)
|
||||
|
||||
app.deleteShapes([arrowBId])
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(1)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(1)
|
||||
editor.deleteShapes([arrowBId])
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(1)
|
||||
|
||||
app.deleteShapes([arrowAId])
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(0)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(0)
|
||||
editor.deleteShapes([arrowAId])
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(0)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('deletes the entries in a batch too', () => {
|
||||
app.deleteShapes([arrowAId, arrowBId, arrowCId, arrowDId, arrowEId])
|
||||
editor.deleteShapes([arrowAId, arrowBId, arrowCId, arrowDId, arrowEId])
|
||||
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(0)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(0)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(0)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('adds new entries after initial creation', () => {
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
|
||||
// draw from box 2 to box 1
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(250, 50).pointerMove(50, 50).pointerUp(50, 50)
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(4)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(4)
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(250, 50).pointerMove(50, 50).pointerUp(50, 50)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(4)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(4)
|
||||
|
||||
// create a new box
|
||||
|
||||
const { box3 } = app.createShapesFromJsx(<TL.geo ref="box3" x={400} y={0} w={100} h={100} />)
|
||||
const { box3 } = editor.createShapesFromJsx(
|
||||
<TL.geo ref="box3" x={400} y={0} w={100} h={100} />
|
||||
)
|
||||
|
||||
// draw from box 2 to box 3
|
||||
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(250, 50).pointerMove(450, 50).pointerUp(450, 50)
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(5)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(4)
|
||||
expect(app.getArrowsBoundTo(box3)).toHaveLength(1)
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(250, 50).pointerMove(450, 50).pointerUp(450, 50)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(5)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(4)
|
||||
expect(editor.getArrowsBoundTo(box3)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('works when copy pasting', () => {
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
|
||||
app.selectAll()
|
||||
app.duplicateShapes()
|
||||
editor.selectAll()
|
||||
editor.duplicateShapes()
|
||||
|
||||
const [box1Clone, box2Clone] = app.selectedShapes
|
||||
const [box1Clone, box2Clone] = editor.selectedShapes
|
||||
.filter((s) => s.type === 'geo')
|
||||
.sort((a, b) => a.x - b.x)
|
||||
|
||||
expect(app.getArrowsBoundTo(box2Clone.id)).toHaveLength(3)
|
||||
expect(app.getArrowsBoundTo(box1Clone.id)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(box2Clone.id)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(box1Clone.id)).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('allows bound shapes to be moved', () => {
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
|
||||
app.nudgeShapes([ids.box2], { x: 0, y: -1 }, true)
|
||||
editor.nudgeShapes([ids.box2], { x: 0, y: -1 }, true)
|
||||
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('allows the arrows bound shape to change', () => {
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
|
||||
// create another box
|
||||
|
||||
const { box3 } = app.createShapesFromJsx(<TL.geo ref="box3" x={400} y={0} w={100} h={100} />)
|
||||
const { box3 } = editor.createShapesFromJsx(
|
||||
<TL.geo ref="box3" x={400} y={0} w={100} h={100} />
|
||||
)
|
||||
|
||||
// move arrowA from box2 to box3
|
||||
app.updateShapes([
|
||||
editor.updateShapes([
|
||||
{
|
||||
id: arrowAId,
|
||||
type: 'arrow',
|
||||
|
@ -223,9 +227,9 @@ describe('arrowBindingsIndex', () => {
|
|||
},
|
||||
])
|
||||
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(2)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
expect(app.getArrowsBoundTo(box3)).toHaveLength(1)
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(2)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(box3)).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { getIndexAbove, getIndexBetween } from '@tldraw/indices'
|
||||
import { createCustomShapeId } from '@tldraw/tlschema'
|
||||
import { TestApp } from '../../test/TestApp'
|
||||
import { TestEditor } from '../../test/TestEditor'
|
||||
|
||||
let app: TestApp
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
app = new TestApp()
|
||||
editor = new TestEditor()
|
||||
})
|
||||
|
||||
const ids = {
|
||||
|
@ -20,78 +20,78 @@ const ids = {
|
|||
|
||||
describe('parentsToChildrenWithIndexes', () => {
|
||||
it('keeps the children and parents up to date', () => {
|
||||
app.createShapes([{ type: 'geo', id: ids.box1 }])
|
||||
app.createShapes([{ type: 'geo', id: ids.box2 }])
|
||||
editor.createShapes([{ type: 'geo', id: ids.box1 }])
|
||||
editor.createShapes([{ type: 'geo', id: ids.box2 }])
|
||||
|
||||
expect(app.getSortedChildIds(ids.box1)).toEqual([])
|
||||
expect(app.getSortedChildIds(ids.box2)).toEqual([])
|
||||
expect(editor.getSortedChildIds(ids.box1)).toEqual([])
|
||||
expect(editor.getSortedChildIds(ids.box2)).toEqual([])
|
||||
|
||||
app.createShapes([{ type: 'geo', id: ids.box3, parentId: ids.box1 }])
|
||||
editor.createShapes([{ type: 'geo', id: ids.box3, parentId: ids.box1 }])
|
||||
|
||||
expect(app.getSortedChildIds(ids.box1)).toEqual([ids.box3])
|
||||
expect(app.getSortedChildIds(ids.box2)).toEqual([])
|
||||
expect(editor.getSortedChildIds(ids.box1)).toEqual([ids.box3])
|
||||
expect(editor.getSortedChildIds(ids.box2)).toEqual([])
|
||||
|
||||
app.updateShapes([{ id: ids.box3, type: 'geo', parentId: ids.box2 }])
|
||||
editor.updateShapes([{ id: ids.box3, type: 'geo', parentId: ids.box2 }])
|
||||
|
||||
expect(app.getSortedChildIds(ids.box1)).toEqual([])
|
||||
expect(app.getSortedChildIds(ids.box2)).toEqual([ids.box3])
|
||||
expect(editor.getSortedChildIds(ids.box1)).toEqual([])
|
||||
expect(editor.getSortedChildIds(ids.box2)).toEqual([ids.box3])
|
||||
|
||||
app.updateShapes([{ id: ids.box1, type: 'geo', parentId: ids.box2 }])
|
||||
editor.updateShapes([{ id: ids.box1, type: 'geo', parentId: ids.box2 }])
|
||||
|
||||
expect(app.getSortedChildIds(ids.box2)).toEqual([ids.box3, ids.box1])
|
||||
expect(editor.getSortedChildIds(ids.box2)).toEqual([ids.box3, ids.box1])
|
||||
})
|
||||
|
||||
it('keeps the children of pages too', () => {
|
||||
app.createShapes([
|
||||
editor.createShapes([
|
||||
{ type: 'geo', id: ids.box1 },
|
||||
{ type: 'geo', id: ids.box2 },
|
||||
{ type: 'geo', id: ids.box3 },
|
||||
])
|
||||
|
||||
expect(app.getSortedChildIds(app.currentPageId)).toEqual([ids.box1, ids.box2, ids.box3])
|
||||
expect(editor.getSortedChildIds(editor.currentPageId)).toEqual([ids.box1, ids.box2, ids.box3])
|
||||
})
|
||||
|
||||
it('keeps children sorted', () => {
|
||||
app.createShapes([
|
||||
editor.createShapes([
|
||||
{ type: 'geo', id: ids.box1 },
|
||||
{ type: 'geo', id: ids.box2 },
|
||||
{ type: 'geo', id: ids.box3 },
|
||||
])
|
||||
|
||||
expect(app.getSortedChildIds(app.currentPageId)).toEqual([ids.box1, ids.box2, ids.box3])
|
||||
expect(editor.getSortedChildIds(editor.currentPageId)).toEqual([ids.box1, ids.box2, ids.box3])
|
||||
|
||||
app.updateShapes([
|
||||
editor.updateShapes([
|
||||
{
|
||||
id: ids.box1,
|
||||
type: 'geo',
|
||||
index: getIndexBetween(
|
||||
app.getShapeById(ids.box2)!.index,
|
||||
app.getShapeById(ids.box3)!.index
|
||||
editor.getShapeById(ids.box2)!.index,
|
||||
editor.getShapeById(ids.box3)!.index
|
||||
),
|
||||
},
|
||||
])
|
||||
expect(app.getSortedChildIds(app.currentPageId)).toEqual([ids.box2, ids.box1, ids.box3])
|
||||
expect(editor.getSortedChildIds(editor.currentPageId)).toEqual([ids.box2, ids.box1, ids.box3])
|
||||
|
||||
app.updateShapes([
|
||||
{ id: ids.box2, type: 'geo', index: getIndexAbove(app.getShapeById(ids.box3)!.index) },
|
||||
editor.updateShapes([
|
||||
{ id: ids.box2, type: 'geo', index: getIndexAbove(editor.getShapeById(ids.box3)!.index) },
|
||||
])
|
||||
|
||||
expect(app.getSortedChildIds(app.currentPageId)).toEqual([ids.box1, ids.box3, ids.box2])
|
||||
expect(editor.getSortedChildIds(editor.currentPageId)).toEqual([ids.box1, ids.box3, ids.box2])
|
||||
})
|
||||
|
||||
it('sorts children of next parent when a shape is reparented', () => {
|
||||
app.createShapes([
|
||||
editor.createShapes([
|
||||
{ type: 'geo', id: ids.box1 },
|
||||
{ type: 'geo', id: ids.box2, parentId: ids.box1 },
|
||||
{ type: 'geo', id: ids.box3, parentId: ids.box1 },
|
||||
{ type: 'geo', id: ids.box4 },
|
||||
])
|
||||
|
||||
const box2Index = app.getShapeById(ids.box2)!.index
|
||||
const box3Index = app.getShapeById(ids.box3)!.index
|
||||
const box2Index = editor.getShapeById(ids.box2)!.index
|
||||
const box3Index = editor.getShapeById(ids.box3)!.index
|
||||
const box4Index = getIndexBetween(box2Index, box3Index)
|
||||
|
||||
app.updateShapes([
|
||||
editor.updateShapes([
|
||||
{
|
||||
id: ids.box4,
|
||||
type: 'geo',
|
||||
|
@ -100,6 +100,6 @@ describe('parentsToChildrenWithIndexes', () => {
|
|||
},
|
||||
])
|
||||
|
||||
expect(app.getSortedChildIds(ids.box1)).toEqual([ids.box2, ids.box4, ids.box3])
|
||||
expect(editor.getSortedChildIds(ids.box1)).toEqual([ids.box2, ids.box4, ids.box3])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { PageRecordType, createCustomShapeId } from '@tldraw/tlschema'
|
||||
import { TestApp } from '../../test/TestApp'
|
||||
import { TestEditor } from '../../test/TestEditor'
|
||||
|
||||
let app: TestApp
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
app = new TestApp()
|
||||
editor = new TestEditor()
|
||||
})
|
||||
|
||||
const ids = {
|
||||
|
@ -19,51 +19,51 @@ const ids = {
|
|||
|
||||
describe('shapeIdsInCurrentPage', () => {
|
||||
it('keeps the shape ids in the current page', () => {
|
||||
expect(new Set(app.shapeIds)).toEqual(new Set([]))
|
||||
app.createShapes([{ type: 'geo', id: ids.box1 }])
|
||||
expect(new Set(editor.shapeIds)).toEqual(new Set([]))
|
||||
editor.createShapes([{ type: 'geo', id: ids.box1 }])
|
||||
|
||||
expect(new Set(app.shapeIds)).toEqual(new Set([ids.box1]))
|
||||
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box1]))
|
||||
|
||||
app.createShapes([{ type: 'geo', id: ids.box2 }])
|
||||
editor.createShapes([{ type: 'geo', id: ids.box2 }])
|
||||
|
||||
expect(new Set(app.shapeIds)).toEqual(new Set([ids.box1, ids.box2]))
|
||||
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box1, ids.box2]))
|
||||
|
||||
app.createShapes([{ type: 'geo', id: ids.box3 }])
|
||||
editor.createShapes([{ type: 'geo', id: ids.box3 }])
|
||||
|
||||
expect(new Set(app.shapeIds)).toEqual(new Set([ids.box1, ids.box2, ids.box3]))
|
||||
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box1, ids.box2, ids.box3]))
|
||||
|
||||
app.deleteShapes([ids.box2])
|
||||
editor.deleteShapes([ids.box2])
|
||||
|
||||
expect(new Set(app.shapeIds)).toEqual(new Set([ids.box1, ids.box3]))
|
||||
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box1, ids.box3]))
|
||||
|
||||
app.deleteShapes([ids.box1])
|
||||
editor.deleteShapes([ids.box1])
|
||||
|
||||
expect(new Set(app.shapeIds)).toEqual(new Set([ids.box3]))
|
||||
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box3]))
|
||||
|
||||
app.deleteShapes([ids.box3])
|
||||
editor.deleteShapes([ids.box3])
|
||||
|
||||
expect(new Set(app.shapeIds)).toEqual(new Set([]))
|
||||
expect(new Set(editor.shapeIds)).toEqual(new Set([]))
|
||||
})
|
||||
|
||||
it('changes when the current page changes', () => {
|
||||
app.createShapes([
|
||||
editor.createShapes([
|
||||
{ type: 'geo', id: ids.box1 },
|
||||
{ type: 'geo', id: ids.box2 },
|
||||
{ type: 'geo', id: ids.box3 },
|
||||
])
|
||||
const id = PageRecordType.createCustomId('page2')
|
||||
app.createPage('New Page 2', id)
|
||||
app.setCurrentPageId(id)
|
||||
app.createShapes([
|
||||
editor.createPage('New Page 2', id)
|
||||
editor.setCurrentPageId(id)
|
||||
editor.createShapes([
|
||||
{ type: 'geo', id: ids.box4 },
|
||||
{ type: 'geo', id: ids.box5 },
|
||||
{ type: 'geo', id: ids.box6 },
|
||||
])
|
||||
|
||||
expect(new Set(app.shapeIds)).toEqual(new Set([ids.box4, ids.box5, ids.box6]))
|
||||
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box4, ids.box5, ids.box6]))
|
||||
|
||||
app.setCurrentPageId(app.pages[0].id)
|
||||
editor.setCurrentPageId(editor.pages[0].id)
|
||||
|
||||
expect(new Set(app.shapeIds)).toEqual(new Set([ids.box1, ids.box2, ids.box3]))
|
||||
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box1, ids.box2, ids.box3]))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { atom } from 'signia'
|
||||
import { App } from '../App'
|
||||
import { Editor } from '../Editor'
|
||||
|
||||
type Offsets = {
|
||||
top: number
|
||||
|
@ -14,8 +14,8 @@ const DEFAULT_OFFSETS = {
|
|||
right: 10,
|
||||
}
|
||||
|
||||
export function getActiveAreaScreenSpace(app: App) {
|
||||
const containerEl = app.getContainer()
|
||||
export function getActiveAreaScreenSpace(editor: Editor) {
|
||||
const containerEl = editor.getContainer()
|
||||
const el = containerEl.querySelector('*[data-tldraw-area="active-drawing"]')
|
||||
const out = {
|
||||
...DEFAULT_OFFSETS,
|
||||
|
@ -31,14 +31,14 @@ export function getActiveAreaScreenSpace(app: App) {
|
|||
out.right = cBbbox.width - bbox.right
|
||||
}
|
||||
|
||||
out.width = app.viewportScreenBounds.width - out.left - out.right
|
||||
out.height = app.viewportScreenBounds.height - out.top - out.bottom
|
||||
out.width = editor.viewportScreenBounds.width - out.left - out.right
|
||||
out.height = editor.viewportScreenBounds.height - out.top - out.bottom
|
||||
return out
|
||||
}
|
||||
|
||||
export function getActiveAreaPageSpace(app: App) {
|
||||
const out = getActiveAreaScreenSpace(app)
|
||||
const z = app.zoomLevel
|
||||
export function getActiveAreaPageSpace(editor: Editor) {
|
||||
const out = getActiveAreaScreenSpace(editor)
|
||||
const z = editor.zoomLevel
|
||||
out.left /= z
|
||||
out.right /= z
|
||||
out.top /= z
|
||||
|
@ -49,15 +49,15 @@ export function getActiveAreaPageSpace(app: App) {
|
|||
}
|
||||
|
||||
export class ActiveAreaManager {
|
||||
constructor(public app: App) {
|
||||
constructor(public editor: Editor) {
|
||||
window.addEventListener('resize', this.updateOffsets)
|
||||
this.app.disposables.add(this.dispose)
|
||||
this.editor.disposables.add(this.dispose)
|
||||
}
|
||||
|
||||
offsets = atom<Offsets>('activeAreaOffsets', DEFAULT_OFFSETS)
|
||||
|
||||
updateOffsets = () => {
|
||||
const offsets = getActiveAreaPageSpace(this.app)
|
||||
const offsets = getActiveAreaPageSpace(this.editor)
|
||||
this.offsets.set(offsets)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { atom } from 'signia'
|
||||
import { App } from '../App'
|
||||
import { Editor } from '../Editor'
|
||||
|
||||
const CAMERA_SETTLE_TIMEOUT = 12
|
||||
|
||||
export class CameraManager {
|
||||
constructor(public app: App) {}
|
||||
constructor(public editor: Editor) {}
|
||||
|
||||
state = atom('camera state', 'idle' as 'idle' | 'moving')
|
||||
|
||||
|
@ -14,8 +14,8 @@ export class CameraManager {
|
|||
this.timeoutRemaining -= elapsed
|
||||
if (this.timeoutRemaining <= 0) {
|
||||
this.state.set('idle')
|
||||
this.app.off('tick', this.decay)
|
||||
this.app.updateCullingBounds()
|
||||
this.editor.off('tick', this.decay)
|
||||
this.editor.updateCullingBounds()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ export class CameraManager {
|
|||
// If the state is idle, then start the tick
|
||||
if (this.state.__unsafe__getWithoutCapture() === 'idle') {
|
||||
this.state.set('moving')
|
||||
this.app.on('tick', this.decay)
|
||||
this.editor.on('tick', this.decay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { TestApp } from '../../test/TestApp'
|
||||
import { TestEditor } from '../../test/TestEditor'
|
||||
|
||||
let app: TestApp
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
app = new TestApp()
|
||||
editor = new TestEditor()
|
||||
// we want to do this in order to avoid creating text shapes. weird
|
||||
app.setSelectedTool('eraser')
|
||||
app._transformPointerDownSpy.mockRestore()
|
||||
app._transformPointerUpSpy.mockRestore()
|
||||
editor.setSelectedTool('eraser')
|
||||
editor._transformPointerDownSpy.mockRestore()
|
||||
editor._transformPointerUpSpy.mockRestore()
|
||||
})
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
@ -15,10 +15,10 @@ jest.useFakeTimers()
|
|||
describe('Handles events', () => {
|
||||
it('Emits single click events', () => {
|
||||
const events: any[] = []
|
||||
app.addListener('event', (info) => events.push(info))
|
||||
editor.addListener('event', (info) => events.push(info))
|
||||
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
|
||||
const eventsBeforeSettle = [{ name: 'pointer_down' }, { name: 'pointer_up' }]
|
||||
|
||||
|
@ -31,7 +31,7 @@ describe('Handles events', () => {
|
|||
// clear events and click again
|
||||
// the interaction should have reset
|
||||
events.length = 0
|
||||
app.pointerDown().pointerUp().pointerDown()
|
||||
editor.pointerDown().pointerUp().pointerDown()
|
||||
expect(events).toMatchObject([
|
||||
{ name: 'pointer_down' },
|
||||
{ name: 'pointer_up' },
|
||||
|
@ -42,12 +42,12 @@ describe('Handles events', () => {
|
|||
|
||||
it('Emits double click events', () => {
|
||||
const events: any[] = []
|
||||
app.addListener('event', (info) => events.push(info))
|
||||
editor.addListener('event', (info) => events.push(info))
|
||||
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
|
||||
const eventsBeforeSettle = [
|
||||
{ name: 'pointer_down' },
|
||||
|
@ -74,7 +74,7 @@ describe('Handles events', () => {
|
|||
// clear events and click again
|
||||
// the interaction should have reset
|
||||
events.length = 0
|
||||
app.pointerDown().pointerUp().pointerDown()
|
||||
editor.pointerDown().pointerUp().pointerDown()
|
||||
expect(events).toMatchObject([
|
||||
{ name: 'pointer_down' },
|
||||
{ name: 'pointer_up' },
|
||||
|
@ -85,14 +85,14 @@ describe('Handles events', () => {
|
|||
|
||||
it('Emits triple click events', () => {
|
||||
const events: any[] = []
|
||||
app.addListener('event', (info) => events.push(info))
|
||||
editor.addListener('event', (info) => events.push(info))
|
||||
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
|
||||
const eventsBeforeSettle = [
|
||||
{ name: 'pointer_down' },
|
||||
|
@ -120,7 +120,7 @@ describe('Handles events', () => {
|
|||
// clear events and click again
|
||||
// the interaction should have reset
|
||||
events.length = 0
|
||||
app.pointerDown().pointerUp().pointerDown()
|
||||
editor.pointerDown().pointerUp().pointerDown()
|
||||
expect(events).toMatchObject([
|
||||
{ name: 'pointer_down' },
|
||||
{ name: 'pointer_up' },
|
||||
|
@ -131,16 +131,16 @@ describe('Handles events', () => {
|
|||
|
||||
it('Emits quadruple click events', () => {
|
||||
const events: any[] = []
|
||||
app.addListener('event', (info) => events.push(info))
|
||||
editor.addListener('event', (info) => events.push(info))
|
||||
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
|
||||
const eventsBeforeSettle = [
|
||||
{ name: 'pointer_down' },
|
||||
|
@ -172,7 +172,7 @@ describe('Handles events', () => {
|
|||
// clear events and click again
|
||||
// the interaction should have reset
|
||||
events.length = 0
|
||||
app.pointerDown().pointerUp().pointerDown()
|
||||
editor.pointerDown().pointerUp().pointerDown()
|
||||
expect(events).toMatchObject([
|
||||
{ name: 'pointer_down' },
|
||||
{ name: 'pointer_up' },
|
||||
|
@ -183,18 +183,18 @@ describe('Handles events', () => {
|
|||
|
||||
it('Emits overflow click events', () => {
|
||||
const events: any[] = []
|
||||
app.addListener('event', (info) => events.push(info))
|
||||
editor.addListener('event', (info) => events.push(info))
|
||||
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
app.pointerDown()
|
||||
app.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
editor.pointerDown()
|
||||
editor.pointerUp()
|
||||
|
||||
const eventsBeforeSettle = [
|
||||
{ name: 'pointer_down' },
|
||||
|
@ -225,7 +225,7 @@ describe('Handles events', () => {
|
|||
// clear events and click again
|
||||
// the interaction should have reset
|
||||
events.length = 0
|
||||
app.pointerDown().pointerUp().pointerDown()
|
||||
editor.pointerDown().pointerUp().pointerDown()
|
||||
expect(events).toMatchObject([
|
||||
{ name: 'pointer_down' },
|
||||
{ name: 'pointer_up' },
|
||||
|
@ -237,22 +237,22 @@ describe('Handles events', () => {
|
|||
|
||||
it('Cancels when click moves', () => {
|
||||
let event: any
|
||||
app.addListener('event', (info) => (event = info))
|
||||
app.pointerDown(0, 0)
|
||||
editor.addListener('event', (info) => (event = info))
|
||||
editor.pointerDown(0, 0)
|
||||
expect(event.name).toBe('pointer_down')
|
||||
app.pointerUp(0, 0)
|
||||
editor.pointerUp(0, 0)
|
||||
expect(event.name).toBe('pointer_up')
|
||||
app.pointerDown(0, 20)
|
||||
editor.pointerDown(0, 20)
|
||||
expect(event.name).toBe('double_click')
|
||||
app.pointerUp(0, 20)
|
||||
editor.pointerUp(0, 20)
|
||||
expect(event.name).toBe('double_click')
|
||||
app.pointerDown(0, 45)
|
||||
editor.pointerDown(0, 45)
|
||||
expect(event.name).toBe('triple_click')
|
||||
app.pointerUp(0, 45)
|
||||
editor.pointerUp(0, 45)
|
||||
expect(event.name).toBe('triple_click')
|
||||
// has to be 40 away from previous click location
|
||||
app.pointerDown(0, 86)
|
||||
editor.pointerDown(0, 86)
|
||||
expect(event.name).toBe('pointer_down')
|
||||
app.pointerUp(0, 86)
|
||||
editor.pointerUp(0, 86)
|
||||
expect(event.name).toBe('pointer_up')
|
||||
})
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
MULTI_CLICK_DURATION,
|
||||
} from '../../constants'
|
||||
import { uniqueId } from '../../utils/data'
|
||||
import type { App } from '../App'
|
||||
import type { Editor } from '../Editor'
|
||||
import { TLClickEventInfo, TLPointerEventInfo } from '../types/event-types'
|
||||
|
||||
type TLClickState =
|
||||
|
@ -20,7 +20,7 @@ type TLClickState =
|
|||
const MAX_CLICK_DISTANCE = 40
|
||||
|
||||
export class ClickManager {
|
||||
constructor(public app: App) {}
|
||||
constructor(public editor: Editor) {}
|
||||
|
||||
private _clickId = ''
|
||||
|
||||
|
@ -38,7 +38,7 @@ export class ClickManager {
|
|||
if (this._clickState === state && this._clickId === id) {
|
||||
switch (this._clickState) {
|
||||
case 'pendingTriple': {
|
||||
this.app.dispatch({
|
||||
this.editor.dispatch({
|
||||
...this.lastPointerInfo,
|
||||
type: 'click',
|
||||
name: 'double_click',
|
||||
|
@ -47,7 +47,7 @@ export class ClickManager {
|
|||
break
|
||||
}
|
||||
case 'pendingQuadruple': {
|
||||
this.app.dispatch({
|
||||
this.editor.dispatch({
|
||||
...this.lastPointerInfo,
|
||||
type: 'click',
|
||||
name: 'triple_click',
|
||||
|
@ -56,7 +56,7 @@ export class ClickManager {
|
|||
break
|
||||
}
|
||||
case 'pendingOverflow': {
|
||||
this.app.dispatch({
|
||||
this.editor.dispatch({
|
||||
...this.lastPointerInfo,
|
||||
type: 'click',
|
||||
name: 'quadruple_click',
|
||||
|
@ -226,8 +226,8 @@ export class ClickManager {
|
|||
if (
|
||||
this._clickState !== 'idle' &&
|
||||
this._clickScreenPoint &&
|
||||
this._clickScreenPoint.dist(this.app.inputs.currentScreenPoint) >
|
||||
(this.app.isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
|
||||
this._clickScreenPoint.dist(this.editor.inputs.currentScreenPoint) >
|
||||
(this.editor.isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
|
||||
) {
|
||||
this.cancelDoubleClickTimeout()
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { atom } from 'signia'
|
||||
import { App } from '../App'
|
||||
import { Editor } from '../Editor'
|
||||
|
||||
export class DprManager {
|
||||
private _currentMM: MediaQueryList | undefined
|
||||
|
||||
constructor(public app: App) {
|
||||
constructor(public editor: Editor) {
|
||||
this.rebind()
|
||||
// Add this class's dispose method (cancel the listener) to the app's disposables
|
||||
this.app.disposables.add(this.dispose)
|
||||
this.editor.disposables.add(this.dispose)
|
||||
}
|
||||
|
||||
// Set a listener to update the dpr when the device pixel ratio changes
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import { compact } from '@tldraw/utils'
|
||||
import type { App } from '../App'
|
||||
import type { Editor } from '../Editor'
|
||||
|
||||
const LAG_DURATION = 100
|
||||
|
||||
export class DragAndDropManager {
|
||||
constructor(public app: App) {
|
||||
app.disposables.add(this.dispose)
|
||||
constructor(public editor: Editor) {
|
||||
editor.disposables.add(this.dispose)
|
||||
}
|
||||
|
||||
prevDroppingShapeId: TLShapeId | null = null
|
||||
|
@ -16,11 +16,11 @@ export class DragAndDropManager {
|
|||
|
||||
updateDroppingNode(movingShapes: TLShape[], cb: () => void) {
|
||||
if (this.droppingNodeTimer === null) {
|
||||
const { currentPagePoint } = this.app.inputs
|
||||
const { currentPagePoint } = this.editor.inputs
|
||||
this.currDroppingShapeId =
|
||||
this.app.getDroppingShape(currentPagePoint, movingShapes)?.id ?? null
|
||||
this.editor.getDroppingShape(currentPagePoint, movingShapes)?.id ?? null
|
||||
this.setDragTimer(movingShapes, LAG_DURATION * 10, cb)
|
||||
} else if (this.app.inputs.pointerVelocity.len() > 0.5) {
|
||||
} else if (this.editor.inputs.pointerVelocity.len() > 0.5) {
|
||||
clearInterval(this.droppingNodeTimer)
|
||||
this.setDragTimer(movingShapes, LAG_DURATION, cb)
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export class DragAndDropManager {
|
|||
|
||||
private setDragTimer(movingShapes: TLShape[], duration: number, cb: () => void) {
|
||||
this.droppingNodeTimer = setTimeout(() => {
|
||||
this.app.batch(() => {
|
||||
this.editor.batch(() => {
|
||||
this.handleDrag(movingShapes, cb)
|
||||
})
|
||||
this.droppingNodeTimer = null
|
||||
|
@ -36,12 +36,12 @@ export class DragAndDropManager {
|
|||
}
|
||||
|
||||
private handleDrag(movingShapes: TLShape[], cb?: () => void) {
|
||||
const { currentPagePoint } = this.app.inputs
|
||||
const { currentPagePoint } = this.editor.inputs
|
||||
|
||||
movingShapes = compact(movingShapes.map((shape) => this.app.getShapeById(shape.id)))
|
||||
movingShapes = compact(movingShapes.map((shape) => this.editor.getShapeById(shape.id)))
|
||||
|
||||
const currDroppingShapeId =
|
||||
this.app.getDroppingShape(currentPagePoint, movingShapes)?.id ?? null
|
||||
this.editor.getDroppingShape(currentPagePoint, movingShapes)?.id ?? null
|
||||
|
||||
if (currDroppingShapeId !== this.currDroppingShapeId) {
|
||||
this.prevDroppingShapeId = this.currDroppingShapeId
|
||||
|
@ -55,8 +55,8 @@ export class DragAndDropManager {
|
|||
return
|
||||
}
|
||||
|
||||
const prevDroppingShape = prevDroppingShapeId && this.app.getShapeById(prevDroppingShapeId)
|
||||
const nextDroppingShape = currDroppingShapeId && this.app.getShapeById(currDroppingShapeId)
|
||||
const prevDroppingShape = prevDroppingShapeId && this.editor.getShapeById(prevDroppingShapeId)
|
||||
const nextDroppingShape = currDroppingShapeId && this.editor.getShapeById(currDroppingShapeId)
|
||||
|
||||
// Even if we don't have a next dropping shape id (i.e. if we're dropping
|
||||
// onto the page) set the prev to the current, to avoid repeat calls to
|
||||
|
@ -64,20 +64,20 @@ export class DragAndDropManager {
|
|||
this.prevDroppingShapeId = this.currDroppingShapeId
|
||||
|
||||
if (prevDroppingShape) {
|
||||
this.app.getShapeUtil(prevDroppingShape).onDragShapesOut?.(prevDroppingShape, movingShapes)
|
||||
this.editor.getShapeUtil(prevDroppingShape).onDragShapesOut?.(prevDroppingShape, movingShapes)
|
||||
}
|
||||
|
||||
if (nextDroppingShape) {
|
||||
const res = this.app
|
||||
const res = this.editor
|
||||
.getShapeUtil(nextDroppingShape)
|
||||
.onDragShapesOver?.(nextDroppingShape, movingShapes)
|
||||
|
||||
if (res && res.shouldHint) {
|
||||
this.app.setHintingIds([nextDroppingShape.id])
|
||||
this.editor.setHintingIds([nextDroppingShape.id])
|
||||
}
|
||||
} else {
|
||||
// If we're dropping onto the page, then clear hinting ids
|
||||
this.app.setHintingIds([])
|
||||
this.editor.setHintingIds([])
|
||||
}
|
||||
|
||||
cb?.()
|
||||
|
@ -89,9 +89,9 @@ export class DragAndDropManager {
|
|||
this.handleDrag(shapes)
|
||||
|
||||
if (currDroppingShapeId) {
|
||||
const shape = this.app.getShapeById(currDroppingShapeId)
|
||||
const shape = this.editor.getShapeById(currDroppingShapeId)
|
||||
if (!shape) return
|
||||
this.app.getShapeUtil(shape).onDropShapesOver?.(shape, shapes)
|
||||
this.editor.getShapeUtil(shape).onDropShapesOver?.(shape, shapes)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,7 @@ export class DragAndDropManager {
|
|||
}
|
||||
|
||||
this.droppingNodeTimer = null
|
||||
this.app.setHintingIds([])
|
||||
this.editor.setHintingIds([])
|
||||
}
|
||||
|
||||
dispose = () => {
|
||||
|
|
|
@ -105,188 +105,188 @@ function createCounterHistoryManager() {
|
|||
}
|
||||
|
||||
describe(HistoryManager, () => {
|
||||
let app = createCounterHistoryManager()
|
||||
let editor = createCounterHistoryManager()
|
||||
beforeEach(() => {
|
||||
app = createCounterHistoryManager()
|
||||
editor = createCounterHistoryManager()
|
||||
})
|
||||
it('creates a serializable undo stack', () => {
|
||||
expect(app.getCount()).toBe(0)
|
||||
app.increment()
|
||||
app.increment()
|
||||
app.history.mark('stop at 2')
|
||||
app.increment()
|
||||
app.increment()
|
||||
app.decrement()
|
||||
expect(app.getCount()).toBe(3)
|
||||
expect(editor.getCount()).toBe(0)
|
||||
editor.increment()
|
||||
editor.increment()
|
||||
editor.history.mark('stop at 2')
|
||||
editor.increment()
|
||||
editor.increment()
|
||||
editor.decrement()
|
||||
expect(editor.getCount()).toBe(3)
|
||||
|
||||
const undos = [...app.history._undos.value]
|
||||
const undos = [...editor.history._undos.value]
|
||||
const parsedUndos = JSON.parse(JSON.stringify(undos))
|
||||
app.history._undos.set(stack(parsedUndos))
|
||||
editor.history._undos.set(stack(parsedUndos))
|
||||
|
||||
app.history.undo()
|
||||
editor.history.undo()
|
||||
|
||||
expect(app.getCount()).toBe(2)
|
||||
expect(editor.getCount()).toBe(2)
|
||||
})
|
||||
|
||||
it('allows undoing and redoing', () => {
|
||||
expect(app.getCount()).toBe(0)
|
||||
app.increment()
|
||||
app.history.mark('stop at 1')
|
||||
app.increment()
|
||||
app.history.mark('stop at 2')
|
||||
app.increment()
|
||||
app.increment()
|
||||
app.history.mark('stop at 4')
|
||||
app.increment()
|
||||
app.increment()
|
||||
app.increment()
|
||||
expect(app.getCount()).toBe(7)
|
||||
expect(editor.getCount()).toBe(0)
|
||||
editor.increment()
|
||||
editor.history.mark('stop at 1')
|
||||
editor.increment()
|
||||
editor.history.mark('stop at 2')
|
||||
editor.increment()
|
||||
editor.increment()
|
||||
editor.history.mark('stop at 4')
|
||||
editor.increment()
|
||||
editor.increment()
|
||||
editor.increment()
|
||||
expect(editor.getCount()).toBe(7)
|
||||
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(4)
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(2)
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(1)
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(0)
|
||||
app.history.undo()
|
||||
app.history.undo()
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(0)
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(4)
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(2)
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(1)
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(0)
|
||||
editor.history.undo()
|
||||
editor.history.undo()
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(0)
|
||||
|
||||
app.history.redo()
|
||||
expect(app.getCount()).toBe(1)
|
||||
app.history.redo()
|
||||
expect(app.getCount()).toBe(2)
|
||||
app.history.redo()
|
||||
expect(app.getCount()).toBe(4)
|
||||
app.history.redo()
|
||||
expect(app.getCount()).toBe(7)
|
||||
editor.history.redo()
|
||||
expect(editor.getCount()).toBe(1)
|
||||
editor.history.redo()
|
||||
expect(editor.getCount()).toBe(2)
|
||||
editor.history.redo()
|
||||
expect(editor.getCount()).toBe(4)
|
||||
editor.history.redo()
|
||||
expect(editor.getCount()).toBe(7)
|
||||
})
|
||||
|
||||
it('clears the redo stack if you execute commands, but not if you mark stopping points', () => {
|
||||
expect(app.getCount()).toBe(0)
|
||||
app.increment()
|
||||
app.history.mark('stop at 1')
|
||||
app.increment()
|
||||
app.history.mark('stop at 2')
|
||||
app.increment()
|
||||
app.increment()
|
||||
app.history.mark('stop at 4')
|
||||
app.increment()
|
||||
app.increment()
|
||||
app.increment()
|
||||
expect(app.getCount()).toBe(7)
|
||||
app.history.undo()
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(2)
|
||||
app.history.mark('wayward stopping point')
|
||||
app.history.redo()
|
||||
app.history.redo()
|
||||
expect(app.getCount()).toBe(7)
|
||||
expect(editor.getCount()).toBe(0)
|
||||
editor.increment()
|
||||
editor.history.mark('stop at 1')
|
||||
editor.increment()
|
||||
editor.history.mark('stop at 2')
|
||||
editor.increment()
|
||||
editor.increment()
|
||||
editor.history.mark('stop at 4')
|
||||
editor.increment()
|
||||
editor.increment()
|
||||
editor.increment()
|
||||
expect(editor.getCount()).toBe(7)
|
||||
editor.history.undo()
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(2)
|
||||
editor.history.mark('wayward stopping point')
|
||||
editor.history.redo()
|
||||
editor.history.redo()
|
||||
expect(editor.getCount()).toBe(7)
|
||||
|
||||
app.history.undo()
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(2)
|
||||
app.increment()
|
||||
expect(app.getCount()).toBe(3)
|
||||
app.history.redo()
|
||||
expect(app.getCount()).toBe(3)
|
||||
app.history.redo()
|
||||
expect(app.getCount()).toBe(3)
|
||||
editor.history.undo()
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(2)
|
||||
editor.increment()
|
||||
expect(editor.getCount()).toBe(3)
|
||||
editor.history.redo()
|
||||
expect(editor.getCount()).toBe(3)
|
||||
editor.history.redo()
|
||||
expect(editor.getCount()).toBe(3)
|
||||
})
|
||||
|
||||
it('allows squashing of commands', () => {
|
||||
app.increment()
|
||||
editor.increment()
|
||||
|
||||
app.history.mark('stop at 1')
|
||||
expect(app.getCount()).toBe(1)
|
||||
editor.history.mark('stop at 1')
|
||||
expect(editor.getCount()).toBe(1)
|
||||
|
||||
app.increment(1, true)
|
||||
app.increment(1, true)
|
||||
app.increment(1, true)
|
||||
app.increment(1, true)
|
||||
editor.increment(1, true)
|
||||
editor.increment(1, true)
|
||||
editor.increment(1, true)
|
||||
editor.increment(1, true)
|
||||
|
||||
expect(app.getCount()).toBe(5)
|
||||
expect(editor.getCount()).toBe(5)
|
||||
|
||||
expect(app.history.numUndos).toBe(3)
|
||||
expect(editor.history.numUndos).toBe(3)
|
||||
})
|
||||
|
||||
it('allows ephemeral commands that do not affect the stack', () => {
|
||||
app.increment()
|
||||
app.history.mark('stop at 1')
|
||||
app.increment()
|
||||
app.setName('wilbur')
|
||||
app.increment()
|
||||
expect(app.getCount()).toBe(3)
|
||||
expect(app.getName()).toBe('wilbur')
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(1)
|
||||
expect(app.getName()).toBe('wilbur')
|
||||
editor.increment()
|
||||
editor.history.mark('stop at 1')
|
||||
editor.increment()
|
||||
editor.setName('wilbur')
|
||||
editor.increment()
|
||||
expect(editor.getCount()).toBe(3)
|
||||
expect(editor.getName()).toBe('wilbur')
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(1)
|
||||
expect(editor.getName()).toBe('wilbur')
|
||||
})
|
||||
|
||||
it('allows inconsequential commands that do not clear the redo stack', () => {
|
||||
app.increment()
|
||||
app.history.mark('stop at 1')
|
||||
app.increment()
|
||||
expect(app.getCount()).toBe(2)
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(1)
|
||||
app.history.mark('stop at age 35')
|
||||
app.setAge(23)
|
||||
app.history.mark('stop at age 23')
|
||||
expect(app.getCount()).toBe(1)
|
||||
app.history.redo()
|
||||
expect(app.getCount()).toBe(2)
|
||||
expect(app.getAge()).toBe(23)
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(1)
|
||||
expect(app.getAge()).toBe(23)
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(1)
|
||||
expect(app.getAge()).toBe(35)
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(0)
|
||||
expect(app.getAge()).toBe(35)
|
||||
editor.increment()
|
||||
editor.history.mark('stop at 1')
|
||||
editor.increment()
|
||||
expect(editor.getCount()).toBe(2)
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(1)
|
||||
editor.history.mark('stop at age 35')
|
||||
editor.setAge(23)
|
||||
editor.history.mark('stop at age 23')
|
||||
expect(editor.getCount()).toBe(1)
|
||||
editor.history.redo()
|
||||
expect(editor.getCount()).toBe(2)
|
||||
expect(editor.getAge()).toBe(23)
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(1)
|
||||
expect(editor.getAge()).toBe(23)
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(1)
|
||||
expect(editor.getAge()).toBe(35)
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(0)
|
||||
expect(editor.getAge()).toBe(35)
|
||||
})
|
||||
|
||||
it('does not allow new history entries to be pushed if a command invokes them while doing or undoing', () => {
|
||||
app.incrementTwice()
|
||||
expect(app.history.numUndos).toBe(1)
|
||||
expect(app.getCount()).toBe(2)
|
||||
app.history.undo()
|
||||
expect(app.getCount()).toBe(0)
|
||||
expect(app.history.numUndos).toBe(0)
|
||||
editor.incrementTwice()
|
||||
expect(editor.history.numUndos).toBe(1)
|
||||
expect(editor.getCount()).toBe(2)
|
||||
editor.history.undo()
|
||||
expect(editor.getCount()).toBe(0)
|
||||
expect(editor.history.numUndos).toBe(0)
|
||||
})
|
||||
|
||||
it('does not allow new history entries to be pushed if a command invokes them while bailing', () => {
|
||||
app.history.mark('0')
|
||||
app.incrementTwice()
|
||||
app.history.mark('2')
|
||||
app.incrementTwice()
|
||||
app.incrementTwice()
|
||||
expect(app.history.numUndos).toBe(5)
|
||||
expect(app.getCount()).toBe(6)
|
||||
app.history.bail()
|
||||
expect(app.getCount()).toBe(2)
|
||||
expect(app.history.numUndos).toBe(2)
|
||||
app.history.bailToMark('0')
|
||||
expect(app.history.numUndos).toBe(0)
|
||||
expect(app.getCount()).toBe(0)
|
||||
editor.history.mark('0')
|
||||
editor.incrementTwice()
|
||||
editor.history.mark('2')
|
||||
editor.incrementTwice()
|
||||
editor.incrementTwice()
|
||||
expect(editor.history.numUndos).toBe(5)
|
||||
expect(editor.getCount()).toBe(6)
|
||||
editor.history.bail()
|
||||
expect(editor.getCount()).toBe(2)
|
||||
expect(editor.history.numUndos).toBe(2)
|
||||
editor.history.bailToMark('0')
|
||||
expect(editor.history.numUndos).toBe(0)
|
||||
expect(editor.getCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('supports bailing to a particular mark', () => {
|
||||
app.increment()
|
||||
app.history.mark('1')
|
||||
app.increment()
|
||||
app.history.mark('2')
|
||||
app.increment()
|
||||
app.history.mark('3')
|
||||
app.increment()
|
||||
editor.increment()
|
||||
editor.history.mark('1')
|
||||
editor.increment()
|
||||
editor.history.mark('2')
|
||||
editor.increment()
|
||||
editor.history.mark('3')
|
||||
editor.increment()
|
||||
|
||||
expect(app.getCount()).toBe(4)
|
||||
app.history.bailToMark('2')
|
||||
expect(app.getCount()).toBe(2)
|
||||
expect(editor.getCount()).toBe(4)
|
||||
editor.history.bailToMark('2')
|
||||
expect(editor.getCount()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -16,7 +16,7 @@ import { TLLineShape, TLParentId, TLShape, TLShapeId, Vec2dModel } from '@tldraw
|
|||
import { compact, dedupe, deepCopy } from '@tldraw/utils'
|
||||
import { atom, computed, EMPTY_ARRAY } from 'signia'
|
||||
import { uniqueId } from '../../utils/data'
|
||||
import type { App } from '../App'
|
||||
import type { Editor } from '../Editor'
|
||||
import { getSplineForLineShape, TLLineUtil } from '../shapeutils/TLLineUtil/TLLineUtil'
|
||||
|
||||
export type PointsSnapLine = {
|
||||
|
@ -221,13 +221,13 @@ export class SnapManager {
|
|||
this._snapLines.set(lines)
|
||||
}
|
||||
|
||||
constructor(public readonly app: App) {}
|
||||
constructor(public readonly editor: Editor) {}
|
||||
|
||||
@computed get snapPointsCache() {
|
||||
return this.app.store.createComputedCache<SnapPoint[], TLShape>('snapPoints', (shape) => {
|
||||
const pageTransfrorm = this.app.getPageTransformById(shape.id)
|
||||
return this.editor.store.createComputedCache<SnapPoint[], TLShape>('snapPoints', (shape) => {
|
||||
const pageTransfrorm = this.editor.getPageTransformById(shape.id)
|
||||
if (!pageTransfrorm) return undefined
|
||||
const util = this.app.getShapeUtil(shape)
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
const snapPoints = util.snapPoints(shape)
|
||||
return snapPoints.map((point, i) => {
|
||||
const { x, y } = Matrix2d.applyToPoint(pageTransfrorm, point)
|
||||
|
@ -237,23 +237,23 @@ export class SnapManager {
|
|||
}
|
||||
|
||||
get snapThreshold() {
|
||||
return 8 / this.app.zoomLevel
|
||||
return 8 / this.editor.zoomLevel
|
||||
}
|
||||
|
||||
// TODO: make this an incremental derivation
|
||||
@computed get visibleShapesNotInSelection() {
|
||||
const selectedIds = this.app.selectedIds
|
||||
const selectedIds = this.editor.selectedIds
|
||||
|
||||
const result: Set<{ id: TLShapeId; pageBounds: Box2d }> = new Set()
|
||||
|
||||
const processParent = (parentId: TLParentId) => {
|
||||
const children = this.app.getSortedChildIds(parentId)
|
||||
const children = this.editor.getSortedChildIds(parentId)
|
||||
for (const id of children) {
|
||||
const shape = this.app.getShapeById(id)
|
||||
const shape = this.editor.getShapeById(id)
|
||||
if (!shape) continue
|
||||
if (shape.type === 'arrow') continue
|
||||
if (selectedIds.includes(id)) continue
|
||||
if (!this.app.isShapeInViewport(shape.id)) continue
|
||||
if (!this.editor.isShapeInViewport(shape.id)) continue
|
||||
|
||||
if (shape.type === 'group') {
|
||||
// snap to children of group but not group itself
|
||||
|
@ -261,7 +261,7 @@ export class SnapManager {
|
|||
continue
|
||||
}
|
||||
|
||||
result.add({ id: shape.id, pageBounds: this.app.getPageBoundsById(shape.id)! })
|
||||
result.add({ id: shape.id, pageBounds: this.editor.getPageBoundsById(shape.id)! })
|
||||
|
||||
// don't snap to children of frame
|
||||
if (shape.type !== 'frame') {
|
||||
|
@ -270,12 +270,12 @@ export class SnapManager {
|
|||
}
|
||||
}
|
||||
|
||||
const commonFrameAncestor = this.app.findCommonAncestor(
|
||||
compact(selectedIds.map((id) => this.app.getShapeById(id))),
|
||||
const commonFrameAncestor = this.editor.findCommonAncestor(
|
||||
compact(selectedIds.map((id) => this.editor.getShapeById(id))),
|
||||
(parent) => parent.type === 'frame'
|
||||
)
|
||||
|
||||
processParent(commonFrameAncestor ?? this.app.currentPageId)
|
||||
processParent(commonFrameAncestor ?? this.editor.currentPageId)
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -484,7 +484,7 @@ export class SnapManager {
|
|||
handleId: string
|
||||
handlePoint: Vec2d
|
||||
}): SnapData {
|
||||
const line = this.app.getShapeById<TLLineShape>(lineId)
|
||||
const line = this.editor.getShapeById<TLLineShape>(lineId)
|
||||
if (!line) {
|
||||
return { nudge: new Vec2d(0, 0) }
|
||||
}
|
||||
|
@ -495,7 +495,7 @@ export class SnapManager {
|
|||
// and then pass them to the snap function as 'additionalOutlines'
|
||||
|
||||
// First, let's find which handle we're dragging
|
||||
const util = this.app.getShapeUtil(TLLineUtil)
|
||||
const util = this.editor.getShapeUtil(TLLineUtil)
|
||||
const handles = util.handles(line).sort(sortByIndex)
|
||||
if (handles.length < 3) return { nudge: new Vec2d(0, 0) }
|
||||
|
||||
|
@ -529,7 +529,7 @@ export class SnapManager {
|
|||
// (and by the way - we want to get the splines in page space, not shape space)
|
||||
const spline = getSplineForLineShape(line)
|
||||
const ignoreCount = 1
|
||||
const pageTransform = this.app.getPageTransform(line)!
|
||||
const pageTransform = this.editor.getPageTransform(line)!
|
||||
|
||||
const pageHeadSegments = spline.segments
|
||||
.slice(0, Math.max(0, segmentNumber - ignoreCount))
|
||||
|
@ -560,21 +560,23 @@ export class SnapManager {
|
|||
const visibleShapesNotInSelection = this.visibleShapesNotInSelection
|
||||
const pageOutlines = []
|
||||
for (const visibleShape of visibleShapesNotInSelection) {
|
||||
const shape = this.app.getShapeById(visibleShape.id)!
|
||||
const shape = this.editor.getShapeById(visibleShape.id)!
|
||||
|
||||
if (shape.type === 'text' || shape.type === 'icon') {
|
||||
continue
|
||||
}
|
||||
|
||||
const outline = deepCopy(this.app.getOutlineById(visibleShape.id))
|
||||
const outline = deepCopy(this.editor.getOutlineById(visibleShape.id))
|
||||
|
||||
const isClosed = this.app.getShapeUtil(shape).isClosed?.(shape)
|
||||
const isClosed = this.editor.getShapeUtil(shape).isClosed?.(shape)
|
||||
|
||||
if (isClosed) {
|
||||
outline.push(outline[0])
|
||||
}
|
||||
|
||||
pageOutlines.push(Matrix2d.applyToPoints(this.app.getPageTransformById(shape.id)!, outline))
|
||||
pageOutlines.push(
|
||||
Matrix2d.applyToPoints(this.editor.getPageTransformById(shape.id)!, outline)
|
||||
)
|
||||
}
|
||||
|
||||
// Find the nearest point that is within the snap threshold
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Box2dModel, TLAlignType } from '@tldraw/tlschema'
|
||||
import { uniqueId } from '../../utils/data'
|
||||
import { App } from '../App'
|
||||
import { Editor } from '../Editor'
|
||||
import { TextHelpers } from '../shapeutils/TLTextUtil/TextHelpers'
|
||||
|
||||
const textAlignmentsForLtr = {
|
||||
|
@ -29,14 +29,14 @@ type MeasureTextSpanOpts = {
|
|||
const spaceCharacterRegex = /\s/
|
||||
|
||||
export class TextManager {
|
||||
constructor(public app: App) {}
|
||||
constructor(public editor: Editor) {}
|
||||
|
||||
getTextElement() {
|
||||
const oldElm = document.querySelector('.tl-text-measure')
|
||||
oldElm?.remove()
|
||||
|
||||
const elm = document.createElement('div')
|
||||
this.app.getContainer().appendChild(elm)
|
||||
this.editor.getContainer().appendChild(elm)
|
||||
|
||||
elm.id = `__textMeasure_${uniqueId()}`
|
||||
elm.classList.add('tl-text')
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Vec2d } from '@tldraw/primitives'
|
||||
import { App } from '../App'
|
||||
import { Editor } from '../Editor'
|
||||
|
||||
export class TickManager {
|
||||
constructor(public app: App) {
|
||||
this.app.disposables.add(this.dispose)
|
||||
constructor(public editor: Editor) {
|
||||
this.editor.disposables.add(this.dispose)
|
||||
this.start()
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ export class TickManager {
|
|||
this.last = now
|
||||
this.t += elapsed
|
||||
|
||||
this.app.emit('frame', elapsed)
|
||||
this.editor.emit('frame', elapsed)
|
||||
|
||||
if (this.t < 16) {
|
||||
this.raf = requestAnimationFrame(this.tick)
|
||||
|
@ -38,7 +38,7 @@ export class TickManager {
|
|||
|
||||
this.t -= 16
|
||||
this.updatePointerVelocity(elapsed)
|
||||
this.app.emit('tick', elapsed)
|
||||
this.editor.emit('tick', elapsed)
|
||||
this.raf = requestAnimationFrame(this.tick)
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ export class TickManager {
|
|||
private updatePointerVelocity = (elapsed: number) => {
|
||||
const {
|
||||
prevPoint,
|
||||
app: {
|
||||
editor: {
|
||||
inputs: { currentScreenPoint, pointerVelocity },
|
||||
},
|
||||
} = this
|
||||
|
@ -74,7 +74,7 @@ export class TickManager {
|
|||
if (Math.abs(next.y) < 0.01) next.y = 0
|
||||
|
||||
if (!pointerVelocity.equals(next)) {
|
||||
this.app.inputs.pointerVelocity = next
|
||||
this.editor.inputs.pointerVelocity = next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { TAU } from '@tldraw/primitives'
|
||||
import { createCustomShapeId, TLArrowShape, TLArrowTerminal, TLShapeId } from '@tldraw/tlschema'
|
||||
import { assert } from '@tldraw/utils'
|
||||
import { TestApp } from '../../../test/TestApp'
|
||||
import { TestEditor } from '../../../test/TestEditor'
|
||||
import { TLArrowUtil } from './TLArrowUtil'
|
||||
|
||||
let app: TestApp
|
||||
let editor: TestEditor
|
||||
|
||||
const ids = {
|
||||
box1: createCustomShapeId('box1'),
|
||||
|
@ -25,8 +25,8 @@ window.cancelAnimationFrame = function cancelAnimationFrame(id) {
|
|||
}
|
||||
|
||||
beforeEach(() => {
|
||||
app = new TestApp()
|
||||
app
|
||||
editor = new TestEditor()
|
||||
editor
|
||||
.selectAll()
|
||||
.deleteShapes()
|
||||
.createShapes([
|
||||
|
@ -57,15 +57,15 @@ beforeEach(() => {
|
|||
|
||||
describe('When translating a bound shape', () => {
|
||||
it('updates the arrow when straight', () => {
|
||||
app.select(ids.box2)
|
||||
app.pointerDown(250, 250, { target: 'shape', shape: app.getShapeById(ids.box2) })
|
||||
app.pointerMove(300, 300) // move box 2 by 50, 50
|
||||
app.expectShapeToMatch({
|
||||
editor.select(ids.box2)
|
||||
editor.pointerDown(250, 250, { target: 'shape', shape: editor.getShapeById(ids.box2) })
|
||||
editor.pointerMove(300, 300) // move box 2 by 50, 50
|
||||
editor.expectShapeToMatch({
|
||||
id: ids.box2,
|
||||
x: 350,
|
||||
y: 350,
|
||||
})
|
||||
app.expectShapeToMatch({
|
||||
editor.expectShapeToMatch({
|
||||
id: ids.arrow1,
|
||||
type: 'arrow',
|
||||
x: 150,
|
||||
|
@ -88,16 +88,16 @@ describe('When translating a bound shape', () => {
|
|||
})
|
||||
|
||||
it('updates the arrow when curved', () => {
|
||||
app.updateShapes([{ id: ids.arrow1, type: 'arrow', props: { bend: 20 } }])
|
||||
app.select(ids.box2)
|
||||
app.pointerDown(250, 250, { target: 'shape', shape: app.getShapeById(ids.box2) })
|
||||
app.pointerMove(300, 300) // move box 2 by 50, 50
|
||||
app.expectShapeToMatch({
|
||||
editor.updateShapes([{ id: ids.arrow1, type: 'arrow', props: { bend: 20 } }])
|
||||
editor.select(ids.box2)
|
||||
editor.pointerDown(250, 250, { target: 'shape', shape: editor.getShapeById(ids.box2) })
|
||||
editor.pointerMove(300, 300) // move box 2 by 50, 50
|
||||
editor.expectShapeToMatch({
|
||||
id: ids.box2,
|
||||
x: 350,
|
||||
y: 350,
|
||||
})
|
||||
app.expectShapeToMatch({
|
||||
editor.expectShapeToMatch({
|
||||
id: ids.arrow1,
|
||||
type: 'arrow',
|
||||
x: 150,
|
||||
|
@ -122,10 +122,10 @@ describe('When translating a bound shape', () => {
|
|||
|
||||
describe('When translating the arrow', () => {
|
||||
it('unbinds all handles if neither bound shape is not also translating', () => {
|
||||
app.select(ids.arrow1)
|
||||
app.pointerDown(200, 200, { target: 'shape', shape: app.getShapeById(ids.arrow1)! })
|
||||
app.pointerMove(200, 190)
|
||||
app.expectShapeToMatch({
|
||||
editor.select(ids.arrow1)
|
||||
editor.pointerDown(200, 200, { target: 'shape', shape: editor.getShapeById(ids.arrow1)! })
|
||||
editor.pointerMove(200, 190)
|
||||
editor.expectShapeToMatch({
|
||||
id: ids.arrow1,
|
||||
type: 'arrow',
|
||||
x: 150,
|
||||
|
@ -138,16 +138,16 @@ describe('When translating the arrow', () => {
|
|||
})
|
||||
|
||||
it('retains all handles if either bound shape is also translating', () => {
|
||||
app.select(ids.arrow1, ids.box2)
|
||||
expect(app.selectedPageBounds).toMatchObject({
|
||||
editor.select(ids.arrow1, ids.box2)
|
||||
expect(editor.selectedPageBounds).toMatchObject({
|
||||
x: 200,
|
||||
y: 200,
|
||||
w: 200,
|
||||
h: 200,
|
||||
})
|
||||
app.pointerDown(300, 300, { target: 'selection' })
|
||||
app.pointerMove(300, 250)
|
||||
app.expectShapeToMatch({
|
||||
editor.pointerDown(300, 300, { target: 'selection' })
|
||||
editor.pointerMove(300, 250)
|
||||
editor.expectShapeToMatch({
|
||||
id: ids.arrow1,
|
||||
type: 'arrow',
|
||||
x: 150,
|
||||
|
@ -172,12 +172,12 @@ describe('When translating the arrow', () => {
|
|||
|
||||
describe('Other cases when arrow are moved', () => {
|
||||
it('nudge', () => {
|
||||
app.select(ids.arrow1, ids.box2)
|
||||
editor.select(ids.arrow1, ids.box2)
|
||||
|
||||
// When box one is not selected, unbinds box1 and keeps binding to box2
|
||||
app.nudgeShapes(app.selectedIds, { x: 0, y: -1 })
|
||||
editor.nudgeShapes(editor.selectedIds, { x: 0, y: -1 })
|
||||
|
||||
expect(app.getShapeById(ids.arrow1)).toMatchObject({
|
||||
expect(editor.getShapeById(ids.arrow1)).toMatchObject({
|
||||
props: {
|
||||
start: { type: 'binding', boundShapeId: ids.box1 },
|
||||
end: { type: 'binding', boundShapeId: ids.box2 },
|
||||
|
@ -185,23 +185,23 @@ describe('Other cases when arrow are moved', () => {
|
|||
})
|
||||
|
||||
// unbinds when only the arrow is selected (not its bound shapes)
|
||||
app.select(ids.arrow1)
|
||||
app.nudgeShapes(app.selectedIds, { x: 0, y: -1 })
|
||||
editor.select(ids.arrow1)
|
||||
editor.nudgeShapes(editor.selectedIds, { x: 0, y: -1 })
|
||||
|
||||
expect(app.getShapeById(ids.arrow1)).toMatchObject({
|
||||
expect(editor.getShapeById(ids.arrow1)).toMatchObject({
|
||||
props: { start: { type: 'point' }, end: { type: 'point' } },
|
||||
})
|
||||
})
|
||||
|
||||
it('align', () => {
|
||||
app.createShapes([{ id: ids.box3, type: 'geo', x: 500, y: 300, props: { w: 100, h: 100 } }])
|
||||
editor.createShapes([{ id: ids.box3, type: 'geo', x: 500, y: 300, props: { w: 100, h: 100 } }])
|
||||
|
||||
// When box one is not selected, unbinds box1 and keeps binding to box2
|
||||
app.select(ids.arrow1, ids.box2, ids.box3)
|
||||
app.alignShapes('right')
|
||||
editor.select(ids.arrow1, ids.box2, ids.box3)
|
||||
editor.alignShapes('right')
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
expect(app.getShapeById(ids.arrow1)).toMatchObject({
|
||||
expect(editor.getShapeById(ids.arrow1)).toMatchObject({
|
||||
props: {
|
||||
start: { type: 'binding', boundShapeId: ids.box1 },
|
||||
end: { type: 'binding', boundShapeId: ids.box2 },
|
||||
|
@ -209,11 +209,11 @@ describe('Other cases when arrow are moved', () => {
|
|||
})
|
||||
|
||||
// unbinds when only the arrow is selected (not its bound shapes)
|
||||
app.select(ids.arrow1, ids.box3)
|
||||
app.alignShapes('top')
|
||||
editor.select(ids.arrow1, ids.box3)
|
||||
editor.alignShapes('top')
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
expect(app.getShapeById(ids.arrow1)).toMatchObject({
|
||||
expect(editor.getShapeById(ids.arrow1)).toMatchObject({
|
||||
props: {
|
||||
start: {
|
||||
type: 'point',
|
||||
|
@ -226,17 +226,17 @@ describe('Other cases when arrow are moved', () => {
|
|||
})
|
||||
|
||||
it('distribute', () => {
|
||||
app.createShapes([
|
||||
editor.createShapes([
|
||||
{ id: ids.box3, type: 'geo', x: 0, y: 300, props: { w: 100, h: 100 } },
|
||||
{ id: ids.box4, type: 'geo', x: 0, y: 600, props: { w: 100, h: 100 } },
|
||||
])
|
||||
|
||||
// When box one is not selected, unbinds box1 and keeps binding to box2
|
||||
app.select(ids.arrow1, ids.box2, ids.box3)
|
||||
app.distributeShapes('horizontal')
|
||||
editor.select(ids.arrow1, ids.box2, ids.box3)
|
||||
editor.distributeShapes('horizontal')
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
expect(app.getShapeById(ids.arrow1)).toMatchObject({
|
||||
expect(editor.getShapeById(ids.arrow1)).toMatchObject({
|
||||
props: {
|
||||
start: {
|
||||
type: 'binding',
|
||||
|
@ -250,12 +250,12 @@ describe('Other cases when arrow are moved', () => {
|
|||
})
|
||||
|
||||
// unbinds when only the arrow is selected (not its bound shapes) if the arrow itself has moved
|
||||
app.select(ids.arrow1, ids.box3, ids.box4)
|
||||
app.distributeShapes('vertical')
|
||||
editor.select(ids.arrow1, ids.box3, ids.box4)
|
||||
editor.distributeShapes('vertical')
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
// The arrow didn't actually move
|
||||
expect(app.getShapeById(ids.arrow1)).toMatchObject({
|
||||
expect(editor.getShapeById(ids.arrow1)).toMatchObject({
|
||||
props: {
|
||||
start: {
|
||||
type: 'binding',
|
||||
|
@ -269,11 +269,11 @@ describe('Other cases when arrow are moved', () => {
|
|||
})
|
||||
|
||||
// The arrow will move this time, so it should unbind
|
||||
app.updateShapes([{ id: ids.box4, type: 'geo', y: -600 }])
|
||||
app.distributeShapes('vertical')
|
||||
editor.updateShapes([{ id: ids.box4, type: 'geo', y: -600 }])
|
||||
editor.distributeShapes('vertical')
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
expect(app.getShapeById(ids.arrow1)).toMatchObject({
|
||||
expect(editor.getShapeById(ids.arrow1)).toMatchObject({
|
||||
props: {
|
||||
start: {
|
||||
type: 'point',
|
||||
|
@ -287,7 +287,7 @@ describe('Other cases when arrow are moved', () => {
|
|||
|
||||
it('when translating with a group that the arrow is bound into', () => {
|
||||
// create shapes in a group:
|
||||
app
|
||||
editor
|
||||
.selectAll()
|
||||
.deleteShapes()
|
||||
.createShapes([
|
||||
|
@ -297,18 +297,18 @@ describe('Other cases when arrow are moved', () => {
|
|||
.selectAll()
|
||||
.groupShapes()
|
||||
|
||||
app.setSelectedTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350)
|
||||
let arrow = app.shapesArray[app.shapesArray.length - 1]
|
||||
assert(app.isShapeOfType(arrow, TLArrowUtil))
|
||||
editor.setSelectedTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350)
|
||||
let arrow = editor.shapesArray[editor.shapesArray.length - 1]
|
||||
assert(editor.isShapeOfType(arrow, TLArrowUtil))
|
||||
assert(arrow.props.end.type === 'binding')
|
||||
expect(arrow.props.end.boundShapeId).toBe(ids.box3)
|
||||
|
||||
// translate:
|
||||
app.selectAll().nudgeShapes(app.selectedIds, { x: 0, y: 1 })
|
||||
editor.selectAll().nudgeShapes(editor.selectedIds, { x: 0, y: 1 })
|
||||
|
||||
// arrow should still be bound to box3
|
||||
arrow = app.getShapeById(arrow.id)!
|
||||
assert(app.isShapeOfType(arrow, TLArrowUtil))
|
||||
arrow = editor.getShapeById(arrow.id)!
|
||||
assert(editor.isShapeOfType(arrow, TLArrowUtil))
|
||||
assert(arrow.props.end.type === 'binding')
|
||||
expect(arrow.props.end.boundShapeId).toBe(ids.box3)
|
||||
})
|
||||
|
@ -316,11 +316,11 @@ describe('Other cases when arrow are moved', () => {
|
|||
|
||||
describe('When a shape it rotated', () => {
|
||||
it('binds correctly', () => {
|
||||
app.setSelectedTool('arrow').pointerDown(0, 0).pointerMove(375, 375)
|
||||
editor.setSelectedTool('arrow').pointerDown(0, 0).pointerMove(375, 375)
|
||||
|
||||
const arrow = app.shapesArray[app.shapesArray.length - 1]
|
||||
const arrow = editor.shapesArray[editor.shapesArray.length - 1]
|
||||
|
||||
expect(app.getShapeById(arrow.id)).toMatchObject({
|
||||
expect(editor.getShapeById(arrow.id)).toMatchObject({
|
||||
props: {
|
||||
start: { type: 'point' },
|
||||
end: {
|
||||
|
@ -331,11 +331,11 @@ describe('When a shape it rotated', () => {
|
|||
},
|
||||
})
|
||||
|
||||
app.updateShapes([{ id: ids.box2, type: 'geo', rotation: TAU }])
|
||||
editor.updateShapes([{ id: ids.box2, type: 'geo', rotation: TAU }])
|
||||
|
||||
app.pointerMove(225, 350)
|
||||
editor.pointerMove(225, 350)
|
||||
|
||||
expect(app.getShapeById(arrow.id)).toMatchObject({
|
||||
expect(editor.getShapeById(arrow.id)).toMatchObject({
|
||||
props: {
|
||||
start: { type: 'point' },
|
||||
end: { type: 'binding', boundShapeId: ids.box2 },
|
||||
|
@ -343,7 +343,9 @@ describe('When a shape it rotated', () => {
|
|||
})
|
||||
|
||||
const anchor = (
|
||||
app.getShapeById<TLArrowShape>(arrow.id)!.props.end as TLArrowTerminal & { type: 'binding' }
|
||||
editor.getShapeById<TLArrowShape>(arrow.id)!.props.end as TLArrowTerminal & {
|
||||
type: 'binding'
|
||||
}
|
||||
).normalizedAnchor
|
||||
expect(anchor.x).toBeCloseTo(0.5)
|
||||
expect(anchor.y).toBeCloseTo(0.75)
|
||||
|
@ -352,7 +354,7 @@ describe('When a shape it rotated', () => {
|
|||
|
||||
describe('resizing', () => {
|
||||
it('resizes', () => {
|
||||
app
|
||||
editor
|
||||
.selectAll()
|
||||
.deleteShapes()
|
||||
.setSelectedTool('arrow')
|
||||
|
@ -365,17 +367,17 @@ describe('resizing', () => {
|
|||
.pointerUp()
|
||||
.setSelectedTool('select')
|
||||
|
||||
const arrow1 = app.shapesArray.at(-2)!
|
||||
const arrow2 = app.shapesArray.at(-1)!
|
||||
const arrow1 = editor.shapesArray.at(-2)!
|
||||
const arrow2 = editor.shapesArray.at(-1)!
|
||||
|
||||
app
|
||||
editor
|
||||
.select(arrow1.id, arrow2.id)
|
||||
.pointerDown(150, 300, { target: 'selection', handle: 'bottom' })
|
||||
.pointerMove(150, 600)
|
||||
|
||||
.expectPathToBe('root.select.resizing')
|
||||
|
||||
expect(app.getShapeById(arrow1.id)).toMatchObject({
|
||||
expect(editor.getShapeById(arrow1.id)).toMatchObject({
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
|
@ -390,7 +392,7 @@ describe('resizing', () => {
|
|||
},
|
||||
})
|
||||
|
||||
expect(app.getShapeById(arrow2.id)).toMatchObject({
|
||||
expect(editor.getShapeById(arrow2.id)).toMatchObject({
|
||||
x: 100,
|
||||
y: 200,
|
||||
props: {
|
||||
|
@ -407,7 +409,7 @@ describe('resizing', () => {
|
|||
})
|
||||
|
||||
it('flips bend when flipping x or y', () => {
|
||||
app
|
||||
editor
|
||||
.selectAll()
|
||||
.deleteShapes()
|
||||
.setSelectedTool('arrow')
|
||||
|
@ -420,39 +422,39 @@ describe('resizing', () => {
|
|||
.pointerUp()
|
||||
.setSelectedTool('select')
|
||||
|
||||
const arrow1 = app.shapesArray.at(-2)!
|
||||
const arrow2 = app.shapesArray.at(-1)!
|
||||
const arrow1 = editor.shapesArray.at(-2)!
|
||||
const arrow2 = editor.shapesArray.at(-1)!
|
||||
|
||||
app.updateShapes([{ id: arrow1.id, type: 'arrow', props: { bend: 50 } }])
|
||||
editor.updateShapes([{ id: arrow1.id, type: 'arrow', props: { bend: 50 } }])
|
||||
|
||||
app
|
||||
editor
|
||||
.select(arrow1.id, arrow2.id)
|
||||
.pointerDown(150, 300, { target: 'selection', handle: 'bottom' })
|
||||
.pointerMove(150, -300)
|
||||
|
||||
.expectPathToBe('root.select.resizing')
|
||||
|
||||
expect(app.getShapeById(arrow1.id)).toCloselyMatchObject({
|
||||
expect(editor.getShapeById(arrow1.id)).toCloselyMatchObject({
|
||||
props: {
|
||||
bend: -50,
|
||||
},
|
||||
})
|
||||
|
||||
expect(app.getShapeById(arrow2.id)).toCloselyMatchObject({
|
||||
expect(editor.getShapeById(arrow2.id)).toCloselyMatchObject({
|
||||
props: {
|
||||
bend: 0,
|
||||
},
|
||||
})
|
||||
|
||||
app.pointerMove(150, 300)
|
||||
editor.pointerMove(150, 300)
|
||||
|
||||
expect(app.getShapeById(arrow1.id)).toCloselyMatchObject({
|
||||
expect(editor.getShapeById(arrow1.id)).toCloselyMatchObject({
|
||||
props: {
|
||||
bend: 50,
|
||||
},
|
||||
})
|
||||
|
||||
expect(app.getShapeById(arrow2.id)).toCloselyMatchObject({
|
||||
expect(editor.getShapeById(arrow2.id)).toCloselyMatchObject({
|
||||
props: {
|
||||
bend: 0,
|
||||
},
|
||||
|
@ -478,41 +480,41 @@ describe("an arrow's parents", () => {
|
|||
let boxCid: TLShapeId
|
||||
|
||||
beforeEach(() => {
|
||||
app.selectAll().deleteShapes()
|
||||
editor.selectAll().deleteShapes()
|
||||
|
||||
app.setSelectedTool('frame')
|
||||
app.pointerDown(0, 0).pointerMove(100, 100).pointerUp()
|
||||
frameId = app.onlySelectedShape!.id
|
||||
editor.setSelectedTool('frame')
|
||||
editor.pointerDown(0, 0).pointerMove(100, 100).pointerUp()
|
||||
frameId = editor.onlySelectedShape!.id
|
||||
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(10, 10).pointerMove(20, 20).pointerUp()
|
||||
boxAid = app.onlySelectedShape!.id
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(10, 80).pointerMove(20, 90).pointerUp()
|
||||
boxBid = app.onlySelectedShape!.id
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(110, 10).pointerMove(120, 20).pointerUp()
|
||||
boxCid = app.onlySelectedShape!.id
|
||||
editor.setSelectedTool('geo')
|
||||
editor.pointerDown(10, 10).pointerMove(20, 20).pointerUp()
|
||||
boxAid = editor.onlySelectedShape!.id
|
||||
editor.setSelectedTool('geo')
|
||||
editor.pointerDown(10, 80).pointerMove(20, 90).pointerUp()
|
||||
boxBid = editor.onlySelectedShape!.id
|
||||
editor.setSelectedTool('geo')
|
||||
editor.pointerDown(110, 10).pointerMove(120, 20).pointerUp()
|
||||
boxCid = editor.onlySelectedShape!.id
|
||||
})
|
||||
|
||||
it("are updated when the arrow's bound shapes change", () => {
|
||||
// draw arrow from a to empty space within frame, but don't pointer up yet
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(15, 15).pointerMove(50, 50)
|
||||
const arrowId = app.onlySelectedShape!.id
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(15, 15).pointerMove(50, 50)
|
||||
const arrowId = editor.onlySelectedShape!.id
|
||||
|
||||
expect(app.getShapeById(arrowId)).toMatchObject({
|
||||
expect(editor.getShapeById(arrowId)).toMatchObject({
|
||||
props: {
|
||||
start: { type: 'binding', boundShapeId: boxAid },
|
||||
end: { type: 'binding', boundShapeId: frameId },
|
||||
},
|
||||
})
|
||||
expect(app.getShapeById(arrowId)?.parentId).toBe(app.currentPageId)
|
||||
expect(editor.getShapeById(arrowId)?.parentId).toBe(editor.currentPageId)
|
||||
|
||||
// move arrow to b
|
||||
app.pointerMove(15, 85)
|
||||
expect(app.getShapeById(arrowId)?.parentId).toBe(frameId)
|
||||
expect(app.getShapeById(arrowId)).toMatchObject({
|
||||
editor.pointerMove(15, 85)
|
||||
expect(editor.getShapeById(arrowId)?.parentId).toBe(frameId)
|
||||
expect(editor.getShapeById(arrowId)).toMatchObject({
|
||||
props: {
|
||||
start: { type: 'binding', boundShapeId: boxAid },
|
||||
end: { type: 'binding', boundShapeId: boxBid },
|
||||
|
@ -520,9 +522,9 @@ describe("an arrow's parents", () => {
|
|||
})
|
||||
|
||||
// move back to empty space
|
||||
app.pointerMove(50, 50)
|
||||
expect(app.getShapeById(arrowId)?.parentId).toBe(app.currentPageId)
|
||||
expect(app.getShapeById(arrowId)).toMatchObject({
|
||||
editor.pointerMove(50, 50)
|
||||
expect(editor.getShapeById(arrowId)?.parentId).toBe(editor.currentPageId)
|
||||
expect(editor.getShapeById(arrowId)).toMatchObject({
|
||||
props: {
|
||||
start: { type: 'binding', boundShapeId: boxAid },
|
||||
end: { type: 'binding', boundShapeId: frameId },
|
||||
|
@ -532,11 +534,11 @@ describe("an arrow's parents", () => {
|
|||
|
||||
it('reparents when one of the shapes is moved outside of the frame', () => {
|
||||
// draw arrow from a to b
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(15, 15).pointerMove(15, 85).pointerUp()
|
||||
const arrowId = app.onlySelectedShape!.id
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(15, 15).pointerMove(15, 85).pointerUp()
|
||||
const arrowId = editor.onlySelectedShape!.id
|
||||
|
||||
expect(app.getShapeById(arrowId)).toMatchObject({
|
||||
expect(editor.getShapeById(arrowId)).toMatchObject({
|
||||
parentId: frameId,
|
||||
props: {
|
||||
start: { type: 'binding', boundShapeId: boxAid },
|
||||
|
@ -544,9 +546,9 @@ describe("an arrow's parents", () => {
|
|||
},
|
||||
})
|
||||
// move b outside of frame
|
||||
app.select(boxBid).translateSelection(200, 0)
|
||||
expect(app.getShapeById(arrowId)).toMatchObject({
|
||||
parentId: app.currentPageId,
|
||||
editor.select(boxBid).translateSelection(200, 0)
|
||||
expect(editor.getShapeById(arrowId)).toMatchObject({
|
||||
parentId: editor.currentPageId,
|
||||
props: {
|
||||
start: { type: 'binding', boundShapeId: boxAid },
|
||||
end: { type: 'binding', boundShapeId: boxBid },
|
||||
|
@ -556,11 +558,11 @@ describe("an arrow's parents", () => {
|
|||
|
||||
it('reparents to the frame when an arrow created outside has both its parents moved inside', () => {
|
||||
// draw arrow from a to c
|
||||
app.setSelectedTool('arrow')
|
||||
app.pointerDown(15, 15).pointerMove(115, 15).pointerUp()
|
||||
const arrowId = app.onlySelectedShape!.id
|
||||
expect(app.getShapeById(arrowId)).toMatchObject({
|
||||
parentId: app.currentPageId,
|
||||
editor.setSelectedTool('arrow')
|
||||
editor.pointerDown(15, 15).pointerMove(115, 15).pointerUp()
|
||||
const arrowId = editor.onlySelectedShape!.id
|
||||
expect(editor.getShapeById(arrowId)).toMatchObject({
|
||||
parentId: editor.currentPageId,
|
||||
props: {
|
||||
start: { type: 'binding', boundShapeId: boxAid },
|
||||
end: { type: 'binding', boundShapeId: boxCid },
|
||||
|
@ -568,9 +570,9 @@ describe("an arrow's parents", () => {
|
|||
})
|
||||
|
||||
// move c inside of frame
|
||||
app.select(boxCid).translateSelection(-40, 0)
|
||||
editor.select(boxCid).translateSelection(-40, 0)
|
||||
|
||||
expect(app.getShapeById(arrowId)).toMatchObject({
|
||||
expect(editor.getShapeById(arrowId)).toMatchObject({
|
||||
parentId: frameId,
|
||||
props: {
|
||||
start: { type: 'binding', boundShapeId: boxAid },
|
||||
|
|
|
@ -201,12 +201,12 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
|
||||
@computed
|
||||
private get infoCache() {
|
||||
return this.app.store.createComputedCache<ArrowInfo, TLArrowShape>(
|
||||
return this.editor.store.createComputedCache<ArrowInfo, TLArrowShape>(
|
||||
'arrow infoCache',
|
||||
(shape) => {
|
||||
return getIsArrowStraight(shape)
|
||||
? getStraightArrowInfo(this.app, shape)
|
||||
: getCurvedArrowInfo(this.app, shape)
|
||||
? getStraightArrowInfo(this.editor, shape)
|
||||
: getCurvedArrowInfo(this.editor, shape)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -251,10 +251,10 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
switch (handle.id) {
|
||||
case 'start':
|
||||
case 'end': {
|
||||
const pageTransform = this.app.getPageTransformById(next.id)!
|
||||
const pageTransform = this.editor.getPageTransformById(next.id)!
|
||||
const pointInPageSpace = Matrix2d.applyToPoint(pageTransform, handle)
|
||||
|
||||
if (this.app.inputs.ctrlKey) {
|
||||
if (this.editor.inputs.ctrlKey) {
|
||||
next.props[handle.id] = {
|
||||
type: 'point',
|
||||
x: handle.x,
|
||||
|
@ -262,25 +262,28 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
}
|
||||
} else {
|
||||
const target = last(
|
||||
this.app.sortedShapesArray.filter((hitShape) => {
|
||||
this.editor.sortedShapesArray.filter((hitShape) => {
|
||||
if (hitShape.id === shape.id) {
|
||||
// We're testing against the arrow
|
||||
return
|
||||
}
|
||||
|
||||
const util = this.app.getShapeUtil(hitShape)
|
||||
const util = this.editor.getShapeUtil(hitShape)
|
||||
if (!util.canBind(hitShape)) {
|
||||
// The shape can't be bound to
|
||||
return
|
||||
}
|
||||
|
||||
// Check the page mask
|
||||
const pageMask = this.app.getPageMaskById(hitShape.id)
|
||||
const pageMask = this.editor.getPageMaskById(hitShape.id)
|
||||
if (pageMask) {
|
||||
if (!pointInPolygon(pointInPageSpace, pageMask)) return
|
||||
}
|
||||
|
||||
const pointInTargetSpace = this.app.getPointInShapeSpace(hitShape, pointInPageSpace)
|
||||
const pointInTargetSpace = this.editor.getPointInShapeSpace(
|
||||
hitShape,
|
||||
pointInPageSpace
|
||||
)
|
||||
|
||||
if (util.isClosed(hitShape)) {
|
||||
// Test the polygon
|
||||
|
@ -293,8 +296,8 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
)
|
||||
|
||||
if (target) {
|
||||
const targetBounds = this.app.getBounds(target)
|
||||
const pointInTargetSpace = this.app.getPointInShapeSpace(target, pointInPageSpace)
|
||||
const targetBounds = this.editor.getBounds(target)
|
||||
const pointInTargetSpace = this.editor.getPointInShapeSpace(target, pointInPageSpace)
|
||||
|
||||
const prevHandle = next.props[handle.id]
|
||||
|
||||
|
@ -308,14 +311,14 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
// If the other handle is bound to the same shape, then precise
|
||||
((startBindingId || endBindingId) && startBindingId === endBindingId) ||
|
||||
// If the other shape is not closed, then precise
|
||||
!this.app.getShapeUtil(target).isClosed(next)
|
||||
!this.editor.getShapeUtil(target).isClosed(next)
|
||||
|
||||
if (
|
||||
// If we're switching to a new bound shape, then precise only if moving slowly
|
||||
prevHandle.type === 'point' ||
|
||||
(prevHandle.type === 'binding' && target.id !== prevHandle.boundShapeId)
|
||||
) {
|
||||
precise = this.app.inputs.pointerVelocity.len() < 0.5
|
||||
precise = this.editor.inputs.pointerVelocity.len() < 0.5
|
||||
}
|
||||
|
||||
if (precise) {
|
||||
|
@ -327,7 +330,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
4,
|
||||
Math.min(Math.min(targetBounds.width, targetBounds.height) * 0.15, 16)
|
||||
) /
|
||||
this.app.zoomLevel
|
||||
this.editor.zoomLevel
|
||||
}
|
||||
|
||||
next.props[handle.id] = {
|
||||
|
@ -339,7 +342,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
y: (pointInTargetSpace.y - targetBounds.minY) / targetBounds.height,
|
||||
}
|
||||
: { x: 0.5, y: 0.5 },
|
||||
isExact: this.app.inputs.altKey,
|
||||
isExact: this.editor.inputs.altKey,
|
||||
}
|
||||
} else {
|
||||
next.props[handle.id] = {
|
||||
|
@ -353,7 +356,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
}
|
||||
|
||||
case 'middle': {
|
||||
const { start, end } = getArrowTerminalsInArrowSpace(this.app, next)
|
||||
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, next)
|
||||
|
||||
const delta = Vec2d.Sub(end, start)
|
||||
const v = Vec2d.Per(delta)
|
||||
|
@ -383,8 +386,8 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
// If no bound shapes are in the selection, unbind any bound shapes
|
||||
|
||||
if (
|
||||
(startBinding && this.app.isWithinSelection(startBinding)) ||
|
||||
(endBinding && this.app.isWithinSelection(endBinding))
|
||||
(startBinding && this.editor.isWithinSelection(startBinding)) ||
|
||||
(endBinding && this.editor.isWithinSelection(endBinding))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
@ -392,7 +395,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
startBinding = null
|
||||
endBinding = null
|
||||
|
||||
const { start, end } = getArrowTerminalsInArrowSpace(this.app, shape)
|
||||
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
|
@ -416,7 +419,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
onResize: OnResizeHandler<TLArrowShape> = (shape, info) => {
|
||||
const { scaleX, scaleY } = info
|
||||
|
||||
const terminals = getArrowTerminalsInArrowSpace(this.app, shape)
|
||||
const terminals = getArrowTerminalsInArrowSpace(this.editor, shape)
|
||||
|
||||
const { start, end } = deepCopy<TLArrowShape['props']>(shape.props)
|
||||
let { bend } = shape.props
|
||||
|
@ -526,8 +529,8 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
|
||||
hitTestPoint(shape: TLArrowShape, point: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
const zoomLevel = this.app.zoomLevel
|
||||
const offsetDist = this.app.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
|
||||
for (let i = 0; i < outline.length - 1; i++) {
|
||||
const C = outline[i]
|
||||
|
@ -553,14 +556,14 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
|
||||
render(shape: TLArrowShape) {
|
||||
// Not a class component, but eslint can't tell that :(
|
||||
const onlySelectedShape = this.app.onlySelectedShape
|
||||
const onlySelectedShape = this.editor.onlySelectedShape
|
||||
const shouldDisplayHandles =
|
||||
this.app.isInAny(
|
||||
this.editor.isInAny(
|
||||
'select.idle',
|
||||
'select.pointing_handle',
|
||||
'select.dragging_handle',
|
||||
'arrow.dragging'
|
||||
) && !this.app.isReadOnly
|
||||
) && !this.editor.isReadOnly
|
||||
|
||||
const info = this.getArrowInfo(shape)
|
||||
const bounds = this.bounds(shape)
|
||||
|
@ -568,13 +571,13 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const changeIndex = React.useMemo<number>(() => {
|
||||
return this.app.isSafari ? (globalRenderIndex += 1) : 0
|
||||
return this.editor.isSafari ? (globalRenderIndex += 1) : 0
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [shape])
|
||||
|
||||
if (!info?.isValid) return null
|
||||
|
||||
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
|
||||
|
||||
const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth)
|
||||
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
|
||||
|
@ -732,14 +735,14 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
size={shape.props.size}
|
||||
position={info.middle}
|
||||
width={labelSize?.w ?? 0}
|
||||
labelColor={this.app.getCssColor(shape.props.labelColor)}
|
||||
labelColor={this.editor.getCssColor(shape.props.labelColor)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
indicator(shape: TLArrowShape) {
|
||||
const { start, end } = getArrowTerminalsInArrowSpace(this.app, shape)
|
||||
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape)
|
||||
|
||||
const info = this.getArrowInfo(shape)
|
||||
const bounds = this.bounds(shape)
|
||||
|
@ -748,7 +751,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
if (!info) return null
|
||||
if (Vec2d.Equals(start, end)) return null
|
||||
|
||||
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
|
||||
|
||||
const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth)
|
||||
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
|
||||
|
@ -834,7 +837,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
}
|
||||
|
||||
@computed get labelBoundsCache(): ComputedCache<Box2d | null, TLArrowShape> {
|
||||
return this.app.store.createComputedCache('labelBoundsCache', (shape) => {
|
||||
return this.editor.store.createComputedCache('labelBoundsCache', (shape) => {
|
||||
const info = this.getArrowInfo(shape)
|
||||
const bounds = this.bounds(shape)
|
||||
const { text, font, size } = shape.props
|
||||
|
@ -842,7 +845,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
if (!info) return null
|
||||
if (!text.trim()) return null
|
||||
|
||||
const { w, h } = this.app.textMeasure.measureText(text, {
|
||||
const { w, h } = this.editor.textMeasure.measureText(text, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[font],
|
||||
fontSize: ARROW_LABEL_FONT_SIZES[size],
|
||||
|
@ -855,7 +858,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
if (bounds.width > bounds.height) {
|
||||
width = Math.max(Math.min(w, 64), Math.min(bounds.width - 64, w))
|
||||
|
||||
const { w: squishedWidth, h: squishedHeight } = this.app.textMeasure.measureText(text, {
|
||||
const { w: squishedWidth, h: squishedHeight } = this.editor.textMeasure.measureText(text, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[font],
|
||||
fontSize: ARROW_LABEL_FONT_SIZES[size],
|
||||
|
@ -869,7 +872,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
if (width > 16 * ARROW_LABEL_FONT_SIZES[size]) {
|
||||
width = 16 * ARROW_LABEL_FONT_SIZES[size]
|
||||
|
||||
const { w: squishedWidth, h: squishedHeight } = this.app.textMeasure.measureText(text, {
|
||||
const { w: squishedWidth, h: squishedHeight } = this.editor.textMeasure.measureText(text, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[font],
|
||||
fontSize: ARROW_LABEL_FONT_SIZES[size],
|
||||
|
@ -905,7 +908,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
} = shape
|
||||
|
||||
if (text.trimEnd() !== shape.props.text) {
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id,
|
||||
type,
|
||||
|
@ -922,7 +925,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
|
||||
const info = this.getArrowInfo(shape)
|
||||
|
||||
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
|
||||
|
||||
// Group for arrow
|
||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
|
@ -1056,8 +1059,8 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
|
|||
}
|
||||
|
||||
const textElm = createTextSvgElementFromSpans(
|
||||
this.app,
|
||||
this.app.textMeasure.measureTextSpans(shape.props.text, opts),
|
||||
this.editor,
|
||||
this.editor.textMeasure.measureTextSpans(shape.props.text, opts),
|
||||
opts
|
||||
)
|
||||
textElm.setAttribute('fill', colors.fill[shape.props.labelColor])
|
||||
|
|
|
@ -18,27 +18,27 @@ import {
|
|||
MIN_ARROW_LENGTH,
|
||||
WAY_TOO_BIG_ARROW_BEND_FACTOR,
|
||||
} from '../../../../constants'
|
||||
import type { App } from '../../../App'
|
||||
import type { Editor } from '../../../Editor'
|
||||
import { ArcInfo, ArrowInfo } from './arrow-types'
|
||||
import { getArrowTerminalsInArrowSpace, getBoundShapeInfoForTerminal } from './shared'
|
||||
import { getStraightArrowInfo } from './straight-arrow'
|
||||
|
||||
export function getCurvedArrowInfo(app: App, shape: TLArrowShape, extraBend = 0): ArrowInfo {
|
||||
export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBend = 0): ArrowInfo {
|
||||
const { arrowheadEnd, arrowheadStart } = shape.props
|
||||
const bend = shape.props.bend + extraBend
|
||||
|
||||
if (Math.abs(bend) > Math.abs(shape.props.bend * WAY_TOO_BIG_ARROW_BEND_FACTOR)) {
|
||||
return getStraightArrowInfo(app, shape)
|
||||
return getStraightArrowInfo(editor, shape)
|
||||
}
|
||||
|
||||
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(app, shape)
|
||||
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(editor, shape)
|
||||
|
||||
const med = Vec2d.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end) // point between start and end
|
||||
const u = Vec2d.Sub(terminalsInArrowSpace.end, terminalsInArrowSpace.start).uni() // unit vector between start and end
|
||||
const middle = Vec2d.Add(med, u.per().mul(-bend)) // middle handle
|
||||
|
||||
const startShapeInfo = getBoundShapeInfoForTerminal(app, shape.props.start)
|
||||
const endShapeInfo = getBoundShapeInfoForTerminal(app, shape.props.end)
|
||||
const startShapeInfo = getBoundShapeInfoForTerminal(editor, shape.props.start)
|
||||
const endShapeInfo = getBoundShapeInfoForTerminal(editor, shape.props.end)
|
||||
|
||||
// The positions of the body of the arrow, which may be different
|
||||
// than the arrow's start / end points if the arrow is bound to shapes
|
||||
|
@ -48,7 +48,7 @@ export function getCurvedArrowInfo(app: App, shape: TLArrowShape, extraBend = 0)
|
|||
|
||||
const handleArc = getArcInfo(a, b, c)
|
||||
|
||||
const arrowPageTransform = app.getPageTransform(shape)!
|
||||
const arrowPageTransform = editor.getPageTransform(shape)!
|
||||
|
||||
if (startShapeInfo && !startShapeInfo.isExact) {
|
||||
// Points in page space
|
||||
|
@ -97,7 +97,7 @@ export function getCurvedArrowInfo(app: App, shape: TLArrowShape, extraBend = 0)
|
|||
|
||||
if (point) {
|
||||
a.setTo(
|
||||
app.getPointInShapeSpace(shape, Matrix2d.applyToPoint(startShapeInfo.transform, point))
|
||||
editor.getPointInShapeSpace(shape, Matrix2d.applyToPoint(startShapeInfo.transform, point))
|
||||
)
|
||||
|
||||
startShapeInfo.didIntersect = true
|
||||
|
@ -105,9 +105,9 @@ export function getCurvedArrowInfo(app: App, shape: TLArrowShape, extraBend = 0)
|
|||
if (arrowheadStart !== 'none') {
|
||||
const offset =
|
||||
BOUND_ARROW_OFFSET +
|
||||
app.getStrokeWidth(shape.props.size) / 2 +
|
||||
editor.getStrokeWidth(shape.props.size) / 2 +
|
||||
('size' in startShapeInfo.shape.props
|
||||
? app.getStrokeWidth(startShapeInfo.shape.props.size) / 2
|
||||
? editor.getStrokeWidth(startShapeInfo.shape.props.size) / 2
|
||||
: 0)
|
||||
|
||||
a.setTo(
|
||||
|
@ -173,16 +173,18 @@ export function getCurvedArrowInfo(app: App, shape: TLArrowShape, extraBend = 0)
|
|||
|
||||
if (point) {
|
||||
// Set b to target local point -> page point -> shape local point
|
||||
b.setTo(app.getPointInShapeSpace(shape, Matrix2d.applyToPoint(endShapeInfo.transform, point)))
|
||||
b.setTo(
|
||||
editor.getPointInShapeSpace(shape, Matrix2d.applyToPoint(endShapeInfo.transform, point))
|
||||
)
|
||||
|
||||
endShapeInfo.didIntersect = true
|
||||
|
||||
if (arrowheadEnd !== 'none') {
|
||||
let offset =
|
||||
BOUND_ARROW_OFFSET +
|
||||
app.getStrokeWidth(shape.props.size) / 2 +
|
||||
editor.getStrokeWidth(shape.props.size) / 2 +
|
||||
('size' in endShapeInfo.shape.props
|
||||
? app.getStrokeWidth(endShapeInfo.shape.props.size) / 2
|
||||
? editor.getStrokeWidth(endShapeInfo.shape.props.size) / 2
|
||||
: 0)
|
||||
|
||||
if (Vec2d.Dist(a, b) < MIN_ARROW_LENGTH) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Matrix2d, Vec2d } from '@tldraw/primitives'
|
||||
import { TLArrowShape, TLArrowTerminal, TLShape } from '@tldraw/tlschema'
|
||||
import { App } from '../../../App'
|
||||
import { Editor } from '../../../Editor'
|
||||
import { TLShapeUtil } from '../../TLShapeUtil'
|
||||
|
||||
export function getIsArrowStraight(shape: TLArrowShape) {
|
||||
|
@ -18,16 +18,16 @@ export type BoundShapeInfo<T extends TLShape = TLShape> = {
|
|||
}
|
||||
|
||||
export function getBoundShapeInfoForTerminal(
|
||||
app: App,
|
||||
editor: Editor,
|
||||
terminal: TLArrowTerminal
|
||||
): BoundShapeInfo | undefined {
|
||||
if (terminal.type === 'point') {
|
||||
return
|
||||
}
|
||||
|
||||
const shape = app.getShapeById(terminal.boundShapeId)!
|
||||
const util = app.getShapeUtil(shape)
|
||||
const transform = app.getPageTransform(shape)!
|
||||
const shape = editor.getShapeById(terminal.boundShapeId)!
|
||||
const util = editor.getShapeUtil(shape)
|
||||
const transform = editor.getPageTransform(shape)!
|
||||
|
||||
return {
|
||||
shape,
|
||||
|
@ -39,7 +39,7 @@ export function getBoundShapeInfoForTerminal(
|
|||
}
|
||||
|
||||
export function getArrowTerminalInArrowSpace(
|
||||
app: App,
|
||||
editor: Editor,
|
||||
arrowPageTransform: Matrix2d,
|
||||
terminal: TLArrowTerminal
|
||||
) {
|
||||
|
@ -47,7 +47,7 @@ export function getArrowTerminalInArrowSpace(
|
|||
return Vec2d.From(terminal)
|
||||
}
|
||||
|
||||
const boundShape = app.getShapeById(terminal.boundShapeId)
|
||||
const boundShape = editor.getShapeById(terminal.boundShapeId)
|
||||
|
||||
if (!boundShape) {
|
||||
console.error('Expected a bound shape!')
|
||||
|
@ -56,19 +56,19 @@ export function getArrowTerminalInArrowSpace(
|
|||
// Find the actual local point of the normalized terminal on
|
||||
// the bound shape and transform it to page space, then transform
|
||||
// it to arrow space
|
||||
const { point, size } = app.getBounds(boundShape)
|
||||
const { point, size } = editor.getBounds(boundShape)
|
||||
const shapePoint = Vec2d.Add(point, Vec2d.MulV(terminal.normalizedAnchor, size))
|
||||
const pagePoint = Matrix2d.applyToPoint(app.getPageTransform(boundShape)!, shapePoint)
|
||||
const pagePoint = Matrix2d.applyToPoint(editor.getPageTransform(boundShape)!, shapePoint)
|
||||
const arrowPoint = Matrix2d.applyToPoint(Matrix2d.Inverse(arrowPageTransform), pagePoint)
|
||||
return arrowPoint
|
||||
}
|
||||
}
|
||||
|
||||
export function getArrowTerminalsInArrowSpace(app: App, shape: TLArrowShape) {
|
||||
const arrowPageTransform = app.getPageTransform(shape)!
|
||||
export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape) {
|
||||
const arrowPageTransform = editor.getPageTransform(shape)!
|
||||
|
||||
const start = getArrowTerminalInArrowSpace(app, arrowPageTransform, shape.props.start)
|
||||
const end = getArrowTerminalInArrowSpace(app, arrowPageTransform, shape.props.end)
|
||||
const start = getArrowTerminalInArrowSpace(editor, arrowPageTransform, shape.props.start)
|
||||
const end = getArrowTerminalInArrowSpace(editor, arrowPageTransform, shape.props.end)
|
||||
|
||||
return { start, end }
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '@tldraw/primitives'
|
||||
import { TLArrowShape } from '@tldraw/tlschema'
|
||||
import { BOUND_ARROW_OFFSET, MIN_ARROW_LENGTH } from '../../../../constants'
|
||||
import { App } from '../../../App'
|
||||
import { Editor } from '../../../Editor'
|
||||
import { ArrowInfo } from './arrow-types'
|
||||
import {
|
||||
BoundShapeInfo,
|
||||
|
@ -17,10 +17,10 @@ import {
|
|||
getBoundShapeInfoForTerminal,
|
||||
} from './shared'
|
||||
|
||||
export function getStraightArrowInfo(app: App, shape: TLArrowShape): ArrowInfo {
|
||||
export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): ArrowInfo {
|
||||
const { start, end, arrowheadStart, arrowheadEnd } = shape.props
|
||||
|
||||
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(app, shape)
|
||||
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(editor, shape)
|
||||
|
||||
const a = terminalsInArrowSpace.start.clone()
|
||||
const b = terminalsInArrowSpace.end.clone()
|
||||
|
@ -29,10 +29,10 @@ export function getStraightArrowInfo(app: App, shape: TLArrowShape): ArrowInfo {
|
|||
|
||||
// Update the arrowhead points using intersections with the bound shapes, if any.
|
||||
|
||||
const startShapeInfo = getBoundShapeInfoForTerminal(app, start)
|
||||
const endShapeInfo = getBoundShapeInfoForTerminal(app, end)
|
||||
const startShapeInfo = getBoundShapeInfoForTerminal(editor, start)
|
||||
const endShapeInfo = getBoundShapeInfoForTerminal(editor, end)
|
||||
|
||||
const arrowPageTransform = app.getPageTransform(shape)!
|
||||
const arrowPageTransform = editor.getPageTransform(shape)!
|
||||
|
||||
// Update the position of the arrowhead's end point
|
||||
updateArrowheadPointWithBoundShape(
|
||||
|
@ -87,9 +87,9 @@ export function getStraightArrowInfo(app: App, shape: TLArrowShape): ArrowInfo {
|
|||
if (startShapeInfo && arrowheadStart !== 'none' && !startShapeInfo.isExact) {
|
||||
const offset =
|
||||
BOUND_ARROW_OFFSET +
|
||||
app.getStrokeWidth(shape.props.size) / 2 +
|
||||
editor.getStrokeWidth(shape.props.size) / 2 +
|
||||
('size' in startShapeInfo.shape.props
|
||||
? app.getStrokeWidth(startShapeInfo.shape.props.size) / 2
|
||||
? editor.getStrokeWidth(startShapeInfo.shape.props.size) / 2
|
||||
: 0)
|
||||
|
||||
minDist -= offset
|
||||
|
@ -101,9 +101,9 @@ export function getStraightArrowInfo(app: App, shape: TLArrowShape): ArrowInfo {
|
|||
if (endShapeInfo && arrowheadEnd !== 'none' && !endShapeInfo.isExact) {
|
||||
const offset =
|
||||
BOUND_ARROW_OFFSET +
|
||||
app.getStrokeWidth(shape.props.size) / 2 +
|
||||
editor.getStrokeWidth(shape.props.size) / 2 +
|
||||
('size' in endShapeInfo.shape.props
|
||||
? app.getStrokeWidth(endShapeInfo.shape.props.size) / 2
|
||||
? editor.getStrokeWidth(endShapeInfo.shape.props.size) / 2
|
||||
: 0)
|
||||
|
||||
minDist -= offset
|
||||
|
|
|
@ -37,10 +37,10 @@ export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
|
|||
|
||||
override render(shape: TLBookmarkShape) {
|
||||
const asset = (
|
||||
shape.props.assetId ? this.app.getAssetById(shape.props.assetId) : null
|
||||
shape.props.assetId ? this.editor.getAssetById(shape.props.assetId) : null
|
||||
) as TLBookmarkAsset
|
||||
|
||||
const pageRotation = this.app.getPageRotation(shape)
|
||||
const pageRotation = this.editor.getPageRotation(shape)
|
||||
|
||||
const address = this.getHumanReadableAddress(shape)
|
||||
|
||||
|
@ -63,7 +63,7 @@ export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
|
|||
) : (
|
||||
<div className="tl-bookmark__placeholder" />
|
||||
)}
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.app.zoomLevel} />
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.zoomLevel} />
|
||||
</div>
|
||||
<div className="tl-bookmark__copy_container">
|
||||
{asset?.props.title && (
|
||||
|
@ -127,13 +127,13 @@ export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
|
|||
protected updateBookmarkAsset = debounce((shape: TLBookmarkShape) => {
|
||||
const { url } = shape.props
|
||||
const assetId: TLAssetId = AssetRecordType.createCustomId(getHashForString(url))
|
||||
const existing = this.app.getAssetById(assetId)
|
||||
const existing = this.editor.getAssetById(assetId)
|
||||
|
||||
if (existing) {
|
||||
// If there's an existing asset with the same URL, use
|
||||
// its asset id instead.
|
||||
if (shape.props.assetId !== existing.id) {
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
|
@ -141,12 +141,12 @@ export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
|
|||
},
|
||||
])
|
||||
}
|
||||
} else if (this.app.onCreateBookmarkFromUrl) {
|
||||
} else if (this.editor.onCreateBookmarkFromUrl) {
|
||||
// Create a bookmark asset for the URL. First get its meta
|
||||
// data, then create the asset and update the shape.
|
||||
this.app.onCreateBookmarkFromUrl(url).then((meta) => {
|
||||
this.editor.onCreateBookmarkFromUrl(url).then((meta) => {
|
||||
if (!meta) {
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
|
@ -156,8 +156,8 @@ export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
|
|||
return
|
||||
}
|
||||
|
||||
this.app.batch(() => {
|
||||
this.app
|
||||
this.editor.batch(() => {
|
||||
this.editor
|
||||
.createAssets([
|
||||
{
|
||||
id: assetId,
|
||||
|
|
|
@ -58,8 +58,8 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
|
|||
|
||||
hitTestPoint(shape: TLDrawShape, point: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
const zoomLevel = this.app.zoomLevel
|
||||
const offsetDist = this.app.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
|
||||
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
||||
if (shape.props.segments[0].points.some((pt) => Vec2d.Dist(point, pt) < offsetDist * 1.5)) {
|
||||
|
@ -87,8 +87,8 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
|
|||
const outline = this.outline(shape)
|
||||
|
||||
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
||||
const zoomLevel = this.app.zoomLevel
|
||||
const offsetDist = this.app.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
|
||||
if (
|
||||
shape.props.segments[0].points.some(
|
||||
|
@ -118,7 +118,7 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
|
|||
|
||||
render(shape: TLDrawShape) {
|
||||
const forceSolid = useForceSolid()
|
||||
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
|
||||
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||
|
||||
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
||||
|
@ -183,7 +183,7 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
|
|||
|
||||
indicator(shape: TLDrawShape) {
|
||||
const forceSolid = useForceSolid()
|
||||
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
|
||||
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||
|
||||
let sw = strokeWidth
|
||||
|
@ -210,7 +210,7 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
|
|||
toSvg(shape: TLDrawShape, _font: string | undefined, colors: TLExportColors) {
|
||||
const { color } = shape.props
|
||||
|
||||
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
|
||||
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||
|
||||
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
||||
|
@ -296,7 +296,7 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
|
|||
|
||||
expandSelectionOutlinePx(shape: TLDrawShape): number {
|
||||
const multiplier = shape.props.dash === 'draw' ? 1.6 : 1
|
||||
return (this.app.getStrokeWidth(shape.props.size) * multiplier) / 2
|
||||
return (this.editor.getStrokeWidth(shape.props.size) * multiplier) / 2
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,10 +84,10 @@ export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
|
|||
const isHoveringWhileEditingSameShape = useValue(
|
||||
'is hovering',
|
||||
() => {
|
||||
const { editingId, hoveredId } = this.app.pageState
|
||||
const { editingId, hoveredId } = this.editor.pageState
|
||||
|
||||
if (editingId && hoveredId !== editingId) {
|
||||
const editingShape = this.app.getShapeById(editingId)
|
||||
const editingShape = this.editor.getShapeById(editingId)
|
||||
if (editingShape && editingShape.type === 'embed') {
|
||||
return true
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
|
|||
[]
|
||||
)
|
||||
|
||||
const pageRotation = this.app.getPageRotation(shape)
|
||||
const pageRotation = this.editor.getPageRotation(shape)
|
||||
|
||||
const isInteractive = isEditing || isHoveringWhileEditingSameShape
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
|
|||
g.appendChild(rect)
|
||||
|
||||
// Text label
|
||||
const pageRotation = canolicalizeRotation(this.app.getPageRotationById(shape.id))
|
||||
const pageRotation = canolicalizeRotation(this.editor.getPageRotationById(shape.id))
|
||||
// rotate right 45 deg
|
||||
const offsetRotation = pageRotation + Math.PI / 4
|
||||
const scaledRotation = (offsetRotation * (2 / Math.PI) + 4) % 4
|
||||
|
@ -107,7 +107,7 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
|
|||
verticalTextAlign: 'middle' as const,
|
||||
}
|
||||
|
||||
const spans = this.app.textMeasure.measureTextSpans(
|
||||
const spans = this.editor.textMeasure.measureTextSpans(
|
||||
defaultEmptyAs(shape.props.name, 'Frame') + String.fromCharCode(8203),
|
||||
opts
|
||||
)
|
||||
|
@ -115,7 +115,7 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
|
|||
const firstSpan = spans[0]
|
||||
const lastSpan = last(spans)!
|
||||
const labelTextWidth = lastSpan.box.w + lastSpan.box.x - firstSpan.box.x
|
||||
const text = createTextSvgElementFromSpans(this.app, spans, {
|
||||
const text = createTextSvgElementFromSpans(this.editor, spans, {
|
||||
offsetY: -opts.height - 2,
|
||||
...opts,
|
||||
})
|
||||
|
@ -162,7 +162,7 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
|
|||
|
||||
override onDragShapesOver = (frame: TLFrameShape, shapes: TLShape[]): { shouldHint: boolean } => {
|
||||
if (!shapes.every((child) => child.parentId === frame.id)) {
|
||||
this.app.reparentShapesById(
|
||||
this.editor.reparentShapesById(
|
||||
shapes.map((shape) => shape.id),
|
||||
frame.id
|
||||
)
|
||||
|
@ -172,39 +172,39 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
|
|||
}
|
||||
|
||||
onDragShapesOut = (_shape: TLFrameShape, shapes: TLShape[]): void => {
|
||||
const parentId = this.app.getShapeById(_shape.parentId)
|
||||
const parentId = this.editor.getShapeById(_shape.parentId)
|
||||
const isInGroup = parentId?.type === 'group'
|
||||
|
||||
// If frame is in a group, keep the shape
|
||||
// moved out in that group
|
||||
if (isInGroup) {
|
||||
this.app.reparentShapesById(
|
||||
this.editor.reparentShapesById(
|
||||
shapes.map((shape) => shape.id),
|
||||
parentId.id
|
||||
)
|
||||
} else {
|
||||
this.app.reparentShapesById(
|
||||
this.editor.reparentShapesById(
|
||||
shapes.map((shape) => shape.id),
|
||||
this.app.currentPageId
|
||||
this.editor.currentPageId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override onResizeEnd: OnResizeEndHandler<TLFrameShape> = (shape) => {
|
||||
const bounds = this.app.getPageBounds(shape)!
|
||||
const children = this.app.getSortedChildIds(shape.id)
|
||||
const bounds = this.editor.getPageBounds(shape)!
|
||||
const children = this.editor.getSortedChildIds(shape.id)
|
||||
|
||||
const shapesToReparent: TLShapeId[] = []
|
||||
|
||||
for (const childId of children) {
|
||||
const childBounds = this.app.getPageBoundsById(childId)!
|
||||
const childBounds = this.editor.getPageBoundsById(childId)!
|
||||
if (!bounds.includes(childBounds)) {
|
||||
shapesToReparent.push(childId)
|
||||
}
|
||||
}
|
||||
|
||||
if (shapesToReparent.length > 0) {
|
||||
this.app.reparentShapesById(shapesToReparent, this.app.currentPageId)
|
||||
this.editor.reparentShapesById(shapesToReparent, this.editor.currentPageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives'
|
||||
import { TLShapeId } from '@tldraw/tlschema'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useApp } from '../../../../hooks/useApp'
|
||||
import { useEditor } from '../../../../hooks/useEditor'
|
||||
import { useIsEditing } from '../../../../hooks/useIsEditing'
|
||||
import { FrameLabelInput } from './FrameLabelInput'
|
||||
|
||||
|
@ -16,9 +16,9 @@ export const FrameHeading = function FrameHeading({
|
|||
width: number
|
||||
height: number
|
||||
}) {
|
||||
const app = useApp()
|
||||
const editor = useEditor()
|
||||
|
||||
const pageRotation = canolicalizeRotation(app.getPageRotationById(id))
|
||||
const pageRotation = canolicalizeRotation(editor.getPageRotationById(id))
|
||||
const isEditing = useIsEditing(id)
|
||||
|
||||
const rInput = useRef<HTMLInputElement>(null)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { TLFrameShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import { forwardRef, useCallback } from 'react'
|
||||
import { useApp } from '../../../../hooks/useApp'
|
||||
import { useEditor } from '../../../../hooks/useEditor'
|
||||
import { defaultEmptyAs } from '../../../../utils/string'
|
||||
|
||||
export const FrameLabelInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
{ id: TLShapeId; name: string; isEditing: boolean }
|
||||
>(({ id, name, isEditing }, ref) => {
|
||||
const app = useApp()
|
||||
const editor = useEditor()
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
|
@ -16,22 +16,22 @@ export const FrameLabelInput = forwardRef<
|
|||
// and sending us back into edit mode
|
||||
e.stopPropagation()
|
||||
e.currentTarget.blur()
|
||||
app.setEditingId(null)
|
||||
editor.setEditingId(null)
|
||||
}
|
||||
},
|
||||
[app]
|
||||
[editor]
|
||||
)
|
||||
|
||||
const handleBlur = useCallback(
|
||||
(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
const shape = app.getShapeById<TLFrameShape>(id)
|
||||
const shape = editor.getShapeById<TLFrameShape>(id)
|
||||
if (!shape) return
|
||||
|
||||
const name = shape.props.name
|
||||
const value = e.currentTarget.value.trim()
|
||||
if (name === value) return
|
||||
|
||||
app.updateShapes(
|
||||
editor.updateShapes(
|
||||
[
|
||||
{
|
||||
id,
|
||||
|
@ -42,19 +42,19 @@ export const FrameLabelInput = forwardRef<
|
|||
true
|
||||
)
|
||||
},
|
||||
[id, app]
|
||||
[id, editor]
|
||||
)
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const shape = app.getShapeById<TLFrameShape>(id)
|
||||
const shape = editor.getShapeById<TLFrameShape>(id)
|
||||
if (!shape) return
|
||||
|
||||
const name = shape.props.name
|
||||
const value = e.currentTarget.value
|
||||
if (name === value) return
|
||||
|
||||
app.updateShapes(
|
||||
editor.updateShapes(
|
||||
[
|
||||
{
|
||||
id,
|
||||
|
@ -65,7 +65,7 @@ export const FrameLabelInput = forwardRef<
|
|||
true
|
||||
)
|
||||
},
|
||||
[id, app]
|
||||
[id, editor]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -16,7 +16,7 @@ import { TLDashType, TLGeoShape, TLGeoShapeProps } from '@tldraw/tlschema'
|
|||
import { SVGContainer } from '../../../components/SVGContainer'
|
||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||
import { getLegacyOffsetX } from '../../../utils/legacy'
|
||||
import { App } from '../../App'
|
||||
import { Editor } from '../../Editor'
|
||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
|
@ -91,8 +91,8 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
const outline = this.outline(shape)
|
||||
|
||||
if (shape.props.fill === 'none') {
|
||||
const zoomLevel = this.app.zoomLevel
|
||||
const offsetDist = this.app.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
// Check the outline
|
||||
for (let i = 0; i < outline.length; i++) {
|
||||
const C = outline[i]
|
||||
|
@ -320,7 +320,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
} = shape
|
||||
|
||||
if (text.trimEnd() !== shape.props.text) {
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id,
|
||||
type,
|
||||
|
@ -336,7 +336,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
const { id, type, props } = shape
|
||||
|
||||
const forceSolid = useForceSolid()
|
||||
const strokeWidth = this.app.getStrokeWidth(props.size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(props.size)
|
||||
|
||||
const { w, color, labelColor, fill, dash, growY, font, align, verticalAlign, size, text } =
|
||||
props
|
||||
|
@ -446,11 +446,11 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
align={align}
|
||||
verticalAlign={verticalAlign}
|
||||
text={text}
|
||||
labelColor={this.app.getCssColor(labelColor)}
|
||||
labelColor={this.editor.getCssColor(labelColor)}
|
||||
wrap
|
||||
/>
|
||||
{'url' in shape.props && shape.props.url && (
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.app.zoomLevel} />
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.zoomLevel} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
@ -461,7 +461,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
const { w, h, growY, size } = props
|
||||
|
||||
const forceSolid = useForceSolid()
|
||||
const strokeWidth = this.app.getStrokeWidth(size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(size)
|
||||
|
||||
switch (props.geo) {
|
||||
case 'ellipse': {
|
||||
|
@ -501,7 +501,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
|
||||
toSvg(shape: TLGeoShape, font: string, colors: TLExportColors) {
|
||||
const { id, props } = shape
|
||||
const strokeWidth = this.app.getStrokeWidth(props.size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(props.size)
|
||||
|
||||
let svgElm: SVGElement
|
||||
|
||||
|
@ -650,7 +650,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
offsetX: 0,
|
||||
}
|
||||
|
||||
const spans = this.app.textMeasure.measureTextSpans(props.text, opts)
|
||||
const spans = this.editor.textMeasure.measureTextSpans(props.text, opts)
|
||||
const offsetX = getLegacyOffsetX(shape.props.align, padding, spans, bounds.width)
|
||||
if (offsetX) {
|
||||
opts.offsetX = offsetX
|
||||
|
@ -658,7 +658,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
|
||||
const groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
|
||||
const textBgEl = createTextSvgElementFromSpans(this.app, spans, {
|
||||
const textBgEl = createTextSvgElementFromSpans(this.editor, spans, {
|
||||
...opts,
|
||||
strokeWidth: 2,
|
||||
stroke: colors.background,
|
||||
|
@ -707,7 +707,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
newH = MIN_SIZE_WITH_LABEL
|
||||
}
|
||||
|
||||
const labelSize = getLabelSize(this.app, {
|
||||
const labelSize = getLabelSize(this.editor, {
|
||||
...shape,
|
||||
props: {
|
||||
...shape.props,
|
||||
|
@ -778,7 +778,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
}
|
||||
|
||||
const prevHeight = shape.props.h
|
||||
const nextHeight = getLabelSize(this.app, shape).h
|
||||
const nextHeight = getLabelSize(this.editor, shape).h
|
||||
|
||||
let growY: number | null = null
|
||||
|
||||
|
@ -825,7 +825,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
|
||||
const prevWidth = prev.props.w
|
||||
const prevHeight = prev.props.h
|
||||
const nextSize = getLabelSize(this.app, next)
|
||||
const nextSize = getLabelSize(this.editor, next)
|
||||
const nextWidth = nextSize.w
|
||||
const nextHeight = nextSize.h
|
||||
|
||||
|
@ -889,7 +889,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
onDoubleClick = (shape: TLGeoShape) => {
|
||||
// Little easter egg: double-clicking a rectangle / checkbox while
|
||||
// holding alt will toggle between check-box and rectangle
|
||||
if (this.app.inputs.altKey) {
|
||||
if (this.editor.inputs.altKey) {
|
||||
switch (shape.props.geo) {
|
||||
case 'rectangle': {
|
||||
return {
|
||||
|
@ -914,14 +914,14 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
|||
}
|
||||
}
|
||||
|
||||
function getLabelSize(app: App, shape: TLGeoShape) {
|
||||
function getLabelSize(editor: Editor, shape: TLGeoShape) {
|
||||
const text = shape.props.text.trimEnd()
|
||||
|
||||
if (!text) {
|
||||
return { w: 0, h: 0 }
|
||||
}
|
||||
|
||||
const minSize = app.textMeasure.measureText('w', {
|
||||
const minSize = editor.textMeasure.measureText('w', {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: LABEL_FONT_SIZES[shape.props.size],
|
||||
|
@ -937,7 +937,7 @@ function getLabelSize(app: App, shape: TLGeoShape) {
|
|||
xl: 10,
|
||||
}
|
||||
|
||||
const size = app.textMeasure.measureText(text, {
|
||||
const size = editor.textMeasure.measureText(text, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: LABEL_FONT_SIZES[shape.props.size],
|
||||
|
|
|
@ -20,16 +20,16 @@ export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
|
|||
}
|
||||
|
||||
getBounds(shape: TLGroupShape): Box2d {
|
||||
const children = this.app.getSortedChildIds(shape.id)
|
||||
const children = this.editor.getSortedChildIds(shape.id)
|
||||
if (children.length === 0) {
|
||||
return new Box2d()
|
||||
}
|
||||
|
||||
const allChildPoints = children.flatMap((childId) => {
|
||||
const shape = this.app.getShapeById(childId)!
|
||||
return this.app
|
||||
const shape = this.editor.getShapeById(childId)!
|
||||
return this.editor
|
||||
.getOutlineById(childId)
|
||||
.map((point) => Matrix2d.applyToPoint(this.app.getTransform(shape), point))
|
||||
.map((point) => Matrix2d.applyToPoint(this.editor.getTransform(shape), point))
|
||||
})
|
||||
|
||||
return Box2d.FromPoints(allChildPoints)
|
||||
|
@ -49,13 +49,13 @@ export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
|
|||
erasingIdsSet,
|
||||
pageState: { hintingIds, focusLayerId },
|
||||
zoomLevel,
|
||||
} = this.app
|
||||
} = this.editor
|
||||
|
||||
const isErasing = erasingIdsSet.has(shape.id)
|
||||
|
||||
const isHintingOtherGroup =
|
||||
hintingIds.length > 0 &&
|
||||
hintingIds.some((id) => id !== shape.id && this.app.getShapeById(id)?.type === 'group')
|
||||
hintingIds.some((id) => id !== shape.id && this.editor.getShapeById(id)?.type === 'group')
|
||||
|
||||
if (
|
||||
// always show the outline while we're erasing the group
|
||||
|
@ -80,7 +80,7 @@ export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
|
|||
// Not a class component, but eslint can't tell that :(
|
||||
const {
|
||||
camera: { z: zoomLevel },
|
||||
} = this.app
|
||||
} = this.editor
|
||||
|
||||
const bounds = this.bounds(shape)
|
||||
|
||||
|
@ -88,19 +88,19 @@ export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
|
|||
}
|
||||
|
||||
onChildrenChange: OnChildrenChangeHandler<TLGroupShape> = (group) => {
|
||||
const children = this.app.getSortedChildIds(group.id)
|
||||
const children = this.editor.getSortedChildIds(group.id)
|
||||
if (children.length === 0) {
|
||||
if (this.app.pageState.focusLayerId === group.id) {
|
||||
this.app.popFocusLayer()
|
||||
if (this.editor.pageState.focusLayerId === group.id) {
|
||||
this.editor.popFocusLayer()
|
||||
}
|
||||
this.app.deleteShapes([group.id])
|
||||
this.editor.deleteShapes([group.id])
|
||||
return
|
||||
} else if (children.length === 1) {
|
||||
if (this.app.pageState.focusLayerId === group.id) {
|
||||
this.app.popFocusLayer()
|
||||
if (this.editor.pageState.focusLayerId === group.id) {
|
||||
this.editor.popFocusLayer()
|
||||
}
|
||||
this.app.reparentShapesById(children, group.parentId)
|
||||
this.app.deleteShapes([group.id])
|
||||
this.editor.reparentShapesById(children, group.parentId)
|
||||
this.editor.deleteShapes([group.id])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ export class TLHighlightUtil extends TLShapeUtil<TLHighlightShape> {
|
|||
|
||||
hitTestPoint(shape: TLHighlightShape, point: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
const zoomLevel = this.app.zoomLevel
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
const offsetDist = getStrokeWidth(shape) / zoomLevel
|
||||
|
||||
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
||||
|
@ -81,7 +81,7 @@ export class TLHighlightUtil extends TLShapeUtil<TLHighlightShape> {
|
|||
const outline = this.outline(shape)
|
||||
|
||||
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
||||
const zoomLevel = this.app.zoomLevel
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
const offsetDist = getStrokeWidth(shape) / zoomLevel
|
||||
|
||||
if (
|
||||
|
|
|
@ -73,7 +73,7 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
|
|||
const [staticFrameSrc, setStaticFrameSrc] = useState('')
|
||||
|
||||
const { w, h } = shape.props
|
||||
const asset = shape.props.assetId ? this.app.getAssetById(shape.props.assetId) : undefined
|
||||
const asset = shape.props.assetId ? this.editor.getAssetById(shape.props.assetId) : undefined
|
||||
|
||||
if (asset?.type === 'bookmark') {
|
||||
throw Error("Bookmark assets can't be rendered as images")
|
||||
|
@ -81,14 +81,14 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
|
|||
|
||||
const isSelected = useValue(
|
||||
'onlySelectedShape',
|
||||
() => shape.id === this.app.onlySelectedShape?.id,
|
||||
[this.app]
|
||||
() => shape.id === this.editor.onlySelectedShape?.id,
|
||||
[this.editor]
|
||||
)
|
||||
|
||||
const showCropPreview =
|
||||
isSelected &&
|
||||
isCropping &&
|
||||
this.app.isInAny('select.crop', 'select.cropping', 'select.pointing_crop_handle')
|
||||
this.editor.isInAny('select.crop', 'select.cropping', 'select.pointing_crop_handle')
|
||||
|
||||
// We only want to reduce motion for mimeTypes that have motion
|
||||
const reduceMotion =
|
||||
|
@ -152,7 +152,7 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
|
|||
</div>
|
||||
</HTMLContainer>
|
||||
{'url' in shape.props && shape.props.url && (
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.app.zoomLevel} />
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.zoomLevel} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
@ -168,7 +168,7 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
|
|||
|
||||
async toSvg(shape: TLImageShape) {
|
||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
const asset = shape.props.assetId ? this.app.getAssetById(shape.props.assetId) : null
|
||||
const asset = shape.props.assetId ? this.editor.getAssetById(shape.props.assetId) : null
|
||||
|
||||
let src = asset?.props.src || ''
|
||||
if (src && src.startsWith('http')) {
|
||||
|
@ -205,7 +205,7 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
|
|||
}
|
||||
|
||||
onDoubleClick = (shape: TLImageShape) => {
|
||||
const asset = shape.props.assetId ? this.app.getAssetById(shape.props.assetId) : undefined
|
||||
const asset = shape.props.assetId ? this.editor.getAssetById(shape.props.assetId) : undefined
|
||||
|
||||
if (!asset) return
|
||||
|
||||
|
@ -214,7 +214,7 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
|
|||
|
||||
if (!canPlay) return
|
||||
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
type: 'image',
|
||||
id: shape.id,
|
||||
|
@ -229,7 +229,7 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
|
|||
const props = shape.props
|
||||
if (!props) return
|
||||
|
||||
if (this.app.croppingId !== shape.id) {
|
||||
if (this.editor.croppingId !== shape.id) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -259,7 +259,7 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
|
|||
},
|
||||
}
|
||||
|
||||
this.app.updateShapes([partial])
|
||||
this.editor.updateShapes([partial])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { createCustomShapeId, TLGeoShape, TLLineShape } from '@tldraw/tlschema'
|
||||
import { deepCopy } from '@tldraw/utils'
|
||||
import { TestApp } from '../../../test/TestApp'
|
||||
import { TestEditor } from '../../../test/TestEditor'
|
||||
|
||||
jest.mock('nanoid', () => {
|
||||
let i = 0
|
||||
return { nanoid: () => 'id' + i++ }
|
||||
})
|
||||
|
||||
let app: TestApp
|
||||
let editor: TestEditor
|
||||
const id = createCustomShapeId('line1')
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
beforeEach(() => {
|
||||
app = new TestApp()
|
||||
app
|
||||
editor = new TestEditor()
|
||||
editor
|
||||
.selectAll()
|
||||
.deleteShapes()
|
||||
.createShapes([
|
||||
|
@ -49,10 +49,10 @@ beforeEach(() => {
|
|||
|
||||
describe('Translating', () => {
|
||||
it('updates the line', () => {
|
||||
app.select(id)
|
||||
app.pointerDown(25, 25, { target: 'shape', shape: app.getShapeById<TLLineShape>(id) })
|
||||
app.pointerMove(50, 50) // Move shape by 25, 25
|
||||
app.expectShapeToMatch({
|
||||
editor.select(id)
|
||||
editor.pointerDown(25, 25, { target: 'shape', shape: editor.getShapeById<TLLineShape>(id) })
|
||||
editor.pointerMove(50, 50) // Move shape by 25, 25
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
x: 175,
|
||||
y: 175,
|
||||
|
@ -60,15 +60,15 @@ describe('Translating', () => {
|
|||
})
|
||||
|
||||
it('updates the line when rotated', () => {
|
||||
app.select(id)
|
||||
editor.select(id)
|
||||
|
||||
const shape = app.getShapeById<TLLineShape>(id)!
|
||||
const shape = editor.getShapeById<TLLineShape>(id)!
|
||||
shape.rotation = Math.PI / 2
|
||||
|
||||
app.pointerDown(250, 250, { target: 'shape', shape: shape })
|
||||
app.pointerMove(300, 400) // Move shape by 50, 150
|
||||
editor.pointerDown(250, 250, { target: 'shape', shape: shape })
|
||||
editor.pointerMove(300, 400) // Move shape by 50, 150
|
||||
|
||||
app.expectShapeToMatch({
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
x: 200,
|
||||
y: 300,
|
||||
|
@ -77,10 +77,10 @@ describe('Translating', () => {
|
|||
})
|
||||
|
||||
it('create new handle', () => {
|
||||
app.select(id)
|
||||
editor.select(id)
|
||||
|
||||
const shape = app.getShapeById<TLLineShape>(id)!
|
||||
app.pointerDown(200, 200, {
|
||||
const shape = editor.getShapeById<TLLineShape>(id)!
|
||||
editor.pointerDown(200, 200, {
|
||||
target: 'handle',
|
||||
shape,
|
||||
handle: {
|
||||
|
@ -91,10 +91,10 @@ it('create new handle', () => {
|
|||
y: 50,
|
||||
},
|
||||
})
|
||||
app.pointerMove(349, 349).pointerMove(350, 350) // Move handle by 150, 150
|
||||
app.pointerUp()
|
||||
editor.pointerMove(349, 349).pointerMove(350, 350) // Move handle by 150, 150
|
||||
editor.pointerUp()
|
||||
|
||||
app.expectShapeToMatch({
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
props: {
|
||||
handles: {
|
||||
|
@ -114,11 +114,11 @@ it('create new handle', () => {
|
|||
|
||||
describe('Misc', () => {
|
||||
it('preserves handle positions on spline type change', () => {
|
||||
app.select(id)
|
||||
const shape = app.getShapeById<TLLineShape>(id)!
|
||||
editor.select(id)
|
||||
const shape = editor.getShapeById<TLLineShape>(id)!
|
||||
const prevHandles = deepCopy(shape.props.handles)
|
||||
|
||||
app.updateShapes([
|
||||
editor.updateShapes([
|
||||
{
|
||||
...shape,
|
||||
props: {
|
||||
|
@ -127,7 +127,7 @@ describe('Misc', () => {
|
|||
},
|
||||
])
|
||||
|
||||
app.expectShapeToMatch({
|
||||
editor.expectShapeToMatch({
|
||||
id,
|
||||
props: {
|
||||
spline: 'cubic',
|
||||
|
@ -137,30 +137,30 @@ describe('Misc', () => {
|
|||
})
|
||||
|
||||
it('resizes', () => {
|
||||
app.select(id)
|
||||
app.getShapeById<TLLineShape>(id)!
|
||||
editor.select(id)
|
||||
editor.getShapeById<TLLineShape>(id)!
|
||||
|
||||
app
|
||||
editor
|
||||
.pointerDown(150, 0, { target: 'selection', handle: 'bottom' })
|
||||
.pointerMove(150, 600) // Resize shape by 0, 600
|
||||
.expectPathToBe('root.select.resizing')
|
||||
|
||||
expect(app.getShapeById(id)!).toMatchSnapshot('line shape after resize')
|
||||
expect(editor.getShapeById(id)!).toMatchSnapshot('line shape after resize')
|
||||
})
|
||||
|
||||
it('nudges', () => {
|
||||
app.select(id)
|
||||
app.nudgeShapes(app.selectedIds, { x: 1, y: 0 })
|
||||
editor.select(id)
|
||||
editor.nudgeShapes(editor.selectedIds, { x: 1, y: 0 })
|
||||
|
||||
app.expectShapeToMatch({
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
x: 151,
|
||||
y: 150,
|
||||
})
|
||||
|
||||
app.nudgeShapes(app.selectedIds, { x: 0, y: 1 }, true)
|
||||
editor.nudgeShapes(editor.selectedIds, { x: 0, y: 1 }, true)
|
||||
|
||||
app.expectShapeToMatch({
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
x: 151,
|
||||
y: 160,
|
||||
|
@ -169,54 +169,54 @@ describe('Misc', () => {
|
|||
|
||||
it('align', () => {
|
||||
const boxID = createCustomShapeId('box1')
|
||||
app.createShapes([{ id: boxID, type: 'geo', x: 500, y: 150, props: { w: 100, h: 50 } }])
|
||||
editor.createShapes([{ id: boxID, type: 'geo', x: 500, y: 150, props: { w: 100, h: 50 } }])
|
||||
|
||||
const box = app.getShapeById<TLGeoShape>(boxID)!
|
||||
const line = app.getShapeById<TLLineShape>(id)!
|
||||
const box = editor.getShapeById<TLGeoShape>(boxID)!
|
||||
const line = editor.getShapeById<TLLineShape>(id)!
|
||||
|
||||
app.select(boxID, id)
|
||||
editor.select(boxID, id)
|
||||
|
||||
expect(app.getPageBounds(box)!.maxX).not.toEqual(app.getPageBounds(line)!.maxX)
|
||||
app.alignShapes('right', app.selectedIds)
|
||||
expect(editor.getPageBounds(box)!.maxX).not.toEqual(editor.getPageBounds(line)!.maxX)
|
||||
editor.alignShapes('right', editor.selectedIds)
|
||||
jest.advanceTimersByTime(1000)
|
||||
expect(app.getPageBounds(box)!.maxX).toEqual(app.getPageBounds(line)!.maxX)
|
||||
expect(editor.getPageBounds(box)!.maxX).toEqual(editor.getPageBounds(line)!.maxX)
|
||||
|
||||
expect(app.getPageBounds(box)!.maxY).not.toEqual(app.getPageBounds(line)!.maxY)
|
||||
app.alignShapes('bottom', app.selectedIds)
|
||||
expect(editor.getPageBounds(box)!.maxY).not.toEqual(editor.getPageBounds(line)!.maxY)
|
||||
editor.alignShapes('bottom', editor.selectedIds)
|
||||
jest.advanceTimersByTime(1000)
|
||||
expect(app.getPageBounds(box)!.maxY).toEqual(app.getPageBounds(line)!.maxY)
|
||||
expect(editor.getPageBounds(box)!.maxY).toEqual(editor.getPageBounds(line)!.maxY)
|
||||
})
|
||||
|
||||
it('duplicates', () => {
|
||||
app.select(id)
|
||||
editor.select(id)
|
||||
|
||||
app
|
||||
editor
|
||||
.keyDown('Alt')
|
||||
.pointerDown(25, 25, { target: 'shape', shape: app.getShapeById<TLLineShape>(id) })
|
||||
app.pointerMove(50, 50) // Move shape by 25, 25
|
||||
app.pointerUp().keyUp('Alt')
|
||||
.pointerDown(25, 25, { target: 'shape', shape: editor.getShapeById<TLLineShape>(id) })
|
||||
editor.pointerMove(50, 50) // Move shape by 25, 25
|
||||
editor.pointerUp().keyUp('Alt')
|
||||
|
||||
expect(Array.from(app.shapeIds.values()).length).toEqual(2)
|
||||
expect(Array.from(editor.shapeIds.values()).length).toEqual(2)
|
||||
})
|
||||
|
||||
it('deletes', () => {
|
||||
app.select(id)
|
||||
editor.select(id)
|
||||
|
||||
app
|
||||
editor
|
||||
.keyDown('Alt')
|
||||
.pointerDown(25, 25, { target: 'shape', shape: app.getShapeById<TLLineShape>(id) })
|
||||
app.pointerMove(50, 50) // Move shape by 25, 25
|
||||
app.pointerUp().keyUp('Alt')
|
||||
.pointerDown(25, 25, { target: 'shape', shape: editor.getShapeById<TLLineShape>(id) })
|
||||
editor.pointerMove(50, 50) // Move shape by 25, 25
|
||||
editor.pointerUp().keyUp('Alt')
|
||||
|
||||
let ids = Array.from(app.shapeIds.values())
|
||||
let ids = Array.from(editor.shapeIds.values())
|
||||
expect(ids.length).toEqual(2)
|
||||
|
||||
const duplicate = ids.filter((i) => i !== id)[0]
|
||||
app.select(duplicate)
|
||||
editor.select(duplicate)
|
||||
|
||||
app.deleteShapes()
|
||||
editor.deleteShapes()
|
||||
|
||||
ids = Array.from(app.shapeIds.values())
|
||||
ids = Array.from(editor.shapeIds.values())
|
||||
expect(ids.length).toEqual(1)
|
||||
expect(ids[0]).toEqual(id)
|
||||
})
|
||||
|
|
|
@ -167,8 +167,8 @@ export class TLLineUtil extends TLShapeUtil<TLLineShape> {
|
|||
}
|
||||
|
||||
hitTestPoint(shape: TLLineShape, point: Vec2d): boolean {
|
||||
const zoomLevel = this.app.zoomLevel
|
||||
const offsetDist = this.app.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
|
||||
return pointNearToPolyline(point, this.outline(shape), offsetDist)
|
||||
}
|
||||
|
||||
|
@ -179,7 +179,7 @@ export class TLLineUtil extends TLShapeUtil<TLLineShape> {
|
|||
render(shape: TLLineShape) {
|
||||
const forceSolid = useForceSolid()
|
||||
const spline = getSplineForLineShape(shape)
|
||||
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
|
||||
|
||||
const { dash, color } = shape.props
|
||||
|
||||
|
@ -305,7 +305,7 @@ export class TLLineUtil extends TLShapeUtil<TLLineShape> {
|
|||
}
|
||||
|
||||
indicator(shape: TLLineShape) {
|
||||
const strokeWidth = this.app.getStrokeWidth(shape.props.size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
|
||||
const spline = getSplineForLineShape(shape)
|
||||
const { dash } = shape.props
|
||||
|
||||
|
@ -330,7 +330,7 @@ export class TLLineUtil extends TLShapeUtil<TLLineShape> {
|
|||
const { color: _color, size } = shape.props
|
||||
const color = colors.fill[_color]
|
||||
const spline = getSplineForLineShape(shape)
|
||||
return getLineSvg(shape, spline, color, this.app.getStrokeWidth(size))
|
||||
return getLineSvg(shape, spline, color, this.editor.getStrokeWidth(size))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
|
|||
import { TLNoteShape } from '@tldraw/tlschema'
|
||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||
import { getLegacyOffsetX } from '../../../utils/legacy'
|
||||
import { App } from '../../App'
|
||||
import { Editor } from '../../Editor'
|
||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
|
@ -90,7 +90,7 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
|
|||
</div>
|
||||
</div>
|
||||
{'url' in shape.props && shape.props.url && (
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.app.zoomLevel} />
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.zoomLevel} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
@ -147,7 +147,7 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
|
|||
offsetX: 0,
|
||||
}
|
||||
|
||||
const spans = this.app.textMeasure.measureTextSpans(shape.props.text, opts)
|
||||
const spans = this.editor.textMeasure.measureTextSpans(shape.props.text, opts)
|
||||
|
||||
opts.width = bounds.width
|
||||
const offsetX = getLegacyOffsetX(shape.props.align, PADDING, spans, bounds.width)
|
||||
|
@ -157,7 +157,7 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
|
|||
|
||||
opts.padding = PADDING
|
||||
|
||||
const textElm = createTextSvgElementFromSpans(this.app, spans, opts)
|
||||
const textElm = createTextSvgElementFromSpans(this.editor, spans, opts)
|
||||
textElm.setAttribute('fill', colors.text)
|
||||
textElm.setAttribute('transform', `translate(0 ${PADDING})`)
|
||||
g.appendChild(textElm)
|
||||
|
@ -166,7 +166,7 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
|
|||
}
|
||||
|
||||
onBeforeCreate = (next: TLNoteShape) => {
|
||||
return getGrowY(this.app, next, next.props.growY)
|
||||
return getGrowY(this.editor, next, next.props.growY)
|
||||
}
|
||||
|
||||
onBeforeUpdate = (prev: TLNoteShape, next: TLNoteShape) => {
|
||||
|
@ -178,7 +178,7 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
|
|||
return
|
||||
}
|
||||
|
||||
return getGrowY(this.app, next, prev.props.growY)
|
||||
return getGrowY(this.editor, next, prev.props.growY)
|
||||
}
|
||||
|
||||
onEditEnd: OnEditEndHandler<TLNoteShape> = (shape) => {
|
||||
|
@ -189,7 +189,7 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
|
|||
} = shape
|
||||
|
||||
if (text.trimEnd() !== shape.props.text) {
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id,
|
||||
type,
|
||||
|
@ -202,10 +202,10 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
|
|||
}
|
||||
}
|
||||
|
||||
function getGrowY(app: App, shape: TLNoteShape, prevGrowY = 0) {
|
||||
function getGrowY(editor: Editor, shape: TLNoteShape, prevGrowY = 0) {
|
||||
const PADDING = 17
|
||||
|
||||
const nextTextSize = app.textMeasure.measureText(shape.props.text, {
|
||||
const nextTextSize = editor.textMeasure.measureText(shape.props.text, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: LABEL_FONT_SIZES[shape.props.size],
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
import { ComputedCache } from '@tldraw/tlstore'
|
||||
import { computed, EMPTY_ARRAY } from 'signia'
|
||||
import { WeakMapCache } from '../../utils/WeakMapCache'
|
||||
import type { App } from '../App'
|
||||
import type { Editor } from '../Editor'
|
||||
import { TLResizeHandle } from '../types/selection-types'
|
||||
import { TLExportColors } from './shared/TLExportColors'
|
||||
|
||||
|
@ -23,7 +23,7 @@ export interface TLShapeUtilConstructor<
|
|||
T extends TLUnknownShape,
|
||||
ShapeUtil extends TLShapeUtil<T> = TLShapeUtil<T>
|
||||
> {
|
||||
new (app: App, type: T['type']): ShapeUtil
|
||||
new (editor: Editor, type: T['type']): ShapeUtil
|
||||
type: T['type']
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ export type TLShapeUtilFlag<T> = (shape: T) => boolean
|
|||
|
||||
/** @public */
|
||||
export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
|
||||
constructor(public app: App, public readonly type: T['type']) {}
|
||||
constructor(public editor: Editor, public readonly type: T['type']) {}
|
||||
|
||||
static type: string
|
||||
|
||||
|
@ -190,7 +190,7 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
|
|||
|
||||
@computed
|
||||
private get handlesCache(): ComputedCache<TLHandle[], TLShape> {
|
||||
return this.app.store.createComputedCache('handles:' + this.type, (shape) => {
|
||||
return this.editor.store.createComputedCache('handles:' + this.type, (shape) => {
|
||||
return this.getHandles!(shape as any)
|
||||
})
|
||||
}
|
||||
|
@ -216,7 +216,7 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
|
|||
|
||||
@computed
|
||||
private get boundsCache(): ComputedCache<Box2d, TLShape> {
|
||||
return this.app.store.createComputedCache('bounds:' + this.type, (shape) => {
|
||||
return this.editor.store.createComputedCache('bounds:' + this.type, (shape) => {
|
||||
return this.getBounds(shape as any)
|
||||
})
|
||||
}
|
||||
|
@ -267,7 +267,7 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
|
|||
|
||||
@computed
|
||||
private get outlineCache(): ComputedCache<Vec2dModel[], TLShape> {
|
||||
return this.app.store.createComputedCache('outline:' + this.type, (shape) => {
|
||||
return this.editor.store.createComputedCache('outline:' + this.type, (shape) => {
|
||||
return this.getOutline(shape as any)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { HTMLContainer } from '../../../components/HTMLContainer'
|
|||
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||
import { stopEventPropagation } from '../../../utils/dom'
|
||||
import { WeakMapCache } from '../../../utils/WeakMapCache'
|
||||
import { App } from '../../App'
|
||||
import { Editor } from '../../Editor'
|
||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||
import { resizeScaled } from '../shared/resizeScaled'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
|
@ -40,7 +40,7 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
|
|||
|
||||
// @computed
|
||||
// private get minDimensionsCache() {
|
||||
// return this.app.store.createSelectedComputedCache<
|
||||
// return this.editor.store.createSelectedComputedCache<
|
||||
// TLTextShape['props'],
|
||||
// { width: number; height: number },
|
||||
// TLTextShape
|
||||
|
@ -49,12 +49,12 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
|
|||
// (shape) => {
|
||||
// return shape.props
|
||||
// },
|
||||
// (props) => getTextSize(this.app, props)
|
||||
// (props) => getTextSize(this.editor, props)
|
||||
// )
|
||||
// }
|
||||
|
||||
getMinDimensions(shape: TLTextShape) {
|
||||
return sizeCache.get(shape.props, (props) => getTextSize(this.app, props))
|
||||
return sizeCache.get(shape.props, (props) => getTextSize(this.editor, props))
|
||||
}
|
||||
|
||||
getBounds(shape: TLTextShape) {
|
||||
|
@ -180,8 +180,8 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
|
|||
const groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
|
||||
const textBgEl = createTextSvgElementFromSpans(
|
||||
this.app,
|
||||
this.app.textMeasure.measureTextSpans(text, opts),
|
||||
this.editor,
|
||||
this.editor.textMeasure.measureTextSpans(text, opts),
|
||||
{
|
||||
...opts,
|
||||
stroke: colors.background,
|
||||
|
@ -266,10 +266,10 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
|
|||
const trimmedText = shape.props.text.trimEnd()
|
||||
|
||||
if (trimmedText.length === 0) {
|
||||
this.app.deleteShapes([shape.id])
|
||||
this.editor.deleteShapes([shape.id])
|
||||
} else {
|
||||
if (trimmedText !== shape.props.text) {
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id,
|
||||
type,
|
||||
|
@ -300,7 +300,7 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
|
|||
const boundsA = this.getMinDimensions(prev)
|
||||
|
||||
// Will always be a fresh call to getTextSize
|
||||
const boundsB = getTextSize(this.app, next.props)
|
||||
const boundsB = getTextSize(this.editor, next.props)
|
||||
|
||||
const wA = boundsA.width * prev.props.scale
|
||||
const hA = boundsA.height * prev.props.scale
|
||||
|
@ -368,7 +368,7 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
|
|||
}
|
||||
}
|
||||
|
||||
function getTextSize(app: App, props: TLTextShape['props']) {
|
||||
function getTextSize(editor: Editor, props: TLTextShape['props']) {
|
||||
const { font, text, autoSize, size, w } = props
|
||||
|
||||
const minWidth = 16
|
||||
|
@ -379,7 +379,7 @@ function getTextSize(app: App, props: TLTextShape['props']) {
|
|||
: // `measureText` floors the number so we need to do the same here to avoid issues.
|
||||
Math.floor(Math.max(minWidth, w)) + 'px'
|
||||
|
||||
const result = app.textMeasure.measureText(text, {
|
||||
const result = editor.textMeasure.measureText(text, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[font],
|
||||
fontSize: fontSize,
|
||||
|
|
|
@ -66,8 +66,8 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
|
|||
videoUtil: TLVideoUtil
|
||||
}) {
|
||||
const { shape, videoUtil } = props
|
||||
const showControls = videoUtil.app.getBounds(shape).w * videoUtil.app.zoomLevel >= 110
|
||||
const asset = shape.props.assetId ? videoUtil.app.getAssetById(shape.props.assetId) : null
|
||||
const showControls = videoUtil.editor.getBounds(shape).w * videoUtil.editor.zoomLevel >= 110
|
||||
const asset = shape.props.assetId ? videoUtil.editor.getAssetById(shape.props.assetId) : null
|
||||
const { w, h, time, playing } = shape.props
|
||||
const isEditing = useIsEditing(shape.id)
|
||||
const prefersReducedMotion = usePrefersReducedMotion()
|
||||
|
@ -78,7 +78,7 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
|
|||
(e) => {
|
||||
const video = e.currentTarget
|
||||
|
||||
videoUtil.app.updateShapes([
|
||||
videoUtil.editor.updateShapes([
|
||||
{
|
||||
type: 'video',
|
||||
id: shape.id,
|
||||
|
@ -89,14 +89,14 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
|
|||
},
|
||||
])
|
||||
},
|
||||
[shape.id, videoUtil.app]
|
||||
[shape.id, videoUtil.editor]
|
||||
)
|
||||
|
||||
const handlePause = React.useCallback<React.ReactEventHandler<HTMLVideoElement>>(
|
||||
(e) => {
|
||||
const video = e.currentTarget
|
||||
|
||||
videoUtil.app.updateShapes([
|
||||
videoUtil.editor.updateShapes([
|
||||
{
|
||||
type: 'video',
|
||||
id: shape.id,
|
||||
|
@ -107,7 +107,7 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
|
|||
},
|
||||
])
|
||||
},
|
||||
[shape.id, videoUtil.app]
|
||||
[shape.id, videoUtil.editor]
|
||||
)
|
||||
|
||||
const handleSetCurrentTime = React.useCallback<React.ReactEventHandler<HTMLVideoElement>>(
|
||||
|
@ -115,7 +115,7 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
|
|||
const video = e.currentTarget
|
||||
|
||||
if (isEditing) {
|
||||
videoUtil.app.updateShapes([
|
||||
videoUtil.editor.updateShapes([
|
||||
{
|
||||
type: 'video',
|
||||
id: shape.id,
|
||||
|
@ -126,7 +126,7 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
|
|||
])
|
||||
}
|
||||
},
|
||||
[isEditing, shape.id, videoUtil.app]
|
||||
[isEditing, shape.id, videoUtil.editor]
|
||||
)
|
||||
|
||||
const [isLoaded, setIsLoaded] = React.useState(false)
|
||||
|
@ -200,7 +200,7 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
|
|||
</div>
|
||||
</HTMLContainer>
|
||||
{'url' in shape.props && shape.props.url && (
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={videoUtil.app.zoomLevel} />
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={videoUtil.editor.zoomLevel} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@ import { TLColorType, TLFillType } from '@tldraw/tlschema'
|
|||
import * as React from 'react'
|
||||
import { useValue } from 'signia-react'
|
||||
import { HASH_PATERN_ZOOM_NAMES } from '../../../constants'
|
||||
import { useApp } from '../../../hooks/useApp'
|
||||
import { useEditor } from '../../../hooks/useEditor'
|
||||
import { TLExportColors } from './TLExportColors'
|
||||
|
||||
export interface ShapeFillProps {
|
||||
|
@ -31,12 +31,12 @@ export const ShapeFill = React.memo(function ShapeFill({ d, color, fill }: Shape
|
|||
})
|
||||
|
||||
const PatternFill = function PatternFill({ d, color }: ShapeFillProps) {
|
||||
const app = useApp()
|
||||
const zoomLevel = useValue('zoomLevel', () => app.zoomLevel, [app])
|
||||
const isDarkMode = useValue('isDarkMode', () => app.isDarkMode, [app])
|
||||
const editor = useEditor()
|
||||
const zoomLevel = useValue('zoomLevel', () => editor.zoomLevel, [editor])
|
||||
const isDarkMode = useValue('isDarkMode', () => editor.isDarkMode, [editor])
|
||||
|
||||
const intZoom = Math.ceil(zoomLevel)
|
||||
const teenyTiny = app.zoomLevel <= 0.18
|
||||
const teenyTiny = editor.zoomLevel <= 0.18
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Box2d } from '@tldraw/primitives'
|
||||
import { Box2dModel, TLAlignType, TLVerticalAlignType } from '@tldraw/tlschema'
|
||||
import { correctSpacesToNbsp } from '../../../utils/string'
|
||||
import { App } from '../../App'
|
||||
import { Editor } from '../../Editor'
|
||||
|
||||
/** Get an SVG element for a text shape. */
|
||||
export function createTextSvgElementFromSpans(
|
||||
app: App,
|
||||
editor: Editor,
|
||||
spans: { text: string; box: Box2dModel }[],
|
||||
opts: {
|
||||
fontSize: number
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { TLShape } from '@tldraw/tlschema'
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
import { useValue } from 'signia-react'
|
||||
import { useApp } from '../../../hooks/useApp'
|
||||
import { useEditor } from '../../../hooks/useEditor'
|
||||
import { preventDefault, stopEventPropagation } from '../../../utils/dom'
|
||||
import { INDENT, TextHelpers } from '../TLTextUtil/TextHelpers'
|
||||
|
||||
|
@ -11,11 +11,11 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
type: T['type'],
|
||||
text: string
|
||||
) {
|
||||
const app = useApp()
|
||||
const editor = useEditor()
|
||||
|
||||
const rInput = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const isEditing = useValue('isEditing', () => app.pageState.editingId === id, [app, id])
|
||||
const isEditing = useValue('isEditing', () => editor.pageState.editingId === id, [editor, id])
|
||||
|
||||
const rSkipSelectOnFocus = useRef(false)
|
||||
const rSelectionRanges = useRef<Range[] | null>()
|
||||
|
@ -23,20 +23,20 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
const isEditableFromHover = useValue(
|
||||
'is editable hovering',
|
||||
() => {
|
||||
if (type === 'text' && app.isIn('text') && app.hoveredId === id) {
|
||||
if (type === 'text' && editor.isIn('text') && editor.hoveredId === id) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (app.isIn('select.editing_shape')) {
|
||||
const { editingShape } = app
|
||||
if (editor.isIn('select.editing_shape')) {
|
||||
const { editingShape } = editor
|
||||
if (!editingShape) return false
|
||||
return (
|
||||
// The shape must be hovered
|
||||
app.hoveredId === id &&
|
||||
editor.hoveredId === id &&
|
||||
// the editing shape must be the same type as this shape
|
||||
editingShape.type === type &&
|
||||
// and this shape must be capable of being editing in its current form
|
||||
app.getShapeUtil(editingShape).canEdit(editingShape)
|
||||
editor.getShapeUtil(editingShape).canEdit(editingShape)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
|
||||
if (!elm) return
|
||||
|
||||
const shape = app.getShapeById<TLShape & { props: { text: string } }>(id)
|
||||
const shape = editor.getShapeById<TLShape & { props: { text: string } }>(id)
|
||||
if (shape) {
|
||||
elm.value = shape.props.text
|
||||
if (elm.value.length && !rSkipSelectOnFocus.current) {
|
||||
|
@ -65,7 +65,7 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
rSkipSelectOnFocus.current = false
|
||||
}
|
||||
})
|
||||
}, [app, id, isEditableFromHover])
|
||||
}, [editor, id, isEditableFromHover])
|
||||
|
||||
// When the label blurs, deselect all of the text and complete.
|
||||
// This makes it so that the canvas does not have to be focused
|
||||
|
@ -75,7 +75,7 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
|
||||
requestAnimationFrame(() => {
|
||||
const elm = rInput.current
|
||||
if (app.isIn('select.editing_shape') && elm) {
|
||||
if (editor.isIn('select.editing_shape') && elm) {
|
||||
if (ranges) {
|
||||
if (!ranges.length) {
|
||||
// If we don't have any ranges, restore selection
|
||||
|
@ -96,10 +96,10 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
}
|
||||
} else {
|
||||
window.getSelection()?.removeAllRanges()
|
||||
app.complete()
|
||||
editor.complete()
|
||||
}
|
||||
})
|
||||
}, [app])
|
||||
}, [editor])
|
||||
|
||||
// When the user presses ctrl / meta enter, complete the editing state.
|
||||
// When the user presses tab, indent or unindent the text.
|
||||
|
@ -110,7 +110,7 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
switch (e.key) {
|
||||
case 'Enter': {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
app.complete()
|
||||
editor.complete()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
}
|
||||
}
|
||||
},
|
||||
[app]
|
||||
[editor]
|
||||
)
|
||||
|
||||
// When the text changes, update the text value.
|
||||
|
@ -145,9 +145,9 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
}
|
||||
// ----------------------------
|
||||
|
||||
app.updateShapes([{ id, type, props: { text } }])
|
||||
editor.updateShapes([{ id, type, props: { text } }])
|
||||
},
|
||||
[app, id, type]
|
||||
[editor, id, type]
|
||||
)
|
||||
|
||||
const isEmpty = text.trim().length === 0
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useValue } from 'signia-react'
|
||||
import { useApp } from '../../../hooks/useApp'
|
||||
import { useEditor } from '../../../hooks/useEditor'
|
||||
|
||||
export function useForceSolid() {
|
||||
const app = useApp()
|
||||
return useValue('zoom', () => app.zoomLevel < 0.35, [app])
|
||||
const editor = useEditor()
|
||||
return useValue('zoom', () => editor.zoomLevel < 0.35, [editor])
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export class RootState extends StateNode {
|
|||
if (!(info.shiftKey || info.ctrlKey)) {
|
||||
const currentTool = this.current.value
|
||||
if (currentTool && currentTool.current.value?.id === 'idle') {
|
||||
this.app.setSelectedTool('zoom', { ...info, onInteractionEnd: currentTool.id })
|
||||
this.editor.setSelectedTool('zoom', { ...info, onInteractionEnd: currentTool.id })
|
||||
}
|
||||
}
|
||||
break
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { TLStyleType } from '@tldraw/tlschema'
|
||||
import { atom, Atom, computed, Computed } from 'signia'
|
||||
import type { App } from '../App'
|
||||
import type { Editor } from '../Editor'
|
||||
import {
|
||||
EVENT_NAME_MAP,
|
||||
TLEventHandlers,
|
||||
|
@ -14,7 +14,7 @@ type StateNodeType = 'branch' | 'leaf' | 'root'
|
|||
|
||||
/** @public */
|
||||
export interface StateNodeConstructor {
|
||||
new (app: App, parent?: StateNode): StateNode
|
||||
new (editor: Editor, parent?: StateNode): StateNode
|
||||
id: string
|
||||
initial?: string
|
||||
children?: () => StateNodeConstructor[]
|
||||
|
@ -23,7 +23,7 @@ export interface StateNodeConstructor {
|
|||
|
||||
/** @public */
|
||||
export abstract class StateNode implements Partial<TLEventHandlers> {
|
||||
constructor(public app: App, parent?: StateNode) {
|
||||
constructor(public editor: Editor, parent?: StateNode) {
|
||||
const { id, children, initial } = this.constructor as StateNodeConstructor
|
||||
|
||||
this.id = id
|
||||
|
@ -41,7 +41,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|||
this.type = 'branch'
|
||||
this.initial = initial
|
||||
this.children = Object.fromEntries(
|
||||
children().map((Ctor) => [Ctor.id, new Ctor(this.app, this)])
|
||||
children().map((Ctor) => [Ctor.id, new Ctor(this.editor, this)])
|
||||
)
|
||||
this.current.set(this.children[this.initial])
|
||||
} else {
|
||||
|
@ -53,7 +53,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|||
if (children && initial) {
|
||||
this.initial = initial
|
||||
this.children = Object.fromEntries(
|
||||
children().map((Ctor) => [Ctor.id, new Ctor(this.app, this)])
|
||||
children().map((Ctor) => [Ctor.id, new Ctor(this.editor, this)])
|
||||
)
|
||||
this.current.set(this.children[this.initial])
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
onEnter = () => {
|
||||
this.app.setCursor({ type: 'cross' })
|
||||
this.editor.setCursor({ type: 'cross' })
|
||||
}
|
||||
|
||||
onCancel = () => {
|
||||
this.app.setSelectedTool('select')
|
||||
this.editor.setSelectedTool('select')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,17 +27,17 @@ export class Pointing extends StateNode {
|
|||
onEnter = () => {
|
||||
const {
|
||||
inputs: { currentPagePoint },
|
||||
} = this.app
|
||||
} = this.editor
|
||||
|
||||
this.didTimeout = false
|
||||
|
||||
const shapeType = (this.parent as TLArrowTool).shapeType
|
||||
|
||||
this.app.mark('creating')
|
||||
this.editor.mark('creating')
|
||||
|
||||
const id = createShapeId()
|
||||
|
||||
this.app.createShapes([
|
||||
this.editor.createShapes([
|
||||
{
|
||||
id,
|
||||
type: shapeType,
|
||||
|
@ -46,15 +46,15 @@ export class Pointing extends StateNode {
|
|||
},
|
||||
])
|
||||
|
||||
const util = this.app.getShapeUtil(TLArrowUtil)
|
||||
const shape = this.app.getShapeById<TLArrowShape>(id)
|
||||
const util = this.editor.getShapeUtil(TLArrowUtil)
|
||||
const shape = this.editor.getShapeById<TLArrowShape>(id)
|
||||
if (!shape) return
|
||||
|
||||
const handles = util.handles?.(shape)
|
||||
|
||||
if (handles) {
|
||||
// start precise
|
||||
const point = this.app.getPointInShapeSpace(shape, currentPagePoint)
|
||||
const point = this.editor.getPointInShapeSpace(shape, currentPagePoint)
|
||||
|
||||
const change = util.onHandleChange?.(shape, {
|
||||
handle: { ...handles[0], x: point.x, y: point.y },
|
||||
|
@ -64,15 +64,15 @@ export class Pointing extends StateNode {
|
|||
if (change) {
|
||||
const startTerminal = change.props?.start
|
||||
if (startTerminal?.type === 'binding') {
|
||||
this.app.setHintingIds([startTerminal.boundShapeId])
|
||||
this.editor.setHintingIds([startTerminal.boundShapeId])
|
||||
}
|
||||
this.app.updateShapes([change], true)
|
||||
this.editor.updateShapes([change], true)
|
||||
}
|
||||
}
|
||||
|
||||
this.app.select(id)
|
||||
this.editor.select(id)
|
||||
|
||||
this.shape = this.app.getShapeById(id)
|
||||
this.shape = this.editor.getShapeById(id)
|
||||
|
||||
this.startPreciseTimeout()
|
||||
}
|
||||
|
@ -84,25 +84,28 @@ export class Pointing extends StateNode {
|
|||
onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
if (!this.shape) return
|
||||
|
||||
if (this.app.inputs.isDragging) {
|
||||
const util = this.app.getShapeUtil(this.shape)
|
||||
if (this.editor.inputs.isDragging) {
|
||||
const util = this.editor.getShapeUtil(this.shape)
|
||||
const handles = util.handles?.(this.shape)
|
||||
|
||||
if (!handles) {
|
||||
this.app.bailToMark('creating')
|
||||
this.editor.bailToMark('creating')
|
||||
throw Error('No handles found')
|
||||
}
|
||||
|
||||
if (!this.didTimeout) {
|
||||
const util = this.app.getShapeUtil(TLArrowUtil)
|
||||
const shape = this.app.getShapeById<TLArrowShape>(this.shape.id)
|
||||
const util = this.editor.getShapeUtil(TLArrowUtil)
|
||||
const shape = this.editor.getShapeById<TLArrowShape>(this.shape.id)
|
||||
|
||||
if (!shape) return
|
||||
|
||||
const handles = util.handles(shape)
|
||||
|
||||
if (handles) {
|
||||
const { x, y } = this.app.getPointInShapeSpace(shape, this.app.inputs.originPagePoint)
|
||||
const { x, y } = this.editor.getPointInShapeSpace(
|
||||
shape,
|
||||
this.editor.inputs.originPagePoint
|
||||
)
|
||||
const change = util.onHandleChange?.(shape, {
|
||||
handle: {
|
||||
...handles[0],
|
||||
|
@ -113,12 +116,12 @@ export class Pointing extends StateNode {
|
|||
})
|
||||
|
||||
if (change) {
|
||||
this.app.updateShapes([change], true)
|
||||
this.editor.updateShapes([change], true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.app.setSelectedTool('select.dragging_handle', {
|
||||
this.editor.setSelectedTool('select.dragging_handle', {
|
||||
shape: this.shape,
|
||||
handle: handles.find((h) => h.id === 'end')! /* end */,
|
||||
isCreating: true,
|
||||
|
@ -144,8 +147,8 @@ export class Pointing extends StateNode {
|
|||
}
|
||||
|
||||
cancel() {
|
||||
this.app.bailToMark('creating')
|
||||
this.app.setHintingIds([])
|
||||
this.editor.bailToMark('creating')
|
||||
this.editor.setHintingIds([])
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
onEnter = () => {
|
||||
this.app.setCursor({ type: 'cross' })
|
||||
this.editor.setCursor({ type: 'cross' })
|
||||
}
|
||||
|
||||
onCancel = () => {
|
||||
this.app.setSelectedTool('select')
|
||||
this.editor.setSelectedTool('select')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,21 +12,21 @@ export class Pointing extends StateNode {
|
|||
wasFocusedOnEnter = false
|
||||
|
||||
onEnter = () => {
|
||||
const { isMenuOpen } = this.app
|
||||
const { isMenuOpen } = this.editor
|
||||
this.wasFocusedOnEnter = !isMenuOpen
|
||||
}
|
||||
|
||||
onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
if (this.app.inputs.isDragging) {
|
||||
const { originPagePoint } = this.app.inputs
|
||||
if (this.editor.inputs.isDragging) {
|
||||
const { originPagePoint } = this.editor.inputs
|
||||
|
||||
const shapeType = (this.parent as TLBoxTool)!.shapeType as TLBoxLike['type']
|
||||
|
||||
const id = createShapeId()
|
||||
|
||||
this.app.mark(this.markId)
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
this.app.createShapes([
|
||||
this.editor.createShapes([
|
||||
{
|
||||
id,
|
||||
type: shapeType,
|
||||
|
@ -39,8 +39,8 @@ export class Pointing extends StateNode {
|
|||
},
|
||||
])
|
||||
|
||||
this.app.setSelectedIds([id])
|
||||
this.app.setSelectedTool('select.resizing', {
|
||||
this.editor.setSelectedIds([id])
|
||||
this.editor.setSelectedTool('select.resizing', {
|
||||
...info,
|
||||
target: 'selection',
|
||||
handle: 'bottom_right',
|
||||
|
@ -68,21 +68,21 @@ export class Pointing extends StateNode {
|
|||
}
|
||||
|
||||
complete() {
|
||||
const { originPagePoint } = this.app.inputs
|
||||
const { originPagePoint } = this.editor.inputs
|
||||
|
||||
if (!this.wasFocusedOnEnter) {
|
||||
return
|
||||
}
|
||||
|
||||
this.app.mark(this.markId)
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
const shapeType = (this.parent as TLBoxTool)!.shapeType as TLBoxLike['type']
|
||||
|
||||
const id = createShapeId()
|
||||
|
||||
this.app.mark(this.markId)
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
this.app.createShapes([
|
||||
this.editor.createShapes([
|
||||
{
|
||||
id,
|
||||
type: shapeType,
|
||||
|
@ -91,11 +91,11 @@ export class Pointing extends StateNode {
|
|||
},
|
||||
])
|
||||
|
||||
const shape = this.app.getShapeById<TLBoxLike>(id)!
|
||||
const { w, h } = this.app.getShapeUtil(shape).defaultProps() as TLBoxLike['props']
|
||||
const delta = this.app.getDeltaInParentSpace(shape, new Vec2d(w / 2, h / 2))
|
||||
const shape = this.editor.getShapeById<TLBoxLike>(id)!
|
||||
const { w, h } = this.editor.getShapeUtil(shape).defaultProps() as TLBoxLike['props']
|
||||
const delta = this.editor.getDeltaInParentSpace(shape, new Vec2d(w / 2, h / 2))
|
||||
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id,
|
||||
type: shapeType,
|
||||
|
@ -104,12 +104,12 @@ export class Pointing extends StateNode {
|
|||
},
|
||||
])
|
||||
|
||||
this.app.setSelectedIds([id])
|
||||
this.editor.setSelectedIds([id])
|
||||
|
||||
if (this.app.instanceState.isToolLocked) {
|
||||
if (this.editor.instanceState.isToolLocked) {
|
||||
this.parent.transition('idle', {})
|
||||
} else {
|
||||
this.app.setSelectedTool('select.idle')
|
||||
this.editor.setSelectedTool('select.idle')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,8 +29,8 @@ export class Drawing extends StateNode {
|
|||
|
||||
util =
|
||||
this.shapeType === 'highlight'
|
||||
? this.app.getShapeUtil(TLHighlightUtil)
|
||||
: this.app.getShapeUtil(TLDrawUtil)
|
||||
? this.editor.getShapeUtil(TLHighlightUtil)
|
||||
: this.editor.getShapeUtil(TLDrawUtil)
|
||||
|
||||
isPen = false
|
||||
|
||||
|
@ -50,8 +50,8 @@ export class Drawing extends StateNode {
|
|||
|
||||
onEnter = (info: TLPointerEventInfo) => {
|
||||
this.info = info
|
||||
this.canDraw = !this.app.isMenuOpen
|
||||
this.lastRecordedPoint = this.app.inputs.currentPagePoint.clone()
|
||||
this.canDraw = !this.editor.isMenuOpen
|
||||
this.lastRecordedPoint = this.editor.inputs.currentPagePoint.clone()
|
||||
if (this.canDraw) {
|
||||
this.startShape()
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ export class Drawing extends StateNode {
|
|||
|
||||
onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
const {
|
||||
app: { inputs },
|
||||
editor: { inputs },
|
||||
} = this
|
||||
|
||||
if (this.isPen !== inputs.isPen) {
|
||||
|
@ -78,7 +78,10 @@ export class Drawing extends StateNode {
|
|||
if (this.canDraw) {
|
||||
// Don't update the shape if we haven't moved far enough from the last time we recorded a point
|
||||
if (inputs.isPen) {
|
||||
if (Vec2d.Dist(inputs.currentPagePoint, this.lastRecordedPoint) >= 1 / this.app.zoomLevel) {
|
||||
if (
|
||||
Vec2d.Dist(inputs.currentPagePoint, this.lastRecordedPoint) >=
|
||||
1 / this.editor.zoomLevel
|
||||
) {
|
||||
this.lastRecordedPoint = inputs.currentPagePoint.clone()
|
||||
this.mergeNextPoint = false
|
||||
} else {
|
||||
|
@ -98,7 +101,7 @@ export class Drawing extends StateNode {
|
|||
case 'free': {
|
||||
// We've just entered straight mode, go to straight mode
|
||||
this.segmentMode = 'starting_straight'
|
||||
this.pagePointWhereNextSegmentChanged = this.app.inputs.currentPagePoint.clone()
|
||||
this.pagePointWhereNextSegmentChanged = this.editor.inputs.currentPagePoint.clone()
|
||||
break
|
||||
}
|
||||
case 'starting_free': {
|
||||
|
@ -111,13 +114,13 @@ export class Drawing extends StateNode {
|
|||
|
||||
onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||
if (info.key === 'Shift') {
|
||||
this.app.snaps.clear()
|
||||
this.editor.snaps.clear()
|
||||
|
||||
switch (this.segmentMode) {
|
||||
case 'straight': {
|
||||
// We've just exited straight mode, go back to free mode
|
||||
this.segmentMode = 'starting_free'
|
||||
this.pagePointWhereNextSegmentChanged = this.app.inputs.currentPagePoint.clone()
|
||||
this.pagePointWhereNextSegmentChanged = this.editor.inputs.currentPagePoint.clone()
|
||||
break
|
||||
}
|
||||
case 'starting_straight': {
|
||||
|
@ -132,8 +135,8 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
|
||||
onExit? = () => {
|
||||
this.app.snaps.clear()
|
||||
this.pagePointWhereCurrentSegmentChanged = this.app.inputs.currentPagePoint.clone()
|
||||
this.editor.snaps.clear()
|
||||
this.pagePointWhereCurrentSegmentChanged = this.editor.inputs.currentPagePoint.clone()
|
||||
}
|
||||
|
||||
canClose() {
|
||||
|
@ -143,7 +146,7 @@ export class Drawing extends StateNode {
|
|||
getIsClosed(segments: TLDrawShapeSegment[], size: TLSizeType) {
|
||||
if (!this.canClose()) return false
|
||||
|
||||
const strokeWidth = this.app.getStrokeWidth(size)
|
||||
const strokeWidth = this.editor.getStrokeWidth(size)
|
||||
const firstPoint = segments[0].points[0]
|
||||
const lastSegment = segments[segments.length - 1]
|
||||
const lastPoint = lastSegment.points[lastSegment.points.length - 1]
|
||||
|
@ -158,22 +161,22 @@ export class Drawing extends StateNode {
|
|||
private startShape() {
|
||||
const {
|
||||
inputs: { originPagePoint, isPen },
|
||||
} = this.app
|
||||
} = this.editor
|
||||
|
||||
this.app.mark('draw create start')
|
||||
this.editor.mark('draw create start')
|
||||
|
||||
this.isPen = isPen
|
||||
|
||||
const pressure = this.isPen ? this.info.point.z! * 1.25 : 0.5
|
||||
|
||||
this.segmentMode = this.app.inputs.shiftKey ? 'straight' : 'free'
|
||||
this.segmentMode = this.editor.inputs.shiftKey ? 'straight' : 'free'
|
||||
|
||||
this.didJustShiftClickToExtendPreviousShapeLine = false
|
||||
|
||||
this.lastRecordedPoint = originPagePoint.clone()
|
||||
|
||||
if (this.initialShape) {
|
||||
const shape = this.app.getShapeById<DrawableShape>(this.initialShape.id)
|
||||
const shape = this.editor.getShapeById<DrawableShape>(this.initialShape.id)
|
||||
|
||||
if (shape && this.segmentMode === 'straight') {
|
||||
// Connect dots
|
||||
|
@ -185,7 +188,7 @@ export class Drawing extends StateNode {
|
|||
const prevPoint = last(prevSegment.points)
|
||||
if (!prevPoint) throw Error('Expected a previous point!')
|
||||
|
||||
const { x, y } = this.app.getPointInShapeSpace(shape, originPagePoint).toFixed()
|
||||
const { x, y } = this.editor.getPointInShapeSpace(shape, originPagePoint).toFixed()
|
||||
|
||||
const pressure = this.isPen ? this.info.point.z! * 1.25 : 0.5
|
||||
|
||||
|
@ -207,7 +210,7 @@ export class Drawing extends StateNode {
|
|||
|
||||
// Convert prevPoint to page space
|
||||
const prevPointPageSpace = Matrix2d.applyToPoint(
|
||||
this.app.getPageTransformById(shape.id)!,
|
||||
this.editor.getPageTransformById(shape.id)!,
|
||||
prevPoint
|
||||
)
|
||||
this.pagePointWhereCurrentSegmentChanged = prevPointPageSpace
|
||||
|
@ -216,7 +219,7 @@ export class Drawing extends StateNode {
|
|||
|
||||
this.currentLineLength = this.getLineLength(segments)
|
||||
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id: shape.id,
|
||||
type: this.shapeType,
|
||||
|
@ -235,7 +238,7 @@ export class Drawing extends StateNode {
|
|||
|
||||
this.pagePointWhereCurrentSegmentChanged = originPagePoint.clone()
|
||||
const id = createShapeId()
|
||||
this.app.createShapes([
|
||||
this.editor.createShapes([
|
||||
{
|
||||
id,
|
||||
type: this.shapeType,
|
||||
|
@ -260,11 +263,11 @@ export class Drawing extends StateNode {
|
|||
])
|
||||
|
||||
this.currentLineLength = 0
|
||||
this.initialShape = this.app.getShapeById<DrawableShape>(id)
|
||||
this.initialShape = this.editor.getShapeById<DrawableShape>(id)
|
||||
}
|
||||
|
||||
private updateShapes() {
|
||||
const { inputs } = this.app
|
||||
const { inputs } = this.editor
|
||||
const { initialShape } = this
|
||||
|
||||
if (!initialShape) return
|
||||
|
@ -274,13 +277,13 @@ export class Drawing extends StateNode {
|
|||
props: { size },
|
||||
} = initialShape
|
||||
|
||||
const shape = this.app.getShapeById<DrawableShape>(id)!
|
||||
const shape = this.editor.getShapeById<DrawableShape>(id)!
|
||||
|
||||
if (!shape) return
|
||||
|
||||
const { segments } = shape.props
|
||||
|
||||
const { x, y, z } = this.app.getPointInShapeSpace(shape, inputs.currentPagePoint).toFixed()
|
||||
const { x, y, z } = this.editor.getPointInShapeSpace(shape, inputs.currentPagePoint).toFixed()
|
||||
|
||||
const newPoint = { x, y, z: this.isPen ? +(z! * 1.25).toFixed(2) : 0.5 }
|
||||
|
||||
|
@ -314,7 +317,7 @@ export class Drawing extends StateNode {
|
|||
|
||||
let newSegment: TLDrawShapeSegment
|
||||
|
||||
const newLastPoint = this.app
|
||||
const newLastPoint = this.editor
|
||||
.getPointInShapeSpace(shape, this.pagePointWhereCurrentSegmentChanged)
|
||||
.toFixed()
|
||||
.toJson()
|
||||
|
@ -327,7 +330,7 @@ export class Drawing extends StateNode {
|
|||
points: [{ ...prevLastPoint }, newLastPoint],
|
||||
}
|
||||
|
||||
const transform = this.app.getPageTransform(shape)!
|
||||
const transform = this.editor.getPageTransform(shape)!
|
||||
|
||||
this.pagePointWhereCurrentSegmentChanged = Matrix2d.applyToPoint(
|
||||
transform,
|
||||
|
@ -340,7 +343,7 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
this.app.updateShapes(
|
||||
this.editor.updateShapes(
|
||||
[
|
||||
{
|
||||
id,
|
||||
|
@ -397,7 +400,7 @@ export class Drawing extends StateNode {
|
|||
const finalSegments = [...newSegments, newFreeSegment]
|
||||
this.currentLineLength = this.getLineLength(finalSegments)
|
||||
|
||||
this.app.updateShapes(
|
||||
this.editor.updateShapes(
|
||||
[
|
||||
{
|
||||
id,
|
||||
|
@ -419,7 +422,7 @@ export class Drawing extends StateNode {
|
|||
const newSegment = newSegments[newSegments.length - 1]
|
||||
|
||||
const { pagePointWhereCurrentSegmentChanged } = this
|
||||
const { currentPagePoint, ctrlKey } = this.app.inputs
|
||||
const { currentPagePoint, ctrlKey } = this.editor.inputs
|
||||
|
||||
if (!pagePointWhereCurrentSegmentChanged)
|
||||
throw Error('We should have a point where the segment changed')
|
||||
|
@ -428,7 +431,7 @@ export class Drawing extends StateNode {
|
|||
let shouldSnapToAngle = false
|
||||
|
||||
if (this.didJustShiftClickToExtendPreviousShapeLine) {
|
||||
if (this.app.inputs.isDragging) {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
// If we've just shift clicked to extend a line, only snap once we've started dragging
|
||||
shouldSnapToAngle = !ctrlKey
|
||||
this.didJustShiftClickToExtendPreviousShapeLine = false
|
||||
|
@ -440,16 +443,16 @@ export class Drawing extends StateNode {
|
|||
shouldSnapToAngle = !ctrlKey // don't snap angle while snapping line
|
||||
}
|
||||
|
||||
let newPoint = this.app.getPointInShapeSpace(shape, currentPagePoint).toFixed().toJson()
|
||||
let newPoint = this.editor.getPointInShapeSpace(shape, currentPagePoint).toFixed().toJson()
|
||||
let didSnap = false
|
||||
let snapSegment: TLDrawShapeSegment | undefined = undefined
|
||||
|
||||
const shouldSnap = this.app.userDocumentSettings.isSnapMode ? !ctrlKey : ctrlKey
|
||||
const shouldSnap = this.editor.userDocumentSettings.isSnapMode ? !ctrlKey : ctrlKey
|
||||
|
||||
if (shouldSnap) {
|
||||
if (newSegments.length > 2) {
|
||||
let nearestPoint: Vec2dModel | undefined = undefined
|
||||
let minDistance = 8 / this.app.zoomLevel
|
||||
let minDistance = 8 / this.editor.zoomLevel
|
||||
|
||||
// Don't try to snap to the last two segments
|
||||
for (let i = 0, n = segments.length - 2; i < n; i++) {
|
||||
|
@ -485,7 +488,7 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
|
||||
if (didSnap && snapSegment) {
|
||||
const transform = this.app.getPageTransform(shape)!
|
||||
const transform = this.editor.getPageTransform(shape)!
|
||||
const first = snapSegment.points[0]
|
||||
const lastPoint = last(snapSegment.points)
|
||||
if (!lastPoint) throw Error('Expected a last point!')
|
||||
|
@ -496,7 +499,7 @@ export class Drawing extends StateNode {
|
|||
|
||||
const snappedPoint = Matrix2d.applyToPoint(transform, newPoint)
|
||||
|
||||
this.app.snaps.setLines([
|
||||
this.editor.snaps.setLines([
|
||||
{
|
||||
id: uniqueId(),
|
||||
type: 'points',
|
||||
|
@ -504,7 +507,7 @@ export class Drawing extends StateNode {
|
|||
},
|
||||
])
|
||||
} else {
|
||||
this.app.snaps.clear()
|
||||
this.editor.snaps.clear()
|
||||
|
||||
if (shouldSnapToAngle) {
|
||||
// Snap line angle to nearest 15 degrees
|
||||
|
@ -521,7 +524,7 @@ export class Drawing extends StateNode {
|
|||
pagePoint = currentPagePoint
|
||||
}
|
||||
|
||||
newPoint = this.app.getPointInShapeSpace(shape, pagePoint).toFixed().toJson()
|
||||
newPoint = this.editor.getPointInShapeSpace(shape, pagePoint).toFixed().toJson()
|
||||
}
|
||||
|
||||
// If the previous segment is a one point free shape and is the first segment of the line,
|
||||
|
@ -536,7 +539,7 @@ export class Drawing extends StateNode {
|
|||
points: [newSegment.points[0], newPoint],
|
||||
}
|
||||
|
||||
this.app.updateShapes(
|
||||
this.editor.updateShapes(
|
||||
[
|
||||
{
|
||||
id,
|
||||
|
@ -578,7 +581,7 @@ export class Drawing extends StateNode {
|
|||
|
||||
this.currentLineLength = this.getLineLength(newSegments)
|
||||
|
||||
this.app.updateShapes(
|
||||
this.editor.updateShapes(
|
||||
[
|
||||
{
|
||||
id,
|
||||
|
@ -594,13 +597,13 @@ export class Drawing extends StateNode {
|
|||
|
||||
// Set a maximum length for the lines array; after 200 points, complete the line.
|
||||
if (newPoints.length > 500) {
|
||||
this.app.updateShapes([{ id, type: this.shapeType, props: { isComplete: true } }])
|
||||
this.editor.updateShapes([{ id, type: this.shapeType, props: { isComplete: true } }])
|
||||
|
||||
const { currentPagePoint } = this.app.inputs
|
||||
const { currentPagePoint } = this.editor.inputs
|
||||
|
||||
const newShapeId = this.app.createShapeId()
|
||||
const newShapeId = this.editor.createShapeId()
|
||||
|
||||
this.app.createShapes([
|
||||
this.editor.createShapes([
|
||||
{
|
||||
id: newShapeId,
|
||||
type: this.shapeType,
|
||||
|
@ -618,9 +621,9 @@ export class Drawing extends StateNode {
|
|||
},
|
||||
])
|
||||
|
||||
this.initialShape = structuredClone(this.app.getShapeById<DrawableShape>(newShapeId)!)
|
||||
this.initialShape = structuredClone(this.editor.getShapeById<DrawableShape>(newShapeId)!)
|
||||
this.mergeNextPoint = false
|
||||
this.lastRecordedPoint = this.app.inputs.currentPagePoint.clone()
|
||||
this.lastRecordedPoint = this.editor.inputs.currentPagePoint.clone()
|
||||
this.currentLineLength = 0
|
||||
}
|
||||
|
||||
|
@ -656,11 +659,11 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
|
||||
override onInterrupt: TLEventHandlers['onInterrupt'] = () => {
|
||||
if (this.app.inputs.isDragging) {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
return
|
||||
}
|
||||
|
||||
this.app.bail()
|
||||
this.editor.bail()
|
||||
this.cancel()
|
||||
}
|
||||
|
||||
|
@ -676,7 +679,7 @@ export class Drawing extends StateNode {
|
|||
|
||||
const { initialShape } = this
|
||||
if (!initialShape) return
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{ id: initialShape.id, type: initialShape.type, props: { isComplete: true } },
|
||||
])
|
||||
|
||||
|
|
|
@ -9,10 +9,10 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
onEnter = () => {
|
||||
this.app.setCursor({ type: 'cross' })
|
||||
this.editor.setCursor({ type: 'cross' })
|
||||
}
|
||||
|
||||
onCancel = () => {
|
||||
this.app.setSelectedTool('select')
|
||||
this.editor.setSelectedTool('select')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,6 @@ export class TLEraserTool extends StateNode {
|
|||
static children = () => [Idle, Pointing, Erasing]
|
||||
|
||||
onEnter = () => {
|
||||
this.app.setCursor({ type: 'cross' })
|
||||
this.editor.setCursor({ type: 'cross' })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,17 +13,17 @@ export class Erasing extends StateNode {
|
|||
private excludedShapeIds = new Set<TLShapeId>()
|
||||
|
||||
override onEnter = (info: TLPointerEventInfo) => {
|
||||
this.markId = this.app.mark('erase scribble begin')
|
||||
this.markId = this.editor.mark('erase scribble begin')
|
||||
this.info = info
|
||||
|
||||
const { originPagePoint } = this.app.inputs
|
||||
const { originPagePoint } = this.editor.inputs
|
||||
this.excludedShapeIds = new Set(
|
||||
this.app.shapesArray
|
||||
this.editor.shapesArray
|
||||
.filter(
|
||||
(shape) =>
|
||||
this.app.isShapeOrAncestorLocked(shape) ||
|
||||
this.editor.isShapeOrAncestorLocked(shape) ||
|
||||
((shape.type === 'group' || shape.type === 'frame') &&
|
||||
this.app.isPointInShape(originPagePoint, shape))
|
||||
this.editor.isPointInShape(originPagePoint, shape))
|
||||
)
|
||||
.map((shape) => shape.id)
|
||||
)
|
||||
|
@ -34,7 +34,7 @@ export class Erasing extends StateNode {
|
|||
|
||||
private startScribble = () => {
|
||||
if (this.scribble.tick) {
|
||||
this.app.off('tick', this.scribble?.tick)
|
||||
this.editor.off('tick', this.scribble?.tick)
|
||||
}
|
||||
|
||||
this.scribble = new ScribbleManager({
|
||||
|
@ -44,21 +44,21 @@ export class Erasing extends StateNode {
|
|||
size: 12,
|
||||
})
|
||||
|
||||
this.app.on('tick', this.scribble.tick)
|
||||
this.editor.on('tick', this.scribble.tick)
|
||||
}
|
||||
|
||||
private pushPointToScribble = () => {
|
||||
const { x, y } = this.app.inputs.currentPagePoint
|
||||
const { x, y } = this.editor.inputs.currentPagePoint
|
||||
this.scribble.addPoint(x, y)
|
||||
}
|
||||
|
||||
private onScribbleUpdate = (scribble: TLScribble) => {
|
||||
this.app.setScribble(scribble)
|
||||
this.editor.setScribble(scribble)
|
||||
}
|
||||
|
||||
private onScribbleComplete = () => {
|
||||
this.app.off('tick', this.scribble.tick)
|
||||
this.app.setScribble(null)
|
||||
this.editor.off('tick', this.scribble.tick)
|
||||
this.editor.setScribble(null)
|
||||
}
|
||||
|
||||
override onExit = () => {
|
||||
|
@ -86,7 +86,7 @@ export class Erasing extends StateNode {
|
|||
shapesArray,
|
||||
erasingIdsSet,
|
||||
inputs: { currentPagePoint, previousPagePoint },
|
||||
} = this.app
|
||||
} = this.editor
|
||||
|
||||
const { excludedShapeIds } = this
|
||||
|
||||
|
@ -98,37 +98,37 @@ export class Erasing extends StateNode {
|
|||
if (shape.type === 'group') continue
|
||||
|
||||
// Avoid testing masked shapes, unless the pointer is inside the mask
|
||||
const pageMask = this.app.getPageMaskById(shape.id)
|
||||
const pageMask = this.editor.getPageMaskById(shape.id)
|
||||
if (pageMask && !pointInPolygon(currentPagePoint, pageMask)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Hit test the shape using a line segment
|
||||
const util = this.app.getShapeUtil(shape)
|
||||
const A = this.app.getPointInShapeSpace(shape, previousPagePoint)
|
||||
const B = this.app.getPointInShapeSpace(shape, currentPagePoint)
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
const A = this.editor.getPointInShapeSpace(shape, previousPagePoint)
|
||||
const B = this.editor.getPointInShapeSpace(shape, currentPagePoint)
|
||||
|
||||
// If it's a hit, erase the outermost selectable shape
|
||||
if (util.hitTestLineSegment(shape, A, B)) {
|
||||
erasing.add(this.app.getOutermostSelectableShape(shape).id)
|
||||
erasing.add(this.editor.getOutermostSelectableShape(shape).id)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the hit shapes, except if they're in the list of excluded shapes
|
||||
// (these excluded shapes will be any frames or groups the pointer was inside of
|
||||
// when the user started erasing)
|
||||
this.app.setErasingIds([...erasing].filter((id) => !excludedShapeIds.has(id)))
|
||||
this.editor.setErasingIds([...erasing].filter((id) => !excludedShapeIds.has(id)))
|
||||
}
|
||||
|
||||
complete() {
|
||||
this.app.deleteShapes(this.app.pageState.erasingIds)
|
||||
this.app.setErasingIds([])
|
||||
this.editor.deleteShapes(this.editor.pageState.erasingIds)
|
||||
this.editor.setErasingIds([])
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.app.setErasingIds([])
|
||||
this.app.bailToMark(this.markId)
|
||||
this.editor.setErasingIds([])
|
||||
this.editor.bailToMark(this.markId)
|
||||
this.parent.transition('idle', this.info)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,18 +6,18 @@ export class Pointing extends StateNode {
|
|||
static override id = 'pointing'
|
||||
|
||||
onEnter = () => {
|
||||
const { inputs } = this.app
|
||||
const { inputs } = this.editor
|
||||
|
||||
const erasing = new Set<TLShapeId>()
|
||||
|
||||
const initialSize = erasing.size
|
||||
|
||||
for (const shape of [...this.app.sortedShapesArray].reverse()) {
|
||||
if (this.app.isPointInShape(inputs.currentPagePoint, shape)) {
|
||||
for (const shape of [...this.editor.sortedShapesArray].reverse()) {
|
||||
if (this.editor.isPointInShape(inputs.currentPagePoint, shape)) {
|
||||
// Skip groups
|
||||
if (shape.type === 'group') continue
|
||||
|
||||
const hitShape = this.app.getOutermostSelectableShape(shape)
|
||||
const hitShape = this.editor.getOutermostSelectableShape(shape)
|
||||
|
||||
// If we've hit a frame after hitting any other shape, stop here
|
||||
if (hitShape.type === 'frame' && erasing.size > initialSize) break
|
||||
|
@ -26,11 +26,11 @@ export class Pointing extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
this.app.setErasingIds([...erasing])
|
||||
this.editor.setErasingIds([...erasing])
|
||||
}
|
||||
|
||||
onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
if (this.app.inputs.isDragging) {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.parent.transition('erasing', info)
|
||||
}
|
||||
}
|
||||
|
@ -52,19 +52,19 @@ export class Pointing extends StateNode {
|
|||
}
|
||||
|
||||
complete() {
|
||||
const { erasingIds } = this.app
|
||||
const { erasingIds } = this.editor
|
||||
|
||||
if (erasingIds.length) {
|
||||
this.app.mark('erase end')
|
||||
this.app.deleteShapes(erasingIds)
|
||||
this.editor.mark('erase end')
|
||||
this.editor.deleteShapes(erasingIds)
|
||||
}
|
||||
|
||||
this.app.setErasingIds([])
|
||||
this.editor.setErasingIds([])
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.app.setErasingIds([])
|
||||
this.editor.setErasingIds([])
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,17 +9,17 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
onEnter = () => {
|
||||
this.app.setCursor({ type: 'cross' })
|
||||
this.editor.setCursor({ type: 'cross' })
|
||||
}
|
||||
|
||||
onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||
if (info.key === 'Enter') {
|
||||
const shape = this.app.onlySelectedShape
|
||||
const shape = this.editor.onlySelectedShape
|
||||
if (shape && shape.type === 'geo') {
|
||||
// todo: ensure that this only works with the most recently created shape, not just any geo shape that happens to be selected at the time
|
||||
this.app.mark('editing shape')
|
||||
this.app.setEditingId(shape.id)
|
||||
this.app.setSelectedTool('select.editing_shape', {
|
||||
this.editor.mark('editing shape')
|
||||
this.editor.setEditingId(shape.id)
|
||||
this.editor.setSelectedTool('select.editing_shape', {
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape,
|
||||
|
@ -29,6 +29,6 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
onCancel = () => {
|
||||
this.app.setSelectedTool('select')
|
||||
this.editor.setSelectedTool('select')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,14 @@ export class Pointing extends StateNode {
|
|||
static override id = 'pointing'
|
||||
|
||||
onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
if (this.app.inputs.isDragging) {
|
||||
const { originPagePoint } = this.app.inputs
|
||||
if (this.editor.inputs.isDragging) {
|
||||
const { originPagePoint } = this.editor.inputs
|
||||
|
||||
const id = createShapeId()
|
||||
|
||||
this.app.mark('creating')
|
||||
this.editor.mark('creating')
|
||||
|
||||
this.app.createShapes([
|
||||
this.editor.createShapes([
|
||||
{
|
||||
id,
|
||||
type: 'geo',
|
||||
|
@ -23,13 +23,13 @@ export class Pointing extends StateNode {
|
|||
props: {
|
||||
w: 1,
|
||||
h: 1,
|
||||
geo: this.app.instanceState.propsForNextShape.geo,
|
||||
geo: this.editor.instanceState.propsForNextShape.geo,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
this.app.select(id)
|
||||
this.app.setSelectedTool('select.resizing', {
|
||||
this.editor.select(id)
|
||||
this.editor.setSelectedTool('select.resizing', {
|
||||
...info,
|
||||
target: 'selection',
|
||||
handle: 'bottom_right',
|
||||
|
@ -57,52 +57,52 @@ export class Pointing extends StateNode {
|
|||
}
|
||||
|
||||
complete() {
|
||||
const { originPagePoint } = this.app.inputs
|
||||
const { originPagePoint } = this.editor.inputs
|
||||
|
||||
const id = createShapeId()
|
||||
|
||||
this.app.mark('creating')
|
||||
this.editor.mark('creating')
|
||||
|
||||
this.app.createShapes([
|
||||
this.editor.createShapes([
|
||||
{
|
||||
id,
|
||||
type: 'geo',
|
||||
x: originPagePoint.x,
|
||||
y: originPagePoint.y,
|
||||
props: {
|
||||
geo: this.app.instanceState.propsForNextShape.geo,
|
||||
geo: this.editor.instanceState.propsForNextShape.geo,
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const shape = this.app.getShapeById<TLGeoShape>(id)!
|
||||
const shape = this.editor.getShapeById<TLGeoShape>(id)!
|
||||
if (!shape) return
|
||||
|
||||
const bounds =
|
||||
shape.props.geo === 'star' ? getStarBounds(5, 200, 200) : new Box2d(0, 0, 200, 200)
|
||||
const delta = this.app.getDeltaInParentSpace(shape, bounds.center)
|
||||
const delta = this.editor.getDeltaInParentSpace(shape, bounds.center)
|
||||
|
||||
this.app.select(id)
|
||||
this.app.updateShapes([
|
||||
this.editor.select(id)
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id: shape.id,
|
||||
type: 'geo',
|
||||
x: shape.x - delta.x,
|
||||
y: shape.y - delta.y,
|
||||
props: {
|
||||
geo: this.app.instanceState.propsForNextShape.geo,
|
||||
geo: this.editor.instanceState.propsForNextShape.geo,
|
||||
w: bounds.width,
|
||||
h: bounds.height,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
if (this.app.instanceState.isToolLocked) {
|
||||
if (this.editor.instanceState.isToolLocked) {
|
||||
this.parent.transition('idle', {})
|
||||
} else {
|
||||
this.app.setSelectedTool('select', {})
|
||||
this.editor.setSelectedTool('select', {})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,15 +15,15 @@ export class TLHandTool extends StateNode {
|
|||
|
||||
onDoubleClick: TLClickEvent = (info) => {
|
||||
if (info.phase === 'settle') {
|
||||
const { currentScreenPoint } = this.app.inputs
|
||||
this.app.zoomIn(currentScreenPoint, { duration: 220, easing: EASINGS.easeOutQuint })
|
||||
const { currentScreenPoint } = this.editor.inputs
|
||||
this.editor.zoomIn(currentScreenPoint, { duration: 220, easing: EASINGS.easeOutQuint })
|
||||
}
|
||||
}
|
||||
|
||||
onTripleClick: TLClickEvent = (info) => {
|
||||
if (info.phase === 'settle') {
|
||||
const { currentScreenPoint } = this.app.inputs
|
||||
this.app.zoomOut(currentScreenPoint, { duration: 320, easing: EASINGS.easeOutQuint })
|
||||
const { currentScreenPoint } = this.editor.inputs
|
||||
this.editor.zoomOut(currentScreenPoint, { duration: 320, easing: EASINGS.easeOutQuint })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,12 +32,12 @@ export class TLHandTool extends StateNode {
|
|||
const {
|
||||
zoomLevel,
|
||||
inputs: { currentScreenPoint },
|
||||
} = this.app
|
||||
} = this.editor
|
||||
|
||||
if (zoomLevel === 1) {
|
||||
this.app.zoomToFit({ duration: 400, easing: EASINGS.easeOutQuint })
|
||||
this.editor.zoomToFit({ duration: 400, easing: EASINGS.easeOutQuint })
|
||||
} else {
|
||||
this.app.resetZoom(currentScreenPoint, { duration: 320, easing: EASINGS.easeOutQuint })
|
||||
this.editor.resetZoom(currentScreenPoint, { duration: 320, easing: EASINGS.easeOutQuint })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,19 +27,19 @@ export class Dragging extends StateNode {
|
|||
}
|
||||
|
||||
private update() {
|
||||
const { currentScreenPoint, previousScreenPoint } = this.app.inputs
|
||||
const { currentScreenPoint, previousScreenPoint } = this.editor.inputs
|
||||
|
||||
const delta = Vec2d.Sub(currentScreenPoint, previousScreenPoint)
|
||||
|
||||
if (Math.abs(delta.x) > 0 || Math.abs(delta.y) > 0) {
|
||||
this.app.pan(delta.x, delta.y)
|
||||
this.editor.pan(delta.x, delta.y)
|
||||
}
|
||||
}
|
||||
|
||||
private complete() {
|
||||
this.app.slideCamera({
|
||||
speed: Math.min(2, this.app.inputs.pointerVelocity.len()),
|
||||
direction: this.app.inputs.pointerVelocity,
|
||||
this.editor.slideCamera({
|
||||
speed: Math.min(2, this.editor.inputs.pointerVelocity.len()),
|
||||
direction: this.editor.inputs.pointerVelocity,
|
||||
friction: HAND_TOOL_FRICTION,
|
||||
})
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ export class Idle extends StateNode {
|
|||
static override id = 'idle'
|
||||
|
||||
onEnter = () => {
|
||||
this.app.setCursor({ type: 'grab' })
|
||||
this.editor.setCursor({ type: 'grab' })
|
||||
}
|
||||
|
||||
onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||
|
@ -13,6 +13,6 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
onCancel = () => {
|
||||
this.app.setSelectedTool('select')
|
||||
this.editor.setSelectedTool('select')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,12 @@ export class Pointing extends StateNode {
|
|||
static override id = 'pointing'
|
||||
|
||||
onEnter = () => {
|
||||
this.app.stopCameraAnimation()
|
||||
this.app.setCursor({ type: 'grabbing' })
|
||||
this.editor.stopCameraAnimation()
|
||||
this.editor.setCursor({ type: 'grabbing' })
|
||||
}
|
||||
|
||||
onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
if (this.app.inputs.isDragging) {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.parent.transition('dragging', info)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,6 @@ export class TLLaserTool extends StateNode {
|
|||
static children = () => [Idle, Lasering]
|
||||
|
||||
onEnter = () => {
|
||||
this.app.setCursor({ type: 'cross' })
|
||||
this.editor.setCursor({ type: 'cross' })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export class Lasering extends StateNode {
|
|||
}
|
||||
|
||||
override onExit = () => {
|
||||
this.app.setErasingIds([])
|
||||
this.editor.setErasingIds([])
|
||||
this.scribble.stop()
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ export class Lasering extends StateNode {
|
|||
|
||||
private startScribble = () => {
|
||||
if (this.scribble.tick) {
|
||||
this.app.off('tick', this.scribble?.tick)
|
||||
this.editor.off('tick', this.scribble?.tick)
|
||||
}
|
||||
|
||||
this.scribble = new ScribbleManager({
|
||||
|
@ -40,21 +40,21 @@ export class Lasering extends StateNode {
|
|||
delay: 1200,
|
||||
})
|
||||
|
||||
this.app.on('tick', this.scribble.tick)
|
||||
this.editor.on('tick', this.scribble.tick)
|
||||
}
|
||||
|
||||
private pushPointToScribble = () => {
|
||||
const { x, y } = this.app.inputs.currentPagePoint
|
||||
const { x, y } = this.editor.inputs.currentPagePoint
|
||||
this.scribble.addPoint(x, y)
|
||||
}
|
||||
|
||||
private onScribbleUpdate = (scribble: TLScribble) => {
|
||||
this.app.setScribble(scribble)
|
||||
this.editor.setScribble(scribble)
|
||||
}
|
||||
|
||||
private onScribbleComplete = () => {
|
||||
this.app.off('tick', this.scribble.tick)
|
||||
this.app.setScribble(null)
|
||||
this.editor.off('tick', this.scribble.tick)
|
||||
this.editor.setScribble(null)
|
||||
}
|
||||
|
||||
override onCancel: TLEventHandlers['onCancel'] = () => {
|
||||
|
|
|
@ -9,7 +9,7 @@ export class Idle extends StateNode {
|
|||
|
||||
onEnter = (info: { shapeId: TLShapeId }) => {
|
||||
this.shapeId = info.shapeId
|
||||
this.app.setCursor({ type: 'cross' })
|
||||
this.editor.setCursor({ type: 'cross' })
|
||||
}
|
||||
|
||||
onPointerDown: TLEventHandlers['onPointerDown'] = () => {
|
||||
|
@ -17,6 +17,6 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
onCancel = () => {
|
||||
this.app.setSelectedTool('select')
|
||||
this.editor.setSelectedTool('select')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,14 +14,14 @@ export class Pointing extends StateNode {
|
|||
markPointId = ''
|
||||
|
||||
onEnter = (info: { shapeId?: TLShapeId }) => {
|
||||
const { inputs } = this.app
|
||||
const { inputs } = this.editor
|
||||
const { currentPagePoint } = inputs
|
||||
|
||||
this.markPointId = this.app.mark('creating')
|
||||
this.markPointId = this.editor.mark('creating')
|
||||
|
||||
let shapeExists = false
|
||||
if (info.shapeId) {
|
||||
const shape = this.app.getShapeById<TLLineShape>(info.shapeId)
|
||||
const shape = this.editor.getShapeById<TLLineShape>(info.shapeId)
|
||||
if (shape) {
|
||||
shapeExists = true
|
||||
this.shape = shape
|
||||
|
@ -30,13 +30,13 @@ export class Pointing extends StateNode {
|
|||
|
||||
// if user is holding shift then we are adding points to an existing line
|
||||
if (inputs.shiftKey && shapeExists) {
|
||||
const handles = this.app.getShapeUtil(this.shape).handles(this.shape)
|
||||
const handles = this.editor.getShapeUtil(this.shape).handles(this.shape)
|
||||
|
||||
const vertexHandles = handles.filter((h) => h.type === 'vertex').sort(sortByIndex)
|
||||
const endHandle = vertexHandles[vertexHandles.length - 1]
|
||||
|
||||
const shapePagePoint = Matrix2d.applyToPoint(
|
||||
this.app.getParentTransform(this.shape)!,
|
||||
this.editor.getParentTransform(this.shape)!,
|
||||
new Vec2d(this.shape.x, this.shape.y)
|
||||
)
|
||||
|
||||
|
@ -67,7 +67,7 @@ export class Pointing extends StateNode {
|
|||
|
||||
nextHandles[nextEndHandle.id] = nextEndHandle
|
||||
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id: this.shape.id,
|
||||
type: this.shape.type,
|
||||
|
@ -79,7 +79,7 @@ export class Pointing extends StateNode {
|
|||
} else {
|
||||
const id = createShapeId()
|
||||
|
||||
this.app.createShapes([
|
||||
this.editor.createShapes([
|
||||
{
|
||||
id,
|
||||
type: (this.parent as TLLineTool).shapeType,
|
||||
|
@ -88,23 +88,23 @@ export class Pointing extends StateNode {
|
|||
},
|
||||
])
|
||||
|
||||
this.app.select(id)
|
||||
this.shape = this.app.getShapeById(id)!
|
||||
this.editor.select(id)
|
||||
this.shape = this.editor.getShapeById(id)!
|
||||
}
|
||||
}
|
||||
|
||||
onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
if (!this.shape) return
|
||||
|
||||
if (this.app.inputs.isDragging) {
|
||||
const util = this.app.getShapeUtil(this.shape)
|
||||
if (this.editor.inputs.isDragging) {
|
||||
const util = this.editor.getShapeUtil(this.shape)
|
||||
const handles = util.handles?.(this.shape)
|
||||
if (!handles) {
|
||||
this.app.bailToMark('creating')
|
||||
this.editor.bailToMark('creating')
|
||||
throw Error('No handles found')
|
||||
}
|
||||
|
||||
this.app.setSelectedTool('select.dragging_handle', {
|
||||
this.editor.setSelectedTool('select.dragging_handle', {
|
||||
shape: this.shape,
|
||||
isCreating: true,
|
||||
handle: last(handles)!,
|
||||
|
@ -127,18 +127,18 @@ export class Pointing extends StateNode {
|
|||
|
||||
override onInterrupt: TLInterruptEvent = () => {
|
||||
this.parent.transition('idle', {})
|
||||
this.app.bailToMark('creating')
|
||||
this.app.snaps.clear()
|
||||
this.editor.bailToMark('creating')
|
||||
this.editor.snaps.clear()
|
||||
}
|
||||
|
||||
complete() {
|
||||
this.parent.transition('idle', { shapeId: this.shape.id })
|
||||
this.app.snaps.clear()
|
||||
this.editor.snaps.clear()
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.app.bailToMark(this.markPointId)
|
||||
this.editor.bailToMark(this.markPointId)
|
||||
this.parent.transition('idle', { shapeId: this.shape.id })
|
||||
this.app.snaps.clear()
|
||||
this.editor.snaps.clear()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
onEnter = () => {
|
||||
this.app.setCursor({ type: 'cross' })
|
||||
this.editor.setCursor({ type: 'cross' })
|
||||
}
|
||||
|
||||
onCancel = () => {
|
||||
this.app.setSelectedTool('select')
|
||||
this.editor.setSelectedTool('select')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,16 +15,16 @@ export class Pointing extends StateNode {
|
|||
markPointId = 'creating'
|
||||
|
||||
onEnter = () => {
|
||||
this.wasFocusedOnEnter = !this.app.isMenuOpen
|
||||
this.wasFocusedOnEnter = !this.editor.isMenuOpen
|
||||
}
|
||||
|
||||
onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
if (this.app.inputs.isDragging) {
|
||||
this.app.mark(this.markPointId)
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.editor.mark(this.markPointId)
|
||||
const shape = this.createShape()
|
||||
if (!shape) return
|
||||
|
||||
this.app.setSelectedTool('select.translating', {
|
||||
this.editor.setSelectedTool('select.translating', {
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape,
|
||||
|
@ -56,16 +56,16 @@ export class Pointing extends StateNode {
|
|||
return
|
||||
}
|
||||
|
||||
this.app.mark(this.markPointId)
|
||||
this.editor.mark(this.markPointId)
|
||||
const shape = this.createShape()
|
||||
|
||||
if (this.app.instanceState.isToolLocked) {
|
||||
if (this.editor.instanceState.isToolLocked) {
|
||||
this.parent.transition('idle', {})
|
||||
} else {
|
||||
if (!shape) return
|
||||
|
||||
this.app.setEditingId(shape.id)
|
||||
this.app.setSelectedTool('select.editing_shape', {
|
||||
this.editor.setEditingId(shape.id)
|
||||
this.editor.setSelectedTool('select.editing_shape', {
|
||||
...this.info,
|
||||
target: 'shape',
|
||||
shape,
|
||||
|
@ -74,18 +74,18 @@ export class Pointing extends StateNode {
|
|||
}
|
||||
|
||||
private cancel() {
|
||||
this.app.bailToMark(this.markPointId)
|
||||
this.editor.bailToMark(this.markPointId)
|
||||
this.parent.transition('idle', this.info)
|
||||
}
|
||||
|
||||
private createShape() {
|
||||
const {
|
||||
inputs: { originPagePoint },
|
||||
} = this.app
|
||||
} = this.editor
|
||||
|
||||
const id = this.app.createShapeId()
|
||||
const id = this.editor.createShapeId()
|
||||
|
||||
this.app.createShapes(
|
||||
this.editor.createShapes(
|
||||
[
|
||||
{
|
||||
id,
|
||||
|
@ -97,12 +97,12 @@ export class Pointing extends StateNode {
|
|||
true
|
||||
)
|
||||
|
||||
const util = this.app.getShapeUtil(TLNoteUtil)
|
||||
const shape = this.app.getShapeById<TLNoteShape>(id)!
|
||||
const util = this.editor.getShapeUtil(TLNoteUtil)
|
||||
const shape = this.editor.getShapeById<TLNoteShape>(id)!
|
||||
const bounds = util.bounds(shape)
|
||||
|
||||
// Center the text around the created point
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id,
|
||||
type: 'note',
|
||||
|
@ -111,6 +111,6 @@ export class Pointing extends StateNode {
|
|||
},
|
||||
])
|
||||
|
||||
return this.app.getShapeById(id)
|
||||
return this.editor.getShapeById(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,8 +45,8 @@ export class TLSelectTool extends StateNode {
|
|||
styles = ['color', 'opacity', 'dash', 'fill', 'size'] as TLStyleType[]
|
||||
|
||||
onExit = () => {
|
||||
if (this.app.pageState.editingId) {
|
||||
this.app.setEditingId(null)
|
||||
if (this.editor.pageState.editingId) {
|
||||
this.editor.setEditingId(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ export class Brushing extends StateNode {
|
|||
initialStartShape: TLShape | null = null
|
||||
|
||||
onEnter = (info: TLPointerEventInfo & { target: 'canvas' }) => {
|
||||
const { altKey, currentPagePoint } = this.app.inputs
|
||||
const { altKey, currentPagePoint } = this.editor.inputs
|
||||
|
||||
if (altKey) {
|
||||
this.parent.transition('scribble_brushing', info)
|
||||
|
@ -38,20 +38,20 @@ export class Brushing extends StateNode {
|
|||
}
|
||||
|
||||
this.excludedShapeIds = new Set(
|
||||
this.app.shapesArray
|
||||
.filter((shape) => shape.type === 'group' || this.app.isShapeOrAncestorLocked(shape))
|
||||
this.editor.shapesArray
|
||||
.filter((shape) => shape.type === 'group' || this.editor.isShapeOrAncestorLocked(shape))
|
||||
.map((shape) => shape.id)
|
||||
)
|
||||
|
||||
this.info = info
|
||||
this.initialSelectedIds = this.app.selectedIds.slice()
|
||||
this.initialStartShape = this.app.getShapesAtPoint(currentPagePoint)[0]
|
||||
this.initialSelectedIds = this.editor.selectedIds.slice()
|
||||
this.initialStartShape = this.editor.getShapesAtPoint(currentPagePoint)[0]
|
||||
this.onPointerMove()
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
this.initialSelectedIds = []
|
||||
this.app.setBrush(null)
|
||||
this.editor.setBrush(null)
|
||||
}
|
||||
|
||||
onPointerMove = () => {
|
||||
|
@ -67,12 +67,12 @@ export class Brushing extends StateNode {
|
|||
}
|
||||
|
||||
onCancel?: TLCancelEvent | undefined = (info) => {
|
||||
this.app.setSelectedIds(this.initialSelectedIds, true)
|
||||
this.editor.setSelectedIds(this.initialSelectedIds, true)
|
||||
this.parent.transition('idle', info)
|
||||
}
|
||||
|
||||
onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
|
||||
if (this.app.inputs.altKey) {
|
||||
if (this.editor.inputs.altKey) {
|
||||
this.parent.transition('scribble_brushing', info)
|
||||
} else {
|
||||
this.hitTestShapes()
|
||||
|
@ -92,7 +92,7 @@ export class Brushing extends StateNode {
|
|||
currentPageId,
|
||||
shapesArray,
|
||||
inputs: { originPagePoint, currentPagePoint, shiftKey, ctrlKey },
|
||||
} = this.app
|
||||
} = this.editor
|
||||
|
||||
// Set the brush to contain the current and origin points
|
||||
this.brush.setTo(Box2d.FromPoints([originPagePoint, currentPagePoint]))
|
||||
|
@ -118,7 +118,7 @@ export class Brushing extends StateNode {
|
|||
if (excludedShapeIds.has(shape.id)) continue testAllShapes
|
||||
if (results.has(shape.id)) continue testAllShapes
|
||||
|
||||
pageBounds = this.app.getPageBounds(shape)
|
||||
pageBounds = this.editor.getPageBounds(shape)
|
||||
if (!pageBounds) continue testAllShapes
|
||||
|
||||
// If the brush fully wraps a shape, it's almost certainly a hit
|
||||
|
@ -139,9 +139,9 @@ export class Brushing extends StateNode {
|
|||
if (this.brush.collides(pageBounds)) {
|
||||
// Shapes expect to hit test line segments in their own coordinate system,
|
||||
// so we first need to get the brush corners in the shape's local space.
|
||||
util = this.app.getShapeUtil(shape)
|
||||
util = this.editor.getShapeUtil(shape)
|
||||
|
||||
pageTransform = this.app.getPageTransform(shape)
|
||||
pageTransform = this.editor.getPageTransform(shape)
|
||||
|
||||
if (!pageTransform) {
|
||||
continue testAllShapes
|
||||
|
@ -162,12 +162,12 @@ export class Brushing extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
this.app.setBrush({ ...this.brush.toJson() })
|
||||
this.app.setSelectedIds(Array.from(results), true)
|
||||
this.editor.setBrush({ ...this.brush.toJson() })
|
||||
this.editor.setSelectedIds(Array.from(results), true)
|
||||
}
|
||||
|
||||
onInterrupt: TLInterruptEvent = () => {
|
||||
this.app.setBrush(null)
|
||||
this.editor.setBrush(null)
|
||||
}
|
||||
|
||||
private handleHit(
|
||||
|
@ -184,8 +184,8 @@ export class Brushing extends StateNode {
|
|||
|
||||
// Find the outermost selectable shape, check to see if it has a
|
||||
// page mask; and if so, check to see if the brush intersects it
|
||||
const selectedShape = this.app.getOutermostSelectableShape(shape)
|
||||
const pageMask = this.app.getPageMaskById(selectedShape.id)
|
||||
const selectedShape = this.editor.getOutermostSelectableShape(shape)
|
||||
const pageMask = this.editor.getPageMaskById(selectedShape.id)
|
||||
|
||||
if (
|
||||
pageMask &&
|
||||
|
|
|
@ -7,39 +7,39 @@ export class Idle extends StateNode {
|
|||
static override id = 'idle'
|
||||
|
||||
onEnter = () => {
|
||||
this.app.setCursor({ type: 'default' })
|
||||
this.editor.setCursor({ type: 'default' })
|
||||
|
||||
const { onlySelectedShape } = this.app
|
||||
const { onlySelectedShape } = this.editor
|
||||
|
||||
// well this fucking sucks. what the fuck.
|
||||
// it's possible for a user to enter cropping, then undo
|
||||
// (which clears the cropping id) but still remain in this state.
|
||||
this.app.on('change-history', this.cleanupCroppingState)
|
||||
this.editor.on('change-history', this.cleanupCroppingState)
|
||||
|
||||
this.app.mark('crop')
|
||||
this.editor.mark('crop')
|
||||
|
||||
if (onlySelectedShape) {
|
||||
this.app.setCroppingId(onlySelectedShape.id)
|
||||
this.editor.setCroppingId(onlySelectedShape.id)
|
||||
}
|
||||
}
|
||||
|
||||
onExit: UiExitHandler = () => {
|
||||
this.app.setCursor({ type: 'default' })
|
||||
this.editor.setCursor({ type: 'default' })
|
||||
|
||||
this.app.off('change-history', this.cleanupCroppingState)
|
||||
this.editor.off('change-history', this.cleanupCroppingState)
|
||||
}
|
||||
|
||||
onCancel: TLEventHandlers['onCancel'] = () => {
|
||||
this.app.setCroppingId(null)
|
||||
this.app.setSelectedTool('select.idle', {})
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setSelectedTool('select.idle', {})
|
||||
}
|
||||
|
||||
onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||
if (this.app.isMenuOpen) return
|
||||
if (this.editor.isMenuOpen) return
|
||||
|
||||
if (info.ctrlKey) {
|
||||
this.app.setCroppingId(null)
|
||||
this.app.setSelectedTool('select.brushing', info)
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setSelectedTool('select.brushing', info)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -49,14 +49,14 @@ export class Idle extends StateNode {
|
|||
break
|
||||
}
|
||||
case 'shape': {
|
||||
if (info.shape.id === this.app.croppingId) {
|
||||
this.app.setSelectedTool('select.crop.pointing_crop', info)
|
||||
if (info.shape.id === this.editor.croppingId) {
|
||||
this.editor.setSelectedTool('select.crop.pointing_crop', info)
|
||||
return
|
||||
} else {
|
||||
if (this.app.getShapeUtil(info.shape)?.canCrop(info.shape)) {
|
||||
this.app.setCroppingId(info.shape.id)
|
||||
this.app.setSelectedIds([info.shape.id])
|
||||
this.app.setSelectedTool('select.crop.pointing_crop', info)
|
||||
if (this.editor.getShapeUtil(info.shape)?.canCrop(info.shape)) {
|
||||
this.editor.setCroppingId(info.shape.id)
|
||||
this.editor.setSelectedIds([info.shape.id])
|
||||
this.editor.setSelectedTool('select.crop.pointing_crop', info)
|
||||
} else {
|
||||
this.cancel()
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ export class Idle extends StateNode {
|
|||
case 'top_right_rotate':
|
||||
case 'bottom_left_rotate':
|
||||
case 'bottom_right_rotate': {
|
||||
this.app.setSelectedTool('select.pointing_rotate_handle', {
|
||||
this.editor.setSelectedTool('select.pointing_rotate_handle', {
|
||||
...info,
|
||||
onInteractionEnd: 'select.crop',
|
||||
})
|
||||
|
@ -80,7 +80,7 @@ export class Idle extends StateNode {
|
|||
case 'right':
|
||||
case 'bottom':
|
||||
case 'left': {
|
||||
this.app.setSelectedTool('select.pointing_crop_handle', {
|
||||
this.editor.setSelectedTool('select.pointing_crop_handle', {
|
||||
...info,
|
||||
onInteractionEnd: 'select.crop',
|
||||
})
|
||||
|
@ -90,7 +90,7 @@ export class Idle extends StateNode {
|
|||
case 'top_right':
|
||||
case 'bottom_left':
|
||||
case 'bottom_right': {
|
||||
this.app.setSelectedTool('select.pointing_crop_handle', {
|
||||
this.editor.setSelectedTool('select.pointing_crop_handle', {
|
||||
...info,
|
||||
onInteractionEnd: 'select.crop',
|
||||
})
|
||||
|
@ -110,11 +110,11 @@ export class Idle extends StateNode {
|
|||
// after the user double clicked the edge to begin cropping
|
||||
if (info.phase !== 'up') return
|
||||
|
||||
if (!this.app.croppingId) return
|
||||
const shape = this.app.getShapeById(this.app.croppingId)
|
||||
if (!this.editor.croppingId) return
|
||||
const shape = this.editor.getShapeById(this.editor.croppingId)
|
||||
if (!shape) return
|
||||
|
||||
const util = this.app.getShapeUtil(shape)
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
if (!util) return
|
||||
|
||||
if (info.target === 'selection') {
|
||||
|
@ -133,33 +133,33 @@ export class Idle extends StateNode {
|
|||
onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||
switch (info.code) {
|
||||
case 'Enter': {
|
||||
this.app.setCroppingId(null)
|
||||
this.app.setSelectedTool('select.idle', {})
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setSelectedTool('select.idle', {})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private cancel() {
|
||||
this.app.setCroppingId(null)
|
||||
this.app.setSelectedTool('select.idle', {})
|
||||
this.editor.setCroppingId(null)
|
||||
this.editor.setSelectedTool('select.idle', {})
|
||||
}
|
||||
|
||||
private cleanupCroppingState = () => {
|
||||
if (!this.app.croppingId) {
|
||||
this.app.setSelectedTool('select.idle', {})
|
||||
if (!this.editor.croppingId) {
|
||||
this.editor.setSelectedTool('select.idle', {})
|
||||
}
|
||||
}
|
||||
|
||||
private nudgeCroppingImage(ephemeral = false) {
|
||||
const {
|
||||
app: {
|
||||
editor: {
|
||||
inputs: { keys },
|
||||
},
|
||||
} = this
|
||||
|
||||
// We want to use the "actual" shift key state,
|
||||
// not the one that's in the app.inputs.shiftKey,
|
||||
// not the one that's in the editor.inputs.shiftKey,
|
||||
// because that one uses a short timeout on release
|
||||
const shiftKey = keys.has('Shift')
|
||||
|
||||
|
@ -174,18 +174,18 @@ export class Idle extends StateNode {
|
|||
|
||||
if (shiftKey) delta.mul(10)
|
||||
|
||||
const shape = this.app.getShapeById(this.app.croppingId!) as ShapeWithCrop
|
||||
const shape = this.editor.getShapeById(this.editor.croppingId!) as ShapeWithCrop
|
||||
if (!shape) return
|
||||
const partial = getTranslateCroppedImageChange(this.app, shape, delta)
|
||||
const partial = getTranslateCroppedImageChange(this.editor, shape, delta)
|
||||
|
||||
if (partial) {
|
||||
if (!ephemeral) {
|
||||
// We don't want to create new marks if the user
|
||||
// is just holding down the arrow keys
|
||||
this.app.mark('translate crop')
|
||||
this.editor.mark('translate crop')
|
||||
}
|
||||
|
||||
this.app.updateShapes([partial])
|
||||
this.editor.updateShapes([partial])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,16 +5,16 @@ export class PointingCrop extends StateNode {
|
|||
static override id = 'pointing_crop'
|
||||
|
||||
onCancel: TLEventHandlers['onCancel'] = () => {
|
||||
this.app.setSelectedTool('select.crop.idle', {})
|
||||
this.editor.setSelectedTool('select.crop.idle', {})
|
||||
}
|
||||
|
||||
onPointerMove: TLPointerEvent = (info) => {
|
||||
if (this.app.inputs.isDragging) {
|
||||
this.app.setSelectedTool('select.crop.translating_crop', info)
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.editor.setSelectedTool('select.crop.translating_crop', info)
|
||||
}
|
||||
}
|
||||
|
||||
onPointerUp: TLPointerEvent = (info) => {
|
||||
this.app.setSelectedTool('select.crop.idle', info)
|
||||
this.editor.setSelectedTool('select.crop.idle', info)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,13 +27,13 @@ export class TranslatingCrop extends StateNode {
|
|||
this.info = info
|
||||
this.snapshot = this.createSnapshot()
|
||||
|
||||
this.app.mark(this.markId)
|
||||
this.app.setCursor({ type: 'move' })
|
||||
this.editor.mark(this.markId)
|
||||
this.editor.setCursor({ type: 'move' })
|
||||
this.updateShapes()
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
this.app.setCursor({ type: 'default' })
|
||||
this.editor.setCursor({ type: 'default' })
|
||||
}
|
||||
|
||||
onPointerMove = () => {
|
||||
|
@ -77,16 +77,16 @@ export class TranslatingCrop extends StateNode {
|
|||
|
||||
protected complete() {
|
||||
this.updateShapes()
|
||||
this.app.setSelectedTool('select.crop.idle', this.info)
|
||||
this.editor.setSelectedTool('select.crop.idle', this.info)
|
||||
}
|
||||
|
||||
private cancel() {
|
||||
this.app.bailToMark(this.markId)
|
||||
this.app.setSelectedTool('select.crop.idle', this.info)
|
||||
this.editor.bailToMark(this.markId)
|
||||
this.editor.setSelectedTool('select.crop.idle', this.info)
|
||||
}
|
||||
|
||||
private createSnapshot() {
|
||||
const shape = this.app.onlySelectedShape as ShapeWithCrop
|
||||
const shape = this.editor.onlySelectedShape as ShapeWithCrop
|
||||
return { shape }
|
||||
}
|
||||
|
||||
|
@ -95,12 +95,12 @@ export class TranslatingCrop extends StateNode {
|
|||
|
||||
if (!shape) return
|
||||
|
||||
const { originPagePoint, currentPagePoint } = this.app.inputs
|
||||
const { originPagePoint, currentPagePoint } = this.editor.inputs
|
||||
const delta = currentPagePoint.clone().sub(originPagePoint)
|
||||
const partial = getTranslateCroppedImageChange(this.app, shape, delta)
|
||||
const partial = getTranslateCroppedImageChange(this.editor, shape, delta)
|
||||
|
||||
if (partial) {
|
||||
this.app.updateShapes([partial], true)
|
||||
this.editor.updateShapes([partial], true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Vec2d } from '@tldraw/primitives'
|
||||
import { TLBaseShape, TLImageCrop, TLShapePartial } from '@tldraw/tlschema'
|
||||
import { deepCopy } from '@tldraw/utils'
|
||||
import { App } from '../../../../../App'
|
||||
import { Editor } from '../../../../../Editor'
|
||||
|
||||
export type ShapeWithCrop = TLBaseShape<string, { w: number; h: number; crop: TLImageCrop }>
|
||||
|
||||
export function getTranslateCroppedImageChange(
|
||||
app: App,
|
||||
editor: Editor,
|
||||
shape: TLBaseShape<string, { w: number; h: number; crop: TLImageCrop }>,
|
||||
delta: Vec2d
|
||||
) {
|
||||
|
@ -20,7 +20,7 @@ export function getTranslateCroppedImageChange(
|
|||
return
|
||||
}
|
||||
|
||||
const flatten: 'x' | 'y' | null = app.inputs.shiftKey
|
||||
const flatten: 'x' | 'y' | null = editor.inputs.shiftKey
|
||||
? Math.abs(delta.x) < Math.abs(delta.y)
|
||||
? 'x'
|
||||
: 'y'
|
||||
|
|
|
@ -36,7 +36,7 @@ export class Cropping extends StateNode {
|
|||
}
|
||||
) => {
|
||||
this.info = info
|
||||
this.markId = this.app.mark('cropping')
|
||||
this.markId = this.editor.mark('cropping')
|
||||
this.snapshot = this.createSnapshot()
|
||||
this.updateShapes()
|
||||
}
|
||||
|
@ -58,11 +58,11 @@ export class Cropping extends StateNode {
|
|||
}
|
||||
|
||||
private updateCursor() {
|
||||
const selectedShape = this.app.selectedShapes[0]
|
||||
const selectedShape = this.editor.selectedShapes[0]
|
||||
if (!selectedShape) return
|
||||
|
||||
const cursorType = CursorTypeMap[this.info.handle!]
|
||||
this.app.setCursor({
|
||||
this.editor.setCursor({
|
||||
type: cursorType,
|
||||
rotation: selectedShape.rotation,
|
||||
})
|
||||
|
@ -77,13 +77,13 @@ export class Cropping extends StateNode {
|
|||
const { shape, cursorHandleOffset } = this.snapshot
|
||||
|
||||
if (!shape) return
|
||||
const util = this.app.getShapeUtil(TLImageUtil)
|
||||
const util = this.editor.getShapeUtil(TLImageUtil)
|
||||
if (!util) return
|
||||
|
||||
const props = shape.props as TLImageShapeProps
|
||||
|
||||
const currentPagePoint = this.app.inputs.currentPagePoint.clone().sub(cursorHandleOffset)
|
||||
const originPagePoint = this.app.inputs.originPagePoint.clone().sub(cursorHandleOffset)
|
||||
const currentPagePoint = this.editor.inputs.currentPagePoint.clone().sub(cursorHandleOffset)
|
||||
const originPagePoint = this.editor.inputs.originPagePoint.clone().sub(cursorHandleOffset)
|
||||
|
||||
const change = currentPagePoint.clone().sub(originPagePoint).rot(-shape.rotation)
|
||||
|
||||
|
@ -196,25 +196,25 @@ export class Cropping extends StateNode {
|
|||
},
|
||||
}
|
||||
|
||||
this.app.updateShapes([partial], true)
|
||||
this.editor.updateShapes([partial], true)
|
||||
this.updateCursor()
|
||||
}
|
||||
|
||||
private complete() {
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.app.setSelectedTool(this.info.onInteractionEnd, this.info)
|
||||
this.editor.setSelectedTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.app.setCroppingId(null)
|
||||
this.editor.setCroppingId(null)
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
||||
private cancel() {
|
||||
this.app.bailToMark(this.markId)
|
||||
this.editor.bailToMark(this.markId)
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.app.setSelectedTool(this.info.onInteractionEnd, this.info)
|
||||
this.editor.setSelectedTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.app.setCroppingId(null)
|
||||
this.editor.setCroppingId(null)
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
@ -223,11 +223,11 @@ export class Cropping extends StateNode {
|
|||
const {
|
||||
selectionRotation,
|
||||
inputs: { originPagePoint },
|
||||
} = this.app
|
||||
} = this.editor
|
||||
|
||||
const shape = this.app.onlySelectedShape as TLShape
|
||||
const shape = this.editor.onlySelectedShape as TLShape
|
||||
|
||||
const selectionBounds = this.app.selectionBounds!
|
||||
const selectionBounds = this.editor.selectionBounds!
|
||||
|
||||
const dragHandlePoint = Vec2d.RotWith(
|
||||
selectionBounds.getHandlePoint(this.info.handle!),
|
||||
|
|
|
@ -50,14 +50,14 @@ export class DraggingHandle extends StateNode {
|
|||
const { shape, isCreating, handle } = info
|
||||
this.info = info
|
||||
this.shapeId = shape.id
|
||||
this.markId = isCreating ? 'creating' : this.app.mark('dragging handle')
|
||||
this.markId = isCreating ? 'creating' : this.editor.mark('dragging handle')
|
||||
this.initialHandle = deepCopy(handle)
|
||||
this.initialPageTransform = this.app.getPageTransform(shape)!
|
||||
this.initialPageRotation = this.app.getPageRotation(shape)!
|
||||
this.initialPageTransform = this.editor.getPageTransform(shape)!
|
||||
this.initialPageRotation = this.editor.getPageRotation(shape)!
|
||||
|
||||
this.app.setCursor({ type: isCreating ? 'cross' : 'grabbing', rotation: 0 })
|
||||
this.editor.setCursor({ type: isCreating ? 'cross' : 'grabbing', rotation: 0 })
|
||||
|
||||
const handles = this.app.getShapeUtil(shape).handles(shape).sort(sortByIndex)
|
||||
const handles = this.editor.getShapeUtil(shape).handles(shape).sort(sortByIndex)
|
||||
const index = handles.findIndex((h) => h.id === info.handle.id)
|
||||
|
||||
this.initialAdjacentHandle = null
|
||||
|
@ -87,7 +87,7 @@ export class DraggingHandle extends StateNode {
|
|||
this.isPrecise = false
|
||||
|
||||
if (initialTerminal?.type === 'binding') {
|
||||
this.app.setHintingIds([initialTerminal.boundShapeId])
|
||||
this.editor.setHintingIds([initialTerminal.boundShapeId])
|
||||
|
||||
this.isPrecise = !Vec2d.Equals(initialTerminal.normalizedAnchor, { x: 0.5, y: 0.5 })
|
||||
if (this.isPrecise) {
|
||||
|
@ -149,19 +149,19 @@ export class DraggingHandle extends StateNode {
|
|||
}
|
||||
|
||||
onExit = () => {
|
||||
this.app.setHintingIds([])
|
||||
this.app.snaps.clear()
|
||||
this.app.setCursor({ type: 'default' })
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.snaps.clear()
|
||||
this.editor.setCursor({ type: 'default' })
|
||||
}
|
||||
|
||||
private complete() {
|
||||
this.app.snaps.clear()
|
||||
this.editor.snaps.clear()
|
||||
|
||||
const { onInteractionEnd } = this.info
|
||||
if (this.app.instanceState.isToolLocked && onInteractionEnd) {
|
||||
if (this.editor.instanceState.isToolLocked && onInteractionEnd) {
|
||||
// Return to the tool that was active before this one,
|
||||
// but only if tool lock is turned on!
|
||||
this.app.setSelectedTool(onInteractionEnd, { shapeId: this.shapeId })
|
||||
this.editor.setSelectedTool(onInteractionEnd, { shapeId: this.shapeId })
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -169,14 +169,14 @@ export class DraggingHandle extends StateNode {
|
|||
}
|
||||
|
||||
private cancel() {
|
||||
this.app.bailToMark(this.markId)
|
||||
this.app.snaps.clear()
|
||||
this.editor.bailToMark(this.markId)
|
||||
this.editor.snaps.clear()
|
||||
|
||||
const { onInteractionEnd } = this.info
|
||||
if (onInteractionEnd) {
|
||||
// Return to the tool that was active before this one,
|
||||
// whether tool lock is turned on or not!
|
||||
this.app.setSelectedTool(onInteractionEnd, { shapeId: this.shapeId })
|
||||
this.editor.setSelectedTool(onInteractionEnd, { shapeId: this.shapeId })
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -184,8 +184,8 @@ export class DraggingHandle extends StateNode {
|
|||
}
|
||||
|
||||
private update() {
|
||||
const { currentPagePoint, originPagePoint, shiftKey } = this.app.inputs
|
||||
const shape = this.app.getShapeById(this.shapeId)
|
||||
const { currentPagePoint, originPagePoint, shiftKey } = this.editor.inputs
|
||||
const shape = this.editor.getShapeById(this.shapeId)
|
||||
|
||||
if (!shape) return
|
||||
|
||||
|
@ -205,14 +205,14 @@ export class DraggingHandle extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
this.app.snaps.clear()
|
||||
this.editor.snaps.clear()
|
||||
|
||||
const { ctrlKey } = this.app.inputs
|
||||
const shouldSnap = this.app.userDocumentSettings.isSnapMode ? !ctrlKey : ctrlKey
|
||||
const { ctrlKey } = this.editor.inputs
|
||||
const shouldSnap = this.editor.userDocumentSettings.isSnapMode ? !ctrlKey : ctrlKey
|
||||
|
||||
if (shouldSnap && shape.type === 'line') {
|
||||
const pagePoint = Matrix2d.applyToPoint(this.app.getPageTransformById(shape.id)!, point)
|
||||
const snapData = this.app.snaps.snapLineHandleTranslate({
|
||||
const pagePoint = Matrix2d.applyToPoint(this.editor.getPageTransformById(shape.id)!, point)
|
||||
const snapData = this.editor.snaps.snapLineHandleTranslate({
|
||||
lineId: shape.id,
|
||||
handleId: this.initialHandle.id,
|
||||
handlePoint: pagePoint,
|
||||
|
@ -220,12 +220,12 @@ export class DraggingHandle extends StateNode {
|
|||
|
||||
const { nudge } = snapData
|
||||
if (nudge.x || nudge.y) {
|
||||
const shapeSpaceNudge = this.app.getDeltaInShapeSpace(shape, nudge)
|
||||
const shapeSpaceNudge = this.editor.getDeltaInShapeSpace(shape, nudge)
|
||||
point = Vec2d.Add(point, shapeSpaceNudge)
|
||||
}
|
||||
}
|
||||
|
||||
const util = this.app.getShapeUtil(shape)
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
|
||||
const changes = util.onHandleChange?.(shape, {
|
||||
handle: {
|
||||
|
@ -233,7 +233,7 @@ export class DraggingHandle extends StateNode {
|
|||
x: point.x,
|
||||
y: point.y,
|
||||
},
|
||||
isPrecise: this.isPrecise || this.app.inputs.altKey,
|
||||
isPrecise: this.isPrecise || this.editor.inputs.altKey,
|
||||
})
|
||||
|
||||
const next: TLShapePartial<any> = { ...shape, ...changes }
|
||||
|
@ -242,16 +242,17 @@ export class DraggingHandle extends StateNode {
|
|||
const bindingAfter = (next.props as any)[this.initialHandle.id] as TLArrowTerminal | undefined
|
||||
|
||||
if (bindingAfter?.type === 'binding') {
|
||||
if (this.app.hintingIds[0] !== bindingAfter.boundShapeId) {
|
||||
this.app.setHintingIds([bindingAfter.boundShapeId])
|
||||
if (this.editor.hintingIds[0] !== bindingAfter.boundShapeId) {
|
||||
this.editor.setHintingIds([bindingAfter.boundShapeId])
|
||||
this.pointingId = bindingAfter.boundShapeId
|
||||
this.isPrecise = this.app.inputs.pointerVelocity.len() < 0.5 || this.app.inputs.altKey
|
||||
this.isPrecise =
|
||||
this.editor.inputs.pointerVelocity.len() < 0.5 || this.editor.inputs.altKey
|
||||
this.isPreciseId = this.isPrecise ? bindingAfter.boundShapeId : null
|
||||
this.resetExactTimeout()
|
||||
}
|
||||
} else {
|
||||
if (this.app.hintingIds.length > 0) {
|
||||
this.app.setHintingIds([])
|
||||
if (this.editor.hintingIds.length > 0) {
|
||||
this.editor.setHintingIds([])
|
||||
this.pointingId = null
|
||||
this.isPrecise = false
|
||||
this.isPreciseId = null
|
||||
|
@ -261,7 +262,7 @@ export class DraggingHandle extends StateNode {
|
|||
}
|
||||
|
||||
if (changes) {
|
||||
this.app.updateShapes([next], true)
|
||||
this.editor.updateShapes([next], true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,13 @@ export class EditingShape extends StateNode {
|
|||
onPointerEnter: TLEventHandlers['onPointerEnter'] = (info) => {
|
||||
switch (info.target) {
|
||||
case 'shape': {
|
||||
const { selectedIds, focusLayerId } = this.app
|
||||
const hoveringShape = this.app.getOutermostSelectableShape(
|
||||
const { selectedIds, focusLayerId } = this.editor
|
||||
const hoveringShape = this.editor.getOutermostSelectableShape(
|
||||
info.shape,
|
||||
(parent) => !selectedIds.includes(parent.id)
|
||||
)
|
||||
if (hoveringShape.id !== focusLayerId) {
|
||||
this.app.setHoveredId(hoveringShape.id)
|
||||
this.editor.setHoveredId(hoveringShape.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -23,22 +23,22 @@ export class EditingShape extends StateNode {
|
|||
onPointerLeave: TLEventHandlers['onPointerEnter'] = (info) => {
|
||||
switch (info.target) {
|
||||
case 'shape': {
|
||||
this.app.setHoveredId(null)
|
||||
this.editor.setHoveredId(null)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
if (!this.app.pageState.editingId) return
|
||||
const { editingId } = this.app.pageState
|
||||
if (!this.editor.pageState.editingId) return
|
||||
const { editingId } = this.editor.pageState
|
||||
if (!editingId) return
|
||||
|
||||
// Clear the editing shape
|
||||
this.app.setEditingId(null)
|
||||
this.editor.setEditingId(null)
|
||||
|
||||
const shape = this.app.getShapeById(editingId)!
|
||||
const util = this.app.getShapeUtil(shape)
|
||||
const shape = this.editor.getShapeById(editingId)!
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
|
||||
// Check for changes on editing end
|
||||
util.onEditEnd?.(shape)
|
||||
|
@ -49,31 +49,31 @@ export class EditingShape extends StateNode {
|
|||
case 'shape': {
|
||||
const { shape } = info
|
||||
|
||||
const { editingId } = this.app.pageState
|
||||
const { editingId } = this.editor.pageState
|
||||
|
||||
if (editingId) {
|
||||
if (shape.id === editingId) {
|
||||
return
|
||||
}
|
||||
|
||||
const editingShape = this.app.getShapeById(editingId)
|
||||
const editingShape = this.editor.getShapeById(editingId)
|
||||
|
||||
if (editingShape) {
|
||||
const editingShapeUtil = this.app.getShapeUtil(editingShape)
|
||||
const editingShapeUtil = this.editor.getShapeUtil(editingShape)
|
||||
editingShapeUtil.onEditEnd?.(editingShape)
|
||||
|
||||
const util = this.app.getShapeUtil(shape)
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
|
||||
// If the user has clicked onto a different shape of the same type
|
||||
// which is available to edit, select it and begin editing it.
|
||||
if (
|
||||
shape.type === editingShape.type &&
|
||||
util.canEdit?.(shape) &&
|
||||
!this.app.isShapeOrAncestorLocked(shape)
|
||||
!this.editor.isShapeOrAncestorLocked(shape)
|
||||
) {
|
||||
this.app.setEditingId(shape.id)
|
||||
this.app.setHoveredId(shape.id)
|
||||
this.app.setSelectedIds([shape.id])
|
||||
this.editor.setEditingId(shape.id)
|
||||
this.editor.setHoveredId(shape.id)
|
||||
this.editor.setSelectedIds([shape.id])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,13 @@ export class Idle extends StateNode {
|
|||
break
|
||||
}
|
||||
case 'shape': {
|
||||
const { selectedIds, focusLayerId } = this.app
|
||||
const hoveringShape = this.app.getOutermostSelectableShape(
|
||||
const { selectedIds, focusLayerId } = this.editor
|
||||
const hoveringShape = this.editor.getOutermostSelectableShape(
|
||||
info.shape,
|
||||
(parent) => !selectedIds.includes(parent.id)
|
||||
)
|
||||
if (hoveringShape.id !== focusLayerId) {
|
||||
this.app.setHoveredId(hoveringShape.id)
|
||||
this.editor.setHoveredId(hoveringShape.id)
|
||||
}
|
||||
|
||||
// Custom cursor debugging!
|
||||
|
@ -34,10 +34,10 @@ export class Idle extends StateNode {
|
|||
if (hoveringShape.type !== 'geo') break
|
||||
const cursorType = (hoveringShape.props as TLGeoShapeProps).text
|
||||
try {
|
||||
this.app.setCursor({ type: cursorType })
|
||||
this.editor.setCursor({ type: cursorType })
|
||||
} catch (e) {
|
||||
console.error(`Cursor type not recognized: '${cursorType}'`)
|
||||
this.app.setCursor({ type: 'default' })
|
||||
this.editor.setCursor({ type: 'default' })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,14 +49,14 @@ export class Idle extends StateNode {
|
|||
onPointerLeave: TLEventHandlers['onPointerEnter'] = (info) => {
|
||||
switch (info.target) {
|
||||
case 'shape': {
|
||||
this.app.setHoveredId(null)
|
||||
this.editor.setHoveredId(null)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||
if (this.app.isMenuOpen) return
|
||||
if (this.editor.isMenuOpen) return
|
||||
|
||||
const shouldEnterCropMode = this.shouldEnterCropMode(info, true)
|
||||
|
||||
|
@ -70,13 +70,13 @@ export class Idle extends StateNode {
|
|||
break
|
||||
}
|
||||
case 'shape': {
|
||||
if (this.app.isShapeOrAncestorLocked(info.shape)) break
|
||||
if (this.editor.isShapeOrAncestorLocked(info.shape)) break
|
||||
this.parent.transition('pointing_shape', info)
|
||||
break
|
||||
}
|
||||
case 'handle': {
|
||||
if (this.app.isReadOnly) break
|
||||
if (this.app.inputs.altKey) {
|
||||
if (this.editor.isReadOnly) break
|
||||
if (this.editor.inputs.altKey) {
|
||||
this.parent.transition('pointing_shape', info)
|
||||
} else {
|
||||
this.parent.transition('pointing_handle', info)
|
||||
|
@ -130,16 +130,16 @@ export class Idle extends StateNode {
|
|||
switch (info.target) {
|
||||
case 'canvas': {
|
||||
// Create text shape and transition to editing_shape
|
||||
if (this.app.isReadOnly) break
|
||||
if (this.editor.isReadOnly) break
|
||||
this.createTextShapeAtPoint(info)
|
||||
break
|
||||
}
|
||||
case 'selection': {
|
||||
if (this.app.isReadOnly) break
|
||||
if (this.editor.isReadOnly) break
|
||||
|
||||
const { onlySelectedShape } = this.app
|
||||
const { onlySelectedShape } = this.editor
|
||||
if (onlySelectedShape) {
|
||||
const util = this.app.getShapeUtil(onlySelectedShape)
|
||||
const util = this.editor.getShapeUtil(onlySelectedShape)
|
||||
|
||||
// Test edges for an onDoubleClickEdge handler
|
||||
if (
|
||||
|
@ -150,8 +150,8 @@ export class Idle extends StateNode {
|
|||
) {
|
||||
const change = util.onDoubleClickEdge?.(onlySelectedShape)
|
||||
if (change) {
|
||||
this.app.mark('double click edge')
|
||||
this.app.updateShapes([change])
|
||||
this.editor.mark('double click edge')
|
||||
this.editor.updateShapes([change])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ export class Idle extends StateNode {
|
|||
// For corners OR edges
|
||||
if (
|
||||
util.canCrop(onlySelectedShape) &&
|
||||
!this.app.isShapeOrAncestorLocked(onlySelectedShape)
|
||||
!this.editor.isShapeOrAncestorLocked(onlySelectedShape)
|
||||
) {
|
||||
this.parent.transition('crop', info)
|
||||
return
|
||||
|
@ -173,21 +173,21 @@ export class Idle extends StateNode {
|
|||
}
|
||||
case 'shape': {
|
||||
const { shape } = info
|
||||
const util = this.app.getShapeUtil(shape)
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
|
||||
// Allow playing videos and embeds
|
||||
if (shape.type !== 'video' && shape.type !== 'embed' && this.app.isReadOnly) break
|
||||
if (shape.type !== 'video' && shape.type !== 'embed' && this.editor.isReadOnly) break
|
||||
|
||||
if (util.onDoubleClick) {
|
||||
// Call the shape's double click handler
|
||||
const change = util.onDoubleClick?.(shape)
|
||||
if (change) {
|
||||
this.app.updateShapes([change])
|
||||
this.editor.updateShapes([change])
|
||||
return
|
||||
} else if (util.canCrop(shape) && !this.app.isShapeOrAncestorLocked(shape)) {
|
||||
} else if (util.canCrop(shape) && !this.editor.isShapeOrAncestorLocked(shape)) {
|
||||
// crop on double click
|
||||
this.app.mark('select and crop')
|
||||
this.app.select(info.shape?.id)
|
||||
this.editor.mark('select and crop')
|
||||
this.editor.select(info.shape?.id)
|
||||
this.parent.transition('crop', info)
|
||||
return
|
||||
}
|
||||
|
@ -204,14 +204,14 @@ export class Idle extends StateNode {
|
|||
break
|
||||
}
|
||||
case 'handle': {
|
||||
if (this.app.isReadOnly) break
|
||||
if (this.editor.isReadOnly) break
|
||||
const { shape, handle } = info
|
||||
|
||||
const util = this.app.getShapeUtil(shape)
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
const changes = util.onDoubleClickHandle?.(shape, handle)
|
||||
|
||||
if (changes) {
|
||||
this.app.updateShapes([changes])
|
||||
this.editor.updateShapes([changes])
|
||||
} else {
|
||||
// If the shape's double click handler has not created a change,
|
||||
// and if the shape can edit, then begin editing the shape.
|
||||
|
@ -226,21 +226,21 @@ export class Idle extends StateNode {
|
|||
onRightClick: TLEventHandlers['onRightClick'] = (info) => {
|
||||
switch (info.target) {
|
||||
case 'canvas': {
|
||||
this.app.selectNone()
|
||||
this.editor.selectNone()
|
||||
break
|
||||
}
|
||||
case 'shape': {
|
||||
const { selectedIds } = this.app.pageState
|
||||
const { selectedIds } = this.editor.pageState
|
||||
const { shape } = info
|
||||
|
||||
const targetShape = this.app.getOutermostSelectableShape(
|
||||
const targetShape = this.editor.getOutermostSelectableShape(
|
||||
shape,
|
||||
(parent) => !this.app.isSelected(parent.id)
|
||||
(parent) => !this.editor.isSelected(parent.id)
|
||||
)
|
||||
|
||||
if (!selectedIds.includes(targetShape.id)) {
|
||||
this.app.mark('selecting shape')
|
||||
this.app.setSelectedIds([targetShape.id])
|
||||
this.editor.mark('selecting shape')
|
||||
this.editor.setSelectedIds([targetShape.id])
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -248,16 +248,19 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
onEnter = () => {
|
||||
this.app.setHoveredId(null)
|
||||
this.app.setCursor({ type: 'default' })
|
||||
this.editor.setHoveredId(null)
|
||||
this.editor.setCursor({ type: 'default' })
|
||||
}
|
||||
|
||||
onCancel: TLEventHandlers['onCancel'] = () => {
|
||||
if (this.app.focusLayerId !== this.app.currentPageId && this.app.selectedIds.length > 0) {
|
||||
this.app.popFocusLayer()
|
||||
if (
|
||||
this.editor.focusLayerId !== this.editor.currentPageId &&
|
||||
this.editor.selectedIds.length > 0
|
||||
) {
|
||||
this.editor.popFocusLayer()
|
||||
} else {
|
||||
this.app.mark('clearing selection')
|
||||
this.app.selectNone()
|
||||
this.editor.mark('clearing selection')
|
||||
this.editor.selectNone()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,14 +289,14 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
onKeyUp = (info: TLKeyboardEventInfo) => {
|
||||
if (this.app.isReadOnly) {
|
||||
if (this.editor.isReadOnly) {
|
||||
switch (info.code) {
|
||||
case 'Enter': {
|
||||
if (this.shouldStartEditingShape() && this.app.onlySelectedShape) {
|
||||
this.startEditingShape(this.app.onlySelectedShape, {
|
||||
if (this.shouldStartEditingShape() && this.editor.onlySelectedShape) {
|
||||
this.startEditingShape(this.editor.onlySelectedShape, {
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape: this.app.onlySelectedShape,
|
||||
shape: this.editor.onlySelectedShape,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -303,20 +306,20 @@ export class Idle extends StateNode {
|
|||
} else {
|
||||
switch (info.code) {
|
||||
case 'Enter': {
|
||||
const { selectedShapes } = this.app
|
||||
const { selectedShapes } = this.editor
|
||||
|
||||
if (selectedShapes.every((shape) => shape.type === 'group')) {
|
||||
this.app.setSelectedIds(
|
||||
selectedShapes.flatMap((shape) => this.app.getSortedChildIds(shape.id))
|
||||
this.editor.setSelectedIds(
|
||||
selectedShapes.flatMap((shape) => this.editor.getSortedChildIds(shape.id))
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.shouldStartEditingShape() && this.app.onlySelectedShape) {
|
||||
this.startEditingShape(this.app.onlySelectedShape, {
|
||||
if (this.shouldStartEditingShape() && this.editor.onlySelectedShape) {
|
||||
this.startEditingShape(this.editor.onlySelectedShape, {
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape: this.app.onlySelectedShape,
|
||||
shape: this.editor.onlySelectedShape,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -330,11 +333,11 @@ export class Idle extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
private shouldStartEditingShape(shape: TLShape | null = this.app.onlySelectedShape): boolean {
|
||||
private shouldStartEditingShape(shape: TLShape | null = this.editor.onlySelectedShape): boolean {
|
||||
if (!shape) return false
|
||||
if (this.app.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return false
|
||||
if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return false
|
||||
|
||||
const util = this.app.getShapeUtil(shape)
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
return util.canEdit(shape)
|
||||
}
|
||||
|
||||
|
@ -342,11 +345,11 @@ export class Idle extends StateNode {
|
|||
info: TLPointerEventInfo | TLKeyboardEventInfo,
|
||||
withCtrlKey: boolean
|
||||
): boolean {
|
||||
const singleShape = this.app.onlySelectedShape
|
||||
const singleShape = this.editor.onlySelectedShape
|
||||
if (!singleShape) return false
|
||||
if (this.app.isShapeOrAncestorLocked(singleShape)) return false
|
||||
if (this.editor.isShapeOrAncestorLocked(singleShape)) return false
|
||||
|
||||
const shapeUtil = this.app.getShapeUtil(singleShape)
|
||||
const shapeUtil = this.editor.getShapeUtil(singleShape)
|
||||
// Should the Ctrl key be pressed to enter crop mode
|
||||
if (withCtrlKey) {
|
||||
return shapeUtil.canCrop(singleShape) && info.ctrlKey
|
||||
|
@ -356,20 +359,20 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
private startEditingShape(shape: TLShape, info: TLClickEventInfo | TLKeyboardEventInfo) {
|
||||
if (this.app.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return
|
||||
this.app.mark('editing shape')
|
||||
this.app.setEditingId(shape.id)
|
||||
if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return
|
||||
this.editor.mark('editing shape')
|
||||
this.editor.setEditingId(shape.id)
|
||||
this.parent.transition('editing_shape', info)
|
||||
}
|
||||
|
||||
private createTextShapeAtPoint(info: TLClickEventInfo) {
|
||||
this.app.mark('creating text shape')
|
||||
this.editor.mark('creating text shape')
|
||||
|
||||
const id = createShapeId()
|
||||
|
||||
const { x, y } = this.app.inputs.currentPagePoint
|
||||
const { x, y } = this.editor.inputs.currentPagePoint
|
||||
|
||||
this.app.createShapes([
|
||||
this.editor.createShapes([
|
||||
{
|
||||
id,
|
||||
type: 'text',
|
||||
|
@ -382,12 +385,12 @@ export class Idle extends StateNode {
|
|||
},
|
||||
])
|
||||
|
||||
const shape = this.app.getShapeById(id)
|
||||
const shape = this.editor.getShapeById(id)
|
||||
if (!shape) return
|
||||
|
||||
const bounds = this.app.getBounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
|
||||
this.app.updateShapes([
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id,
|
||||
type: 'text',
|
||||
|
@ -396,20 +399,20 @@ export class Idle extends StateNode {
|
|||
},
|
||||
])
|
||||
|
||||
this.app.setEditingId(id)
|
||||
this.app.select(id)
|
||||
this.editor.setEditingId(id)
|
||||
this.editor.select(id)
|
||||
this.parent.transition('editing_shape', info)
|
||||
}
|
||||
|
||||
private nudgeSelectedShapes(ephemeral = false) {
|
||||
const {
|
||||
app: {
|
||||
editor: {
|
||||
inputs: { keys },
|
||||
},
|
||||
} = this
|
||||
|
||||
// We want to use the "actual" shift key state,
|
||||
// not the one that's in the app.inputs.shiftKey,
|
||||
// not the one that's in the editor.inputs.shiftKey,
|
||||
// because that one uses a short timeout on release
|
||||
const shiftKey = keys.has('Shift')
|
||||
|
||||
|
@ -422,8 +425,8 @@ export class Idle extends StateNode {
|
|||
|
||||
if (delta.equals(new Vec2d(0, 0))) return
|
||||
|
||||
if (!ephemeral) this.app.mark('nudge shapes')
|
||||
if (!ephemeral) this.editor.mark('nudge shapes')
|
||||
|
||||
this.app.nudgeShapes(this.app.selectedIds, delta, shiftKey)
|
||||
this.editor.nudgeShapes(this.editor.selectedIds, delta, shiftKey)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,32 +6,32 @@ export class PointingCanvas extends StateNode {
|
|||
static override id = 'pointing_canvas'
|
||||
|
||||
onEnter = () => {
|
||||
const { inputs } = this.app
|
||||
const { inputs } = this.editor
|
||||
|
||||
if (!inputs.shiftKey) {
|
||||
if (this.app.selectedIds.length > 0) {
|
||||
this.app.mark('selecting none')
|
||||
this.app.selectNone()
|
||||
if (this.editor.selectedIds.length > 0) {
|
||||
this.editor.mark('selecting none')
|
||||
this.editor.selectNone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clickWasInsideFocusedGroup() {
|
||||
const { focusLayerId, inputs } = this.app
|
||||
const { focusLayerId, inputs } = this.editor
|
||||
if (!isShapeId(focusLayerId)) {
|
||||
return false
|
||||
}
|
||||
const groupShape = this.app.getShapeById(focusLayerId)
|
||||
const groupShape = this.editor.getShapeById(focusLayerId)
|
||||
if (!groupShape) {
|
||||
return false
|
||||
}
|
||||
const clickPoint = this.app.getPointInShapeSpace(groupShape, inputs.currentPagePoint)
|
||||
const util = this.app.getShapeUtil(groupShape)
|
||||
const clickPoint = this.editor.getPointInShapeSpace(groupShape, inputs.currentPagePoint)
|
||||
const util = this.editor.getShapeUtil(groupShape)
|
||||
return util.hitTestPoint(groupShape, clickPoint)
|
||||
}
|
||||
|
||||
onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
if (this.app.inputs.isDragging) {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.parent.transition('brushing', info)
|
||||
}
|
||||
}
|
||||
|
@ -49,11 +49,11 @@ export class PointingCanvas extends StateNode {
|
|||
}
|
||||
|
||||
private complete() {
|
||||
const { shiftKey } = this.app.inputs
|
||||
const { shiftKey } = this.editor.inputs
|
||||
if (!shiftKey) {
|
||||
this.app.selectNone()
|
||||
this.editor.selectNone()
|
||||
if (!this._clickWasInsideFocusedGroup()) {
|
||||
this.app.setFocusLayer(null)
|
||||
this.editor.setFocusLayer(null)
|
||||
}
|
||||
}
|
||||
this.parent.transition('idle', {})
|
||||
|
|
|
@ -19,7 +19,7 @@ export class PointingCropHandle extends StateNode {
|
|||
|
||||
private updateCursor(shape: TLShape) {
|
||||
const cursorType = CursorTypeMap[this.info.handle!]
|
||||
this.app.setCursor({
|
||||
this.editor.setCursor({
|
||||
type: cursorType,
|
||||
rotation: shape.rotation,
|
||||
})
|
||||
|
@ -27,15 +27,15 @@ export class PointingCropHandle extends StateNode {
|
|||
|
||||
override onEnter = (info: TLPointingCropHandleInfo) => {
|
||||
this.info = info
|
||||
const selectedShape = this.app.selectedShapes[0]
|
||||
const selectedShape = this.editor.selectedShapes[0]
|
||||
if (!selectedShape) return
|
||||
|
||||
this.updateCursor(selectedShape)
|
||||
this.app.setCroppingId(selectedShape.id)
|
||||
this.editor.setCroppingId(selectedShape.id)
|
||||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
const isDragging = this.app.inputs.isDragging
|
||||
const isDragging = this.editor.inputs.isDragging
|
||||
|
||||
if (isDragging) {
|
||||
this.parent.transition('cropping', {
|
||||
|
@ -47,9 +47,9 @@ export class PointingCropHandle extends StateNode {
|
|||
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.app.setSelectedTool(this.info.onInteractionEnd, this.info)
|
||||
this.editor.setSelectedTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.app.setCroppingId(null)
|
||||
this.editor.setCroppingId(null)
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
@ -68,9 +68,9 @@ export class PointingCropHandle extends StateNode {
|
|||
|
||||
private cancel() {
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.app.setSelectedTool(this.info.onInteractionEnd, this.info)
|
||||
this.editor.setSelectedTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.app.setCroppingId(null)
|
||||
this.editor.setCroppingId(null)
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,15 +13,15 @@ export class PointingHandle extends StateNode {
|
|||
const initialTerminal = (info.shape as TLArrowShape).props[info.handle.id as 'start' | 'end']
|
||||
|
||||
if (initialTerminal?.type === 'binding') {
|
||||
this.app.setHintingIds([initialTerminal.boundShapeId])
|
||||
this.editor.setHintingIds([initialTerminal.boundShapeId])
|
||||
}
|
||||
|
||||
this.app.setCursor({ type: 'grabbing' })
|
||||
this.editor.setCursor({ type: 'grabbing' })
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
this.app.setHintingIds([])
|
||||
this.app.setCursor({ type: 'default' })
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.setCursor({ type: 'default' })
|
||||
}
|
||||
|
||||
onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
||||
|
@ -29,7 +29,7 @@ export class PointingHandle extends StateNode {
|
|||
}
|
||||
|
||||
onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
if (this.app.inputs.isDragging) {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.parent.transition('dragging_handle', this.info)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,9 +29,9 @@ export class PointingResizeHandle extends StateNode {
|
|||
private info = {} as PointingResizeHandleInfo
|
||||
|
||||
private updateCursor() {
|
||||
const selected = this.app.selectedShapes
|
||||
const selected = this.editor.selectedShapes
|
||||
const cursorType = CursorTypeMap[this.info.handle!]
|
||||
this.app.setCursor({
|
||||
this.editor.setCursor({
|
||||
type: cursorType,
|
||||
rotation: selected.length === 1 ? selected[0].rotation : 0,
|
||||
})
|
||||
|
@ -43,7 +43,7 @@ export class PointingResizeHandle extends StateNode {
|
|||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
const isDragging = this.app.inputs.isDragging
|
||||
const isDragging = this.editor.inputs.isDragging
|
||||
|
||||
if (isDragging) {
|
||||
this.parent.transition('resizing', this.info)
|
||||
|
@ -72,7 +72,7 @@ export class PointingResizeHandle extends StateNode {
|
|||
|
||||
private complete() {
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.app.setSelectedTool(this.info.onInteractionEnd, {})
|
||||
this.editor.setSelectedTool(this.info.onInteractionEnd, {})
|
||||
} else {
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ export class PointingResizeHandle extends StateNode {
|
|||
|
||||
private cancel() {
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.app.setSelectedTool(this.info.onInteractionEnd, {})
|
||||
this.editor.setSelectedTool(this.info.onInteractionEnd, {})
|
||||
} else {
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue