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:
Steve Ruiz 2023-06-02 16:21:45 +01:00 committed by GitHub
parent 640bc9de24
commit 735f1c41b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
311 changed files with 8365 additions and 8209 deletions

View file

@ -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)
```
---

View file

@ -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:

View file

@ -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:

View file

@ -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.

View file

@ -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']) {

View file

@ -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',

View file

@ -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)
})
})

View file

@ -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')

View file

@ -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([])
})

View file

@ -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)
}
}}

View file

@ -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' }}>

View file

@ -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
}

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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)
})
}}

View file

@ -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
}

View file

@ -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
}

View file

@ -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) => {

View file

@ -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)
}

View file

@ -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();

View file

@ -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?: {

View file

@ -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'

View file

@ -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>
)

View file

@ -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)
})
})
})

View file

@ -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])
})
})

View file

@ -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]))
})
})

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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')
})

View file

@ -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()
}

View file

@ -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

View file

@ -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 = () => {

View file

@ -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)
})
})

View file

@ -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

View file

@ -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')

View file

@ -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
}
}
}

View file

@ -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 },

View file

@ -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])

View file

@ -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) {

View file

@ -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 }
}

View file

@ -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

View file

@ -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,

View file

@ -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
}
}

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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 (

View file

@ -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],

View file

@ -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
}
}

View file

@ -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 (

View file

@ -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])
}
}

View file

@ -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)
})

View file

@ -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))
}
}

View file

@ -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],

View file

@ -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)
})
}

View file

@ -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,

View file

@ -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} />
)}
</>
)

View file

@ -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 (
<>

View file

@ -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

View file

@ -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

View file

@ -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])
}

View file

@ -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

View file

@ -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])
}

View file

@ -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')
}
}

View file

@ -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', {})
}
}

View file

@ -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')
}
}

View file

@ -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')
}
}

View file

@ -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 } },
])

View file

@ -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')
}
}

View file

@ -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' })
}
}

View file

@ -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)
}
}

View file

@ -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', {})
}
}

View file

@ -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')
}
}

View file

@ -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', {})
}
}

View file

@ -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 })
}
}
}

View file

@ -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,
})

View file

@ -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')
}
}

View file

@ -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)
}
}

View file

@ -10,6 +10,6 @@ export class TLLaserTool extends StateNode {
static children = () => [Idle, Lasering]
onEnter = () => {
this.app.setCursor({ type: 'cross' })
this.editor.setCursor({ type: 'cross' })
}
}

View file

@ -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'] = () => {

View file

@ -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')
}
}

View file

@ -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()
}
}

View file

@ -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')
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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 &&

View file

@ -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])
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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'

View file

@ -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!),

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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', {})

View file

@ -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', {})
}
}

View file

@ -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)
}
}

View file

@ -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