snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
1
packages/driver/LICENSE.md
Normal file
1
packages/driver/LICENSE.md
Normal file
@@ -0,0 +1 @@
|
||||
This code is licensed under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md)
|
||||
125
packages/driver/README.md
Normal file
125
packages/driver/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# @tldraw/driver
|
||||
|
||||
Imperative API for driving the tldraw editor programmatically. Useful for scripting, automation, REPL usage, and testing.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @tldraw/driver
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { Driver } from '@tldraw/driver'
|
||||
|
||||
const driver = new Driver(editor)
|
||||
|
||||
// Simulate user interactions
|
||||
driver.click(100, 200).pointerDown(300, 400).pointerMove(500, 600).pointerUp()
|
||||
|
||||
// Keyboard input
|
||||
driver.keyPress('a')
|
||||
driver.keyDown('Shift')
|
||||
driver.keyUp('Shift')
|
||||
|
||||
// Clipboard
|
||||
driver.copy()
|
||||
driver.paste({ x: 100, y: 100 })
|
||||
|
||||
// Selection manipulation
|
||||
driver.translateSelection(50, 0)
|
||||
driver.rotateSelection(Math.PI / 4)
|
||||
driver.resizeSelection({ scaleX: 2 }, 'bottom_right')
|
||||
|
||||
// Camera
|
||||
driver.pan({ x: 100, y: 0 })
|
||||
driver.wheel(0, -100)
|
||||
|
||||
// Cleanup
|
||||
driver.dispose()
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Constructor
|
||||
|
||||
#### `new Driver(editor: Editor)`
|
||||
|
||||
Wraps an existing `Editor` instance. All methods use only public Editor APIs.
|
||||
|
||||
### Input events
|
||||
|
||||
All input methods return `this` for fluent chaining. Coordinates are in screen space.
|
||||
|
||||
| Method | Description |
|
||||
| --------------------------------------------- | -------------------------------------- |
|
||||
| `pointerDown(x?, y?, options?, modifiers?)` | Dispatch a pointer down event |
|
||||
| `pointerMove(x?, y?, options?, modifiers?)` | Dispatch a pointer move event |
|
||||
| `pointerUp(x?, y?, options?, modifiers?)` | Dispatch a pointer up event |
|
||||
| `click(x?, y?, options?, modifiers?)` | Pointer down + up |
|
||||
| `rightClick(x?, y?, options?, modifiers?)` | Right-click (button 2) down + up |
|
||||
| `doubleClick(x?, y?, options?, modifiers?)` | Double-click sequence |
|
||||
| `keyDown(key, options?)` | Dispatch a key down event |
|
||||
| `keyUp(key, options?)` | Dispatch a key up event |
|
||||
| `keyPress(key, options?)` | Key down + up |
|
||||
| `keyRepeat(key, options?)` | Dispatch a key repeat event |
|
||||
| `wheel(dx, dy, options?)` | Dispatch a wheel/scroll event |
|
||||
| `pinchStart(x?, y?, z, dx, dy, dz, options?)` | Begin a pinch gesture |
|
||||
| `pinchTo(x?, y?, z, dx, dy, dz, options?)` | Continue a pinch gesture |
|
||||
| `pinchEnd(x?, y?, z, dx, dy, dz, options?)` | End a pinch gesture |
|
||||
| `forceTick(count?)` | Emit tick events to advance the editor |
|
||||
|
||||
### Clipboard
|
||||
|
||||
| Method | Description |
|
||||
| --------------- | --------------------------------------------------- |
|
||||
| `copy(ids?)` | Copy shapes to the driver's clipboard |
|
||||
| `cut(ids?)` | Cut shapes (copy + delete) |
|
||||
| `paste(point?)` | Paste from the driver's clipboard |
|
||||
| `clipboard` | The current clipboard content (`TLContent \| null`) |
|
||||
|
||||
### Selection manipulation
|
||||
|
||||
These methods work in page coordinates and handle the screen-space conversion internally.
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------------------------- | ------------------------------------------- |
|
||||
| `translateSelection(dx, dy, options?)` | Move the selection by a page-space delta |
|
||||
| `rotateSelection(angle, options?)` | Rotate the selection by an angle in radians |
|
||||
| `resizeSelection(scale?, handle, options?)` | Resize the selection via a handle |
|
||||
|
||||
### Queries
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------------ | ------------------------------------------- |
|
||||
| `getViewportPageCenter()` | Center of the viewport in page coordinates |
|
||||
| `getSelectionPageCenter()` | Center of the selection in page coordinates |
|
||||
| `getPageCenter(shape)` | Center of a shape in page coordinates |
|
||||
| `getPageRotation(shape)` | Rotation of a shape in page space (radians) |
|
||||
| `getPageRotationById(id)` | Rotation of a shape by ID (radians) |
|
||||
| `getArrowsBoundTo(shapeId)` | All arrows bound to a shape |
|
||||
| `getLastCreatedShape()` | The most recently created shape |
|
||||
| `getLastCreatedShapes(count?)` | The last N created shapes |
|
||||
|
||||
### Camera
|
||||
|
||||
| Method | Description |
|
||||
| ------------- | ------------------------------------- |
|
||||
| `pan(offset)` | Pan the camera by a page-space offset |
|
||||
|
||||
### Lifecycle
|
||||
|
||||
| Method | Description |
|
||||
| ----------- | -------------------------------------------- |
|
||||
| `dispose()` | Remove side-effect handlers. Call when done. |
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation for the most recent release can be found on [tldraw.dev/docs](https://tldraw.dev/docs), including [reference docs](https://tldraw.dev/reference/editor/Editor). Our release notes can be found [here](https://tldraw.dev/releases).
|
||||
|
||||
For more agent-friendly docs, see our [LLMs.txt](https://tldraw.dev/llms.txt).
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE.md](../../LICENSE.md).
|
||||
4
packages/driver/api-extractor.json
Normal file
4
packages/driver/api-extractor.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
||||
"extends": "../../internal/config/api-extractor.json"
|
||||
}
|
||||
78
packages/driver/api-report.api.md
Normal file
78
packages/driver/api-report.api.md
Normal file
@@ -0,0 +1,78 @@
|
||||
## API Report File for "@tldraw/driver"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
|
||||
import { Editor } from '@tldraw/editor';
|
||||
import { RotateCorner } from '@tldraw/editor';
|
||||
import { SelectionHandle } from '@tldraw/editor';
|
||||
import { TLArrowShape } from '@tldraw/editor';
|
||||
import { TLContent } from '@tldraw/editor';
|
||||
import { TLKeyboardEventInfo } from '@tldraw/editor';
|
||||
import { TLPageId } from '@tldraw/tlschema';
|
||||
import { TLPinchEventInfo } from '@tldraw/editor';
|
||||
import { TLPointerEventInfo } from '@tldraw/editor';
|
||||
import { TLShape } from '@tldraw/editor';
|
||||
import { TLShapeId } from '@tldraw/editor';
|
||||
import { TLWheelEventInfo } from '@tldraw/editor';
|
||||
import { Vec } from '@tldraw/editor';
|
||||
import { VecLike } from '@tldraw/editor';
|
||||
|
||||
// @public
|
||||
export class Driver {
|
||||
constructor(editor: Editor);
|
||||
click(x?: number, y?: number, options?: PointerEventInit_2, modifiers?: EventModifiers): this;
|
||||
clipboard: null | TLContent;
|
||||
copy(ids?: TLShapeId[]): this;
|
||||
createPageID(id: string): TLPageId;
|
||||
createShapeID(id: string): TLShapeId;
|
||||
cut(ids?: TLShapeId[]): this;
|
||||
dispose(): void;
|
||||
doubleClick(x?: number, y?: number, options?: PointerEventInit_2, modifiers?: EventModifiers): this;
|
||||
// (undocumented)
|
||||
readonly editor: Editor;
|
||||
forceTick(count?: number): this;
|
||||
getArrowsBoundTo(shapeId: TLShapeId): TLArrowShape[];
|
||||
getLastCreatedShape<T extends TLShape>(): T;
|
||||
getLastCreatedShapes(count?: number): TLShape[];
|
||||
getPageCenter(shape: TLShape): null | Vec;
|
||||
getPageRotation(shape: TLShape): number;
|
||||
getPageRotationById(id: TLShapeId): number;
|
||||
getSelectionPageCenter(): null | Vec;
|
||||
getViewportPageCenter(): Vec;
|
||||
keyDown(key: string, options?: Partial<Omit<TLKeyboardEventInfo, 'key'>>): this;
|
||||
keyPress(key: string, options?: Partial<Omit<TLKeyboardEventInfo, 'key'>>): this;
|
||||
keyRepeat(key: string, options?: Partial<Omit<TLKeyboardEventInfo, 'key'>>): this;
|
||||
keyUp(key: string, options?: Partial<Omit<TLKeyboardEventInfo, 'key'>>): this;
|
||||
pan(offset: VecLike): this;
|
||||
paste(point?: VecLike): this;
|
||||
pinchEnd(x: number | undefined, y: number | undefined, z: number, dx: number, dy: number, dz: number, options?: Partial<Omit<TLPinchEventInfo, 'delta' | 'offset' | 'point'>>): this;
|
||||
pinchStart(x: number | undefined, y: number | undefined, z: number, dx: number, dy: number, dz: number, options?: Partial<Omit<TLPinchEventInfo, 'delta' | 'offset' | 'point'>>): this;
|
||||
pinchTo(x: number | undefined, y: number | undefined, z: number, dx: number, dy: number, dz: number, options?: Partial<Omit<TLPinchEventInfo, 'delta' | 'offset' | 'point'>>): this;
|
||||
pointerDown(x?: number, y?: number, options?: PointerEventInit_2, modifiers?: EventModifiers): this;
|
||||
pointerMove(x?: number, y?: number, options?: PointerEventInit_2, modifiers?: EventModifiers): this;
|
||||
pointerUp(x?: number, y?: number, options?: PointerEventInit_2, modifiers?: EventModifiers): this;
|
||||
resizeSelection(scale: {
|
||||
scaleX?: number;
|
||||
scaleY?: number;
|
||||
} | undefined, handle: SelectionHandle, options?: Partial<TLPointerEventInfo>): this;
|
||||
rightClick(x?: number, y?: number, options?: PointerEventInit_2, modifiers?: EventModifiers): this;
|
||||
rotateSelection(angleRadians: number, options?: {
|
||||
handle?: RotateCorner;
|
||||
shiftKey?: boolean;
|
||||
}): this;
|
||||
translateSelection(dx: number, dy: number, options?: Partial<TLPointerEventInfo>): this;
|
||||
wheel(dx: number, dy: number, options?: Partial<Omit<TLWheelEventInfo, 'delta'>>): this;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type EventModifiers = Partial<Pick<TLPointerEventInfo, 'altKey' | 'ctrlKey' | 'shiftKey'>>;
|
||||
|
||||
// @public
|
||||
type PointerEventInit_2 = Partial<TLPointerEventInfo> | TLShapeId;
|
||||
export { PointerEventInit_2 as PointerEventInit }
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
```
|
||||
58
packages/driver/package.json
Normal file
58
packages/driver/package.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@tldraw/driver",
|
||||
"description": "Imperative API for driving the tldraw editor programmatically.",
|
||||
"version": "5.4.2",
|
||||
"author": {
|
||||
"name": "tldraw Inc.",
|
||||
"email": "hello@tldraw.com"
|
||||
},
|
||||
"homepage": "https://tldraw.dev",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"tldraw_product": {
|
||||
"stableId": "tldraw:driver",
|
||||
"name": "Driver",
|
||||
"type": "feature",
|
||||
"parent": "tldraw:sdk-core",
|
||||
"premium": false
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tldraw/tldraw"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tldraw/tldraw/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"tldraw",
|
||||
"sdk",
|
||||
"driver",
|
||||
"automation",
|
||||
"scripting"
|
||||
],
|
||||
"/* NOTE */": "These `main` and `types` fields are rewritten by the build script. They are not the actual values we publish",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./.tsbuild/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test-ci": "yarn run -T vitest run --passWithNoTests",
|
||||
"test": "yarn run -T vitest --passWithNoTests",
|
||||
"test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
|
||||
"build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
|
||||
"build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
|
||||
"prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
|
||||
"postpack": "../../internal/scripts/postpack.sh",
|
||||
"pack-tarball": "yarn pack",
|
||||
"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tldraw/editor": "workspace:*",
|
||||
"@tldraw/utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"vitest": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
}
|
||||
10
packages/driver/src/index.ts
Normal file
10
packages/driver/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { registerTldrawLibraryVersion } from '@tldraw/utils'
|
||||
|
||||
export { Driver } from './lib/Driver'
|
||||
export type { EventModifiers, PointerEventInit } from './lib/Driver'
|
||||
|
||||
registerTldrawLibraryVersion(
|
||||
(globalThis as any).TLDRAW_LIBRARY_NAME,
|
||||
(globalThis as any).TLDRAW_LIBRARY_VERSION,
|
||||
(globalThis as any).TLDRAW_LIBRARY_MODULES
|
||||
)
|
||||
730
packages/driver/src/lib/Driver.ts
Normal file
730
packages/driver/src/lib/Driver.ts
Normal file
@@ -0,0 +1,730 @@
|
||||
import {
|
||||
Editor,
|
||||
Mat,
|
||||
PageRecordType,
|
||||
ROTATE_CORNER_TO_SELECTION_CORNER,
|
||||
RotateCorner,
|
||||
SelectionHandle,
|
||||
TLArrowShape,
|
||||
TLContent,
|
||||
TLKeyboardEventInfo,
|
||||
TLPinchEventInfo,
|
||||
TLPointerEventInfo,
|
||||
TLShape,
|
||||
TLShapeId,
|
||||
TLWheelEventInfo,
|
||||
Vec,
|
||||
VecLike,
|
||||
compact,
|
||||
createShapeId,
|
||||
isAccelKey,
|
||||
rotateSelectionHandle,
|
||||
tlenv,
|
||||
} from '@tldraw/editor'
|
||||
|
||||
/** Options for pointer events. Either partial pointer event info or a shape ID to target. @public */
|
||||
export type PointerEventInit = Partial<TLPointerEventInfo> | TLShapeId
|
||||
|
||||
/** Modifier keys for events. @public */
|
||||
export type EventModifiers = Partial<Pick<TLPointerEventInfo, 'shiftKey' | 'ctrlKey' | 'altKey'>>
|
||||
|
||||
const KEY_CODES: Record<string, string> = {
|
||||
Shift: 'ShiftLeft',
|
||||
Alt: 'AltLeft',
|
||||
Control: 'CtrlLeft',
|
||||
Meta: 'MetaLeft',
|
||||
' ': 'Space',
|
||||
Enter: 'Enter',
|
||||
ArrowRight: 'ArrowRight',
|
||||
ArrowLeft: 'ArrowLeft',
|
||||
ArrowUp: 'ArrowUp',
|
||||
ArrowDown: 'ArrowDown',
|
||||
}
|
||||
|
||||
/**
|
||||
* Driver wraps an Editor instance and provides an imperative API for driving it
|
||||
* programmatically. Useful for scripting, automation, REPL usage, and testing.
|
||||
*
|
||||
* All methods use only public Editor APIs and return `this` for fluent chaining.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class Driver {
|
||||
/** The underlying Editor instance. */
|
||||
private _cleanup: (() => void) | null
|
||||
private _lastCreatedShapes: TLShape[] = []
|
||||
|
||||
constructor(public readonly editor: Editor) {
|
||||
this._cleanup = this.editor.sideEffects.registerAfterCreateHandler('shape', (record) => {
|
||||
this._lastCreatedShapes.push(record)
|
||||
if (this._lastCreatedShapes.length > 1000) {
|
||||
this._lastCreatedShapes = this._lastCreatedShapes.slice(-500)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Remove all registered side-effect handlers. Call when this controller is no longer needed. */
|
||||
dispose() {
|
||||
this._cleanup?.()
|
||||
this._cleanup = null
|
||||
}
|
||||
|
||||
/** Local clipboard content. Used by copy, cut, and paste. */
|
||||
clipboard: TLContent | null = null
|
||||
|
||||
/**
|
||||
* Get the last created shapes.
|
||||
* @param count - The number of shapes to get.
|
||||
*/
|
||||
getLastCreatedShapes(count = 1) {
|
||||
return this._lastCreatedShapes.slice(-count).map((s) => this.editor.getShape(s)!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last created shape.
|
||||
*/
|
||||
getLastCreatedShape<T extends TLShape>() {
|
||||
const lastShape = this._lastCreatedShapes[this._lastCreatedShapes.length - 1] as T
|
||||
return this.editor.getShape<T>(lastShape)!
|
||||
}
|
||||
|
||||
/* ---------------------- IDs ---------------------- */
|
||||
|
||||
/**
|
||||
* Creates a shape ID from a string.
|
||||
* @param id - The string to convert to a shape ID.
|
||||
*/
|
||||
createShapeID(id: string) {
|
||||
return createShapeId(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a page ID from a string.
|
||||
* @param id - The string to convert to a page ID.
|
||||
*/
|
||||
createPageID(id: string) {
|
||||
return PageRecordType.createId(id)
|
||||
}
|
||||
|
||||
/* ------------------- Clipboard ------------------- */
|
||||
|
||||
/**
|
||||
* Copies the given shapes to the controller clipboard. Defaults to the current selection.
|
||||
* @param ids - Shape IDs to copy. Defaults to the current selection.
|
||||
*/
|
||||
copy(ids = this.editor.getSelectedShapeIds()) {
|
||||
if (ids.length > 0) {
|
||||
const content = this.editor.getContentFromCurrentPage(ids)
|
||||
if (content) {
|
||||
this.clipboard = content
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuts the given shapes (copy to clipboard, then delete). Defaults to the current selection.
|
||||
* @param ids - Shape IDs to cut. Defaults to the current selection.
|
||||
*/
|
||||
cut(ids = this.editor.getSelectedShapeIds()) {
|
||||
if (ids.length > 0) {
|
||||
this.copy(ids)
|
||||
this.editor.deleteShapes(ids)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes content from the controller clipboard. If shift is held, uses current pointer. Otherwise uses the given point.
|
||||
* @param point - Page coordinates for paste location. If omitted and shift is not held, placement may vary.
|
||||
*/
|
||||
paste(point?: VecLike) {
|
||||
if (this.clipboard !== null) {
|
||||
const p = this.editor.inputs.getShiftKey() ? this.editor.inputs.getCurrentPagePoint() : point
|
||||
|
||||
this.editor.markHistoryStoppingPoint('pasting')
|
||||
this.editor.putContentOntoCurrentPage(this.clipboard, {
|
||||
point: p,
|
||||
select: true,
|
||||
})
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/* ------------------- Queries ------------------- */
|
||||
|
||||
/** Returns the center of the viewport in page coordinates. */
|
||||
getViewportPageCenter() {
|
||||
return this.editor.getViewportPageBounds().center
|
||||
}
|
||||
|
||||
/** Returns the center of the current selection in page coordinates, or null if nothing is selected. */
|
||||
getSelectionPageCenter() {
|
||||
const selectionRotation = this.editor.getSelectionRotation()
|
||||
const selectionBounds = this.editor.getSelectionRotatedPageBounds()
|
||||
if (!selectionBounds) return null
|
||||
return Vec.RotWith(selectionBounds.center, selectionBounds.point, selectionRotation)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the center of a shape in page coordinates, or null if the shape has no page transform.
|
||||
* @param shape - The shape to get the center of.
|
||||
*/
|
||||
getPageCenter(shape: TLShape) {
|
||||
const pageTransform = this.editor.getShapePageTransform(shape.id)
|
||||
if (!pageTransform) return null
|
||||
const center = this.editor.getShapeGeometry(shape).bounds.center
|
||||
return Mat.applyToPoint(pageTransform, center)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rotation of a shape in page space by ID, in radians.
|
||||
* @param id - The shape ID.
|
||||
*/
|
||||
getPageRotationById(id: TLShapeId): number {
|
||||
const pageTransform = this.editor.getShapePageTransform(id)
|
||||
return pageTransform ? pageTransform.rotation() : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rotation of a shape in page space, in radians.
|
||||
* @param shape - The shape to get the rotation of.
|
||||
*/
|
||||
getPageRotation(shape: TLShape) {
|
||||
return this.getPageRotationById(shape.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all arrow shapes bound to the given shape.
|
||||
* @param shapeId - The shape ID to find arrows bound to.
|
||||
*/
|
||||
getArrowsBoundTo(shapeId: TLShapeId) {
|
||||
const ids = new Set(this.editor.getBindingsToShape(shapeId, 'arrow').map((b) => b.fromId))
|
||||
return compact(Array.from(ids, (id) => this.editor.getShape<TLArrowShape>(id)))
|
||||
}
|
||||
|
||||
/* --------------- Event building --------------- */
|
||||
|
||||
private getModifierKeys() {
|
||||
const { inputs } = this.editor
|
||||
return {
|
||||
shiftKey: inputs.getShiftKey(),
|
||||
ctrlKey: inputs.getCtrlKey(),
|
||||
altKey: inputs.getAltKey(),
|
||||
metaKey: inputs.getMetaKey(),
|
||||
accelKey: isAccelKey(inputs),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a TLPointerEventInfo object for input simulation.
|
||||
* @param x - Screen x coordinate. Defaults to current pointer.
|
||||
* @param y - Screen y coordinate. Defaults to current pointer.
|
||||
* @param options - Target shape/selection or partial event info.
|
||||
* @param modifiers - Override shift, ctrl, or alt key state.
|
||||
*/
|
||||
private getPointerEventInfo(
|
||||
x = this.editor.inputs.getCurrentScreenPoint().x,
|
||||
y = this.editor.inputs.getCurrentScreenPoint().y,
|
||||
options?: PointerEventInit,
|
||||
modifiers?: EventModifiers
|
||||
): TLPointerEventInfo {
|
||||
if (typeof options === 'string') {
|
||||
options = { target: 'shape', shape: this.editor.getShape(options) }
|
||||
} else if (options === undefined) {
|
||||
options = { target: 'canvas' }
|
||||
}
|
||||
const info = {
|
||||
name: 'pointer_down',
|
||||
type: 'pointer',
|
||||
pointerId: 1,
|
||||
...this.getModifierKeys(),
|
||||
accelKey: isAccelKey({ ...this.editor.inputs.toJson(), ...modifiers }),
|
||||
point: { x, y, z: null },
|
||||
button: 0,
|
||||
isPen: false,
|
||||
...options,
|
||||
...modifiers,
|
||||
} as TLPointerEventInfo
|
||||
|
||||
// In simulated input, a pen is assumed to be a direct-display pen (which auto-enables pen
|
||||
// mode) unless a test explicitly opts out with `isPenDirect: false`.
|
||||
if (info.isPenDirect === undefined) info.isPenDirect = info.isPen
|
||||
|
||||
if (tlenv.isDarwin && info.button === 0 && info.ctrlKey && !info.metaKey) {
|
||||
info.button = 2
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a TLKeyboardEventInfo object for input simulation.
|
||||
* @param key - The key being pressed.
|
||||
* @param name - The event name (key_down, key_up, key_repeat).
|
||||
* @param options - Partial event info overrides.
|
||||
*/
|
||||
private getKeyboardEventInfo(
|
||||
key: string,
|
||||
name: TLKeyboardEventInfo['name'],
|
||||
options = {} as Partial<Omit<TLKeyboardEventInfo, 'point'>>
|
||||
): TLKeyboardEventInfo {
|
||||
// Like a real DOM keyboard event, the flags reflect every modifier currently held — not
|
||||
// just the key being pressed. We OR in the editor's current modifier state so that, e.g.,
|
||||
// pressing Shift while Control is held still reports ctrlKey: true. (keyUp passes explicit
|
||||
// flag overrides for its release semantics, so those win over these defaults.)
|
||||
const flags = {
|
||||
shiftKey: key === 'Shift' || this.editor.inputs.getShiftKey(),
|
||||
ctrlKey: key === 'Control' || key === 'Meta' || this.editor.inputs.getCtrlKey(),
|
||||
altKey: key === 'Alt' || this.editor.inputs.getAltKey(),
|
||||
metaKey: key === 'Meta' || this.editor.inputs.getMetaKey(),
|
||||
...options,
|
||||
}
|
||||
return {
|
||||
...flags,
|
||||
accelKey: flags.accelKey ?? isAccelKey(flags),
|
||||
name,
|
||||
code: options.code ?? KEY_CODES[key] ?? 'Key' + key[0].toUpperCase() + key.slice(1),
|
||||
type: 'keyboard',
|
||||
key,
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------- Input events --------------- */
|
||||
|
||||
/**
|
||||
* Emits tick events to advance the editor by the given number of frames (default 1).
|
||||
* @param count - Number of tick events to emit. Defaults to 1.
|
||||
*/
|
||||
forceTick(count = 1) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.editor.emit('tick', 16)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a pointer move event at the given screen coordinates.
|
||||
* @param x - Screen x coordinate. Defaults to current pointer.
|
||||
* @param y - Screen y coordinate. Defaults to current pointer.
|
||||
* @param options - Target shape/selection or partial event info.
|
||||
* @param modifiers - Override shift, ctrl, or alt key state.
|
||||
*/
|
||||
pointerMove(
|
||||
x = this.editor.inputs.getCurrentScreenPoint().x,
|
||||
y = this.editor.inputs.getCurrentScreenPoint().y,
|
||||
options?: PointerEventInit,
|
||||
modifiers?: EventModifiers
|
||||
) {
|
||||
this.editor.dispatch({
|
||||
...this.getPointerEventInfo(x, y, options, modifiers),
|
||||
name: 'pointer_move',
|
||||
})
|
||||
this.forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a pointer down event at the given screen coordinates.
|
||||
* @param x - Screen x coordinate. Defaults to current pointer.
|
||||
* @param y - Screen y coordinate. Defaults to current pointer.
|
||||
* @param options - Target shape/selection or partial event info.
|
||||
* @param modifiers - Override shift, ctrl, or alt key state.
|
||||
*/
|
||||
pointerDown(
|
||||
x = this.editor.inputs.getCurrentScreenPoint().x,
|
||||
y = this.editor.inputs.getCurrentScreenPoint().y,
|
||||
options?: PointerEventInit,
|
||||
modifiers?: EventModifiers
|
||||
) {
|
||||
this.editor.dispatch({
|
||||
...this.getPointerEventInfo(x, y, options, modifiers),
|
||||
name: 'pointer_down',
|
||||
})
|
||||
this.forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a pointer up event at the given screen coordinates.
|
||||
* @param x - Screen x coordinate. Defaults to current pointer.
|
||||
* @param y - Screen y coordinate. Defaults to current pointer.
|
||||
* @param options - Target shape/selection or partial event info.
|
||||
* @param modifiers - Override shift, ctrl, or alt key state.
|
||||
*/
|
||||
pointerUp(
|
||||
x = this.editor.inputs.getCurrentScreenPoint().x,
|
||||
y = this.editor.inputs.getCurrentScreenPoint().y,
|
||||
options?: PointerEventInit,
|
||||
modifiers?: EventModifiers
|
||||
) {
|
||||
this.editor.dispatch({
|
||||
...this.getPointerEventInfo(x, y, options, modifiers),
|
||||
name: 'pointer_up',
|
||||
})
|
||||
this.forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a pointer down followed by pointer up at the given screen coordinates.
|
||||
* @param x - Screen x coordinate. Defaults to current pointer.
|
||||
* @param y - Screen y coordinate. Defaults to current pointer.
|
||||
* @param options - Target shape/selection or partial event info.
|
||||
* @param modifiers - Override shift, ctrl, or alt key state.
|
||||
*/
|
||||
click(
|
||||
x = this.editor.inputs.getCurrentScreenPoint().x,
|
||||
y = this.editor.inputs.getCurrentScreenPoint().y,
|
||||
options?: PointerEventInit,
|
||||
modifiers?: EventModifiers
|
||||
) {
|
||||
this.pointerDown(x, y, options, modifiers)
|
||||
this.pointerUp(x, y, options, modifiers)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a right-click (button 2) down and up at the given screen coordinates.
|
||||
* @param x - Screen x coordinate. Defaults to current pointer.
|
||||
* @param y - Screen y coordinate. Defaults to current pointer.
|
||||
* @param options - Target shape/selection or partial event info.
|
||||
* @param modifiers - Override shift, ctrl, or alt key state.
|
||||
*/
|
||||
rightClick(
|
||||
x = this.editor.inputs.getCurrentScreenPoint().x,
|
||||
y = this.editor.inputs.getCurrentScreenPoint().y,
|
||||
options?: PointerEventInit,
|
||||
modifiers?: EventModifiers
|
||||
) {
|
||||
this.editor.dispatch({
|
||||
...this.getPointerEventInfo(x, y, options, modifiers),
|
||||
name: 'pointer_down',
|
||||
button: 2,
|
||||
})
|
||||
this.forceTick()
|
||||
this.editor.dispatch({
|
||||
...this.getPointerEventInfo(x, y, options, modifiers),
|
||||
name: 'pointer_up',
|
||||
button: 2,
|
||||
})
|
||||
this.forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a double-click sequence at the given screen coordinates.
|
||||
* @param x - Screen x coordinate. Defaults to current pointer.
|
||||
* @param y - Screen y coordinate. Defaults to current pointer.
|
||||
* @param options - Target shape/selection or partial event info.
|
||||
* @param modifiers - Override shift, ctrl, or alt key state.
|
||||
*/
|
||||
doubleClick(
|
||||
x = this.editor.inputs.getCurrentScreenPoint().x,
|
||||
y = this.editor.inputs.getCurrentScreenPoint().y,
|
||||
options?: PointerEventInit,
|
||||
modifiers?: EventModifiers
|
||||
) {
|
||||
this.click(x, y, options, modifiers)
|
||||
this.editor.dispatch({
|
||||
...this.getPointerEventInfo(x, y, options, modifiers),
|
||||
type: 'click',
|
||||
name: 'double_click',
|
||||
phase: 'down',
|
||||
})
|
||||
this.editor.dispatch({
|
||||
...this.getPointerEventInfo(x, y, options, modifiers),
|
||||
type: 'click',
|
||||
name: 'double_click',
|
||||
phase: 'up',
|
||||
})
|
||||
this.forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a key down followed by key up for the given key.
|
||||
* @param key - The key to press (e.g. 'a', 'Enter', 'Shift').
|
||||
* @param options - Partial keyboard event overrides.
|
||||
*/
|
||||
keyPress(key: string, options = {} as Partial<Omit<TLKeyboardEventInfo, 'key'>>) {
|
||||
this.keyDown(key, options)
|
||||
this.keyUp(key, options)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a key down event for the given key.
|
||||
* @param key - The key to press (e.g. 'a', 'Enter', 'Shift').
|
||||
* @param options - Partial keyboard event overrides.
|
||||
*/
|
||||
keyDown(key: string, options = {} as Partial<Omit<TLKeyboardEventInfo, 'key'>>) {
|
||||
this.editor.dispatch(this.getKeyboardEventInfo(key, 'key_down', options))
|
||||
this.forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a key repeat event for the given key.
|
||||
* @param key - The key that is repeating (e.g. 'a', 'ArrowDown').
|
||||
* @param options - Partial keyboard event overrides.
|
||||
*/
|
||||
keyRepeat(key: string, options = {} as Partial<Omit<TLKeyboardEventInfo, 'key'>>) {
|
||||
this.editor.dispatch(this.getKeyboardEventInfo(key, 'key_repeat', options))
|
||||
this.forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a key up event for the given key.
|
||||
* @param key - The key to release (e.g. 'a', 'Enter', 'Shift').
|
||||
* @param options - Partial keyboard event overrides.
|
||||
*/
|
||||
keyUp(key: string, options = {} as Partial<Omit<TLKeyboardEventInfo, 'key'>>) {
|
||||
this.editor.dispatch(
|
||||
this.getKeyboardEventInfo(key, 'key_up', {
|
||||
shiftKey: this.editor.inputs.getShiftKey() && key !== 'Shift',
|
||||
ctrlKey: this.editor.inputs.getCtrlKey() && !(key === 'Control' || key === 'Meta'),
|
||||
altKey: this.editor.inputs.getAltKey() && key !== 'Alt',
|
||||
metaKey: this.editor.inputs.getMetaKey() && key !== 'Meta',
|
||||
...options,
|
||||
})
|
||||
)
|
||||
this.forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a wheel event with the given delta values.
|
||||
* @param dx - Horizontal scroll delta.
|
||||
* @param dy - Vertical scroll delta.
|
||||
* @param options - Partial wheel event overrides.
|
||||
*/
|
||||
wheel(dx: number, dy: number, options = {} as Partial<Omit<TLWheelEventInfo, 'delta'>>) {
|
||||
const currentScreenPoint = this.editor.inputs.getCurrentScreenPoint()
|
||||
this.editor.dispatch({
|
||||
type: 'wheel',
|
||||
name: 'wheel',
|
||||
point: new Vec(currentScreenPoint.x, currentScreenPoint.y),
|
||||
...this.getModifierKeys(),
|
||||
...options,
|
||||
delta: { x: dx, y: dy },
|
||||
})
|
||||
this.forceTick(2)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Pans the camera by the given offset (in page coordinates). Does nothing if camera is locked.
|
||||
* @param offset - The pan delta (x, y) in page coordinates.
|
||||
*/
|
||||
pan(offset: VecLike) {
|
||||
const { isLocked, panSpeed } = this.editor.getCameraOptions()
|
||||
if (isLocked) return this
|
||||
const { x: cx, y: cy, z: cz } = this.editor.getCamera()
|
||||
this.editor.setCamera(
|
||||
new Vec(cx + (offset.x * panSpeed) / cz, cy + (offset.y * panSpeed) / cz, cz),
|
||||
{ immediate: true }
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
private pinch(
|
||||
name: TLPinchEventInfo['name'],
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dz: number,
|
||||
options: Partial<Omit<TLPinchEventInfo, 'point' | 'delta' | 'offset'>>
|
||||
) {
|
||||
this.editor.dispatch({
|
||||
type: 'pinch',
|
||||
name,
|
||||
...this.getModifierKeys(),
|
||||
...options,
|
||||
point: { x, y, z },
|
||||
delta: { x: dx, y: dy, z: dz },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a pinch start event.
|
||||
* @param x - Screen x coordinate. Defaults to current pointer.
|
||||
* @param y - Screen y coordinate. Defaults to current pointer.
|
||||
* @param z - Pinch scale/factor.
|
||||
* @param dx - Delta x for pinch.
|
||||
* @param dy - Delta y for pinch.
|
||||
* @param dz - Delta z for pinch.
|
||||
* @param options - Partial pinch event overrides.
|
||||
*/
|
||||
pinchStart(
|
||||
x = this.editor.inputs.getCurrentScreenPoint().x,
|
||||
y = this.editor.inputs.getCurrentScreenPoint().y,
|
||||
z: number,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dz: number,
|
||||
options = {} as Partial<Omit<TLPinchEventInfo, 'point' | 'delta' | 'offset'>>
|
||||
) {
|
||||
this.pinch('pinch_start', x, y, z, dx, dy, dz, options)
|
||||
this.forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a pinch move event (pinch_to).
|
||||
* @param x - Screen x coordinate. Defaults to current pointer.
|
||||
* @param y - Screen y coordinate. Defaults to current pointer.
|
||||
* @param z - Pinch scale/factor.
|
||||
* @param dx - Delta x for pinch.
|
||||
* @param dy - Delta y for pinch.
|
||||
* @param dz - Delta z for pinch.
|
||||
* @param options - Partial pinch event overrides.
|
||||
*/
|
||||
pinchTo(
|
||||
x = this.editor.inputs.getCurrentScreenPoint().x,
|
||||
y = this.editor.inputs.getCurrentScreenPoint().y,
|
||||
z: number,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dz: number,
|
||||
options = {} as Partial<Omit<TLPinchEventInfo, 'point' | 'delta' | 'offset'>>
|
||||
) {
|
||||
this.pinch('pinch', x, y, z, dx, dy, dz, options)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a pinch end event.
|
||||
* @param x - Screen x coordinate. Defaults to current pointer.
|
||||
* @param y - Screen y coordinate. Defaults to current pointer.
|
||||
* @param z - Pinch scale/factor.
|
||||
* @param dx - Delta x for pinch.
|
||||
* @param dy - Delta y for pinch.
|
||||
* @param dz - Delta z for pinch.
|
||||
* @param options - Partial pinch event overrides.
|
||||
*/
|
||||
pinchEnd(
|
||||
x = this.editor.inputs.getCurrentScreenPoint().x,
|
||||
y = this.editor.inputs.getCurrentScreenPoint().y,
|
||||
z: number,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dz: number,
|
||||
options = {} as Partial<Omit<TLPinchEventInfo, 'point' | 'delta' | 'offset'>>
|
||||
) {
|
||||
this.pinch('pinch_end', x, y, z, dx, dy, dz, options)
|
||||
this.forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
/* --------------- Interaction helpers --------------- */
|
||||
|
||||
/**
|
||||
* Simulates rotating the current selection by the given angle in radians via the rotation handle.
|
||||
* @param angleRadians - Rotation angle in radians.
|
||||
* @param options - Optional handle and shiftKey. handle defaults to 'top_left_rotate'.
|
||||
*/
|
||||
rotateSelection(
|
||||
angleRadians: number,
|
||||
options: { handle?: RotateCorner; shiftKey?: boolean } = {}
|
||||
) {
|
||||
const { handle = 'top_left_rotate', shiftKey = false } = options
|
||||
if (this.editor.getSelectedShapeIds().length === 0) {
|
||||
throw new Error('No selection')
|
||||
}
|
||||
|
||||
this.editor.setCurrentTool('select')
|
||||
|
||||
const bounds = this.editor.getSelectionRotatedPageBounds()!
|
||||
const handlePoint = bounds
|
||||
.getHandlePoint(ROTATE_CORNER_TO_SELECTION_CORNER[handle])
|
||||
.clone()
|
||||
.rotWith(bounds.point, this.editor.getSelectionRotation())
|
||||
|
||||
const targetHandlePoint = Vec.RotWith(handlePoint, this.getSelectionPageCenter()!, angleRadians)
|
||||
|
||||
const screenHandle = this.editor.pageToScreen(handlePoint)
|
||||
const screenTarget = this.editor.pageToScreen(targetHandlePoint)
|
||||
|
||||
this.pointerDown(screenHandle.x, screenHandle.y, { target: 'selection', handle })
|
||||
this.pointerMove(screenTarget.x, screenTarget.y, { shiftKey })
|
||||
this.pointerUp()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates translating the current selection by the given delta in page coordinates.
|
||||
* @param dx - Horizontal delta in page coordinates.
|
||||
* @param dy - Vertical delta in page coordinates.
|
||||
* @param options - Partial pointer event overrides (e.g. altKey for center-based scaling).
|
||||
*/
|
||||
translateSelection(dx: number, dy: number, options?: Partial<TLPointerEventInfo>) {
|
||||
if (this.editor.getSelectedShapeIds().length === 0) {
|
||||
throw new Error('No selection')
|
||||
}
|
||||
this.editor.setCurrentTool('select')
|
||||
|
||||
const center = this.getSelectionPageCenter()!
|
||||
const screenCenter = this.editor.pageToScreen(center)
|
||||
|
||||
this.pointerDown(screenCenter.x, screenCenter.y, this.editor.getSelectedShapeIds()[0])
|
||||
const numSteps = 10
|
||||
for (let i = 1; i < numSteps; i++) {
|
||||
const p = this.editor.pageToScreen({
|
||||
x: center.x + (i * dx) / numSteps,
|
||||
y: center.y + (i * dy) / numSteps,
|
||||
})
|
||||
this.pointerMove(p.x, p.y, options)
|
||||
}
|
||||
const endScreen = this.editor.pageToScreen({ x: center.x + dx, y: center.y + dy })
|
||||
this.pointerUp(endScreen.x, endScreen.y, options)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates resizing the current selection via the given handle, with optional scale factors.
|
||||
* @param scale - Scale factors for x and y. Defaults to `\{ scaleX: 1, scaleY: 1 \}`.
|
||||
* @param handle - The selection handle to drag (e.g. 'top', 'bottom_right').
|
||||
* @param options - Partial pointer event overrides (e.g. altKey to scale from center).
|
||||
*/
|
||||
resizeSelection(
|
||||
scale: { scaleX?: number; scaleY?: number } = {},
|
||||
handle: SelectionHandle,
|
||||
options?: Partial<TLPointerEventInfo>
|
||||
) {
|
||||
const { scaleX = 1, scaleY = 1 } = scale
|
||||
if (this.editor.getSelectedShapeIds().length === 0) {
|
||||
throw new Error('No selection')
|
||||
}
|
||||
this.editor.setCurrentTool('select')
|
||||
const bounds = this.editor.getSelectionRotatedPageBounds()!
|
||||
const rotation = this.editor.getSelectionRotation()
|
||||
const preRotationHandlePoint = bounds.getHandlePoint(handle)
|
||||
|
||||
const preRotationScaleOriginPoint = options?.altKey
|
||||
? bounds.center
|
||||
: bounds.getHandlePoint(rotateSelectionHandle(handle, Math.PI))
|
||||
|
||||
const preRotationTargetHandlePoint = Vec.Add(
|
||||
Vec.Sub(preRotationHandlePoint, preRotationScaleOriginPoint).mulV({
|
||||
x: scaleX,
|
||||
y: scaleY,
|
||||
}),
|
||||
preRotationScaleOriginPoint
|
||||
)
|
||||
|
||||
const handlePoint = Vec.RotWith(preRotationHandlePoint, bounds.point, rotation)
|
||||
const targetHandlePoint = Vec.RotWith(preRotationTargetHandlePoint, bounds.point, rotation)
|
||||
|
||||
const screenHandle = this.editor.pageToScreen(handlePoint)
|
||||
const screenTarget = this.editor.pageToScreen(targetHandlePoint)
|
||||
|
||||
this.pointerDown(screenHandle.x, screenHandle.y, { target: 'selection', handle }, options)
|
||||
this.pointerMove(screenTarget.x, screenTarget.y, options)
|
||||
this.pointerUp(screenTarget.x, screenTarget.y, options)
|
||||
return this
|
||||
}
|
||||
}
|
||||
10
packages/driver/tsconfig.json
Normal file
10
packages/driver/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../internal/config/tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", ".tsbuild*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./.tsbuild",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"references": [{ "path": "../editor" }, { "path": "../utils" }]
|
||||
}
|
||||
3
packages/driver/vitest.config.ts
Normal file
3
packages/driver/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import baseConfig from '../../internal/config/vitest/node-preset'
|
||||
|
||||
export default baseConfig
|
||||
Reference in New Issue
Block a user