rework canBind callback (#3797)

This PR reworks the `canBind` callback to work with customizable
bindings. It now accepts an object with a the shape, the other shape
(optional - it may not exist yet), the direction, and the type of the
binding. Devs can use this to create shapes that only participate in
certain binding types, can have bindings from but not to them, etc.

If you're implementing a binding, you can see if binding two shapes is
allowed using `editor.canBindShapes(fromShape, toShape, 'my binding
type')`

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features

### Release Notes

#### Breaking changes
The `canBind` flag now accepts an options object instead of just the
shape in question. If you're relying on its arguments, you need to
change from `canBind(shape) {}` to `canBind({shape}) {}`.
This commit is contained in:
alex 2024-05-23 14:32:02 +01:00 committed by GitHub
parent 2b2778b4f9
commit 87e3d60c90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 188 additions and 72 deletions

View file

@ -23,7 +23,6 @@ export class CardShapeUtil extends ShapeUtil<ICardShape> {
// [3]
override isAspectRatioLocked = (_shape: ICardShape) => false
override canResize = (_shape: ICardShape) => true
override canBind = (_shape: ICardShape) => true
// [4]
getDefaultProps(): ICardShape['props'] {

View file

@ -44,7 +44,6 @@ export class MyShapeUtil extends ShapeUtil<ICustomShape> {
}
// [c]
override canBind = () => true
override canEdit = () => false
override canResize = () => true
override isAspectRatioLocked = () => false

View file

@ -18,6 +18,7 @@ import {
TLOnTranslateEndHandler,
TLOnTranslateStartHandler,
TLShapeId,
TLShapeUtilCanBindOpts,
TLUiComponents,
TLUiOverrides,
Tldraw,
@ -44,7 +45,10 @@ class PinShapeUtil extends ShapeUtil<PinShape> {
return {}
}
override canBind = () => false
override canBind({ toShapeType }: TLShapeUtilCanBindOpts<PinShape>) {
// bindings can go _from_ pins to other shapes, but not the other way round
return toShapeType !== 'pin'
}
override canEdit = () => false
override canResize = () => false
override hideRotateHandle = () => true
@ -93,7 +97,9 @@ class PinShapeUtil extends ShapeUtil<PinShape> {
.getShapesAtPoint(pageAnchor, { hitInside: true })
.filter(
(shape) =>
shape.type !== 'pin' && shape.parentId === pin.parentId && shape.index < pin.index
this.editor.canBindShapes({ fromShape: pin, toShape: shape, binding: 'pin' }) &&
shape.parentId === pin.parentId &&
shape.index < pin.index
)
for (const target of targets) {

View file

@ -65,8 +65,6 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
override canResize = (_shape: SpeechBubbleShape) => true
override canBind = (_shape: SpeechBubbleShape) => true
override canEdit = () => true
// [3]

View file

@ -15,6 +15,7 @@ import {
TLEventHandlers,
TLOnTranslateEndHandler,
TLOnTranslateStartHandler,
TLShapeUtilCanBindOpts,
TLUiComponents,
TLUiOverrides,
Tldraw,
@ -40,7 +41,10 @@ class StickerShapeUtil extends ShapeUtil<StickerShape> {
return {}
}
override canBind = () => false
override canBind({ toShapeType }: TLShapeUtilCanBindOpts<StickerShape>) {
// bindings can go _from_ stickers to other shapes, but not the other way round
return toShapeType !== 'sticker'
}
override canEdit = () => false
override canResize = () => false
override hideRotateHandle = () => true
@ -86,7 +90,8 @@ class StickerShapeUtil extends ShapeUtil<StickerShape> {
const pageAnchor = this.editor.getShapePageTransform(sticker).applyToPoint({ x: 0, y: 0 })
const target = this.editor.getShapeAtPoint(pageAnchor, {
hitInside: true,
filter: (shape) => shape.id !== sticker.id,
filter: (shape) =>
this.editor.canBindShapes({ fromShape: sticker, toShape: shape, binding: 'sticker' }),
})
if (!target) return

View file

@ -46,8 +46,9 @@ import { TLAssetId } from '@tldraw/tlschema';
import { TLAssetPartial } from '@tldraw/tlschema';
import { TLBaseShape } from '@tldraw/tlschema';
import { TLBinding } from '@tldraw/tlschema';
import { TLBindingCreate } from '@tldraw/tlschema';
import { TLBindingId } from '@tldraw/tlschema';
import { TLBindingPartial } from '@tldraw/tlschema';
import { TLBindingUpdate } from '@tldraw/tlschema';
import { TLBookmarkAsset } from '@tldraw/tlschema';
import { TLCamera } from '@tldraw/tlschema';
import { TLCursor } from '@tldraw/tlschema';
@ -702,6 +703,18 @@ export class Editor extends EventEmitter<TLEventMap> {
};
bringForward(shapes: TLShape[] | TLShapeId[]): this;
bringToFront(shapes: TLShape[] | TLShapeId[]): this;
// (undocumented)
canBindShapes({ fromShape, toShape, binding, }: {
binding: {
type: TLBinding['type'];
} | TLBinding | TLBinding['type'];
fromShape: {
type: TLShape['type'];
} | TLShape | TLShape['type'];
toShape: {
type: TLShape['type'];
} | TLShape | TLShape['type'];
}): boolean;
cancel(): this;
cancelDoubleClick(): void;
// @internal (undocumented)
@ -715,9 +728,9 @@ export class Editor extends EventEmitter<TLEventMap> {
crash(error: unknown): this;
createAssets(assets: TLAsset[]): this;
// (undocumented)
createBinding(partial: RequiredKeys<TLBindingPartial, 'fromId' | 'toId' | 'type'>): this;
createBinding<B extends TLBinding = TLBinding>(partial: TLBindingCreate<B>): this;
// (undocumented)
createBindings(partials: RequiredKeys<TLBindingPartial, 'fromId' | 'toId' | 'type'>[]): this;
createBindings(partials: TLBindingCreate[]): this;
// @internal (undocumented)
createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): {
extras: {
@ -788,7 +801,9 @@ export class Editor extends EventEmitter<TLEventMap> {
getBindingsInvolvingShape<Binding extends TLUnknownBinding = TLBinding>(shape: TLShape | TLShapeId, type?: Binding['type']): Binding[];
// (undocumented)
getBindingsToShape<Binding extends TLUnknownBinding = TLBinding>(shape: TLShape | TLShapeId, type: Binding['type']): Binding[];
getBindingUtil<S extends TLUnknownBinding>(binding: S | TLBindingPartial<S>): BindingUtil<S>;
getBindingUtil<S extends TLUnknownBinding>(binding: {
type: S['type'];
} | S): BindingUtil<S>;
// (undocumented)
getBindingUtil<S extends TLUnknownBinding>(type: S['type']): BindingUtil<S>;
// (undocumented)
@ -1043,15 +1058,15 @@ export class Editor extends EventEmitter<TLEventMap> {
ungroupShapes(ids: TLShape[]): this;
updateAssets(assets: TLAssetPartial[]): this;
// (undocumented)
updateBinding(partial: TLBindingPartial): this;
updateBinding<B extends TLBinding = TLBinding>(partial: TLBindingUpdate<B>): this;
// (undocumented)
updateBindings(partials: (null | TLBindingPartial | undefined)[]): this;
updateBindings(partials: (null | TLBindingUpdate | undefined)[]): this;
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions): this;
// (undocumented)
_updateCurrentPageState: (partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions) => void;
updateDocumentSettings(settings: Partial<TLDocument>): this;
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: TLHistoryBatchOptions): this;
updatePage(partial: RequiredKeys<TLPage, 'id'>): this;
updatePage(partial: RequiredKeys<Partial<TLPage>, 'id'>): this;
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined): this;
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[]): this;
updateViewportScreenBounds(screenBounds: Box, center?: boolean): this;
@ -1706,7 +1721,7 @@ export function refreshPage(): void;
export function releasePointerCapture(element: Element, event: PointerEvent | React_2.PointerEvent<Element>): void;
// @public (undocumented)
export type RequiredKeys<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>;
export type RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
// @public (undocumented)
export function resizeBox(shape: TLBaseBoxShape, info: {
@ -1785,7 +1800,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
// @internal
backgroundComponent?(shape: Shape): any;
canBeLaidOut: TLShapeUtilFlag<Shape>;
canBind: <K>(_shape: Shape, _otherShape?: K) => boolean;
canBind(opts: TLShapeUtilCanBindOpts<Shape>): boolean;
canCrop: TLShapeUtilFlag<Shape>;
canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
canEdit: TLShapeUtilFlag<Shape>;
@ -2686,6 +2701,13 @@ export interface TLShapeIndicatorProps {
shapeId: TLShapeId;
}
// @public
export interface TLShapeUtilCanBindOpts<Shape extends TLUnknownShape = TLShape> {
bindingType: string;
fromShapeType: string;
toShapeType: string;
}
// @public (undocumented)
export interface TLShapeUtilCanvasSvgDef {
// (undocumented)

View file

@ -168,6 +168,7 @@ export {
type TLOnTranslateStartHandler,
type TLResizeInfo,
type TLResizeMode,
type TLShapeUtilCanBindOpts,
type TLShapeUtilCanvasSvgDef,
type TLShapeUtilConstructor,
type TLShapeUtilFlag,

View file

@ -18,8 +18,9 @@ import {
TLAssetId,
TLAssetPartial,
TLBinding,
TLBindingCreate,
TLBindingId,
TLBindingPartial,
TLBindingUpdate,
TLCamera,
TLCursor,
TLCursorType,
@ -853,7 +854,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getBindingUtil<S extends TLUnknownBinding>(binding: S | TLBindingPartial<S>): BindingUtil<S>
getBindingUtil<S extends TLUnknownBinding>(binding: S | { type: S['type'] }): BindingUtil<S>
getBindingUtil<S extends TLUnknownBinding>(type: S['type']): BindingUtil<S>
getBindingUtil<T extends BindingUtil>(
type: T extends BindingUtil<infer R> ? R['type'] : string
@ -3619,7 +3620,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
updatePage(partial: RequiredKeys<TLPage, 'id'>): this {
updatePage(partial: RequiredKeys<Partial<TLPage>, 'id'>): this {
if (this.getInstanceState().isReadonly) return this
const prev = this.getPage(partial.id)
@ -5146,27 +5147,36 @@ export class Editor extends EventEmitter<TLEventMap> {
return result.filter((b) => b.type === type) as Binding[]
}
createBindings(partials: RequiredKeys<TLBindingPartial, 'type' | 'toId' | 'fromId'>[]) {
const bindings = partials.map((partial) => {
createBindings(partials: TLBindingCreate[]) {
const bindings: TLBinding[] = []
for (const partial of partials) {
const fromShape = this.getShape(partial.fromId)
const toShape = this.getShape(partial.toId)
if (!fromShape || !toShape) continue
if (!this.canBindShapes({ fromShape, toShape, binding: partial })) continue
const util = this.getBindingUtil<TLUnknownBinding>(partial.type)
const defaultProps = util.getDefaultProps()
return this.store.schema.types.binding.create({
const binding = this.store.schema.types.binding.create({
...partial,
id: partial.id ?? createBindingId(),
props: {
...defaultProps,
...partial.props,
},
})
})
}) as TLBinding
bindings.push(binding)
}
this.store.put(bindings)
return this
}
createBinding(partial: RequiredKeys<TLBindingPartial, 'type' | 'fromId' | 'toId'>) {
createBinding<B extends TLBinding = TLBinding>(partial: TLBindingCreate<B>) {
return this.createBindings([partial])
}
updateBindings(partials: (TLBindingPartial | null | undefined)[]) {
updateBindings(partials: (TLBindingUpdate | null | undefined)[]) {
const updated: TLBinding[] = []
for (const partial of partials) {
@ -5178,6 +5188,11 @@ export class Editor extends EventEmitter<TLEventMap> {
const updatedBinding = applyPartialToRecordWithProps(current, partial)
if (updatedBinding === current) continue
const fromShape = this.getShape(updatedBinding.fromId)
const toShape = this.getShape(updatedBinding.toId)
if (!fromShape || !toShape) continue
if (!this.canBindShapes({ fromShape, toShape, binding: updatedBinding })) continue
updated.push(updatedBinding)
}
@ -5186,7 +5201,7 @@ export class Editor extends EventEmitter<TLEventMap> {
return this
}
updateBinding(partial: TLBindingPartial) {
updateBinding<B extends TLBinding = TLBinding>(partial: TLBindingUpdate<B>) {
return this.updateBindings([partial])
}
@ -5198,6 +5213,30 @@ export class Editor extends EventEmitter<TLEventMap> {
deleteBinding(binding: TLBinding | TLBindingId) {
return this.deleteBindings([binding])
}
canBindShapes({
fromShape,
toShape,
binding,
}: {
fromShape: TLShape | { type: TLShape['type'] } | TLShape['type']
toShape: TLShape | { type: TLShape['type'] } | TLShape['type']
binding: TLBinding | { type: TLBinding['type'] } | TLBinding['type']
}): boolean {
const fromShapeType = typeof fromShape === 'string' ? fromShape : fromShape.type
const toShapeType = typeof toShape === 'string' ? toShape : toShape.type
const bindingType = typeof binding === 'string' ? binding : binding.type
const canBindOpts = { fromShapeType, toShapeType, bindingType }
if (fromShapeType === toShapeType) {
return this.getShapeUtil(fromShapeType).canBind(canBindOpts)
}
return (
this.getShapeUtil(fromShapeType).canBind(canBindOpts) &&
this.getShapeUtil(toShapeType).canBind(canBindOpts)
)
}
/* -------------------- Commands -------------------- */

View file

@ -32,6 +32,21 @@ export interface TLShapeUtilConstructor<
/** @public */
export type TLShapeUtilFlag<T> = (shape: T) => boolean
/**
* Options passed to {@link ShapeUtil.canBind}. A binding that could be made. At least one of
* `fromShapeType` or `toShapeType` will belong to this shape util.
*
* @public
*/
export interface TLShapeUtilCanBindOpts<Shape extends TLUnknownShape = TLShape> {
/** The type of shape referenced by the `fromId` of the binding. */
fromShapeType: string
/** The type of shape referenced by the `toId` of the binding. */
toShapeType: string
/** The type of binding. */
bindingType: string
}
/** @public */
export interface TLShapeUtilCanvasSvgDef {
key: string
@ -97,12 +112,13 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
canScroll: TLShapeUtilFlag<Shape> = () => false
/**
* Whether the shape can be bound to by an arrow.
* Whether the shape can be bound to. See {@link TLShapeUtilCanBindOpts} for details.
*
* @param _otherShape - The other shape attempting to bind to this shape.
* @public
*/
canBind = <K>(_shape: Shape, _otherShape?: K) => true
canBind(opts: TLShapeUtilCanBindOpts<Shape>): boolean {
return true
}
/**
* Whether the shape can be double clicked to edit.

View file

@ -3,7 +3,7 @@ import { Box } from '../../primitives/Box'
import { VecLike } from '../../primitives/Vec'
/** @public */
export type RequiredKeys<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>
export type RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>
/** @public */
export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

View file

@ -115,6 +115,7 @@ import { TLSelectionHandle } from '@tldraw/editor';
import { TLShape } from '@tldraw/editor';
import { TLShapeId } from '@tldraw/editor';
import { TLShapePartial } from '@tldraw/editor';
import { TLShapeUtilCanBindOpts } from '@tldraw/editor';
import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
import { TLShapeUtilFlag } from '@tldraw/editor';
import { TLStore } from '@tldraw/editor';
@ -169,7 +170,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// (undocumented)
canBeLaidOut: TLShapeUtilFlag<TLArrowShape>;
// (undocumented)
canBind: () => boolean;
canBind({ toShapeType }: TLShapeUtilCanBindOpts<TLArrowShape>): boolean;
// (undocumented)
canEdit: () => boolean;
// (undocumented)
@ -618,8 +619,6 @@ export class FrameShapeTool extends BaseBoxShapeTool {
// @public (undocumented)
export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
// (undocumented)
canBind: () => boolean;
// (undocumented)
canDropShapes: (shape: TLFrameShape, _shapes: TLShape[]) => boolean;
// (undocumented)

View file

@ -18,6 +18,7 @@ import {
TLOnTranslateHandler,
TLOnTranslateStartHandler,
TLShapePartial,
TLShapeUtilCanBindOpts,
TLShapeUtilCanvasSvgDef,
TLShapeUtilFlag,
Vec,
@ -75,7 +76,10 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
static override migrations = arrowShapeMigrations
override canEdit = () => true
override canBind = () => false
override canBind({ toShapeType }: TLShapeUtilCanBindOpts<TLArrowShape>): boolean {
// bindings can go from arrows to shapes, but not from shapes to arrows
return toShapeType !== 'arrow'
}
override canSnap = () => false
override hideResizeHandles: TLShapeUtilFlag<TLArrowShape> = () => true
override hideRotateHandle: TLShapeUtilFlag<TLArrowShape> = () => true
@ -153,7 +157,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
index: 'a0',
x: info.start.handle.x,
y: info.start.handle.y,
canBind: true,
},
{
id: ARROW_HANDLES.MIDDLE,
@ -161,7 +164,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
index: 'a2',
x: info.middle.x,
y: info.middle.y,
canBind: false,
},
{
id: ARROW_HANDLES.END,
@ -169,7 +171,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
index: 'a3',
x: info.end.handle.x,
y: info.end.handle.y,
canBind: true,
},
].filter(Boolean) as TLHandle[]
}
@ -223,7 +224,10 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
hitFrameInside: true,
margin: 0,
filter: (targetShape) => {
return !targetShape.isLocked && this.editor.getShapeUtil(targetShape).canBind(targetShape)
return (
!targetShape.isLocked &&
this.editor.canBindShapes({ fromShape: shape, toShape: targetShape, binding: 'arrow' })
)
},
})
@ -384,7 +388,10 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
hitFrameInside: true,
margin: 0,
filter: (targetShape) => {
return !targetShape.isLocked && this.editor.getShapeUtil(targetShape).canBind(targetShape)
return (
!targetShape.isLocked &&
this.editor.canBindShapes({ fromShape: shape, toShape: targetShape, binding: 'arrow' })
)
},
})

View file

@ -12,7 +12,10 @@ export class Pointing extends StateNode {
const target = this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, {
filter: (targetShape) => {
return !targetShape.isLocked && this.editor.getShapeUtil(targetShape).canBind(targetShape)
return (
!targetShape.isLocked &&
this.editor.canBindShapes({ fromShape: 'arrow', toShape: targetShape, binding: 'arrow' })
)
},
margin: 0,
hitInside: true,
@ -47,7 +50,7 @@ export class Pointing extends StateNode {
this.editor.setCurrentTool('select.dragging_handle', {
shape: this.shape,
handle: { id: 'end', type: 'vertex', index: 'a3', x: 0, y: 0, canBind: true },
handle: { id: 'end', type: 'vertex', index: 'a3', x: 0, y: 0 },
isCreating: true,
onInteractionEnd: 'arrow',
})

View file

@ -35,8 +35,6 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
static override props = frameShapeProps
static override migrations = frameShapeMigrations
override canBind = () => true
override canEdit = () => true
override getDefaultProps(): TLFrameShape['props'] {

View file

@ -283,7 +283,10 @@ export class DraggingHandle extends StateNode {
const next: TLShapePartial<any> = { id: shape.id, type: shape.type, ...changes }
// Arrows
if (initialHandle.canBind && this.editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
if (
initialHandle.type === 'vertex' &&
this.editor.isShapeOfType<TLArrowShape>(shape, 'arrow')
) {
const bindingAfter = getArrowBindings(editor, shape)[initialHandle.id as 'start' | 'end']
if (bindingAfter) {

View file

@ -253,10 +253,10 @@ export class TestEditor extends Editor {
}
expectShapeToMatch = <T extends TLShape = TLShape>(
...model: RequiredKeys<TLShapePartial<T>, 'id'>[]
...model: RequiredKeys<Partial<TLShapePartial<T>>, 'id'>[]
) => {
model.forEach((model) => {
const shape = this.getShape(model.id)!
const shape = this.getShape(model.id!)!
const next = { ...shape, ...model }
expect(shape).toCloselyMatchObject(next)
})

View file

@ -223,7 +223,6 @@ describe('Custom shapes', () => {
override isAspectRatioLocked = (_shape: CardShape) => false
override canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true
override getDefaultProps(): CardShape['props'] {
return {

View file

@ -94,19 +94,16 @@ describe('Making an arrow on the page', () => {
x: 0,
y: 0,
type: 'vertex',
canBind: true,
},
{
x: 50,
y: 0,
type: 'virtual',
canBind: false,
},
{
x: 100,
y: 0,
type: 'vertex',
canBind: true,
},
])
})
@ -488,7 +485,8 @@ describe('When starting an arrow inside of multiple shapes', () => {
expect(
editor.getShapeAtPoint(new Vec(25, 25), {
filter: (shape) => editor.getShapeUtil(shape).canBind(shape),
filter: (shape) =>
editor.canBindShapes({ fromShape: 'arrow', toShape: shape, binding: 'arrow' }),
hitInside: true,
hitFrameInside: true,
margin: 0,

View file

@ -2,7 +2,7 @@ import {
createBindingId,
createShapeId,
TLArrowShape,
TLBindingPartial,
TLBindingCreate,
TLShapePartial,
} from '@tldraw/editor'
import { getArrowBindings } from '../lib/shapes/arrow/shared'
@ -92,7 +92,7 @@ it('creates new bindings for arrows when pasting', async () => {
// blood moat incoming
describe('When duplicating shapes that include arrows', () => {
let shapes: TLShapePartial[]
let bindings: TLBindingPartial[]
let bindings: TLBindingCreate[]
beforeEach(() => {
const box1 = createShapeId()

View file

@ -3,7 +3,7 @@ import {
PI,
TLArrowShape,
TLArrowShapeProps,
TLBindingPartial,
TLBindingCreate,
TLShapeId,
TLShapePartial,
createBindingId,
@ -467,7 +467,7 @@ describe('flipping rotated shapes', () => {
describe('When flipping shapes that include arrows', () => {
let shapes: TLShapePartial[]
let bindings: TLBindingPartial[]
let bindings: TLBindingCreate[]
beforeEach(() => {
const box1 = createShapeId()

View file

@ -945,16 +945,30 @@ export interface TLBaseShape<Type extends string, Props extends object> extends
// @public
export type TLBinding = TLDefaultBinding | TLUnknownBinding;
// @public (undocumented)
export type TLBindingCreate<T extends TLBinding = TLBinding> = Expand<{
fromId: T['fromId'];
id?: TLBindingId;
meta?: Partial<T['meta']>;
props?: Partial<T['props']>;
toId: T['toId'];
type: T['type'];
typeName?: T['typeName'];
}>;
// @public (undocumented)
export type TLBindingId = RecordId<TLUnknownBinding>;
// @public (undocumented)
export type TLBindingPartial<T extends TLBinding = TLBinding> = T extends T ? {
export type TLBindingUpdate<T extends TLBinding = TLBinding> = Expand<{
fromId?: T['fromId'];
id: TLBindingId;
meta?: Partial<T['meta']>;
props?: Partial<T['props']>;
toId?: T['toId'];
type: T['type'];
} & Partial<Omit<T, 'id' | 'meta' | 'props' | 'type'>> : never;
typeName?: T['typeName'];
}>;
// @public
export type TLBookmarkAsset = TLBaseAsset<'bookmark', {
@ -1092,8 +1106,6 @@ export type TLGroupShape = TLBaseShape<'group', TLGroupShapeProps>;
// @public
export interface TLHandle {
// (undocumented)
canBind?: boolean;
// (undocumented)
canSnap?: boolean;
id: string;

View file

@ -60,8 +60,9 @@ export {
isBindingId,
rootBindingMigrations,
type TLBinding,
type TLBindingCreate,
type TLBindingId,
type TLBindingPartial,
type TLBindingUpdate,
type TLDefaultBinding,
type TLUnknownBinding,
} from './records/TLBinding'

View file

@ -22,7 +22,6 @@ export interface TLHandle {
/** A unique identifier for the handle. */
id: string
type: TLHandleType
canBind?: boolean
canSnap?: boolean
index: IndexKey
x: number

View file

@ -5,7 +5,7 @@ import {
createRecordMigrationSequence,
createRecordType,
} from '@tldraw/store'
import { mapObjectMapValues } from '@tldraw/utils'
import { Expand, mapObjectMapValues } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { nanoid } from 'nanoid'
import { TLArrowBinding } from '../bindings/TLArrowBinding'
@ -34,14 +34,26 @@ export type TLUnknownBinding = TLBaseBinding<string, object>
export type TLBinding = TLDefaultBinding | TLUnknownBinding
/** @public */
export type TLBindingPartial<T extends TLBinding = TLBinding> = T extends T
? {
id: TLBindingId
type: T['type']
props?: Partial<T['props']>
meta?: Partial<T['meta']>
} & Partial<Omit<T, 'type' | 'id' | 'props' | 'meta'>>
: never
export type TLBindingUpdate<T extends TLBinding = TLBinding> = Expand<{
id: TLBindingId
type: T['type']
typeName?: T['typeName']
fromId?: T['fromId']
toId?: T['toId']
props?: Partial<T['props']>
meta?: Partial<T['meta']>
}>
/** @public */
export type TLBindingCreate<T extends TLBinding = TLBinding> = Expand<{
id?: TLBindingId
type: T['type']
typeName?: T['typeName']
fromId: T['fromId']
toId: T['toId']
props?: Partial<T['props']>
meta?: Partial<T['meta']>
}>
/** @public */
export type TLBindingId = RecordId<TLUnknownBinding>