Bindings (#3326)
First draft of the new bindings API. We'll follow this up with some API refinements, tests, documentation, and examples. Bindings are a new record type for establishing relationships between two shapes so they can update at the same time. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `feature` — New feature ### Release Notes #### Breaking changes - The `start` and `end` properties on `TLArrowShape` no longer have `type: point | binding`. Instead, they're always a point, which may be out of date if a binding exists. To check for & retrieve arrow bindings, use `getArrowBindings(editor, shape)` instead. - `getArrowTerminalsInArrowSpace` must be passed a `TLArrowBindings` as a third argument: `getArrowTerminalsInArrowSpace(editor, shape, getArrowBindings(editor, shape))` - The following types have been renamed: - `ShapeProps` -> `RecordProps` - `ShapePropsType` -> `RecordPropsType` - `TLShapePropsMigrations` -> `TLPropsMigrations` - `SchemaShapeInfo` -> `SchemaPropsInfo` --------- Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
This commit is contained in:
parent
0a7816e34d
commit
da35f2bd75
95 changed files with 4087 additions and 2446 deletions
|
@ -69,8 +69,8 @@ test.describe('Export snapshots', () => {
|
||||||
fill: fill,
|
fill: fill,
|
||||||
arrowheadStart: 'square',
|
arrowheadStart: 'square',
|
||||||
arrowheadEnd: 'dot',
|
arrowheadEnd: 'dot',
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
start: { x: 0, y: 0 },
|
||||||
end: { type: 'point', x: 100, y: 100 },
|
end: { x: 100, y: 100 },
|
||||||
bend: 20,
|
bend: 20,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -149,8 +149,8 @@ test.describe('Export snapshots', () => {
|
||||||
arrowheadStart: 'square',
|
arrowheadStart: 'square',
|
||||||
arrowheadEnd: 'arrow',
|
arrowheadEnd: 'arrow',
|
||||||
font,
|
font,
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
start: { x: 0, y: 0 },
|
||||||
end: { type: 'point', x: 100, y: 100 },
|
end: { x: 100, y: 100 },
|
||||||
bend: 20,
|
bend: 20,
|
||||||
text: 'test',
|
text: 'test',
|
||||||
},
|
},
|
||||||
|
@ -167,8 +167,8 @@ test.describe('Export snapshots', () => {
|
||||||
arrowheadStart: 'square',
|
arrowheadStart: 'square',
|
||||||
arrowheadEnd: 'arrow',
|
arrowheadEnd: 'arrow',
|
||||||
font,
|
font,
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
start: { x: 0, y: 0 },
|
||||||
end: { type: 'point', x: 100, y: 100 },
|
end: { x: 100, y: 100 },
|
||||||
bend: 20,
|
bend: 20,
|
||||||
text: 'test',
|
text: 'test',
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,8 +2,8 @@ import {
|
||||||
BaseBoxShapeUtil,
|
BaseBoxShapeUtil,
|
||||||
BoundsSnapGeometry,
|
BoundsSnapGeometry,
|
||||||
HTMLContainer,
|
HTMLContainer,
|
||||||
|
RecordProps,
|
||||||
Rectangle2d,
|
Rectangle2d,
|
||||||
ShapeProps,
|
|
||||||
T,
|
T,
|
||||||
TLBaseShape,
|
TLBaseShape,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
|
@ -23,7 +23,7 @@ type IPlayingCard = TLBaseShape<
|
||||||
export class PlayingCardUtil extends BaseBoxShapeUtil<IPlayingCard> {
|
export class PlayingCardUtil extends BaseBoxShapeUtil<IPlayingCard> {
|
||||||
// [2]
|
// [2]
|
||||||
static override type = 'PlayingCard' as const
|
static override type = 'PlayingCard' as const
|
||||||
static override props: ShapeProps<IPlayingCard> = {
|
static override props: RecordProps<IPlayingCard> = {
|
||||||
w: T.number,
|
w: T.number,
|
||||||
h: T.number,
|
h: T.number,
|
||||||
suit: T.string,
|
suit: T.string,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { DefaultColorStyle, ShapeProps, T } from 'tldraw'
|
import { DefaultColorStyle, RecordProps, T } from 'tldraw'
|
||||||
import { ICardShape } from './card-shape-types'
|
import { ICardShape } from './card-shape-types'
|
||||||
|
|
||||||
// Validation for our custom card shape's props, using one of tldraw's default styles
|
// Validation for our custom card shape's props, using one of tldraw's default styles
|
||||||
export const cardShapeProps: ShapeProps<ICardShape> = {
|
export const cardShapeProps: RecordProps<ICardShape> = {
|
||||||
w: T.number,
|
w: T.number,
|
||||||
h: T.number,
|
h: T.number,
|
||||||
color: DefaultColorStyle,
|
color: DefaultColorStyle,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {
|
import {
|
||||||
Geometry2d,
|
Geometry2d,
|
||||||
HTMLContainer,
|
HTMLContainer,
|
||||||
|
RecordProps,
|
||||||
Rectangle2d,
|
Rectangle2d,
|
||||||
ShapeProps,
|
|
||||||
ShapeUtil,
|
ShapeUtil,
|
||||||
T,
|
T,
|
||||||
TLBaseShape,
|
TLBaseShape,
|
||||||
|
@ -28,7 +28,7 @@ type ICustomShape = TLBaseShape<
|
||||||
export class MyShapeUtil extends ShapeUtil<ICustomShape> {
|
export class MyShapeUtil extends ShapeUtil<ICustomShape> {
|
||||||
// [a]
|
// [a]
|
||||||
static override type = 'my-custom-shape' as const
|
static override type = 'my-custom-shape' as const
|
||||||
static override props: ShapeProps<ICustomShape> = {
|
static override props: RecordProps<ICustomShape> = {
|
||||||
w: T.number,
|
w: T.number,
|
||||||
h: T.number,
|
h: T.number,
|
||||||
text: T.string,
|
text: T.string,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
BaseBoxShapeUtil,
|
BaseBoxShapeUtil,
|
||||||
HTMLContainer,
|
HTMLContainer,
|
||||||
ShapeProps,
|
RecordProps,
|
||||||
T,
|
T,
|
||||||
TLBaseShape,
|
TLBaseShape,
|
||||||
TLOnEditEndHandler,
|
TLOnEditEndHandler,
|
||||||
|
@ -23,7 +23,7 @@ type IMyEditableShape = TLBaseShape<
|
||||||
|
|
||||||
export class EditableShapeUtil extends BaseBoxShapeUtil<IMyEditableShape> {
|
export class EditableShapeUtil extends BaseBoxShapeUtil<IMyEditableShape> {
|
||||||
static override type = 'my-editable-shape' as const
|
static override type = 'my-editable-shape' as const
|
||||||
static override props: ShapeProps<IMyEditableShape> = {
|
static override props: RecordProps<IMyEditableShape> = {
|
||||||
w: T.number,
|
w: T.number,
|
||||||
h: T.number,
|
h: T.number,
|
||||||
animal: T.number,
|
animal: T.number,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
TldrawSelectionBackground,
|
TldrawSelectionBackground,
|
||||||
TldrawSelectionForeground,
|
TldrawSelectionForeground,
|
||||||
TldrawUi,
|
TldrawUi,
|
||||||
|
defaultBindingUtils,
|
||||||
defaultEditorAssetUrls,
|
defaultEditorAssetUrls,
|
||||||
defaultShapeTools,
|
defaultShapeTools,
|
||||||
defaultShapeUtils,
|
defaultShapeUtils,
|
||||||
|
@ -45,6 +46,7 @@ export default function ExplodedExample() {
|
||||||
<TldrawEditor
|
<TldrawEditor
|
||||||
initialState="select"
|
initialState="select"
|
||||||
shapeUtils={defaultShapeUtils}
|
shapeUtils={defaultShapeUtils}
|
||||||
|
bindingUtils={defaultBindingUtils}
|
||||||
tools={[...defaultTools, ...defaultShapeTools]}
|
tools={[...defaultTools, ...defaultShapeTools]}
|
||||||
components={defaultComponents}
|
components={defaultComponents}
|
||||||
persistenceKey="exploded-example"
|
persistenceKey="exploded-example"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { BaseBoxShapeUtil, HTMLContainer, ShapeProps, T, TLBaseShape } from 'tldraw'
|
import { BaseBoxShapeUtil, HTMLContainer, RecordProps, T, TLBaseShape } from 'tldraw'
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ type IMyInteractiveShape = TLBaseShape<
|
||||||
|
|
||||||
export class myInteractiveShape extends BaseBoxShapeUtil<IMyInteractiveShape> {
|
export class myInteractiveShape extends BaseBoxShapeUtil<IMyInteractiveShape> {
|
||||||
static override type = 'my-interactive-shape' as const
|
static override type = 'my-interactive-shape' as const
|
||||||
static override props: ShapeProps<IMyInteractiveShape> = {
|
static override props: RecordProps<IMyInteractiveShape> = {
|
||||||
w: T.number,
|
w: T.number,
|
||||||
h: T.number,
|
h: T.number,
|
||||||
checked: T.boolean,
|
checked: T.boolean,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
BaseBoxShapeUtil,
|
BaseBoxShapeUtil,
|
||||||
HTMLContainer,
|
HTMLContainer,
|
||||||
ShapeProps,
|
RecordProps,
|
||||||
T,
|
T,
|
||||||
TLBaseShape,
|
TLBaseShape,
|
||||||
stopEventPropagation,
|
stopEventPropagation,
|
||||||
|
@ -20,7 +20,7 @@ type IMyPopupShape = TLBaseShape<
|
||||||
|
|
||||||
export class PopupShapeUtil extends BaseBoxShapeUtil<IMyPopupShape> {
|
export class PopupShapeUtil extends BaseBoxShapeUtil<IMyPopupShape> {
|
||||||
static override type = 'my-popup-shape' as const
|
static override type = 'my-popup-shape' as const
|
||||||
static override props: ShapeProps<IMyPopupShape> = {
|
static override props: RecordProps<IMyPopupShape> = {
|
||||||
w: T.number,
|
w: T.number,
|
||||||
h: T.number,
|
h: T.number,
|
||||||
animal: T.number,
|
animal: T.number,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import {
|
import {
|
||||||
Geometry2d,
|
Geometry2d,
|
||||||
|
RecordProps,
|
||||||
Rectangle2d,
|
Rectangle2d,
|
||||||
SVGContainer,
|
SVGContainer,
|
||||||
ShapeProps,
|
|
||||||
ShapeUtil,
|
ShapeUtil,
|
||||||
T,
|
T,
|
||||||
TLBaseShape,
|
TLBaseShape,
|
||||||
|
@ -24,7 +24,7 @@ export type SlideShape = TLBaseShape<
|
||||||
|
|
||||||
export class SlideShapeUtil extends ShapeUtil<SlideShape> {
|
export class SlideShapeUtil extends ShapeUtil<SlideShape> {
|
||||||
static override type = 'slide' as const
|
static override type = 'slide' as const
|
||||||
static override props: ShapeProps<SlideShape> = {
|
static override props: RecordProps<SlideShape> = {
|
||||||
w: T.number,
|
w: T.number,
|
||||||
h: T.number,
|
h: T.number,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
Geometry2d,
|
Geometry2d,
|
||||||
LABEL_FONT_SIZES,
|
LABEL_FONT_SIZES,
|
||||||
Polygon2d,
|
Polygon2d,
|
||||||
ShapePropsType,
|
RecordPropsType,
|
||||||
ShapeUtil,
|
ShapeUtil,
|
||||||
T,
|
T,
|
||||||
TEXT_PROPS,
|
TEXT_PROPS,
|
||||||
|
@ -52,7 +52,7 @@ export const speechBubbleShapeProps = {
|
||||||
tail: vecModelValidator,
|
tail: vecModelValidator,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SpeechBubbleShapeProps = ShapePropsType<typeof speechBubbleShapeProps>
|
export type SpeechBubbleShapeProps = RecordPropsType<typeof speechBubbleShapeProps>
|
||||||
export type SpeechBubbleShape = TLBaseShape<'speech-bubble', SpeechBubbleShapeProps>
|
export type SpeechBubbleShape = TLBaseShape<'speech-bubble', SpeechBubbleShapeProps>
|
||||||
|
|
||||||
export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
||||||
|
|
9
apps/examples/src/examples/sticker-bindings/README.md
Normal file
9
apps/examples/src/examples/sticker-bindings/README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
title: Sticker (bindings)
|
||||||
|
component: ./StickerExample.tsx
|
||||||
|
category: shapes/tools
|
||||||
|
---
|
||||||
|
|
||||||
|
A sticker shape, using bindings to attach shapes to one and other
|
||||||
|
|
||||||
|
---
|
235
apps/examples/src/examples/sticker-bindings/StickerExample.tsx
Normal file
235
apps/examples/src/examples/sticker-bindings/StickerExample.tsx
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
import {
|
||||||
|
BindingOnShapeChangeOptions,
|
||||||
|
BindingOnShapeDeleteOptions,
|
||||||
|
BindingUtil,
|
||||||
|
Box,
|
||||||
|
DefaultToolbar,
|
||||||
|
DefaultToolbarContent,
|
||||||
|
RecordProps,
|
||||||
|
Rectangle2d,
|
||||||
|
ShapeUtil,
|
||||||
|
StateNode,
|
||||||
|
TLBaseBinding,
|
||||||
|
TLBaseShape,
|
||||||
|
TLEventHandlers,
|
||||||
|
TLOnTranslateEndHandler,
|
||||||
|
TLOnTranslateStartHandler,
|
||||||
|
TLUiComponents,
|
||||||
|
TLUiOverrides,
|
||||||
|
Tldraw,
|
||||||
|
TldrawUiMenuItem,
|
||||||
|
VecModel,
|
||||||
|
createShapeId,
|
||||||
|
invLerp,
|
||||||
|
lerp,
|
||||||
|
useIsToolSelected,
|
||||||
|
useTools,
|
||||||
|
} from 'tldraw'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
type StickerShape = TLBaseShape<'sticker', {}>
|
||||||
|
|
||||||
|
const offsetX = -16
|
||||||
|
const offsetY = -26
|
||||||
|
class StickerShapeUtil extends ShapeUtil<StickerShape> {
|
||||||
|
static override type = 'sticker' as const
|
||||||
|
static override props: RecordProps<StickerShape> = {}
|
||||||
|
|
||||||
|
override getDefaultProps() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
override canBind = () => false
|
||||||
|
override canEdit = () => false
|
||||||
|
override canResize = () => false
|
||||||
|
override hideRotateHandle = () => true
|
||||||
|
override isAspectRatioLocked = () => true
|
||||||
|
|
||||||
|
override getGeometry() {
|
||||||
|
return new Rectangle2d({
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
x: offsetX,
|
||||||
|
y: offsetY,
|
||||||
|
isFilled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override component() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
marginLeft: offsetX,
|
||||||
|
marginTop: offsetY,
|
||||||
|
fontSize: '26px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
❤️
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override indicator() {
|
||||||
|
return <rect width={32} height={32} x={offsetX} y={offsetY} />
|
||||||
|
}
|
||||||
|
|
||||||
|
override onTranslateStart: TLOnTranslateStartHandler<StickerShape> = (shape) => {
|
||||||
|
const bindings = this.editor.getBindingsFromShape(shape, 'sticker')
|
||||||
|
this.editor.deleteBindings(bindings)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onTranslateEnd: TLOnTranslateEndHandler<StickerShape> = (initial, sticker) => {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
const targetBounds = Box.ZeroFix(this.editor.getShapeGeometry(target)!.bounds)
|
||||||
|
const pointInTargetSpace = this.editor.getPointInShapeSpace(target, pageAnchor)
|
||||||
|
|
||||||
|
const anchor = {
|
||||||
|
x: invLerp(targetBounds.minX, targetBounds.maxX, pointInTargetSpace.x),
|
||||||
|
y: invLerp(targetBounds.minY, targetBounds.maxY, pointInTargetSpace.y),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.createBinding({
|
||||||
|
type: 'sticker',
|
||||||
|
fromId: sticker.id,
|
||||||
|
toId: target.id,
|
||||||
|
props: {
|
||||||
|
anchor,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StickerBinding = TLBaseBinding<
|
||||||
|
'sticker',
|
||||||
|
{
|
||||||
|
anchor: VecModel
|
||||||
|
}
|
||||||
|
>
|
||||||
|
class StickerBindingUtil extends BindingUtil<StickerBinding> {
|
||||||
|
static override type = 'sticker' as const
|
||||||
|
|
||||||
|
override getDefaultProps() {
|
||||||
|
return {
|
||||||
|
anchor: { x: 0.5, y: 0.5 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the shape we're stuck to changes, update the sticker's position
|
||||||
|
override onAfterChangeToShape({
|
||||||
|
binding,
|
||||||
|
shapeAfter,
|
||||||
|
}: BindingOnShapeChangeOptions<StickerBinding>): void {
|
||||||
|
const sticker = this.editor.getShape<StickerShape>(binding.fromId)!
|
||||||
|
|
||||||
|
const shapeBounds = this.editor.getShapeGeometry(shapeAfter)!.bounds
|
||||||
|
const shapeAnchor = {
|
||||||
|
x: lerp(shapeBounds.minX, shapeBounds.maxX, binding.props.anchor.x),
|
||||||
|
y: lerp(shapeBounds.minY, shapeBounds.maxY, binding.props.anchor.y),
|
||||||
|
}
|
||||||
|
const pageAnchor = this.editor.getShapePageTransform(shapeAfter).applyToPoint(shapeAnchor)
|
||||||
|
|
||||||
|
const stickerParentAnchor = this.editor
|
||||||
|
.getShapeParentTransform(sticker)
|
||||||
|
.invert()
|
||||||
|
.applyToPoint(pageAnchor)
|
||||||
|
|
||||||
|
this.editor.updateShape({
|
||||||
|
id: sticker.id,
|
||||||
|
type: 'sticker',
|
||||||
|
x: stickerParentAnchor.x,
|
||||||
|
y: stickerParentAnchor.y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the thing we're stuck to is deleted, delete the sticker too
|
||||||
|
override onBeforeDeleteToShape({ binding }: BindingOnShapeDeleteOptions<StickerBinding>): void {
|
||||||
|
const sticker = this.editor.getShape<StickerShape>(binding.fromId)
|
||||||
|
if (sticker) this.editor.deleteShape(sticker.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StickerTool extends StateNode {
|
||||||
|
static override id = 'sticker'
|
||||||
|
|
||||||
|
override onEnter = () => {
|
||||||
|
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||||
|
const { currentPagePoint } = this.editor.inputs
|
||||||
|
const stickerId = createShapeId()
|
||||||
|
this.editor.mark(`creating:${stickerId}`)
|
||||||
|
this.editor.createShape({
|
||||||
|
id: stickerId,
|
||||||
|
type: 'sticker',
|
||||||
|
x: currentPagePoint.x,
|
||||||
|
y: currentPagePoint.y,
|
||||||
|
})
|
||||||
|
this.editor.setSelectedShapes([stickerId])
|
||||||
|
this.editor.setCurrentTool('select.translating', {
|
||||||
|
...info,
|
||||||
|
target: 'shape',
|
||||||
|
shape: this.editor.getShape(stickerId),
|
||||||
|
isCreating: true,
|
||||||
|
onInteractionEnd: 'sticker',
|
||||||
|
onCreate: () => {
|
||||||
|
this.editor.setCurrentTool('sticker')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const overrides: TLUiOverrides = {
|
||||||
|
tools(editor, schema) {
|
||||||
|
schema['sticker'] = {
|
||||||
|
id: 'sticker',
|
||||||
|
label: 'Sticker',
|
||||||
|
icon: 'heart-icon',
|
||||||
|
kbd: 'p',
|
||||||
|
onSelect: () => {
|
||||||
|
editor.setCurrentTool('sticker')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return schema
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const components: TLUiComponents = {
|
||||||
|
Toolbar: (...props) => {
|
||||||
|
const sticker = useTools().sticker
|
||||||
|
const isStickerSelected = useIsToolSelected(sticker)
|
||||||
|
return (
|
||||||
|
<DefaultToolbar {...props}>
|
||||||
|
<TldrawUiMenuItem {...sticker} isSelected={isStickerSelected} />
|
||||||
|
<DefaultToolbarContent />
|
||||||
|
</DefaultToolbar>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StickerExample() {
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<Tldraw
|
||||||
|
onMount={(editor) => {
|
||||||
|
;(window as any).editor = editor
|
||||||
|
}}
|
||||||
|
shapeUtils={[StickerShapeUtil]}
|
||||||
|
bindingUtils={[StickerBindingUtil]}
|
||||||
|
tools={[StickerTool]}
|
||||||
|
overrides={overrides}
|
||||||
|
components={components}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -30,22 +30,27 @@ import { default as React_2 } from 'react';
|
||||||
import * as React_3 from 'react';
|
import * as React_3 from 'react';
|
||||||
import { ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import { RecordProps } from '@tldraw/tlschema';
|
||||||
import { RecordsDiff } from '@tldraw/store';
|
import { RecordsDiff } from '@tldraw/store';
|
||||||
import { SerializedSchema } from '@tldraw/store';
|
import { SerializedSchema } from '@tldraw/store';
|
||||||
import { SerializedStore } from '@tldraw/store';
|
import { SerializedStore } from '@tldraw/store';
|
||||||
import { ShapeProps } from '@tldraw/tlschema';
|
|
||||||
import { Signal } from '@tldraw/state';
|
import { Signal } from '@tldraw/state';
|
||||||
import { Store } from '@tldraw/store';
|
import { Store } from '@tldraw/store';
|
||||||
import { StoreSchema } from '@tldraw/store';
|
import { StoreSchema } from '@tldraw/store';
|
||||||
import { StoreSnapshot } from '@tldraw/store';
|
import { StoreSnapshot } from '@tldraw/store';
|
||||||
import { StyleProp } from '@tldraw/tlschema';
|
import { StyleProp } from '@tldraw/tlschema';
|
||||||
import { StylePropValue } from '@tldraw/tlschema';
|
import { StylePropValue } from '@tldraw/tlschema';
|
||||||
|
import { TLArrowBinding } from '@tldraw/tlschema';
|
||||||
|
import { TLArrowBindingProps } from '@tldraw/tlschema';
|
||||||
import { TLArrowShape } from '@tldraw/tlschema';
|
import { TLArrowShape } from '@tldraw/tlschema';
|
||||||
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
|
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
|
||||||
import { TLAsset } from '@tldraw/tlschema';
|
import { TLAsset } from '@tldraw/tlschema';
|
||||||
import { TLAssetId } from '@tldraw/tlschema';
|
import { TLAssetId } from '@tldraw/tlschema';
|
||||||
import { TLAssetPartial } from '@tldraw/tlschema';
|
import { TLAssetPartial } from '@tldraw/tlschema';
|
||||||
import { TLBaseShape } from '@tldraw/tlschema';
|
import { TLBaseShape } from '@tldraw/tlschema';
|
||||||
|
import { TLBinding } from '@tldraw/tlschema';
|
||||||
|
import { TLBindingId } from '@tldraw/tlschema';
|
||||||
|
import { TLBindingPartial } from '@tldraw/tlschema';
|
||||||
import { TLBookmarkAsset } from '@tldraw/tlschema';
|
import { TLBookmarkAsset } from '@tldraw/tlschema';
|
||||||
import { TLCamera } from '@tldraw/tlschema';
|
import { TLCamera } from '@tldraw/tlschema';
|
||||||
import { TLCursor } from '@tldraw/tlschema';
|
import { TLCursor } from '@tldraw/tlschema';
|
||||||
|
@ -61,14 +66,15 @@ import { TLInstancePresence } from '@tldraw/tlschema';
|
||||||
import { TLPage } from '@tldraw/tlschema';
|
import { TLPage } from '@tldraw/tlschema';
|
||||||
import { TLPageId } from '@tldraw/tlschema';
|
import { TLPageId } from '@tldraw/tlschema';
|
||||||
import { TLParentId } from '@tldraw/tlschema';
|
import { TLParentId } from '@tldraw/tlschema';
|
||||||
|
import { TLPropsMigrations } from '@tldraw/tlschema';
|
||||||
import { TLRecord } from '@tldraw/tlschema';
|
import { TLRecord } from '@tldraw/tlschema';
|
||||||
import { TLScribble } from '@tldraw/tlschema';
|
import { TLScribble } from '@tldraw/tlschema';
|
||||||
import { TLShape } from '@tldraw/tlschema';
|
import { TLShape } from '@tldraw/tlschema';
|
||||||
import { TLShapeId } from '@tldraw/tlschema';
|
import { TLShapeId } from '@tldraw/tlschema';
|
||||||
import { TLShapePartial } from '@tldraw/tlschema';
|
import { TLShapePartial } from '@tldraw/tlschema';
|
||||||
import { TLShapePropsMigrations } from '@tldraw/tlschema';
|
|
||||||
import { TLStore } from '@tldraw/tlschema';
|
import { TLStore } from '@tldraw/tlschema';
|
||||||
import { TLStoreProps } from '@tldraw/tlschema';
|
import { TLStoreProps } from '@tldraw/tlschema';
|
||||||
|
import { TLUnknownBinding } from '@tldraw/tlschema';
|
||||||
import { TLUnknownShape } from '@tldraw/tlschema';
|
import { TLUnknownShape } from '@tldraw/tlschema';
|
||||||
import { TLVideoAsset } from '@tldraw/tlschema';
|
import { TLVideoAsset } from '@tldraw/tlschema';
|
||||||
import { track } from '@tldraw/state';
|
import { track } from '@tldraw/state';
|
||||||
|
@ -170,6 +176,77 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
||||||
onResize: TLOnResizeHandler<any>;
|
onResize: TLOnResizeHandler<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface BindingOnChangeOptions<Binding extends TLUnknownBinding> {
|
||||||
|
// (undocumented)
|
||||||
|
bindingAfter: Binding;
|
||||||
|
// (undocumented)
|
||||||
|
bindingBefore: Binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface BindingOnCreateOptions<Binding extends TLUnknownBinding> {
|
||||||
|
// (undocumented)
|
||||||
|
binding: Binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface BindingOnDeleteOptions<Binding extends TLUnknownBinding> {
|
||||||
|
// (undocumented)
|
||||||
|
binding: Binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface BindingOnShapeChangeOptions<Binding extends TLUnknownBinding> {
|
||||||
|
// (undocumented)
|
||||||
|
binding: Binding;
|
||||||
|
// (undocumented)
|
||||||
|
shapeAfter: TLShape;
|
||||||
|
// (undocumented)
|
||||||
|
shapeBefore: TLShape;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface BindingOnShapeDeleteOptions<Binding extends TLUnknownBinding> {
|
||||||
|
// (undocumented)
|
||||||
|
binding: Binding;
|
||||||
|
// (undocumented)
|
||||||
|
shape: TLShape;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export abstract class BindingUtil<Binding extends TLUnknownBinding = TLUnknownBinding> {
|
||||||
|
constructor(editor: Editor);
|
||||||
|
// (undocumented)
|
||||||
|
editor: Editor;
|
||||||
|
abstract getDefaultProps(): Partial<Binding['props']>;
|
||||||
|
// (undocumented)
|
||||||
|
static migrations?: TLPropsMigrations;
|
||||||
|
// (undocumented)
|
||||||
|
onAfterChange?(options: BindingOnChangeOptions<Binding>): void;
|
||||||
|
// (undocumented)
|
||||||
|
onAfterChangeFromShape?(options: BindingOnShapeChangeOptions<Binding>): void;
|
||||||
|
// (undocumented)
|
||||||
|
onAfterChangeToShape?(options: BindingOnShapeChangeOptions<Binding>): void;
|
||||||
|
// (undocumented)
|
||||||
|
onAfterCreate?(options: BindingOnCreateOptions<Binding>): void;
|
||||||
|
// (undocumented)
|
||||||
|
onAfterDelete?(options: BindingOnDeleteOptions<Binding>): void;
|
||||||
|
// (undocumented)
|
||||||
|
onBeforeChange?(options: BindingOnChangeOptions<Binding>): Binding | void;
|
||||||
|
// (undocumented)
|
||||||
|
onBeforeCreate?(options: BindingOnCreateOptions<Binding>): Binding | void;
|
||||||
|
// (undocumented)
|
||||||
|
onBeforeDelete?(options: BindingOnDeleteOptions<Binding>): void;
|
||||||
|
// (undocumented)
|
||||||
|
onBeforeDeleteFromShape?(options: BindingOnShapeDeleteOptions<Binding>): void;
|
||||||
|
// (undocumented)
|
||||||
|
onBeforeDeleteToShape?(options: BindingOnShapeDeleteOptions<Binding>): void;
|
||||||
|
// (undocumented)
|
||||||
|
static props?: RecordProps<TLUnknownBinding>;
|
||||||
|
static type: string;
|
||||||
|
}
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export interface BoundsSnapGeometry {
|
export interface BoundsSnapGeometry {
|
||||||
points?: VecModel[];
|
points?: VecModel[];
|
||||||
|
@ -374,6 +451,9 @@ export const coreShapes: readonly [typeof GroupShapeUtil];
|
||||||
// @public
|
// @public
|
||||||
export function counterClockwiseAngleDist(a0: number, a1: number): number;
|
export function counterClockwiseAngleDist(a0: number, a1: number): number;
|
||||||
|
|
||||||
|
// @internal
|
||||||
|
export function createOrUpdateArrowBinding(editor: Editor, arrow: TLArrowShape | TLShapeId, target: TLShape | TLShapeId, props: TLArrowBindingProps): void;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function createSessionStateSnapshotSignal(store: TLStore): Signal<null | TLSessionStateSnapshot>;
|
export function createSessionStateSnapshotSignal(store: TLStore): Signal<null | TLSessionStateSnapshot>;
|
||||||
|
|
||||||
|
@ -590,7 +670,7 @@ export class Edge2d extends Geometry2d {
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class Editor extends EventEmitter<TLEventMap> {
|
export class Editor extends EventEmitter<TLEventMap> {
|
||||||
constructor({ store, user, shapeUtils, tools, getContainer, cameraOptions, initialState, inferDarkMode, }: TLEditorOptions);
|
constructor({ store, user, shapeUtils, bindingUtils, tools, getContainer, cameraOptions, initialState, inferDarkMode, }: TLEditorOptions);
|
||||||
addOpenMenu(id: string): this;
|
addOpenMenu(id: string): this;
|
||||||
alignShapes(shapes: TLShape[] | TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
|
alignShapes(shapes: TLShape[] | TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
|
||||||
animateShape(partial: null | TLShapePartial | undefined, opts?: Partial<{
|
animateShape(partial: null | TLShapePartial | undefined, opts?: Partial<{
|
||||||
|
@ -621,6 +701,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
bail(): this;
|
bail(): this;
|
||||||
bailToMark(id: string): this;
|
bailToMark(id: string): this;
|
||||||
batch(fn: () => void, opts?: TLHistoryBatchOptions): this;
|
batch(fn: () => void, opts?: TLHistoryBatchOptions): this;
|
||||||
|
bindingUtils: {
|
||||||
|
readonly [K in string]?: BindingUtil<TLUnknownBinding>;
|
||||||
|
};
|
||||||
bringForward(shapes: TLShape[] | TLShapeId[]): this;
|
bringForward(shapes: TLShape[] | TLShapeId[]): this;
|
||||||
bringToFront(shapes: TLShape[] | TLShapeId[]): this;
|
bringToFront(shapes: TLShape[] | TLShapeId[]): this;
|
||||||
cancel(): this;
|
cancel(): this;
|
||||||
|
@ -635,6 +718,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
crash(error: unknown): this;
|
crash(error: unknown): this;
|
||||||
createAssets(assets: TLAsset[]): this;
|
createAssets(assets: TLAsset[]): this;
|
||||||
|
// (undocumented)
|
||||||
|
createBinding(partial: RequiredKeys<TLBindingPartial, 'fromId' | 'toId' | 'type'>): this;
|
||||||
|
// (undocumented)
|
||||||
|
createBindings(partials: RequiredKeys<TLBindingPartial, 'fromId' | 'toId' | 'type'>[]): this;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): {
|
createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): {
|
||||||
extras: {
|
extras: {
|
||||||
|
@ -652,6 +739,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
createShape<T extends TLUnknownShape>(shape: OptionalKeys<TLShapePartial<T>, 'id'>): this;
|
createShape<T extends TLUnknownShape>(shape: OptionalKeys<TLShapePartial<T>, 'id'>): this;
|
||||||
createShapes<T extends TLUnknownShape>(shapes: OptionalKeys<TLShapePartial<T>, 'id'>[]): this;
|
createShapes<T extends TLUnknownShape>(shapes: OptionalKeys<TLShapePartial<T>, 'id'>[]): this;
|
||||||
deleteAssets(assets: TLAsset[] | TLAssetId[]): this;
|
deleteAssets(assets: TLAsset[] | TLAssetId[]): this;
|
||||||
|
// (undocumented)
|
||||||
|
deleteBinding(binding: TLBinding | TLBindingId): this;
|
||||||
|
// (undocumented)
|
||||||
|
deleteBindings(bindings: (TLBinding | TLBindingId)[]): this;
|
||||||
deleteOpenMenu(id: string): this;
|
deleteOpenMenu(id: string): this;
|
||||||
deletePage(page: TLPage | TLPageId): this;
|
deletePage(page: TLPage | TLPageId): this;
|
||||||
deleteShape(id: TLShapeId): this;
|
deleteShape(id: TLShapeId): this;
|
||||||
|
@ -687,16 +778,28 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
findCommonAncestor(shapes: TLShape[] | TLShapeId[], predicate?: (shape: TLShape) => boolean): TLShapeId | undefined;
|
findCommonAncestor(shapes: TLShape[] | TLShapeId[], predicate?: (shape: TLShape) => boolean): TLShapeId | undefined;
|
||||||
findShapeAncestor(shape: TLShape | TLShapeId, predicate: (parent: TLShape) => boolean): TLShape | undefined;
|
findShapeAncestor(shape: TLShape | TLShapeId, predicate: (parent: TLShape) => boolean): TLShape | undefined;
|
||||||
flipShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
|
flipShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
|
||||||
|
// (undocumented)
|
||||||
|
getAllBindingsFromShape(shape: TLShape | TLShapeId): TLBinding[];
|
||||||
|
// (undocumented)
|
||||||
|
getAllBindingsToShape(shape: TLShape | TLShapeId): TLBinding[];
|
||||||
getAncestorPageId(shape?: TLShape | TLShapeId): TLPageId | undefined;
|
getAncestorPageId(shape?: TLShape | TLShapeId): TLPageId | undefined;
|
||||||
getArrowInfo(shape: TLArrowShape | TLShapeId): TLArrowInfo | undefined;
|
getArrowInfo(shape: TLArrowShape | TLShapeId): TLArrowInfo | undefined;
|
||||||
getArrowsBoundTo(shapeId: TLShapeId): {
|
getArrowsBoundTo(shapeId: TLShapeId): TLArrowShape[];
|
||||||
arrowId: TLShapeId;
|
|
||||||
handleId: "end" | "start";
|
|
||||||
}[];
|
|
||||||
getAsset(asset: TLAsset | TLAssetId): TLAsset | undefined;
|
getAsset(asset: TLAsset | TLAssetId): TLAsset | undefined;
|
||||||
getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined>;
|
getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined>;
|
||||||
getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[];
|
getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[];
|
||||||
getBaseZoom(): number;
|
getBaseZoom(): number;
|
||||||
|
// (undocumented)
|
||||||
|
getBinding(id: TLBindingId): TLBinding | undefined;
|
||||||
|
// (undocumented)
|
||||||
|
getBindingsFromShape<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>;
|
||||||
|
// (undocumented)
|
||||||
|
getBindingUtil<S extends TLUnknownBinding>(type: S['type']): BindingUtil<S>;
|
||||||
|
// (undocumented)
|
||||||
|
getBindingUtil<T extends BindingUtil>(type: T extends BindingUtil<infer R> ? R['type'] : string): T;
|
||||||
getCamera(): TLCamera;
|
getCamera(): TLCamera;
|
||||||
getCameraOptions(): TLCameraOptions;
|
getCameraOptions(): TLCameraOptions;
|
||||||
getCameraState(): "idle" | "moving";
|
getCameraState(): "idle" | "moving";
|
||||||
|
@ -783,6 +886,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
getShapeLocalTransform(shape: TLShape | TLShapeId): Mat;
|
getShapeLocalTransform(shape: TLShape | TLShapeId): Mat;
|
||||||
getShapeMask(shape: TLShape | TLShapeId): undefined | VecLike[];
|
getShapeMask(shape: TLShape | TLShapeId): undefined | VecLike[];
|
||||||
getShapeMaskedPageBounds(shape: TLShape | TLShapeId): Box | undefined;
|
getShapeMaskedPageBounds(shape: TLShape | TLShapeId): Box | undefined;
|
||||||
|
// @internal
|
||||||
|
getShapeNearestSibling(siblingShape: TLShape, targetShape: TLShape | undefined): TLShape | undefined;
|
||||||
getShapePageBounds(shape: TLShape | TLShapeId): Box | undefined;
|
getShapePageBounds(shape: TLShape | TLShapeId): Box | undefined;
|
||||||
getShapePageTransform(shape: TLShape | TLShapeId): Mat;
|
getShapePageTransform(shape: TLShape | TLShapeId): Mat;
|
||||||
getShapeParent(shape?: TLShape | TLShapeId): TLShape | undefined;
|
getShapeParent(shape?: TLShape | TLShapeId): TLShape | undefined;
|
||||||
|
@ -944,6 +1049,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
ungroupShapes(ids: TLShape[]): this;
|
ungroupShapes(ids: TLShape[]): this;
|
||||||
updateAssets(assets: TLAssetPartial[]): this;
|
updateAssets(assets: TLAssetPartial[]): this;
|
||||||
|
// (undocumented)
|
||||||
|
updateBinding(partial: TLBindingPartial): this;
|
||||||
|
// (undocumented)
|
||||||
|
updateBindings(partials: (null | TLBindingPartial | undefined)[]): this;
|
||||||
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions): this;
|
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
_updateCurrentPageState: (partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions) => void;
|
_updateCurrentPageState: (partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions) => void;
|
||||||
|
@ -1088,7 +1197,10 @@ export abstract class Geometry2d {
|
||||||
export function getArcMeasure(A: number, B: number, sweepFlag: number, largeArcFlag: number): number;
|
export function getArcMeasure(A: number, B: number, sweepFlag: number, largeArcFlag: number): number;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape): {
|
export function getArrowBindings(editor: Editor, shape: TLArrowShape): TLArrowBindings;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape, bindings: TLArrowBindings): {
|
||||||
end: Vec;
|
end: Vec;
|
||||||
start: Vec;
|
start: Vec;
|
||||||
};
|
};
|
||||||
|
@ -1184,11 +1296,11 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLGroupShape): JSX_2.Element;
|
indicator(shape: TLGroupShape): JSX_2.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onChildrenChange: TLOnChildrenChangeHandler<TLGroupShape>;
|
onChildrenChange: TLOnChildrenChangeHandler<TLGroupShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static props: ShapeProps<TLGroupShape>;
|
static props: RecordProps<TLGroupShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static type: "group";
|
static type: "group";
|
||||||
}
|
}
|
||||||
|
@ -1602,6 +1714,9 @@ export function refreshPage(): void;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function releasePointerCapture(element: Element, event: PointerEvent | React_2.PointerEvent<Element>): void;
|
export function releasePointerCapture(element: Element, event: PointerEvent | React_2.PointerEvent<Element>): void;
|
||||||
|
|
||||||
|
// @internal
|
||||||
|
export function removeArrowBinding(editor: Editor, arrow: TLArrowShape, terminal: 'end' | 'start'): void;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @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> = Partial<Omit<T, K>> & Pick<T, K>;
|
||||||
|
|
||||||
|
@ -1681,6 +1796,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
constructor(editor: Editor);
|
constructor(editor: Editor);
|
||||||
// @internal
|
// @internal
|
||||||
backgroundComponent?(shape: Shape): any;
|
backgroundComponent?(shape: Shape): any;
|
||||||
|
canBeLaidOut: TLShapeUtilFlag<Shape>;
|
||||||
canBind: <K>(_shape: Shape, _otherShape?: K) => boolean;
|
canBind: <K>(_shape: Shape, _otherShape?: K) => boolean;
|
||||||
canCrop: TLShapeUtilFlag<Shape>;
|
canCrop: TLShapeUtilFlag<Shape>;
|
||||||
canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
|
canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
|
||||||
|
@ -1708,7 +1824,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
abstract indicator(shape: Shape): any;
|
abstract indicator(shape: Shape): any;
|
||||||
isAspectRatioLocked: TLShapeUtilFlag<Shape>;
|
isAspectRatioLocked: TLShapeUtilFlag<Shape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations?: LegacyMigrations | TLShapePropsMigrations;
|
static migrations?: LegacyMigrations | MigrationSequence | TLPropsMigrations;
|
||||||
onBeforeCreate?: TLOnBeforeCreateHandler<Shape>;
|
onBeforeCreate?: TLOnBeforeCreateHandler<Shape>;
|
||||||
onBeforeUpdate?: TLOnBeforeUpdateHandler<Shape>;
|
onBeforeUpdate?: TLOnBeforeUpdateHandler<Shape>;
|
||||||
// @internal
|
// @internal
|
||||||
|
@ -1733,7 +1849,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
onTranslateEnd?: TLOnTranslateEndHandler<Shape>;
|
onTranslateEnd?: TLOnTranslateEndHandler<Shape>;
|
||||||
onTranslateStart?: TLOnTranslateStartHandler<Shape>;
|
onTranslateStart?: TLOnTranslateStartHandler<Shape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static props?: ShapeProps<TLUnknownShape>;
|
static props?: RecordProps<TLUnknownShape>;
|
||||||
// @internal
|
// @internal
|
||||||
providesBackgroundForChildren(shape: Shape): boolean;
|
providesBackgroundForChildren(shape: Shape): boolean;
|
||||||
toBackgroundSvg?(shape: Shape, ctx: SvgExportContext): null | Promise<null | ReactElement> | ReactElement;
|
toBackgroundSvg?(shape: Shape, ctx: SvgExportContext): null | Promise<null | ReactElement> | ReactElement;
|
||||||
|
@ -1974,6 +2090,9 @@ export type TLAfterCreateHandler<R extends TLRecord> = (record: R, source: 'remo
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLAfterDeleteHandler<R extends TLRecord> = (record: R, source: 'remote' | 'user') => void;
|
export type TLAfterDeleteHandler<R extends TLRecord> = (record: R, source: 'remote' | 'user') => void;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type TLAnyBindingUtilConstructor = TLBindingUtilConstructor<any>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLAnyShapeUtilConstructor = TLShapeUtilConstructor<any>;
|
export type TLAnyShapeUtilConstructor = TLShapeUtilConstructor<any>;
|
||||||
|
|
||||||
|
@ -1993,8 +2112,17 @@ export interface TLArcInfo {
|
||||||
sweepFlag: number;
|
sweepFlag: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface TLArrowBindings {
|
||||||
|
// (undocumented)
|
||||||
|
end: TLArrowBinding | undefined;
|
||||||
|
// (undocumented)
|
||||||
|
start: TLArrowBinding | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLArrowInfo = {
|
export type TLArrowInfo = {
|
||||||
|
bindings: TLArrowBindings;
|
||||||
bodyArc: TLArcInfo;
|
bodyArc: TLArcInfo;
|
||||||
end: TLArrowPoint;
|
end: TLArrowPoint;
|
||||||
handleArc: TLArcInfo;
|
handleArc: TLArcInfo;
|
||||||
|
@ -2003,6 +2131,7 @@ export type TLArrowInfo = {
|
||||||
middle: VecLike;
|
middle: VecLike;
|
||||||
start: TLArrowPoint;
|
start: TLArrowPoint;
|
||||||
} | {
|
} | {
|
||||||
|
bindings: TLArrowBindings;
|
||||||
end: TLArrowPoint;
|
end: TLArrowPoint;
|
||||||
isStraight: true;
|
isStraight: true;
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
|
@ -2048,6 +2177,18 @@ export type TLBeforeCreateHandler<R extends TLRecord> = (record: R, source: 'rem
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLBeforeDeleteHandler<R extends TLRecord> = (record: R, source: 'remote' | 'user') => false | void;
|
export type TLBeforeDeleteHandler<R extends TLRecord> = (record: R, source: 'remote' | 'user') => false | void;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface TLBindingUtilConstructor<T extends TLUnknownBinding, U extends BindingUtil<T> = BindingUtil<T>> {
|
||||||
|
// (undocumented)
|
||||||
|
new (editor: Editor): U;
|
||||||
|
// (undocumented)
|
||||||
|
migrations?: TLPropsMigrations;
|
||||||
|
// (undocumented)
|
||||||
|
props?: RecordProps<T>;
|
||||||
|
// (undocumented)
|
||||||
|
type: T['type'];
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLBrushProps = {
|
export type TLBrushProps = {
|
||||||
brush: BoxModel;
|
brush: BoxModel;
|
||||||
|
@ -2136,6 +2277,8 @@ export interface TLContent {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
assets: TLAsset[];
|
assets: TLAsset[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
bindings: TLBinding[] | undefined;
|
||||||
|
// (undocumented)
|
||||||
rootShapeIds: TLShapeId[];
|
rootShapeIds: TLShapeId[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
schema: SerializedSchema;
|
schema: SerializedSchema;
|
||||||
|
@ -2159,6 +2302,7 @@ export const TldrawEditor: React_2.NamedExoticComponent<TldrawEditorProps>;
|
||||||
// @public
|
// @public
|
||||||
export interface TldrawEditorBaseProps {
|
export interface TldrawEditorBaseProps {
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
|
bindingUtils?: readonly TLAnyBindingUtilConstructor[];
|
||||||
cameraOptions?: Partial<TLCameraOptions>;
|
cameraOptions?: Partial<TLCameraOptions>;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -2191,6 +2335,7 @@ export type TLEditorComponents = Partial<{
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export interface TLEditorOptions {
|
export interface TLEditorOptions {
|
||||||
|
bindingUtils: readonly TLBindingUtilConstructor<TLUnknownBinding>[];
|
||||||
cameraOptions?: Partial<TLCameraOptions>;
|
cameraOptions?: Partial<TLCameraOptions>;
|
||||||
getContainer: () => HTMLElement;
|
getContainer: () => HTMLElement;
|
||||||
inferDarkMode?: boolean;
|
inferDarkMode?: boolean;
|
||||||
|
@ -2618,9 +2763,9 @@ export interface TLShapeUtilConstructor<T extends TLUnknownShape, U extends Shap
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
new (editor: Editor): U;
|
new (editor: Editor): U;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
migrations?: LegacyMigrations | MigrationSequence | TLShapePropsMigrations;
|
migrations?: LegacyMigrations | MigrationSequence | TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
props?: ShapeProps<T>;
|
props?: RecordProps<T>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
type: T['type'];
|
type: T['type'];
|
||||||
}
|
}
|
||||||
|
@ -2656,6 +2801,7 @@ export type TLStoreOptions = {
|
||||||
id?: string;
|
id?: string;
|
||||||
initialData?: SerializedStore<TLRecord>;
|
initialData?: SerializedStore<TLRecord>;
|
||||||
} & ({
|
} & ({
|
||||||
|
bindingUtils?: readonly TLAnyBindingUtilConstructor[];
|
||||||
migrations?: readonly MigrationSequence[];
|
migrations?: readonly MigrationSequence[];
|
||||||
shapeUtils?: readonly TLAnyShapeUtilConstructor[];
|
shapeUtils?: readonly TLAnyShapeUtilConstructor[];
|
||||||
} | {
|
} | {
|
||||||
|
|
|
@ -104,6 +104,7 @@ export {
|
||||||
type TLStoreOptions,
|
type TLStoreOptions,
|
||||||
} from './lib/config/createTLStore'
|
} from './lib/config/createTLStore'
|
||||||
export { createTLUser } from './lib/config/createTLUser'
|
export { createTLUser } from './lib/config/createTLUser'
|
||||||
|
export { type TLAnyBindingUtilConstructor } from './lib/config/defaultBindings'
|
||||||
export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
|
export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
|
||||||
export {
|
export {
|
||||||
ANIMATION_MEDIUM_MS,
|
ANIMATION_MEDIUM_MS,
|
||||||
|
@ -122,6 +123,15 @@ export {
|
||||||
SVG_PADDING,
|
SVG_PADDING,
|
||||||
} from './lib/constants'
|
} from './lib/constants'
|
||||||
export { Editor, type TLEditorOptions, type TLResizeShapeOptions } from './lib/editor/Editor'
|
export { Editor, type TLEditorOptions, type TLResizeShapeOptions } from './lib/editor/Editor'
|
||||||
|
export {
|
||||||
|
BindingUtil,
|
||||||
|
type BindingOnChangeOptions,
|
||||||
|
type BindingOnCreateOptions,
|
||||||
|
type BindingOnDeleteOptions,
|
||||||
|
type BindingOnShapeChangeOptions,
|
||||||
|
type BindingOnShapeDeleteOptions,
|
||||||
|
type TLBindingUtilConstructor,
|
||||||
|
} from './lib/editor/bindings/BindingUtil'
|
||||||
export { HistoryManager } from './lib/editor/managers/HistoryManager'
|
export { HistoryManager } from './lib/editor/managers/HistoryManager'
|
||||||
export type {
|
export type {
|
||||||
SideEffectManager,
|
SideEffectManager,
|
||||||
|
@ -178,7 +188,13 @@ export {
|
||||||
type TLArrowInfo,
|
type TLArrowInfo,
|
||||||
type TLArrowPoint,
|
type TLArrowPoint,
|
||||||
} from './lib/editor/shapes/shared/arrow/arrow-types'
|
} from './lib/editor/shapes/shared/arrow/arrow-types'
|
||||||
export { getArrowTerminalsInArrowSpace } from './lib/editor/shapes/shared/arrow/shared'
|
export {
|
||||||
|
createOrUpdateArrowBinding,
|
||||||
|
getArrowBindings,
|
||||||
|
getArrowTerminalsInArrowSpace,
|
||||||
|
removeArrowBinding,
|
||||||
|
type TLArrowBindings,
|
||||||
|
} from './lib/editor/shapes/shared/arrow/shared'
|
||||||
export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox'
|
export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox'
|
||||||
export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
|
export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
|
||||||
export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode'
|
export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode'
|
||||||
|
|
|
@ -15,6 +15,7 @@ import classNames from 'classnames'
|
||||||
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
||||||
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
|
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
|
||||||
import { TLUser, createTLUser } from './config/createTLUser'
|
import { TLUser, createTLUser } from './config/createTLUser'
|
||||||
|
import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
|
||||||
import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
|
import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
|
||||||
import { Editor } from './editor/Editor'
|
import { Editor } from './editor/Editor'
|
||||||
import { TLStateNodeConstructor } from './editor/tools/StateNode'
|
import { TLStateNodeConstructor } from './editor/tools/StateNode'
|
||||||
|
@ -76,6 +77,11 @@ export interface TldrawEditorBaseProps {
|
||||||
*/
|
*/
|
||||||
shapeUtils?: readonly TLAnyShapeUtilConstructor[]
|
shapeUtils?: readonly TLAnyShapeUtilConstructor[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of binding utils to use in the editor.
|
||||||
|
*/
|
||||||
|
bindingUtils?: readonly TLAnyBindingUtilConstructor[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of tools to add to the editor's state chart.
|
* An array of tools to add to the editor's state chart.
|
||||||
*/
|
*/
|
||||||
|
@ -141,6 +147,7 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_SHAPE_UTILS_ARRAY = [] as const
|
const EMPTY_SHAPE_UTILS_ARRAY = [] as const
|
||||||
|
const EMPTY_BINDING_UTILS_ARRAY = [] as const
|
||||||
const EMPTY_TOOLS_ARRAY = [] as const
|
const EMPTY_TOOLS_ARRAY = [] as const
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -163,6 +170,7 @@ export const TldrawEditor = memo(function TldrawEditor({
|
||||||
const withDefaults = {
|
const withDefaults = {
|
||||||
...rest,
|
...rest,
|
||||||
shapeUtils: rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY,
|
shapeUtils: rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY,
|
||||||
|
bindingUtils: rest.bindingUtils ?? EMPTY_BINDING_UTILS_ARRAY,
|
||||||
tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
|
tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
|
||||||
components,
|
components,
|
||||||
}
|
}
|
||||||
|
@ -203,12 +211,25 @@ export const TldrawEditor = memo(function TldrawEditor({
|
||||||
})
|
})
|
||||||
|
|
||||||
function TldrawEditorWithOwnStore(
|
function TldrawEditorWithOwnStore(
|
||||||
props: Required<TldrawEditorProps & { store: undefined; user: TLUser }, 'shapeUtils' | 'tools'>
|
props: Required<
|
||||||
|
TldrawEditorProps & { store: undefined; user: TLUser },
|
||||||
|
'shapeUtils' | 'bindingUtils' | 'tools'
|
||||||
|
>
|
||||||
) {
|
) {
|
||||||
const { defaultName, snapshot, initialData, shapeUtils, persistenceKey, sessionId, user } = props
|
const {
|
||||||
|
defaultName,
|
||||||
|
snapshot,
|
||||||
|
initialData,
|
||||||
|
shapeUtils,
|
||||||
|
bindingUtils,
|
||||||
|
persistenceKey,
|
||||||
|
sessionId,
|
||||||
|
user,
|
||||||
|
} = props
|
||||||
|
|
||||||
const syncedStore = useLocalStore({
|
const syncedStore = useLocalStore({
|
||||||
shapeUtils,
|
shapeUtils,
|
||||||
|
bindingUtils,
|
||||||
initialData,
|
initialData,
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
sessionId,
|
sessionId,
|
||||||
|
@ -225,7 +246,7 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
|
||||||
...rest
|
...rest
|
||||||
}: Required<
|
}: Required<
|
||||||
TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser },
|
TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser },
|
||||||
'shapeUtils' | 'tools'
|
'shapeUtils' | 'bindingUtils' | 'tools'
|
||||||
>) {
|
>) {
|
||||||
const container = useContainer()
|
const container = useContainer()
|
||||||
|
|
||||||
|
@ -268,6 +289,7 @@ function TldrawEditorWithReadyStore({
|
||||||
store,
|
store,
|
||||||
tools,
|
tools,
|
||||||
shapeUtils,
|
shapeUtils,
|
||||||
|
bindingUtils,
|
||||||
user,
|
user,
|
||||||
initialState,
|
initialState,
|
||||||
autoFocus = true,
|
autoFocus = true,
|
||||||
|
@ -278,7 +300,7 @@ function TldrawEditorWithReadyStore({
|
||||||
store: TLStore
|
store: TLStore
|
||||||
user: TLUser
|
user: TLUser
|
||||||
},
|
},
|
||||||
'shapeUtils' | 'tools'
|
'shapeUtils' | 'bindingUtils' | 'tools'
|
||||||
>) {
|
>) {
|
||||||
const { ErrorFallback } = useEditorComponents()
|
const { ErrorFallback } = useEditorComponents()
|
||||||
const container = useContainer()
|
const container = useContainer()
|
||||||
|
@ -288,6 +310,7 @@ function TldrawEditorWithReadyStore({
|
||||||
const editor = new Editor({
|
const editor = new Editor({
|
||||||
store,
|
store,
|
||||||
shapeUtils,
|
shapeUtils,
|
||||||
|
bindingUtils,
|
||||||
tools,
|
tools,
|
||||||
getContainer: () => container,
|
getContainer: () => container,
|
||||||
user,
|
user,
|
||||||
|
@ -300,7 +323,17 @@ function TldrawEditorWithReadyStore({
|
||||||
return () => {
|
return () => {
|
||||||
editor.dispose()
|
editor.dispose()
|
||||||
}
|
}
|
||||||
}, [container, shapeUtils, tools, store, user, initialState, inferDarkMode, cameraOptions])
|
}, [
|
||||||
|
container,
|
||||||
|
shapeUtils,
|
||||||
|
bindingUtils,
|
||||||
|
tools,
|
||||||
|
store,
|
||||||
|
user,
|
||||||
|
initialState,
|
||||||
|
inferDarkMode,
|
||||||
|
cameraOptions,
|
||||||
|
])
|
||||||
|
|
||||||
const crashingError = useSyncExternalStore(
|
const crashingError = useSyncExternalStore(
|
||||||
useCallback(
|
useCallback(
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
import { HistoryEntry, MigrationSequence, SerializedStore, Store, StoreSchema } from '@tldraw/store'
|
import { HistoryEntry, MigrationSequence, SerializedStore, Store, StoreSchema } from '@tldraw/store'
|
||||||
import {
|
import { SchemaPropsInfo, TLRecord, TLStore, TLStoreProps, createTLSchema } from '@tldraw/tlschema'
|
||||||
SchemaShapeInfo,
|
import { TLAnyBindingUtilConstructor, checkBindings } from './defaultBindings'
|
||||||
TLRecord,
|
|
||||||
TLStore,
|
|
||||||
TLStoreProps,
|
|
||||||
TLUnknownShape,
|
|
||||||
createTLSchema,
|
|
||||||
} from '@tldraw/tlschema'
|
|
||||||
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
|
|
||||||
import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShapes'
|
import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShapes'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -16,7 +9,11 @@ export type TLStoreOptions = {
|
||||||
defaultName?: string
|
defaultName?: string
|
||||||
id?: string
|
id?: string
|
||||||
} & (
|
} & (
|
||||||
| { shapeUtils?: readonly TLAnyShapeUtilConstructor[]; migrations?: readonly MigrationSequence[] }
|
| {
|
||||||
|
shapeUtils?: readonly TLAnyShapeUtilConstructor[]
|
||||||
|
migrations?: readonly MigrationSequence[]
|
||||||
|
bindingUtils?: readonly TLAnyBindingUtilConstructor[]
|
||||||
|
}
|
||||||
| { schema?: StoreSchema<TLRecord, TLStoreProps> }
|
| { schema?: StoreSchema<TLRecord, TLStoreProps> }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,9 +38,12 @@ export function createTLStore({
|
||||||
rest.schema
|
rest.schema
|
||||||
: // we need a schema
|
: // we need a schema
|
||||||
createTLSchema({
|
createTLSchema({
|
||||||
shapes: currentPageShapesToShapeMap(
|
shapes: utilsToMap(
|
||||||
checkShapesAndAddCore('shapeUtils' in rest && rest.shapeUtils ? rest.shapeUtils : [])
|
checkShapesAndAddCore('shapeUtils' in rest && rest.shapeUtils ? rest.shapeUtils : [])
|
||||||
),
|
),
|
||||||
|
bindings: utilsToMap(
|
||||||
|
checkBindings('bindingUtils' in rest && rest.bindingUtils ? rest.bindingUtils : [])
|
||||||
|
),
|
||||||
migrations: 'migrations' in rest ? rest.migrations : [],
|
migrations: 'migrations' in rest ? rest.migrations : [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -57,9 +57,9 @@ export function createTLStore({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentPageShapesToShapeMap(shapeUtils: TLShapeUtilConstructor<TLUnknownShape>[]) {
|
function utilsToMap<T extends SchemaPropsInfo & { type: string }>(utils: T[]) {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
shapeUtils.map((s): [string, SchemaShapeInfo] => [
|
utils.map((s): [string, SchemaPropsInfo] => [
|
||||||
s.type,
|
s.type,
|
||||||
{
|
{
|
||||||
props: s.props,
|
props: s.props,
|
||||||
|
|
19
packages/editor/src/lib/config/defaultBindings.ts
Normal file
19
packages/editor/src/lib/config/defaultBindings.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { TLBindingUtilConstructor } from '../editor/bindings/BindingUtil'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLAnyBindingUtilConstructor = TLBindingUtilConstructor<any>
|
||||||
|
|
||||||
|
export function checkBindings(customBindings: readonly TLAnyBindingUtilConstructor[]) {
|
||||||
|
const bindings = [] as TLAnyBindingUtilConstructor[]
|
||||||
|
|
||||||
|
const addedCustomBindingTypes = new Set<string>()
|
||||||
|
for (const customBinding of customBindings) {
|
||||||
|
if (addedCustomBindingTypes.has(customBinding.type)) {
|
||||||
|
throw new Error(`Binding type "${customBinding.type}" is defined more than once`)
|
||||||
|
}
|
||||||
|
bindings.push(customBinding)
|
||||||
|
addedCustomBindingTypes.add(customBinding.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
77
packages/editor/src/lib/editor/bindings/BindingUtil.ts
Normal file
77
packages/editor/src/lib/editor/bindings/BindingUtil.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { RecordProps, TLPropsMigrations, TLShape, TLUnknownBinding } from '@tldraw/tlschema'
|
||||||
|
import { Editor } from '../Editor'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export interface TLBindingUtilConstructor<
|
||||||
|
T extends TLUnknownBinding,
|
||||||
|
U extends BindingUtil<T> = BindingUtil<T>,
|
||||||
|
> {
|
||||||
|
new (editor: Editor): U
|
||||||
|
type: T['type']
|
||||||
|
props?: RecordProps<T>
|
||||||
|
migrations?: TLPropsMigrations
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export interface BindingOnCreateOptions<Binding extends TLUnknownBinding> {
|
||||||
|
binding: Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export interface BindingOnChangeOptions<Binding extends TLUnknownBinding> {
|
||||||
|
bindingBefore: Binding
|
||||||
|
bindingAfter: Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export interface BindingOnDeleteOptions<Binding extends TLUnknownBinding> {
|
||||||
|
binding: Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export interface BindingOnShapeChangeOptions<Binding extends TLUnknownBinding> {
|
||||||
|
binding: Binding
|
||||||
|
shapeBefore: TLShape
|
||||||
|
shapeAfter: TLShape
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export interface BindingOnShapeDeleteOptions<Binding extends TLUnknownBinding> {
|
||||||
|
binding: Binding
|
||||||
|
shape: TLShape
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export abstract class BindingUtil<Binding extends TLUnknownBinding = TLUnknownBinding> {
|
||||||
|
constructor(public editor: Editor) {}
|
||||||
|
static props?: RecordProps<TLUnknownBinding>
|
||||||
|
static migrations?: TLPropsMigrations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the binding util, which should match the binding's type.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
static type: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default props for a binding.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
abstract getDefaultProps(): Partial<Binding['props']>
|
||||||
|
|
||||||
|
// self lifecycle hooks
|
||||||
|
onBeforeCreate?(options: BindingOnCreateOptions<Binding>): Binding | void
|
||||||
|
onAfterCreate?(options: BindingOnCreateOptions<Binding>): void
|
||||||
|
onBeforeChange?(options: BindingOnChangeOptions<Binding>): Binding | void
|
||||||
|
onAfterChange?(options: BindingOnChangeOptions<Binding>): void
|
||||||
|
onBeforeDelete?(options: BindingOnDeleteOptions<Binding>): void
|
||||||
|
onAfterDelete?(options: BindingOnDeleteOptions<Binding>): void
|
||||||
|
|
||||||
|
onAfterChangeFromShape?(options: BindingOnShapeChangeOptions<Binding>): void
|
||||||
|
onAfterChangeToShape?(options: BindingOnShapeChangeOptions<Binding>): void
|
||||||
|
|
||||||
|
onBeforeDeleteFromShape?(options: BindingOnShapeDeleteOptions<Binding>): void
|
||||||
|
onBeforeDeleteToShape?(options: BindingOnShapeDeleteOptions<Binding>): void
|
||||||
|
}
|
|
@ -1,141 +0,0 @@
|
||||||
import { Computed, RESET_VALUE, computed, isUninitialized } from '@tldraw/state'
|
|
||||||
import { TLArrowShape, TLShape, TLShapeId } from '@tldraw/tlschema'
|
|
||||||
import { Editor } from '../Editor'
|
|
||||||
|
|
||||||
type TLArrowBindingsIndex = Record<
|
|
||||||
TLShapeId,
|
|
||||||
undefined | { arrowId: TLShapeId; handleId: 'start' | 'end' }[]
|
|
||||||
>
|
|
||||||
|
|
||||||
export const arrowBindingsIndex = (editor: Editor): Computed<TLArrowBindingsIndex> => {
|
|
||||||
const { store } = editor
|
|
||||||
const shapeHistory = store.query.filterHistory('shape')
|
|
||||||
const arrowQuery = store.query.records('shape', () => ({ type: { eq: 'arrow' as const } }))
|
|
||||||
function fromScratch() {
|
|
||||||
const allArrows = arrowQuery.get() as TLArrowShape[]
|
|
||||||
|
|
||||||
const bindings2Arrows: TLArrowBindingsIndex = {}
|
|
||||||
|
|
||||||
for (const arrow of allArrows) {
|
|
||||||
const { start, end } = arrow.props
|
|
||||||
if (start.type === 'binding') {
|
|
||||||
const arrows = bindings2Arrows[start.boundShapeId]
|
|
||||||
if (arrows) arrows.push({ arrowId: arrow.id, handleId: 'start' })
|
|
||||||
else bindings2Arrows[start.boundShapeId] = [{ arrowId: arrow.id, handleId: 'start' }]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end.type === 'binding') {
|
|
||||||
const arrows = bindings2Arrows[end.boundShapeId]
|
|
||||||
if (arrows) arrows.push({ arrowId: arrow.id, handleId: 'end' })
|
|
||||||
else bindings2Arrows[end.boundShapeId] = [{ arrowId: arrow.id, handleId: 'end' }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bindings2Arrows
|
|
||||||
}
|
|
||||||
|
|
||||||
return computed<TLArrowBindingsIndex>('arrowBindingsIndex', (_lastValue, lastComputedEpoch) => {
|
|
||||||
if (isUninitialized(_lastValue)) {
|
|
||||||
return fromScratch()
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastValue = _lastValue
|
|
||||||
|
|
||||||
const diff = shapeHistory.getDiffSince(lastComputedEpoch)
|
|
||||||
|
|
||||||
if (diff === RESET_VALUE) {
|
|
||||||
return fromScratch()
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextValue: TLArrowBindingsIndex | undefined = undefined
|
|
||||||
|
|
||||||
function ensureNewArray(boundShapeId: TLShapeId) {
|
|
||||||
// this will never happen
|
|
||||||
if (!nextValue) {
|
|
||||||
nextValue = { ...lastValue }
|
|
||||||
}
|
|
||||||
if (!nextValue[boundShapeId]) {
|
|
||||||
nextValue[boundShapeId] = []
|
|
||||||
} else if (nextValue[boundShapeId] === lastValue[boundShapeId]) {
|
|
||||||
nextValue[boundShapeId] = [...nextValue[boundShapeId]!]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removingBinding(
|
|
||||||
boundShapeId: TLShapeId,
|
|
||||||
arrowId: TLShapeId,
|
|
||||||
handleId: 'start' | 'end'
|
|
||||||
) {
|
|
||||||
ensureNewArray(boundShapeId)
|
|
||||||
nextValue![boundShapeId] = nextValue![boundShapeId]!.filter(
|
|
||||||
(binding) => binding.arrowId !== arrowId || binding.handleId !== handleId
|
|
||||||
)
|
|
||||||
if (nextValue![boundShapeId]!.length === 0) {
|
|
||||||
delete nextValue![boundShapeId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addBinding(boundShapeId: TLShapeId, arrowId: TLShapeId, handleId: 'start' | 'end') {
|
|
||||||
ensureNewArray(boundShapeId)
|
|
||||||
nextValue![boundShapeId]!.push({ arrowId, handleId })
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const changes of diff) {
|
|
||||||
for (const newShape of Object.values(changes.added)) {
|
|
||||||
if (editor.isShapeOfType<TLArrowShape>(newShape, 'arrow')) {
|
|
||||||
const { start, end } = newShape.props
|
|
||||||
if (start.type === 'binding') {
|
|
||||||
addBinding(start.boundShapeId, newShape.id, 'start')
|
|
||||||
}
|
|
||||||
if (end.type === 'binding') {
|
|
||||||
addBinding(end.boundShapeId, newShape.id, 'end')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [prev, next] of Object.values(changes.updated) as [TLShape, TLShape][]) {
|
|
||||||
if (
|
|
||||||
!editor.isShapeOfType<TLArrowShape>(prev, 'arrow') ||
|
|
||||||
!editor.isShapeOfType<TLArrowShape>(next, 'arrow')
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
for (const handle of ['start', 'end'] as const) {
|
|
||||||
const prevTerminal = prev.props[handle]
|
|
||||||
const nextTerminal = next.props[handle]
|
|
||||||
|
|
||||||
if (prevTerminal.type === 'binding' && nextTerminal.type === 'point') {
|
|
||||||
// if the binding was removed
|
|
||||||
removingBinding(prevTerminal.boundShapeId, prev.id, handle)
|
|
||||||
} else if (prevTerminal.type === 'point' && nextTerminal.type === 'binding') {
|
|
||||||
// if the binding was added
|
|
||||||
addBinding(nextTerminal.boundShapeId, next.id, handle)
|
|
||||||
} else if (
|
|
||||||
prevTerminal.type === 'binding' &&
|
|
||||||
nextTerminal.type === 'binding' &&
|
|
||||||
prevTerminal.boundShapeId !== nextTerminal.boundShapeId
|
|
||||||
) {
|
|
||||||
// if the binding was changed
|
|
||||||
removingBinding(prevTerminal.boundShapeId, prev.id, handle)
|
|
||||||
addBinding(nextTerminal.boundShapeId, next.id, handle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const prev of Object.values(changes.removed)) {
|
|
||||||
if (editor.isShapeOfType<TLArrowShape>(prev, 'arrow')) {
|
|
||||||
const { start, end } = prev.props
|
|
||||||
if (start.type === 'binding') {
|
|
||||||
removingBinding(start.boundShapeId, prev.id, 'start')
|
|
||||||
}
|
|
||||||
if (end.type === 'binding') {
|
|
||||||
removingBinding(end.boundShapeId, prev.id, 'end')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add diff entries if we need them
|
|
||||||
return nextValue ?? lastValue
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,11 +1,11 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { LegacyMigrations, MigrationSequence } from '@tldraw/store'
|
import { LegacyMigrations, MigrationSequence } from '@tldraw/store'
|
||||||
import {
|
import {
|
||||||
ShapeProps,
|
RecordProps,
|
||||||
TLHandle,
|
TLHandle,
|
||||||
|
TLPropsMigrations,
|
||||||
TLShape,
|
TLShape,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
TLShapePropsMigrations,
|
|
||||||
TLUnknownShape,
|
TLUnknownShape,
|
||||||
} from '@tldraw/tlschema'
|
} from '@tldraw/tlschema'
|
||||||
import { ReactElement } from 'react'
|
import { ReactElement } from 'react'
|
||||||
|
@ -25,8 +25,8 @@ export interface TLShapeUtilConstructor<
|
||||||
> {
|
> {
|
||||||
new (editor: Editor): U
|
new (editor: Editor): U
|
||||||
type: T['type']
|
type: T['type']
|
||||||
props?: ShapeProps<T>
|
props?: RecordProps<T>
|
||||||
migrations?: LegacyMigrations | TLShapePropsMigrations | MigrationSequence
|
migrations?: LegacyMigrations | TLPropsMigrations | MigrationSequence
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -41,8 +41,8 @@ export interface TLShapeUtilCanvasSvgDef {
|
||||||
/** @public */
|
/** @public */
|
||||||
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
constructor(public editor: Editor) {}
|
constructor(public editor: Editor) {}
|
||||||
static props?: ShapeProps<TLUnknownShape>
|
static props?: RecordProps<TLUnknownShape>
|
||||||
static migrations?: LegacyMigrations | TLShapePropsMigrations
|
static migrations?: LegacyMigrations | TLPropsMigrations | MigrationSequence
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the shape util, which should match the shape's type.
|
* The type of the shape util, which should match the shape's type.
|
||||||
|
@ -132,6 +132,13 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
*/
|
*/
|
||||||
canCrop: TLShapeUtilFlag<Shape> = () => false
|
canCrop: TLShapeUtilFlag<Shape> = () => false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the shape participates in stacking, aligning, and distributing.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
canBeLaidOut: TLShapeUtilFlag<Shape> = () => true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does this shape provide a background for its children? If this is true,
|
* Does this shape provide a background for its children? If this is true,
|
||||||
* then any children with a `renderBackground` method will have their
|
* then any children with a `renderBackground` method will have their
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema'
|
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema'
|
||||||
import { VecLike } from '../../../../primitives/Vec'
|
import { VecLike } from '../../../../primitives/Vec'
|
||||||
|
import { TLArrowBindings } from './shared'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLArrowPoint = {
|
export type TLArrowPoint = {
|
||||||
|
@ -21,6 +22,7 @@ export interface TLArcInfo {
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLArrowInfo =
|
export type TLArrowInfo =
|
||||||
| {
|
| {
|
||||||
|
bindings: TLArrowBindings
|
||||||
isStraight: false
|
isStraight: false
|
||||||
start: TLArrowPoint
|
start: TLArrowPoint
|
||||||
end: TLArrowPoint
|
end: TLArrowPoint
|
||||||
|
@ -30,6 +32,7 @@ export type TLArrowInfo =
|
||||||
isValid: boolean
|
isValid: boolean
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
bindings: TLArrowBindings
|
||||||
isStraight: true
|
isStraight: true
|
||||||
start: TLArrowPoint
|
start: TLArrowPoint
|
||||||
end: TLArrowPoint
|
end: TLArrowPoint
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
BOUND_ARROW_OFFSET,
|
BOUND_ARROW_OFFSET,
|
||||||
MIN_ARROW_LENGTH,
|
MIN_ARROW_LENGTH,
|
||||||
STROKE_SIZES,
|
STROKE_SIZES,
|
||||||
|
TLArrowBindings,
|
||||||
WAY_TOO_BIG_ARROW_BEND_FACTOR,
|
WAY_TOO_BIG_ARROW_BEND_FACTOR,
|
||||||
getArrowTerminalsInArrowSpace,
|
getArrowTerminalsInArrowSpace,
|
||||||
getBoundShapeInfoForTerminal,
|
getBoundShapeInfoForTerminal,
|
||||||
|
@ -25,16 +26,16 @@ import { getStraightArrowInfo } from './straight-arrow'
|
||||||
export function getCurvedArrowInfo(
|
export function getCurvedArrowInfo(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
shape: TLArrowShape,
|
shape: TLArrowShape,
|
||||||
extraBend = 0
|
bindings: TLArrowBindings
|
||||||
): TLArrowInfo {
|
): TLArrowInfo {
|
||||||
const { arrowheadEnd, arrowheadStart } = shape.props
|
const { arrowheadEnd, arrowheadStart } = shape.props
|
||||||
const bend = shape.props.bend + extraBend
|
const bend = shape.props.bend
|
||||||
|
|
||||||
if (Math.abs(bend) > Math.abs(shape.props.bend * WAY_TOO_BIG_ARROW_BEND_FACTOR)) {
|
if (Math.abs(bend) > Math.abs(shape.props.bend * WAY_TOO_BIG_ARROW_BEND_FACTOR)) {
|
||||||
return getStraightArrowInfo(editor, shape)
|
return getStraightArrowInfo(editor, shape, bindings)
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(editor, shape)
|
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(editor, shape, bindings)
|
||||||
|
|
||||||
const med = Vec.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end) // point between start and end
|
const med = Vec.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end) // point between start and end
|
||||||
const distance = Vec.Sub(terminalsInArrowSpace.end, terminalsInArrowSpace.start)
|
const distance = Vec.Sub(terminalsInArrowSpace.end, terminalsInArrowSpace.start)
|
||||||
|
@ -42,8 +43,8 @@ export function getCurvedArrowInfo(
|
||||||
const u = Vec.Len(distance) ? distance.uni() : Vec.From(distance) // unit vector between start and end
|
const u = Vec.Len(distance) ? distance.uni() : Vec.From(distance) // unit vector between start and end
|
||||||
const middle = Vec.Add(med, u.per().mul(-bend)) // middle handle
|
const middle = Vec.Add(med, u.per().mul(-bend)) // middle handle
|
||||||
|
|
||||||
const startShapeInfo = getBoundShapeInfoForTerminal(editor, shape.props.start)
|
const startShapeInfo = getBoundShapeInfoForTerminal(editor, shape, 'start')
|
||||||
const endShapeInfo = getBoundShapeInfoForTerminal(editor, shape.props.end)
|
const endShapeInfo = getBoundShapeInfoForTerminal(editor, shape, 'end')
|
||||||
|
|
||||||
// The positions of the body of the arrow, which may be different
|
// 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
|
// than the arrow's start / end points if the arrow is bound to shapes
|
||||||
|
@ -53,6 +54,7 @@ export function getCurvedArrowInfo(
|
||||||
|
|
||||||
if (Vec.Equals(a, b)) {
|
if (Vec.Equals(a, b)) {
|
||||||
return {
|
return {
|
||||||
|
bindings,
|
||||||
isStraight: true,
|
isStraight: true,
|
||||||
start: {
|
start: {
|
||||||
handle: a,
|
handle: a,
|
||||||
|
@ -84,7 +86,7 @@ export function getCurvedArrowInfo(
|
||||||
!isSafeFloat(handleArc.length) ||
|
!isSafeFloat(handleArc.length) ||
|
||||||
!isSafeFloat(handleArc.size)
|
!isSafeFloat(handleArc.size)
|
||||||
) {
|
) {
|
||||||
return getStraightArrowInfo(editor, shape)
|
return getStraightArrowInfo(editor, shape, bindings)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempA = a.clone()
|
const tempA = a.clone()
|
||||||
|
@ -341,6 +343,7 @@ export function getCurvedArrowInfo(
|
||||||
const bodyArc = getArcInfo(a, b, c)
|
const bodyArc = getArcInfo(a, b, c)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
bindings,
|
||||||
isStraight: false,
|
isStraight: false,
|
||||||
start: {
|
start: {
|
||||||
point: a,
|
point: a,
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { TLArrowShape, TLArrowShapeTerminal, TLShape, TLShapeId } from '@tldraw/tlschema'
|
import {
|
||||||
|
TLArrowBinding,
|
||||||
|
TLArrowBindingProps,
|
||||||
|
TLArrowShape,
|
||||||
|
TLShape,
|
||||||
|
TLShapeId,
|
||||||
|
} from '@tldraw/tlschema'
|
||||||
import { Mat } from '../../../../primitives/Mat'
|
import { Mat } from '../../../../primitives/Mat'
|
||||||
import { Vec } from '../../../../primitives/Vec'
|
import { Vec } from '../../../../primitives/Vec'
|
||||||
import { Group2d } from '../../../../primitives/geometry/Group2d'
|
import { Group2d } from '../../../../primitives/geometry/Group2d'
|
||||||
|
@ -19,16 +25,18 @@ export type BoundShapeInfo<T extends TLShape = TLShape> = {
|
||||||
|
|
||||||
export function getBoundShapeInfoForTerminal(
|
export function getBoundShapeInfoForTerminal(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
terminal: TLArrowShapeTerminal
|
arrow: TLArrowShape,
|
||||||
|
terminalName: 'start' | 'end'
|
||||||
): BoundShapeInfo | undefined {
|
): BoundShapeInfo | undefined {
|
||||||
if (terminal.type === 'point') {
|
const binding = editor
|
||||||
return
|
.getBindingsFromShape<TLArrowBinding>(arrow, 'arrow')
|
||||||
}
|
.find((b) => b.props.terminal === terminalName)
|
||||||
|
if (!binding) return
|
||||||
|
|
||||||
const shape = editor.getShape(terminal.boundShapeId)
|
const boundShape = editor.getShape(binding.toId)!
|
||||||
if (!shape) return
|
if (!boundShape) return
|
||||||
const transform = editor.getShapePageTransform(shape)
|
const transform = editor.getShapePageTransform(boundShape)!
|
||||||
const geometry = editor.getShapeGeometry(shape)
|
const geometry = editor.getShapeGeometry(boundShape)
|
||||||
|
|
||||||
// This is hacky: we're only looking at the first child in the group. Really the arrow should
|
// This is hacky: we're only looking at the first child in the group. Really the arrow should
|
||||||
// consider all items in the group which are marked as snappable as separate polygons with which
|
// consider all items in the group which are marked as snappable as separate polygons with which
|
||||||
|
@ -37,10 +45,10 @@ export function getBoundShapeInfoForTerminal(
|
||||||
const outline = geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
|
const outline = geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shape,
|
shape: boundShape,
|
||||||
transform,
|
transform,
|
||||||
isClosed: geometry.isClosed,
|
isClosed: geometry.isClosed,
|
||||||
isExact: terminal.isExact,
|
isExact: binding.props.isExact,
|
||||||
didIntersect: false,
|
didIntersect: false,
|
||||||
outline,
|
outline,
|
||||||
}
|
}
|
||||||
|
@ -49,14 +57,10 @@ export function getBoundShapeInfoForTerminal(
|
||||||
function getArrowTerminalInArrowSpace(
|
function getArrowTerminalInArrowSpace(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
arrowPageTransform: Mat,
|
arrowPageTransform: Mat,
|
||||||
terminal: TLArrowShapeTerminal,
|
binding: TLArrowBinding,
|
||||||
forceImprecise: boolean
|
forceImprecise: boolean
|
||||||
) {
|
) {
|
||||||
if (terminal.type === 'point') {
|
const boundShape = editor.getShape(binding.toId)
|
||||||
return Vec.From(terminal)
|
|
||||||
}
|
|
||||||
|
|
||||||
const boundShape = editor.getShape(terminal.boundShapeId)
|
|
||||||
|
|
||||||
if (!boundShape) {
|
if (!boundShape) {
|
||||||
// this can happen in multiplayer contexts where the shape is being deleted
|
// this can happen in multiplayer contexts where the shape is being deleted
|
||||||
|
@ -70,7 +74,9 @@ function getArrowTerminalInArrowSpace(
|
||||||
point,
|
point,
|
||||||
Vec.MulV(
|
Vec.MulV(
|
||||||
// if the parent is the bound shape, then it's ALWAYS precise
|
// if the parent is the bound shape, then it's ALWAYS precise
|
||||||
terminal.isPrecise || forceImprecise ? terminal.normalizedAnchor : { x: 0.5, y: 0.5 },
|
binding.props.isPrecise || forceImprecise
|
||||||
|
? binding.props.normalizedAnchor
|
||||||
|
: { x: 0.5, y: 0.5 },
|
||||||
size
|
size
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -81,40 +87,108 @@ function getArrowTerminalInArrowSpace(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape) {
|
export interface TLArrowBindings {
|
||||||
const arrowPageTransform = editor.getShapePageTransform(shape)!
|
start: TLArrowBinding | undefined
|
||||||
|
end: TLArrowBinding | undefined
|
||||||
|
}
|
||||||
|
|
||||||
let startBoundShapeId: TLShapeId | undefined
|
/** @public */
|
||||||
let endBoundShapeId: TLShapeId | undefined
|
export function getArrowBindings(editor: Editor, shape: TLArrowShape): TLArrowBindings {
|
||||||
|
const bindings = editor.getBindingsFromShape<TLArrowBinding>(shape, 'arrow')
|
||||||
if (shape.props.start.type === 'binding' && shape.props.end.type === 'binding') {
|
return {
|
||||||
startBoundShapeId = shape.props.start.boundShapeId
|
start: bindings.find((b) => b.props.terminal === 'start'),
|
||||||
endBoundShapeId = shape.props.end.boundShapeId
|
end: bindings.find((b) => b.props.terminal === 'end'),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function getArrowTerminalsInArrowSpace(
|
||||||
|
editor: Editor,
|
||||||
|
shape: TLArrowShape,
|
||||||
|
bindings: TLArrowBindings
|
||||||
|
) {
|
||||||
|
const arrowPageTransform = editor.getShapePageTransform(shape)!
|
||||||
|
|
||||||
const boundShapeRelationships = getBoundShapeRelationships(
|
const boundShapeRelationships = getBoundShapeRelationships(
|
||||||
editor,
|
editor,
|
||||||
startBoundShapeId,
|
bindings.start?.toId,
|
||||||
endBoundShapeId
|
bindings.end?.toId
|
||||||
)
|
)
|
||||||
|
|
||||||
const start = getArrowTerminalInArrowSpace(
|
const start = bindings.start
|
||||||
editor,
|
? getArrowTerminalInArrowSpace(
|
||||||
arrowPageTransform,
|
editor,
|
||||||
shape.props.start,
|
arrowPageTransform,
|
||||||
boundShapeRelationships === 'double-bound' || boundShapeRelationships === 'start-contains-end'
|
bindings.start,
|
||||||
)
|
boundShapeRelationships === 'double-bound' ||
|
||||||
|
boundShapeRelationships === 'start-contains-end'
|
||||||
|
)
|
||||||
|
: Vec.From(shape.props.start)
|
||||||
|
|
||||||
const end = getArrowTerminalInArrowSpace(
|
const end = bindings.end
|
||||||
editor,
|
? getArrowTerminalInArrowSpace(
|
||||||
arrowPageTransform,
|
editor,
|
||||||
shape.props.end,
|
arrowPageTransform,
|
||||||
boundShapeRelationships === 'double-bound' || boundShapeRelationships === 'end-contains-start'
|
bindings.end,
|
||||||
)
|
boundShapeRelationships === 'double-bound' ||
|
||||||
|
boundShapeRelationships === 'end-contains-start'
|
||||||
|
)
|
||||||
|
: Vec.From(shape.props.end)
|
||||||
|
|
||||||
return { start, end }
|
return { start, end }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update the arrow binding for a particular arrow terminal. Will clear up if needed.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function createOrUpdateArrowBinding(
|
||||||
|
editor: Editor,
|
||||||
|
arrow: TLArrowShape | TLShapeId,
|
||||||
|
target: TLShape | TLShapeId,
|
||||||
|
props: TLArrowBindingProps
|
||||||
|
) {
|
||||||
|
const arrowId = typeof arrow === 'string' ? arrow : arrow.id
|
||||||
|
const targetId = typeof target === 'string' ? target : target.id
|
||||||
|
|
||||||
|
const existingMany = editor
|
||||||
|
.getBindingsFromShape<TLArrowBinding>(arrowId, 'arrow')
|
||||||
|
.filter((b) => b.props.terminal === props.terminal)
|
||||||
|
|
||||||
|
// if we've somehow ended up with too many bindings, delete the extras
|
||||||
|
if (existingMany.length > 1) {
|
||||||
|
editor.deleteBindings(existingMany.slice(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = existingMany[0]
|
||||||
|
if (existing) {
|
||||||
|
editor.updateBinding({
|
||||||
|
...existing,
|
||||||
|
toId: targetId,
|
||||||
|
props,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
editor.createBinding({
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrowId,
|
||||||
|
toId: targetId,
|
||||||
|
props,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove any arrow bindings for a particular terminal.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function removeArrowBinding(editor: Editor, arrow: TLArrowShape, terminal: 'start' | 'end') {
|
||||||
|
const existing = editor
|
||||||
|
.getBindingsFromShape<TLArrowBinding>(arrow, 'arrow')
|
||||||
|
.filter((b) => b.props.terminal === terminal)
|
||||||
|
|
||||||
|
editor.deleteBindings(existing)
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const MIN_ARROW_LENGTH = 10
|
export const MIN_ARROW_LENGTH = 10
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
|
|
@ -12,15 +12,20 @@ import {
|
||||||
BoundShapeInfo,
|
BoundShapeInfo,
|
||||||
MIN_ARROW_LENGTH,
|
MIN_ARROW_LENGTH,
|
||||||
STROKE_SIZES,
|
STROKE_SIZES,
|
||||||
|
TLArrowBindings,
|
||||||
getArrowTerminalsInArrowSpace,
|
getArrowTerminalsInArrowSpace,
|
||||||
getBoundShapeInfoForTerminal,
|
getBoundShapeInfoForTerminal,
|
||||||
getBoundShapeRelationships,
|
getBoundShapeRelationships,
|
||||||
} from './shared'
|
} from './shared'
|
||||||
|
|
||||||
export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArrowInfo {
|
export function getStraightArrowInfo(
|
||||||
const { start, end, arrowheadStart, arrowheadEnd } = shape.props
|
editor: Editor,
|
||||||
|
shape: TLArrowShape,
|
||||||
|
bindings: TLArrowBindings
|
||||||
|
): TLArrowInfo {
|
||||||
|
const { arrowheadStart, arrowheadEnd } = shape.props
|
||||||
|
|
||||||
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(editor, shape)
|
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(editor, shape, bindings)
|
||||||
|
|
||||||
const a = terminalsInArrowSpace.start.clone()
|
const a = terminalsInArrowSpace.start.clone()
|
||||||
const b = terminalsInArrowSpace.end.clone()
|
const b = terminalsInArrowSpace.end.clone()
|
||||||
|
@ -28,6 +33,7 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
|
||||||
|
|
||||||
if (Vec.Equals(a, b)) {
|
if (Vec.Equals(a, b)) {
|
||||||
return {
|
return {
|
||||||
|
bindings,
|
||||||
isStraight: true,
|
isStraight: true,
|
||||||
start: {
|
start: {
|
||||||
handle: a,
|
handle: a,
|
||||||
|
@ -49,8 +55,8 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
|
||||||
|
|
||||||
// Update the arrowhead points using intersections with the bound shapes, if any.
|
// Update the arrowhead points using intersections with the bound shapes, if any.
|
||||||
|
|
||||||
const startShapeInfo = getBoundShapeInfoForTerminal(editor, start)
|
const startShapeInfo = getBoundShapeInfoForTerminal(editor, shape, 'start')
|
||||||
const endShapeInfo = getBoundShapeInfoForTerminal(editor, end)
|
const endShapeInfo = getBoundShapeInfoForTerminal(editor, shape, 'end')
|
||||||
|
|
||||||
const arrowPageTransform = editor.getShapePageTransform(shape)!
|
const arrowPageTransform = editor.getShapePageTransform(shape)!
|
||||||
|
|
||||||
|
@ -189,6 +195,7 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
|
||||||
const length = Vec.Dist(a, b)
|
const length = Vec.Dist(a, b)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
bindings,
|
||||||
isStraight: true,
|
isStraight: true,
|
||||||
start: {
|
start: {
|
||||||
handle: terminalsInArrowSpace.start,
|
handle: terminalsInArrowSpace.start,
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { SerializedSchema } from '@tldraw/store'
|
import { SerializedSchema } from '@tldraw/store'
|
||||||
import { TLAsset, TLShape, TLShapeId } from '@tldraw/tlschema'
|
import { TLAsset, TLBinding, TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export interface TLContent {
|
export interface TLContent {
|
||||||
shapes: TLShape[]
|
shapes: TLShape[]
|
||||||
|
bindings: TLBinding[] | undefined
|
||||||
rootShapeIds: TLShapeId[]
|
rootShapeIds: TLShapeId[]
|
||||||
assets: TLAsset[]
|
assets: TLAsset[]
|
||||||
schema: SerializedSchema
|
schema: SerializedSchema
|
||||||
|
|
|
@ -24,6 +24,7 @@ beforeEach(() => {
|
||||||
editor = new Editor({
|
editor = new Editor({
|
||||||
initialState: 'A',
|
initialState: 'A',
|
||||||
shapeUtils: [],
|
shapeUtils: [],
|
||||||
|
bindingUtils: [],
|
||||||
tools: [A, B, C],
|
tools: [A, B, C],
|
||||||
store: createTLStore({ shapeUtils: [] }),
|
store: createTLStore({ shapeUtils: [] }),
|
||||||
getContainer: () => document.body,
|
getContainer: () => document.body,
|
||||||
|
|
|
@ -6,6 +6,7 @@ let editor: Editor
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
editor = new Editor({
|
editor = new Editor({
|
||||||
shapeUtils: [],
|
shapeUtils: [],
|
||||||
|
bindingUtils: [],
|
||||||
tools: [],
|
tools: [],
|
||||||
store: createTLStore({ shapeUtils: [] }),
|
store: createTLStore({ shapeUtils: [] }),
|
||||||
getContainer: () => document.body,
|
getContainer: () => document.body,
|
||||||
|
|
|
@ -37,7 +37,7 @@ export type ComputedCache<Data, R extends UnknownRecord> = {
|
||||||
export function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R>;
|
export function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function createMigrationIds<ID extends string, Versions extends Record<string, number>>(sequenceId: ID, versions: Versions): {
|
export function createMigrationIds<const ID extends string, const Versions extends Record<string, number>>(sequenceId: ID, versions: Versions): {
|
||||||
[K in keyof Versions]: `${ID}/${Versions[K]}`;
|
[K in keyof Versions]: `${ID}/${Versions[K]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -265,6 +265,11 @@ export function squashRecordDiffs<T extends UnknownRecord>(diffs: RecordsDiff<T>
|
||||||
// @internal
|
// @internal
|
||||||
export function squashRecordDiffsMutable<T extends UnknownRecord>(target: RecordsDiff<T>, diffs: RecordsDiff<T>[]): void;
|
export function squashRecordDiffsMutable<T extends UnknownRecord>(target: RecordsDiff<T>, diffs: RecordsDiff<T>[]): void;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type StandaloneDependsOn = {
|
||||||
|
readonly dependsOn: readonly MigrationId[];
|
||||||
|
};
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
constructor(config: {
|
constructor(config: {
|
||||||
|
|
|
@ -43,5 +43,6 @@ export {
|
||||||
type MigrationId,
|
type MigrationId,
|
||||||
type MigrationResult,
|
type MigrationResult,
|
||||||
type MigrationSequence,
|
type MigrationSequence,
|
||||||
|
type StandaloneDependsOn,
|
||||||
} from './lib/migrate'
|
} from './lib/migrate'
|
||||||
export type { AllRecords } from './lib/type-utils'
|
export type { AllRecords } from './lib/type-utils'
|
||||||
|
|
|
@ -91,10 +91,10 @@ export function createMigrationSequence({
|
||||||
* @public
|
* @public
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function createMigrationIds<ID extends string, Versions extends Record<string, number>>(
|
export function createMigrationIds<
|
||||||
sequenceId: ID,
|
const ID extends string,
|
||||||
versions: Versions
|
const Versions extends Record<string, number>,
|
||||||
): { [K in keyof Versions]: `${ID}/${Versions[K]}` } {
|
>(sequenceId: ID, versions: Versions): { [K in keyof Versions]: `${ID}/${Versions[K]}` } {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
objectMapEntries(versions).map(([key, version]) => [key, `${sequenceId}/${version}`] as const)
|
objectMapEntries(versions).map(([key, version]) => [key, `${sequenceId}/${version}`] as const)
|
||||||
) as any
|
) as any
|
||||||
|
@ -136,6 +136,7 @@ export type LegacyMigration<Before = any, After = any> = {
|
||||||
/** @public */
|
/** @public */
|
||||||
export type MigrationId = `${string}/${number}`
|
export type MigrationId = `${string}/${number}`
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export type StandaloneDependsOn = {
|
export type StandaloneDependsOn = {
|
||||||
readonly dependsOn: readonly MigrationId[]
|
readonly dependsOn: readonly MigrationId[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@ import { MemoExoticComponent } from 'react';
|
||||||
import { MigrationFailureReason } from '@tldraw/editor';
|
import { MigrationFailureReason } from '@tldraw/editor';
|
||||||
import { MigrationSequence } from '@tldraw/editor';
|
import { MigrationSequence } from '@tldraw/editor';
|
||||||
import { NamedExoticComponent } from 'react';
|
import { NamedExoticComponent } from 'react';
|
||||||
import { ObjectValidator } from '@tldraw/editor';
|
|
||||||
import { Polygon2d } from '@tldraw/editor';
|
import { Polygon2d } from '@tldraw/editor';
|
||||||
import { Polyline2d } from '@tldraw/editor';
|
import { Polyline2d } from '@tldraw/editor';
|
||||||
import { default as React_2 } from 'react';
|
import { default as React_2 } from 'react';
|
||||||
|
@ -54,6 +53,7 @@ import { StoreSnapshot } from '@tldraw/editor';
|
||||||
import { StyleProp } from '@tldraw/editor';
|
import { StyleProp } from '@tldraw/editor';
|
||||||
import { SvgExportContext } from '@tldraw/editor';
|
import { SvgExportContext } from '@tldraw/editor';
|
||||||
import { T } from '@tldraw/editor';
|
import { T } from '@tldraw/editor';
|
||||||
|
import { TLAnyBindingUtilConstructor } from '@tldraw/editor';
|
||||||
import { TLAnyShapeUtilConstructor } from '@tldraw/editor';
|
import { TLAnyShapeUtilConstructor } from '@tldraw/editor';
|
||||||
import { TLArrowShape } from '@tldraw/editor';
|
import { TLArrowShape } from '@tldraw/editor';
|
||||||
import { TLAssetId } from '@tldraw/editor';
|
import { TLAssetId } from '@tldraw/editor';
|
||||||
|
@ -95,6 +95,7 @@ import { TLOnDoubleClickHandler } from '@tldraw/editor';
|
||||||
import { TLOnEditEndHandler } from '@tldraw/editor';
|
import { TLOnEditEndHandler } from '@tldraw/editor';
|
||||||
import { TLOnHandleDragHandler } from '@tldraw/editor';
|
import { TLOnHandleDragHandler } from '@tldraw/editor';
|
||||||
import { TLOnResizeHandler } from '@tldraw/editor';
|
import { TLOnResizeHandler } from '@tldraw/editor';
|
||||||
|
import { TLOnResizeStartHandler } from '@tldraw/editor';
|
||||||
import { TLOnTranslateHandler } from '@tldraw/editor';
|
import { TLOnTranslateHandler } from '@tldraw/editor';
|
||||||
import { TLOnTranslateStartHandler } from '@tldraw/editor';
|
import { TLOnTranslateStartHandler } from '@tldraw/editor';
|
||||||
import { TLPageId } from '@tldraw/editor';
|
import { TLPageId } from '@tldraw/editor';
|
||||||
|
@ -102,6 +103,7 @@ import { TLParentId } from '@tldraw/editor';
|
||||||
import { TLPointerEvent } from '@tldraw/editor';
|
import { TLPointerEvent } from '@tldraw/editor';
|
||||||
import { TLPointerEventInfo } from '@tldraw/editor';
|
import { TLPointerEventInfo } from '@tldraw/editor';
|
||||||
import { TLPointerEventName } from '@tldraw/editor';
|
import { TLPointerEventName } from '@tldraw/editor';
|
||||||
|
import { TLPropsMigrations } from '@tldraw/editor';
|
||||||
import { TLRecord } from '@tldraw/editor';
|
import { TLRecord } from '@tldraw/editor';
|
||||||
import { TLRotationSnapshot } from '@tldraw/editor';
|
import { TLRotationSnapshot } from '@tldraw/editor';
|
||||||
import { TLSchema } from '@tldraw/editor';
|
import { TLSchema } from '@tldraw/editor';
|
||||||
|
@ -112,7 +114,6 @@ import { TLSelectionHandle } from '@tldraw/editor';
|
||||||
import { TLShape } from '@tldraw/editor';
|
import { TLShape } from '@tldraw/editor';
|
||||||
import { TLShapeId } from '@tldraw/editor';
|
import { TLShapeId } from '@tldraw/editor';
|
||||||
import { TLShapePartial } from '@tldraw/editor';
|
import { TLShapePartial } from '@tldraw/editor';
|
||||||
import { TLShapePropsMigrations } from '@tldraw/editor';
|
|
||||||
import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
|
import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
|
||||||
import { TLShapeUtilFlag } from '@tldraw/editor';
|
import { TLShapeUtilFlag } from '@tldraw/editor';
|
||||||
import { TLStore } from '@tldraw/editor';
|
import { TLStore } from '@tldraw/editor';
|
||||||
|
@ -121,7 +122,6 @@ import { TLSvgOptions } from '@tldraw/editor';
|
||||||
import { TLTextShape } from '@tldraw/editor';
|
import { TLTextShape } from '@tldraw/editor';
|
||||||
import { TLUnknownShape } from '@tldraw/editor';
|
import { TLUnknownShape } from '@tldraw/editor';
|
||||||
import { TLVideoShape } from '@tldraw/editor';
|
import { TLVideoShape } from '@tldraw/editor';
|
||||||
import { UnionValidator } from '@tldraw/editor';
|
|
||||||
import { UnknownRecord } from '@tldraw/editor';
|
import { UnknownRecord } from '@tldraw/editor';
|
||||||
import { Validator } from '@tldraw/editor';
|
import { Validator } from '@tldraw/editor';
|
||||||
import { Vec } from '@tldraw/editor';
|
import { Vec } from '@tldraw/editor';
|
||||||
|
@ -165,6 +165,8 @@ export class ArrowShapeTool extends StateNode {
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
// (undocumented)
|
||||||
|
canBeLaidOut: TLShapeUtilFlag<TLArrowShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
canBind: () => boolean;
|
canBind: () => boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -192,7 +194,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLArrowShape): JSX_2.Element | null;
|
indicator(shape: TLArrowShape): JSX_2.Element | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: MigrationSequence;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onDoubleClickHandle: (shape: TLArrowShape, handle: TLHandle) => TLShapePartial<TLArrowShape> | void;
|
onDoubleClickHandle: (shape: TLArrowShape, handle: TLHandle) => TLShapePartial<TLArrowShape> | void;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -202,6 +204,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onResize: TLOnResizeHandler<TLArrowShape>;
|
onResize: TLOnResizeHandler<TLArrowShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
onResizeStart?: TLOnResizeStartHandler<TLArrowShape>;
|
||||||
|
// (undocumented)
|
||||||
onTranslate?: TLOnTranslateHandler<TLArrowShape>;
|
onTranslate?: TLOnTranslateHandler<TLArrowShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onTranslateStart: TLOnTranslateStartHandler<TLArrowShape>;
|
onTranslateStart: TLOnTranslateStartHandler<TLArrowShape>;
|
||||||
|
@ -212,39 +216,13 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
bend: Validator<number>;
|
bend: Validator<number>;
|
||||||
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
end: UnionValidator<"type", {
|
end: Validator<VecModel>;
|
||||||
binding: ObjectValidator< {
|
|
||||||
boundShapeId: TLShapeId;
|
|
||||||
isExact: boolean;
|
|
||||||
isPrecise: boolean;
|
|
||||||
normalizedAnchor: VecModel;
|
|
||||||
type: "binding";
|
|
||||||
}>;
|
|
||||||
point: ObjectValidator< {
|
|
||||||
type: "point";
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}>;
|
|
||||||
}, never>;
|
|
||||||
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
||||||
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
||||||
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
labelPosition: Validator<number>;
|
labelPosition: Validator<number>;
|
||||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||||
start: UnionValidator<"type", {
|
start: Validator<VecModel>;
|
||||||
binding: ObjectValidator< {
|
|
||||||
boundShapeId: TLShapeId;
|
|
||||||
isExact: boolean;
|
|
||||||
isPrecise: boolean;
|
|
||||||
normalizedAnchor: VecModel;
|
|
||||||
type: "binding";
|
|
||||||
}>;
|
|
||||||
point: ObjectValidator< {
|
|
||||||
type: "point";
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}>;
|
|
||||||
}, never>;
|
|
||||||
text: Validator<string>;
|
text: Validator<string>;
|
||||||
};
|
};
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -281,7 +259,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLBookmarkShape): JSX_2.Element;
|
indicator(shape: TLBookmarkShape): JSX_2.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onBeforeCreate?: TLOnBeforeCreateHandler<TLBookmarkShape>;
|
onBeforeCreate?: TLOnBeforeCreateHandler<TLBookmarkShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -360,6 +338,9 @@ export const DefaultActionsMenu: NamedExoticComponent<TLUiActionsMenuProps>;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function DefaultActionsMenuContent(): JSX_2.Element;
|
export function DefaultActionsMenuContent(): JSX_2.Element;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const defaultBindingUtils: TLAnyBindingUtilConstructor[];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
const DefaultContextMenu: NamedExoticComponent<TLUiContextMenuProps>;
|
const DefaultContextMenu: NamedExoticComponent<TLUiContextMenuProps>;
|
||||||
export { DefaultContextMenu as ContextMenu }
|
export { DefaultContextMenu as ContextMenu }
|
||||||
|
@ -492,7 +473,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLDrawShape): JSX_2.Element;
|
indicator(shape: TLDrawShape): JSX_2.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onResize: TLOnResizeHandler<TLDrawShape>;
|
onResize: TLOnResizeHandler<TLDrawShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -549,7 +530,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isAspectRatioLocked: TLShapeUtilFlag<TLEmbedShape>;
|
isAspectRatioLocked: TLShapeUtilFlag<TLEmbedShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onResize: TLOnResizeHandler<TLEmbedShape>;
|
onResize: TLOnResizeHandler<TLEmbedShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -656,7 +637,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLFrameShape): JSX_2.Element;
|
indicator(shape: TLFrameShape): JSX_2.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onDragShapesOut: (_shape: TLFrameShape, shapes: TLShape[]) => void;
|
onDragShapesOut: (_shape: TLFrameShape, shapes: TLShape[]) => void;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -709,7 +690,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLGeoShape): JSX_2.Element;
|
indicator(shape: TLGeoShape): JSX_2.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onBeforeCreate: (shape: TLGeoShape) => {
|
onBeforeCreate: (shape: TLGeoShape) => {
|
||||||
id: TLShapeId;
|
id: TLShapeId;
|
||||||
|
@ -923,7 +904,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLHighlightShape): JSX_2.Element;
|
indicator(shape: TLHighlightShape): JSX_2.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onResize: TLOnResizeHandler<TLHighlightShape>;
|
onResize: TLOnResizeHandler<TLHighlightShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -961,7 +942,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isAspectRatioLocked: () => boolean;
|
isAspectRatioLocked: () => boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onDoubleClick: (shape: TLImageShape) => void;
|
onDoubleClick: (shape: TLImageShape) => void;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -1065,7 +1046,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLLineShape): JSX_2.Element;
|
indicator(shape: TLLineShape): JSX_2.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onHandleDrag: TLOnHandleDragHandler<TLLineShape>;
|
onHandleDrag: TLOnHandleDragHandler<TLLineShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -1129,7 +1110,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLNoteShape): JSX_2.Element;
|
indicator(shape: TLNoteShape): JSX_2.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onBeforeCreate: (next: TLNoteShape) => {
|
onBeforeCreate: (next: TLNoteShape) => {
|
||||||
id: TLShapeId;
|
id: TLShapeId;
|
||||||
|
@ -1376,7 +1357,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isAspectRatioLocked: TLShapeUtilFlag<TLTextShape>;
|
isAspectRatioLocked: TLShapeUtilFlag<TLTextShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onBeforeCreate: (shape: TLTextShape) => {
|
onBeforeCreate: (shape: TLTextShape) => {
|
||||||
id: TLShapeId;
|
id: TLShapeId;
|
||||||
|
@ -1496,6 +1477,7 @@ export function TldrawHandles({ children }: TLHandlesProps): JSX_2.Element | nul
|
||||||
// @public
|
// @public
|
||||||
export const TldrawImage: NamedExoticComponent< {
|
export const TldrawImage: NamedExoticComponent< {
|
||||||
background?: boolean | undefined;
|
background?: boolean | undefined;
|
||||||
|
bindingUtils?: readonly TLAnyBindingUtilConstructor[] | undefined;
|
||||||
bounds?: Box | undefined;
|
bounds?: Box | undefined;
|
||||||
darkMode?: boolean | undefined;
|
darkMode?: boolean | undefined;
|
||||||
format?: "png" | "svg" | undefined;
|
format?: "png" | "svg" | undefined;
|
||||||
|
@ -1509,6 +1491,7 @@ snapshot: StoreSnapshot<TLRecord>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export type TldrawImageProps = Expand<{
|
export type TldrawImageProps = Expand<{
|
||||||
|
bindingUtils?: readonly TLAnyBindingUtilConstructor[];
|
||||||
shapeUtils?: readonly TLAnyShapeUtilConstructor[];
|
shapeUtils?: readonly TLAnyShapeUtilConstructor[];
|
||||||
format?: 'png' | 'svg';
|
format?: 'png' | 'svg';
|
||||||
pageId?: TLPageId;
|
pageId?: TLPageId;
|
||||||
|
@ -2669,7 +2652,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isAspectRatioLocked: () => boolean;
|
isAspectRatioLocked: () => boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations: TLShapePropsMigrations;
|
static migrations: TLPropsMigrations;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static props: {
|
static props: {
|
||||||
assetId: Validator<TLAssetId | null>;
|
assetId: Validator<TLAssetId | null>;
|
||||||
|
|
|
@ -12,6 +12,7 @@ export { TldrawHandles } from './lib/canvas/TldrawHandles'
|
||||||
export { TldrawScribble } from './lib/canvas/TldrawScribble'
|
export { TldrawScribble } from './lib/canvas/TldrawScribble'
|
||||||
export { TldrawSelectionBackground } from './lib/canvas/TldrawSelectionBackground'
|
export { TldrawSelectionBackground } from './lib/canvas/TldrawSelectionBackground'
|
||||||
export { TldrawSelectionForeground } from './lib/canvas/TldrawSelectionForeground'
|
export { TldrawSelectionForeground } from './lib/canvas/TldrawSelectionForeground'
|
||||||
|
export { defaultBindingUtils } from './lib/defaultBindingUtils'
|
||||||
export { defaultShapeTools } from './lib/defaultShapeTools'
|
export { defaultShapeTools } from './lib/defaultShapeTools'
|
||||||
export { defaultShapeUtils } from './lib/defaultShapeUtils'
|
export { defaultShapeUtils } from './lib/defaultShapeUtils'
|
||||||
export { defaultTools } from './lib/defaultTools'
|
export { defaultTools } from './lib/defaultTools'
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { TldrawHandles } from './canvas/TldrawHandles'
|
||||||
import { TldrawScribble } from './canvas/TldrawScribble'
|
import { TldrawScribble } from './canvas/TldrawScribble'
|
||||||
import { TldrawSelectionBackground } from './canvas/TldrawSelectionBackground'
|
import { TldrawSelectionBackground } from './canvas/TldrawSelectionBackground'
|
||||||
import { TldrawSelectionForeground } from './canvas/TldrawSelectionForeground'
|
import { TldrawSelectionForeground } from './canvas/TldrawSelectionForeground'
|
||||||
|
import { defaultBindingUtils } from './defaultBindingUtils'
|
||||||
import {
|
import {
|
||||||
TLExternalContentProps,
|
TLExternalContentProps,
|
||||||
registerDefaultExternalContentHandlers,
|
registerDefaultExternalContentHandlers,
|
||||||
|
@ -79,6 +80,7 @@ export function Tldraw(props: TldrawProps) {
|
||||||
onMount,
|
onMount,
|
||||||
components = {},
|
components = {},
|
||||||
shapeUtils = [],
|
shapeUtils = [],
|
||||||
|
bindingUtils = [],
|
||||||
tools = [],
|
tools = [],
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props
|
||||||
|
@ -102,6 +104,12 @@ export function Tldraw(props: TldrawProps) {
|
||||||
[_shapeUtils]
|
[_shapeUtils]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const _bindingUtils = useShallowArrayIdentity(bindingUtils)
|
||||||
|
const bindingUtilsWithDefaults = useMemo(
|
||||||
|
() => [...defaultBindingUtils, ..._bindingUtils],
|
||||||
|
[_bindingUtils]
|
||||||
|
)
|
||||||
|
|
||||||
const _tools = useShallowArrayIdentity(tools)
|
const _tools = useShallowArrayIdentity(tools)
|
||||||
const toolsWithDefaults = useMemo(
|
const toolsWithDefaults = useMemo(
|
||||||
() => [...defaultTools, ...defaultShapeTools, ..._tools],
|
() => [...defaultTools, ...defaultShapeTools, ..._tools],
|
||||||
|
@ -123,6 +131,7 @@ export function Tldraw(props: TldrawProps) {
|
||||||
{...rest}
|
{...rest}
|
||||||
components={componentsWithDefault}
|
components={componentsWithDefault}
|
||||||
shapeUtils={shapeUtilsWithDefaults}
|
shapeUtils={shapeUtilsWithDefaults}
|
||||||
|
bindingUtils={bindingUtilsWithDefaults}
|
||||||
tools={toolsWithDefaults}
|
tools={toolsWithDefaults}
|
||||||
>
|
>
|
||||||
<TldrawUi {...rest} components={componentsWithDefault}>
|
<TldrawUi {...rest} components={componentsWithDefault}>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
Expand,
|
Expand,
|
||||||
LoadingScreen,
|
LoadingScreen,
|
||||||
StoreSnapshot,
|
StoreSnapshot,
|
||||||
|
TLAnyBindingUtilConstructor,
|
||||||
TLAnyShapeUtilConstructor,
|
TLAnyShapeUtilConstructor,
|
||||||
TLPageId,
|
TLPageId,
|
||||||
TLRecord,
|
TLRecord,
|
||||||
|
@ -12,6 +13,7 @@ import {
|
||||||
useTLStore,
|
useTLStore,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { memo, useLayoutEffect, useMemo, useState } from 'react'
|
import { memo, useLayoutEffect, useMemo, useState } from 'react'
|
||||||
|
import { defaultBindingUtils } from './defaultBindingUtils'
|
||||||
import { defaultShapeUtils } from './defaultShapeUtils'
|
import { defaultShapeUtils } from './defaultShapeUtils'
|
||||||
import { usePreloadAssets } from './ui/hooks/usePreloadAssets'
|
import { usePreloadAssets } from './ui/hooks/usePreloadAssets'
|
||||||
import { getSvgAsImage } from './utils/export/export'
|
import { getSvgAsImage } from './utils/export/export'
|
||||||
|
@ -43,6 +45,10 @@ export type TldrawImageProps = Expand<
|
||||||
* Additional shape utils to use.
|
* Additional shape utils to use.
|
||||||
*/
|
*/
|
||||||
shapeUtils?: readonly TLAnyShapeUtilConstructor[]
|
shapeUtils?: readonly TLAnyShapeUtilConstructor[]
|
||||||
|
/**
|
||||||
|
* Additional binding utils to use.
|
||||||
|
*/
|
||||||
|
bindingUtils?: readonly TLAnyBindingUtilConstructor[]
|
||||||
} & Partial<TLSvgOptions>
|
} & Partial<TLSvgOptions>
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -69,6 +75,11 @@ export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
|
||||||
|
|
||||||
const shapeUtils = useShallowArrayIdentity(props.shapeUtils ?? [])
|
const shapeUtils = useShallowArrayIdentity(props.shapeUtils ?? [])
|
||||||
const shapeUtilsWithDefaults = useMemo(() => [...defaultShapeUtils, ...shapeUtils], [shapeUtils])
|
const shapeUtilsWithDefaults = useMemo(() => [...defaultShapeUtils, ...shapeUtils], [shapeUtils])
|
||||||
|
const bindingUtils = useShallowArrayIdentity(props.bindingUtils ?? [])
|
||||||
|
const bindingUtilsWithDefaults = useMemo(
|
||||||
|
() => [...defaultBindingUtils, ...bindingUtils],
|
||||||
|
[bindingUtils]
|
||||||
|
)
|
||||||
const store = useTLStore({ snapshot: props.snapshot, shapeUtils: shapeUtilsWithDefaults })
|
const store = useTLStore({ snapshot: props.snapshot, shapeUtils: shapeUtilsWithDefaults })
|
||||||
|
|
||||||
const assets = useDefaultEditorAssetsWithOverrides()
|
const assets = useDefaultEditorAssetsWithOverrides()
|
||||||
|
@ -98,7 +109,8 @@ export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
|
||||||
|
|
||||||
const editor = new Editor({
|
const editor = new Editor({
|
||||||
store,
|
store,
|
||||||
shapeUtils: shapeUtilsWithDefaults ?? [],
|
shapeUtils: shapeUtilsWithDefaults,
|
||||||
|
bindingUtils: bindingUtilsWithDefaults,
|
||||||
tools: [],
|
tools: [],
|
||||||
getContainer: () => tempElm,
|
getContainer: () => tempElm,
|
||||||
})
|
})
|
||||||
|
@ -152,6 +164,7 @@ export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
|
||||||
container,
|
container,
|
||||||
store,
|
store,
|
||||||
shapeUtilsWithDefaults,
|
shapeUtilsWithDefaults,
|
||||||
|
bindingUtilsWithDefaults,
|
||||||
pageId,
|
pageId,
|
||||||
bounds,
|
bounds,
|
||||||
scale,
|
scale,
|
||||||
|
|
224
packages/tldraw/src/lib/bindings/arrow/ArrowBindingUtil.ts
Normal file
224
packages/tldraw/src/lib/bindings/arrow/ArrowBindingUtil.ts
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
import {
|
||||||
|
BindingOnChangeOptions,
|
||||||
|
BindingOnCreateOptions,
|
||||||
|
BindingOnShapeChangeOptions,
|
||||||
|
BindingOnShapeDeleteOptions,
|
||||||
|
BindingUtil,
|
||||||
|
Editor,
|
||||||
|
IndexKey,
|
||||||
|
TLArrowBinding,
|
||||||
|
TLArrowBindingProps,
|
||||||
|
TLArrowShape,
|
||||||
|
TLParentId,
|
||||||
|
TLShape,
|
||||||
|
TLShapeId,
|
||||||
|
TLShapePartial,
|
||||||
|
Vec,
|
||||||
|
arrowBindingMigrations,
|
||||||
|
arrowBindingProps,
|
||||||
|
assert,
|
||||||
|
getArrowBindings,
|
||||||
|
getIndexAbove,
|
||||||
|
getIndexBetween,
|
||||||
|
intersectLineSegmentCircle,
|
||||||
|
removeArrowBinding,
|
||||||
|
} from '@tldraw/editor'
|
||||||
|
|
||||||
|
export class ArrowBindingUtil extends BindingUtil<TLArrowBinding> {
|
||||||
|
static override type = 'arrow'
|
||||||
|
|
||||||
|
static override props = arrowBindingProps
|
||||||
|
static override migrations = arrowBindingMigrations
|
||||||
|
|
||||||
|
override getDefaultProps(): Partial<TLArrowBindingProps> {
|
||||||
|
return {
|
||||||
|
isPrecise: false,
|
||||||
|
isExact: false,
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the binding itself changes
|
||||||
|
override onAfterCreate({ binding }: BindingOnCreateOptions<TLArrowBinding>): void {
|
||||||
|
arrowDidUpdate(this.editor, this.editor.getShape(binding.fromId) as TLArrowShape)
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the binding itself changes
|
||||||
|
override onAfterChange({ bindingAfter }: BindingOnChangeOptions<TLArrowBinding>): void {
|
||||||
|
arrowDidUpdate(this.editor, this.editor.getShape(bindingAfter.fromId) as TLArrowShape)
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the arrow itself changes
|
||||||
|
override onAfterChangeFromShape({
|
||||||
|
shapeAfter,
|
||||||
|
}: BindingOnShapeChangeOptions<TLArrowBinding>): void {
|
||||||
|
arrowDidUpdate(this.editor, shapeAfter as TLArrowShape)
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the shape an arrow is bound to changes
|
||||||
|
override onAfterChangeToShape({ binding }: BindingOnShapeChangeOptions<TLArrowBinding>): void {
|
||||||
|
reparentArrow(this.editor, binding.fromId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the shape the arrow is pointing to is deleted
|
||||||
|
override onBeforeDeleteToShape({ binding }: BindingOnShapeDeleteOptions<TLArrowBinding>): void {
|
||||||
|
const arrow = this.editor.getShape<TLArrowShape>(binding.fromId)
|
||||||
|
if (!arrow) return
|
||||||
|
unbindArrowTerminal(this.editor, arrow, binding.props.terminal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reparentArrow(editor: Editor, arrowId: TLShapeId) {
|
||||||
|
const arrow = editor.getShape<TLArrowShape>(arrowId)
|
||||||
|
if (!arrow) return
|
||||||
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
|
const { start, end } = bindings
|
||||||
|
const startShape = start ? editor.getShape(start.toId) : undefined
|
||||||
|
const endShape = end ? editor.getShape(end.toId) : undefined
|
||||||
|
|
||||||
|
const parentPageId = editor.getAncestorPageId(arrow)
|
||||||
|
if (!parentPageId) return
|
||||||
|
|
||||||
|
let nextParentId: TLParentId
|
||||||
|
if (startShape && endShape) {
|
||||||
|
// if arrow has two bindings, always parent arrow to closest common ancestor of the bindings
|
||||||
|
nextParentId = editor.findCommonAncestor([startShape, endShape]) ?? parentPageId
|
||||||
|
} else if (startShape || endShape) {
|
||||||
|
const bindingParentId = (startShape || endShape)?.parentId
|
||||||
|
// If the arrow and the shape that it is bound to have the same parent, then keep that parent
|
||||||
|
if (bindingParentId && bindingParentId === arrow.parentId) {
|
||||||
|
nextParentId = arrow.parentId
|
||||||
|
} else {
|
||||||
|
// if arrow has one binding, keep arrow on its own page
|
||||||
|
nextParentId = parentPageId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextParentId && nextParentId !== arrow.parentId) {
|
||||||
|
editor.reparentShapes([arrowId], nextParentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const reparentedArrow = editor.getShape<TLArrowShape>(arrowId)
|
||||||
|
if (!reparentedArrow) throw Error('no reparented arrow')
|
||||||
|
|
||||||
|
const startSibling = editor.getShapeNearestSibling(reparentedArrow, startShape)
|
||||||
|
const endSibling = editor.getShapeNearestSibling(reparentedArrow, endShape)
|
||||||
|
|
||||||
|
let highestSibling: TLShape | undefined
|
||||||
|
|
||||||
|
if (startSibling && endSibling) {
|
||||||
|
highestSibling = startSibling.index > endSibling.index ? startSibling : endSibling
|
||||||
|
} else if (startSibling && !endSibling) {
|
||||||
|
highestSibling = startSibling
|
||||||
|
} else if (endSibling && !startSibling) {
|
||||||
|
highestSibling = endSibling
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalIndex: IndexKey
|
||||||
|
|
||||||
|
const higherSiblings = editor
|
||||||
|
.getSortedChildIdsForParent(highestSibling.parentId)
|
||||||
|
.map((id) => editor.getShape(id)!)
|
||||||
|
.filter((sibling) => sibling.index > highestSibling!.index)
|
||||||
|
|
||||||
|
if (higherSiblings.length) {
|
||||||
|
// there are siblings above the highest bound sibling, we need to
|
||||||
|
// insert between them.
|
||||||
|
|
||||||
|
// if the next sibling is also a bound arrow though, we can end up
|
||||||
|
// all fighting for the same indexes. so lets find the next
|
||||||
|
// non-arrow sibling...
|
||||||
|
const nextHighestNonArrowSibling = higherSiblings.find((sibling) => sibling.type !== 'arrow')
|
||||||
|
|
||||||
|
if (
|
||||||
|
// ...then, if we're above the last shape we want to be above...
|
||||||
|
reparentedArrow.index > highestSibling.index &&
|
||||||
|
// ...but below the next non-arrow sibling...
|
||||||
|
(!nextHighestNonArrowSibling || reparentedArrow.index < nextHighestNonArrowSibling.index)
|
||||||
|
) {
|
||||||
|
// ...then we're already in the right place. no need to update!
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, we need to find the index between the highest sibling
|
||||||
|
// we want to be above, and the next highest sibling we want to be
|
||||||
|
// below:
|
||||||
|
finalIndex = getIndexBetween(highestSibling.index, higherSiblings[0].index)
|
||||||
|
} else {
|
||||||
|
// if there are no siblings above us, we can just get the next index:
|
||||||
|
finalIndex = getIndexAbove(highestSibling.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalIndex !== reparentedArrow.index) {
|
||||||
|
editor.updateShapes<TLArrowShape>([{ id: arrowId, type: 'arrow', index: finalIndex }])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrowDidUpdate(editor: Editor, arrow: TLArrowShape) {
|
||||||
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
|
// if the shape is an arrow and its bound shape is on another page
|
||||||
|
// or was deleted, unbind it
|
||||||
|
for (const handle of ['start', 'end'] as const) {
|
||||||
|
const binding = bindings[handle]
|
||||||
|
if (!binding) continue
|
||||||
|
const boundShape = editor.getShape(binding.toId)
|
||||||
|
const isShapeInSamePageAsArrow =
|
||||||
|
editor.getAncestorPageId(arrow) === editor.getAncestorPageId(boundShape)
|
||||||
|
if (!boundShape || !isShapeInSamePageAsArrow) {
|
||||||
|
unbindArrowTerminal(editor, arrow, handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// always check the arrow parents
|
||||||
|
reparentArrow(editor, arrow.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbindArrowTerminal(editor: Editor, arrow: TLArrowShape, terminal: 'start' | 'end') {
|
||||||
|
const info = editor.getArrowInfo(arrow)!
|
||||||
|
if (!info) {
|
||||||
|
throw new Error('expected arrow info')
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = {
|
||||||
|
id: arrow.id,
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
[terminal]: { x: info[terminal].point.x, y: info[terminal].point.y },
|
||||||
|
bend: arrow.props.bend,
|
||||||
|
},
|
||||||
|
} satisfies TLShapePartial<TLArrowShape>
|
||||||
|
|
||||||
|
// fix up the bend:
|
||||||
|
if (!info.isStraight) {
|
||||||
|
// find the new start/end points of the resulting arrow
|
||||||
|
const newStart = terminal === 'start' ? info.start.point : info.start.handle
|
||||||
|
const newEnd = terminal === 'end' ? info.end.point : info.end.handle
|
||||||
|
const newMidPoint = Vec.Med(newStart, newEnd)
|
||||||
|
|
||||||
|
// intersect a line segment perpendicular to the new arrow with the old arrow arc to
|
||||||
|
// find the new mid-point
|
||||||
|
const lineSegment = Vec.Sub(newStart, newEnd)
|
||||||
|
.per()
|
||||||
|
.uni()
|
||||||
|
.mul(info.handleArc.radius * 2 * Math.sign(arrow.props.bend))
|
||||||
|
|
||||||
|
// find the intersections with the old arrow arc:
|
||||||
|
const intersections = intersectLineSegmentCircle(
|
||||||
|
info.handleArc.center,
|
||||||
|
Vec.Add(newMidPoint, lineSegment),
|
||||||
|
info.handleArc.center,
|
||||||
|
info.handleArc.radius
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(intersections?.length === 1)
|
||||||
|
const bend = Vec.Dist(newMidPoint, intersections[0]) * Math.sign(arrow.props.bend)
|
||||||
|
update.props.bend = bend
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.updateShape(update)
|
||||||
|
removeArrowBinding(editor, arrow, terminal)
|
||||||
|
}
|
5
packages/tldraw/src/lib/defaultBindingUtils.ts
Normal file
5
packages/tldraw/src/lib/defaultBindingUtils.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { TLAnyBindingUtilConstructor } from '@tldraw/editor'
|
||||||
|
import { ArrowBindingUtil } from './bindings/arrow/ArrowBindingUtil'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const defaultBindingUtils: TLAnyBindingUtilConstructor[] = [ArrowBindingUtil]
|
|
@ -1,4 +1,11 @@
|
||||||
import { IndexKey, TLArrowShape, Vec, createShapeId } from '@tldraw/editor'
|
import {
|
||||||
|
IndexKey,
|
||||||
|
TLArrowShape,
|
||||||
|
TLShapeId,
|
||||||
|
Vec,
|
||||||
|
createShapeId,
|
||||||
|
getArrowBindings,
|
||||||
|
} from '@tldraw/editor'
|
||||||
import { TestEditor } from '../../../test/TestEditor'
|
import { TestEditor } from '../../../test/TestEditor'
|
||||||
|
|
||||||
let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
@ -19,6 +26,10 @@ const ids = {
|
||||||
box3: createShapeId('box3'),
|
box3: createShapeId('box3'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindings(id: TLShapeId) {
|
||||||
|
return getArrowBindings(editor, editor.getShape(id) as TLArrowShape)
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
editor = new TestEditor()
|
editor = new TestEditor()
|
||||||
editor
|
editor
|
||||||
|
@ -89,10 +100,11 @@ describe('When dragging the arrow', () => {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
props: {
|
props: {
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
start: { x: 0, y: 0 },
|
||||||
end: { type: 'point', x: 10, y: 10 },
|
end: { x: 10, y: 10 },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
expect(bindings(arrow.id)).toMatchObject({ start: undefined, end: undefined })
|
||||||
editor.expectToBeIn('select.dragging_handle')
|
editor.expectToBeIn('select.dragging_handle')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -146,15 +158,20 @@ describe('When pointing a start shape', () => {
|
||||||
x: 375,
|
x: 375,
|
||||||
y: 375,
|
y: 375,
|
||||||
props: {
|
props: {
|
||||||
start: {
|
start: { x: 0, y: 0 },
|
||||||
type: 'binding',
|
end: { x: 0, y: 125 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(bindings(arrow.id)).toMatchObject({
|
||||||
|
start: {
|
||||||
|
toId: ids.box3,
|
||||||
|
props: {
|
||||||
isExact: false,
|
isExact: false,
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 }, // center!
|
normalizedAnchor: { x: 0.5, y: 0.5 }, // center!
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
boundShapeId: ids.box3,
|
|
||||||
},
|
},
|
||||||
end: { type: 'point', x: 0, y: 125 },
|
|
||||||
},
|
},
|
||||||
|
end: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
|
@ -187,13 +204,17 @@ describe('When pointing an end shape', () => {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
props: {
|
props: {
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
start: { x: 0, y: 0 },
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
})
|
||||||
|
expect(bindings(arrow.id)).toMatchObject({
|
||||||
|
start: undefined,
|
||||||
|
end: {
|
||||||
|
toId: ids.box3,
|
||||||
|
props: {
|
||||||
isExact: false,
|
isExact: false,
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 }, // center!
|
normalizedAnchor: { x: 0.5, y: 0.5 }, // center!
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
boundShapeId: ids.box3,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -214,19 +235,14 @@ describe('When pointing an end shape', () => {
|
||||||
|
|
||||||
expect(editor.getHintingShapeIds().length).toBe(1)
|
expect(editor.getHintingShapeIds().length).toBe(1)
|
||||||
|
|
||||||
editor.expectShapeToMatch(arrow, {
|
expect(bindings(arrow.id)).toMatchObject({
|
||||||
id: arrow.id,
|
start: undefined,
|
||||||
type: 'arrow',
|
end: {
|
||||||
x: 0,
|
toId: ids.box3,
|
||||||
y: 0,
|
props: {
|
||||||
props: {
|
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
isExact: false,
|
isExact: false,
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
boundShapeId: ids.box3,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -235,19 +251,14 @@ describe('When pointing an end shape', () => {
|
||||||
|
|
||||||
arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]
|
arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]
|
||||||
|
|
||||||
editor.expectShapeToMatch(arrow, {
|
expect(bindings(arrow.id)).toMatchObject({
|
||||||
id: arrow.id,
|
start: undefined,
|
||||||
type: 'arrow',
|
end: {
|
||||||
x: 0,
|
toId: ids.box3,
|
||||||
y: 0,
|
props: {
|
||||||
props: {
|
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
isExact: false,
|
isExact: false,
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: true,
|
isPrecise: true,
|
||||||
boundShapeId: ids.box3,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -262,10 +273,14 @@ describe('When pointing an end shape', () => {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
props: {
|
props: {
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
start: { x: 0, y: 0 },
|
||||||
end: { type: 'point', x: 375, y: 0 },
|
end: { x: 375, y: 0 },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
expect(bindings(arrow.id)).toMatchObject({
|
||||||
|
start: undefined,
|
||||||
|
end: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
// Build up some velocity
|
// Build up some velocity
|
||||||
editor.inputs.pointerVelocity = new Vec(1, 1)
|
editor.inputs.pointerVelocity = new Vec(1, 1)
|
||||||
|
@ -280,13 +295,17 @@ describe('When pointing an end shape', () => {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
props: {
|
props: {
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
start: { x: 0, y: 0 },
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
})
|
||||||
|
expect(bindings(arrow.id)).toMatchObject({
|
||||||
|
start: undefined,
|
||||||
|
end: {
|
||||||
|
toId: ids.box2,
|
||||||
|
props: {
|
||||||
isExact: false,
|
isExact: false,
|
||||||
normalizedAnchor: { x: 0.25, y: 0.25 }, // center!
|
normalizedAnchor: { x: 0.25, y: 0.25 }, // center!
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
boundShapeId: ids.box2,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -296,18 +315,14 @@ describe('When pointing an end shape', () => {
|
||||||
|
|
||||||
arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]
|
arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]
|
||||||
|
|
||||||
editor.expectShapeToMatch(arrow, {
|
expect(bindings(arrow.id)).toMatchObject({
|
||||||
id: arrow.id,
|
start: undefined,
|
||||||
type: 'arrow',
|
end: {
|
||||||
x: 0,
|
toId: ids.box2,
|
||||||
y: 0,
|
props: {
|
||||||
props: {
|
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
isExact: false,
|
isExact: false,
|
||||||
normalizedAnchor: { x: 0.25, y: 0.25 }, // precise!
|
normalizedAnchor: { x: 0.25, y: 0.25 }, // precise!
|
||||||
boundShapeId: ids.box2,
|
isPrecise: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -325,19 +340,14 @@ describe('When pointing an end shape', () => {
|
||||||
|
|
||||||
expect(editor.getHintingShapeIds().length).toBe(1)
|
expect(editor.getHintingShapeIds().length).toBe(1)
|
||||||
|
|
||||||
editor.expectShapeToMatch(arrow, {
|
expect(bindings(arrow.id)).toMatchObject({
|
||||||
id: arrow.id,
|
start: undefined,
|
||||||
type: 'arrow',
|
end: {
|
||||||
x: 0,
|
toId: ids.box3,
|
||||||
y: 0,
|
props: {
|
||||||
props: {
|
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
isExact: false,
|
isExact: false,
|
||||||
normalizedAnchor: { x: 0.4, y: 0.4 },
|
normalizedAnchor: { x: 0.4, y: 0.4 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
boundShapeId: ids.box3,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -348,15 +358,9 @@ describe('When pointing an end shape', () => {
|
||||||
|
|
||||||
let arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]
|
let arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]
|
||||||
|
|
||||||
editor.expectShapeToMatch(arrow, {
|
expect(bindings(arrow.id)).toMatchObject({
|
||||||
id: arrow.id,
|
start: undefined,
|
||||||
type: 'arrow',
|
end: undefined,
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
props: {
|
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
|
||||||
end: { type: 'point', x: 2, y: 0 },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(editor.getHintingShapeIds().length).toBe(0)
|
expect(editor.getHintingShapeIds().length).toBe(0)
|
||||||
|
@ -373,14 +377,15 @@ describe('When pointing an end shape', () => {
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
props: {
|
})
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
expect(bindings(arrow.id)).toMatchObject({
|
||||||
end: {
|
start: undefined,
|
||||||
type: 'binding',
|
end: {
|
||||||
|
toId: ids.box3,
|
||||||
|
props: {
|
||||||
isExact: false,
|
isExact: false,
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: true,
|
isPrecise: true,
|
||||||
boundShapeId: ids.box3,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -423,8 +428,8 @@ describe('reparenting issue', () => {
|
||||||
editor.expectShapeToMatch({
|
editor.expectShapeToMatch({
|
||||||
id: arrowId,
|
id: arrowId,
|
||||||
index: 'a3V' as IndexKey,
|
index: 'a3V' as IndexKey,
|
||||||
props: { end: { boundShapeId: ids.box2 } },
|
|
||||||
}) // between box 2 (a3) and 3 (a4)
|
}) // between box 2 (a3) and 3 (a4)
|
||||||
|
expect(bindings(arrowId)).toMatchObject({ end: { toId: ids.box2 } })
|
||||||
|
|
||||||
expect(editor.getShapeAtPoint({ x: 350, y: 350 }, { hitInside: true })).toMatchObject({
|
expect(editor.getShapeAtPoint({ x: 350, y: 350 }, { hitInside: true })).toMatchObject({
|
||||||
id: ids.box3,
|
id: ids.box3,
|
||||||
|
@ -434,8 +439,8 @@ describe('reparenting issue', () => {
|
||||||
editor.expectShapeToMatch({
|
editor.expectShapeToMatch({
|
||||||
id: arrowId,
|
id: arrowId,
|
||||||
index: 'a5' as IndexKey,
|
index: 'a5' as IndexKey,
|
||||||
props: { end: { boundShapeId: ids.box3 } },
|
|
||||||
}) // above box 3 (a4)
|
}) // above box 3 (a4)
|
||||||
|
expect(bindings(arrowId)).toMatchObject({ end: { toId: ids.box3 } })
|
||||||
|
|
||||||
editor.pointerMove(150, 150) // over box 1
|
editor.pointerMove(150, 150) // over box 1
|
||||||
editor.expectShapeToMatch({ id: arrowId, index: 'a2V' as IndexKey }) // between box 1 (a2) and box 3 (a3)
|
editor.expectShapeToMatch({ id: arrowId, index: 'a2V' as IndexKey }) // between box 1 (a2) and box 3 (a3)
|
||||||
|
@ -465,14 +470,14 @@ describe('reparenting issue', () => {
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
props: { start: { type: 'point', x: 0, y: 0 }, end: { type: 'point', x: 100, y: 100 } },
|
props: { start: { x: 0, y: 0 }, end: { x: 100, y: 100 } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: arrow2Id,
|
id: arrow2Id,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
props: { start: { type: 'point', x: 0, y: 0 }, end: { type: 'point', x: 100, y: 100 } },
|
props: { start: { x: 0, y: 0 }, end: { x: 100, y: 100 } },
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -530,8 +535,8 @@ describe('line bug', () => {
|
||||||
.keyUp('Shift')
|
.keyUp('Shift')
|
||||||
|
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(2)
|
expect(editor.getCurrentPageShapes().length).toBe(2)
|
||||||
const arrow = editor.getCurrentPageShapes()[1] as TLArrowShape
|
const bindings = getArrowBindings(editor, editor.getCurrentPageShapes()[1] as TLArrowShape)
|
||||||
expect(arrow.props.end.type).toBe('binding')
|
expect(bindings.end).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('works as expected when binding to a straight horizontal line', () => {
|
it('works as expected when binding to a straight horizontal line', () => {
|
||||||
|
@ -552,7 +557,7 @@ describe('line bug', () => {
|
||||||
.pointerUp()
|
.pointerUp()
|
||||||
|
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(2)
|
expect(editor.getCurrentPageShapes().length).toBe(2)
|
||||||
const arrow = editor.getCurrentPageShapes()[1] as TLArrowShape
|
const bindings = getArrowBindings(editor, editor.getCurrentPageShapes()[1] as TLArrowShape)
|
||||||
expect(arrow.props.end.type).toBe('binding')
|
expect(bindings.end).toBeDefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import {
|
import {
|
||||||
assert,
|
|
||||||
createShapeId,
|
|
||||||
HALF_PI,
|
HALF_PI,
|
||||||
TLArrowShape,
|
TLArrowShape,
|
||||||
TLArrowShapeTerminal,
|
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
|
createOrUpdateArrowBinding,
|
||||||
|
createShapeId,
|
||||||
|
getArrowBindings,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { TestEditor } from '../../../test/TestEditor'
|
import { TestEditor } from '../../../test/TestEditor'
|
||||||
|
|
||||||
|
@ -28,6 +28,13 @@ window.cancelAnimationFrame = function cancelAnimationFrame(id) {
|
||||||
clearTimeout(id)
|
clearTimeout(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function arrow(id = ids.arrow1) {
|
||||||
|
return editor.getShape(id) as TLArrowShape
|
||||||
|
}
|
||||||
|
function bindings(id = ids.arrow1) {
|
||||||
|
return getArrowBindings(editor, arrow(id))
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
editor = new TestEditor()
|
editor = new TestEditor()
|
||||||
editor
|
editor
|
||||||
|
@ -42,23 +49,25 @@ beforeEach(() => {
|
||||||
x: 150,
|
x: 150,
|
||||||
y: 150,
|
y: 150,
|
||||||
props: {
|
props: {
|
||||||
start: {
|
start: { x: 0, y: 0 },
|
||||||
type: 'binding',
|
end: { x: 0, y: 0 },
|
||||||
isExact: false,
|
|
||||||
boundShapeId: ids.box1,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
||||||
isPrecise: false,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
isExact: false,
|
|
||||||
boundShapeId: ids.box2,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
||||||
isPrecise: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
createOrUpdateArrowBinding(editor, ids.arrow1, ids.box1, {
|
||||||
|
terminal: 'start',
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: false,
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
|
})
|
||||||
|
|
||||||
|
createOrUpdateArrowBinding(editor, ids.arrow1, ids.box2, {
|
||||||
|
terminal: 'end',
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: false,
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When translating a bound shape', () => {
|
describe('When translating a bound shape', () => {
|
||||||
|
@ -77,17 +86,23 @@ describe('When translating a bound shape', () => {
|
||||||
x: 150,
|
x: 150,
|
||||||
y: 150,
|
y: 150,
|
||||||
props: {
|
props: {
|
||||||
start: {
|
start: { x: 0, y: 0 },
|
||||||
type: 'binding',
|
end: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(bindings()).toMatchObject({
|
||||||
|
start: {
|
||||||
|
toId: ids.box1,
|
||||||
|
props: {
|
||||||
isExact: false,
|
isExact: false,
|
||||||
boundShapeId: ids.box1,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
end: {
|
||||||
|
toId: ids.box2,
|
||||||
|
props: {
|
||||||
isExact: false,
|
isExact: false,
|
||||||
boundShapeId: ids.box2,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
|
@ -111,17 +126,24 @@ describe('When translating a bound shape', () => {
|
||||||
x: 150,
|
x: 150,
|
||||||
y: 150,
|
y: 150,
|
||||||
props: {
|
props: {
|
||||||
start: {
|
start: { x: 0, y: 0 },
|
||||||
type: 'binding',
|
end: { x: 0, y: 0 },
|
||||||
|
bend: 20,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(bindings()).toMatchObject({
|
||||||
|
start: {
|
||||||
|
toId: ids.box1,
|
||||||
|
props: {
|
||||||
isExact: false,
|
isExact: false,
|
||||||
boundShapeId: ids.box1,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
end: {
|
||||||
|
toId: ids.box2,
|
||||||
|
props: {
|
||||||
isExact: false,
|
isExact: false,
|
||||||
boundShapeId: ids.box2,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
|
@ -147,17 +169,23 @@ describe('When translating the arrow', () => {
|
||||||
x: 150,
|
x: 150,
|
||||||
y: 100,
|
y: 100,
|
||||||
props: {
|
props: {
|
||||||
start: {
|
start: { x: 0, y: 0 },
|
||||||
type: 'binding',
|
end: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(bindings()).toMatchObject({
|
||||||
|
start: {
|
||||||
|
toId: ids.box1,
|
||||||
|
props: {
|
||||||
isExact: false,
|
isExact: false,
|
||||||
boundShapeId: ids.box1,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
end: {
|
||||||
|
toId: ids.box2,
|
||||||
|
props: {
|
||||||
isExact: false,
|
isExact: false,
|
||||||
boundShapeId: ids.box2,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
|
@ -172,23 +200,18 @@ describe('Other cases when arrow are moved', () => {
|
||||||
|
|
||||||
// When box one is not selected, unbinds box1 and keeps binding to box2
|
// When box one is not selected, unbinds box1 and keeps binding to box2
|
||||||
editor.nudgeShapes(editor.getSelectedShapeIds(), { x: 0, y: -1 })
|
editor.nudgeShapes(editor.getSelectedShapeIds(), { x: 0, y: -1 })
|
||||||
|
expect(bindings()).toMatchObject({
|
||||||
expect(editor.getShape(ids.arrow1)).toMatchObject({
|
start: { toId: ids.box1, props: { isPrecise: false } },
|
||||||
props: {
|
end: { toId: ids.box2, props: { isPrecise: false } },
|
||||||
start: { type: 'binding', boundShapeId: ids.box1 },
|
|
||||||
end: { type: 'binding', boundShapeId: ids.box2 },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// when only the arrow is selected, we keep the binding but make it precise:
|
// when only the arrow is selected, we keep the binding but make it precise:
|
||||||
editor.select(ids.arrow1)
|
editor.select(ids.arrow1)
|
||||||
editor.nudgeShapes(editor.getSelectedShapeIds(), { x: 0, y: -1 })
|
editor.nudgeShapes(editor.getSelectedShapeIds(), { x: 0, y: -1 })
|
||||||
|
|
||||||
expect(editor.getShape(ids.arrow1)).toMatchObject({
|
expect(bindings()).toMatchObject({
|
||||||
props: {
|
start: { toId: ids.box1, props: { isPrecise: true } },
|
||||||
start: { type: 'binding', boundShapeId: ids.box1, isPrecise: true },
|
end: { toId: ids.box2, props: { isPrecise: true } },
|
||||||
end: { type: 'binding', boundShapeId: ids.box2, isPrecise: true },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -200,11 +223,9 @@ describe('Other cases when arrow are moved', () => {
|
||||||
editor.alignShapes(editor.getSelectedShapeIds(), 'right')
|
editor.alignShapes(editor.getSelectedShapeIds(), 'right')
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
||||||
expect(editor.getShape(ids.arrow1)).toMatchObject({
|
expect(bindings()).toMatchObject({
|
||||||
props: {
|
start: { toId: ids.box1, props: { isPrecise: false } },
|
||||||
start: { type: 'binding', boundShapeId: ids.box1 },
|
end: { toId: ids.box2, props: { isPrecise: false } },
|
||||||
end: { type: 'binding', boundShapeId: ids.box2 },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// maintains bindings if they would still be over the same shape (but makes them precise), but unbinds others
|
// maintains bindings if they would still be over the same shape (but makes them precise), but unbinds others
|
||||||
|
@ -212,16 +233,9 @@ describe('Other cases when arrow are moved', () => {
|
||||||
editor.alignShapes(editor.getSelectedShapeIds(), 'top')
|
editor.alignShapes(editor.getSelectedShapeIds(), 'top')
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
||||||
expect(editor.getShape(ids.arrow1)).toMatchObject({
|
expect(bindings()).toMatchObject({
|
||||||
props: {
|
start: { toId: ids.box1, props: { isPrecise: true } },
|
||||||
start: {
|
end: undefined,
|
||||||
type: 'binding',
|
|
||||||
isPrecise: true,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'point',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -236,17 +250,9 @@ describe('Other cases when arrow are moved', () => {
|
||||||
editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
|
editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
||||||
expect(editor.getShape(ids.arrow1)).toMatchObject({
|
expect(bindings()).toMatchObject({
|
||||||
props: {
|
start: { toId: ids.box1, props: { isPrecise: false } },
|
||||||
start: {
|
end: { toId: ids.box2, props: { isPrecise: false } },
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box1,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// unbinds when only the arrow is selected (not its bound shapes) if the arrow itself has moved
|
// unbinds when only the arrow is selected (not its bound shapes) if the arrow itself has moved
|
||||||
|
@ -255,17 +261,9 @@ describe('Other cases when arrow are moved', () => {
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
||||||
// The arrow didn't actually move
|
// The arrow didn't actually move
|
||||||
expect(editor.getShape(ids.arrow1)).toMatchObject({
|
expect(bindings()).toMatchObject({
|
||||||
props: {
|
start: { toId: ids.box1, props: { isPrecise: false } },
|
||||||
start: {
|
end: { toId: ids.box2, props: { isPrecise: false } },
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box1,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// The arrow will move this time, so it should unbind
|
// The arrow will move this time, so it should unbind
|
||||||
|
@ -273,15 +271,9 @@ describe('Other cases when arrow are moved', () => {
|
||||||
editor.distributeShapes(editor.getSelectedShapeIds(), 'vertical')
|
editor.distributeShapes(editor.getSelectedShapeIds(), 'vertical')
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
||||||
expect(editor.getShape(ids.arrow1)).toMatchObject({
|
expect(bindings()).toMatchObject({
|
||||||
props: {
|
start: undefined,
|
||||||
start: {
|
end: undefined,
|
||||||
type: 'point',
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'point',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -298,57 +290,44 @@ describe('Other cases when arrow are moved', () => {
|
||||||
.groupShapes(editor.getSelectedShapeIds())
|
.groupShapes(editor.getSelectedShapeIds())
|
||||||
|
|
||||||
editor.setCurrentTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350)
|
editor.setCurrentTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350)
|
||||||
let arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]
|
const arrowId = editor.getOnlySelectedShape()!.id
|
||||||
assert(editor.isShapeOfType<TLArrowShape>(arrow, 'arrow'))
|
expect(bindings(arrowId).end?.toId).toBe(ids.box3)
|
||||||
assert(arrow.props.end.type === 'binding')
|
|
||||||
expect(arrow.props.end.boundShapeId).toBe(ids.box3)
|
|
||||||
|
|
||||||
// translate:
|
// translate:
|
||||||
editor.selectAll().nudgeShapes(editor.getSelectedShapeIds(), { x: 0, y: 1 })
|
editor.selectAll().nudgeShapes(editor.getSelectedShapeIds(), { x: 0, y: 1 })
|
||||||
|
|
||||||
// arrow should still be bound to box3
|
// arrow should still be bound to box3
|
||||||
arrow = editor.getShape(arrow.id)!
|
expect(bindings(arrowId).end?.toId).toBe(ids.box3)
|
||||||
assert(editor.isShapeOfType<TLArrowShape>(arrow, 'arrow'))
|
|
||||||
assert(arrow.props.end.type === 'binding')
|
|
||||||
expect(arrow.props.end.boundShapeId).toBe(ids.box3)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When a shape is rotated', () => {
|
describe('When a shape is rotated', () => {
|
||||||
it('binds correctly', () => {
|
it('binds correctly', () => {
|
||||||
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(375, 375)
|
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(375, 375)
|
||||||
|
const arrowId = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1].id
|
||||||
|
|
||||||
const arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]
|
expect(bindings(arrowId)).toMatchObject({
|
||||||
|
start: undefined,
|
||||||
expect(editor.getShape(arrow.id)).toMatchObject({
|
end: {
|
||||||
props: {
|
toId: ids.box2,
|
||||||
start: { type: 'point' },
|
props: {
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box2,
|
|
||||||
normalizedAnchor: { x: 0.75, y: 0.75 }, // moving slowly
|
normalizedAnchor: { x: 0.75, y: 0.75 }, // moving slowly
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.updateShapes([{ id: ids.box2, type: 'geo', rotation: HALF_PI }])
|
editor.updateShapes([{ id: ids.box2, type: 'geo', rotation: HALF_PI }])
|
||||||
|
|
||||||
editor.pointerMove(225, 350)
|
editor.pointerMove(225, 350)
|
||||||
|
|
||||||
expect(editor.getShape(arrow.id)).toMatchObject({
|
expect(bindings(arrowId)).toCloselyMatchObject({
|
||||||
props: {
|
start: undefined,
|
||||||
start: { type: 'point' },
|
end: {
|
||||||
end: { type: 'binding', boundShapeId: ids.box2 },
|
toId: ids.box2,
|
||||||
|
props: {
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.75 }, // moving slowly
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const anchor = (
|
|
||||||
editor.getShape<TLArrowShape>(arrow.id)!.props.end as TLArrowShapeTerminal & {
|
|
||||||
type: 'binding'
|
|
||||||
}
|
|
||||||
).normalizedAnchor
|
|
||||||
expect(anchor.x).toBeCloseTo(0.5)
|
|
||||||
expect(anchor.y).toBeCloseTo(0.75)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -362,8 +341,7 @@ describe('Arrow labels', () => {
|
||||||
|
|
||||||
it('should create an arrow with a label', () => {
|
it('should create an arrow with a label', () => {
|
||||||
const arrowId = editor.getOnlySelectedShape()!.id
|
const arrowId = editor.getOnlySelectedShape()!.id
|
||||||
const arrow = editor.getShape(arrowId)
|
expect(arrow(arrowId)).toMatchObject({
|
||||||
expect(arrow).toMatchObject({
|
|
||||||
props: {
|
props: {
|
||||||
text: 'Test Label',
|
text: 'Test Label',
|
||||||
},
|
},
|
||||||
|
@ -373,8 +351,7 @@ describe('Arrow labels', () => {
|
||||||
it('should update the label of an arrow', () => {
|
it('should update the label of an arrow', () => {
|
||||||
const arrowId = editor.getOnlySelectedShape()!.id
|
const arrowId = editor.getOnlySelectedShape()!.id
|
||||||
editor.updateShapes([{ id: arrowId, type: 'arrow', props: { text: 'New Label' } }])
|
editor.updateShapes([{ id: arrowId, type: 'arrow', props: { text: 'New Label' } }])
|
||||||
const arrow = editor.getShape(arrowId)
|
expect(arrow(arrowId)).toMatchObject({
|
||||||
expect(arrow).toMatchObject({
|
|
||||||
props: {
|
props: {
|
||||||
text: 'New Label',
|
text: 'New Label',
|
||||||
},
|
},
|
||||||
|
@ -533,32 +510,22 @@ describe("an arrow's parents", () => {
|
||||||
editor.pointerDown(15, 15).pointerMove(50, 50)
|
editor.pointerDown(15, 15).pointerMove(50, 50)
|
||||||
const arrowId = editor.getOnlySelectedShape()!.id
|
const arrowId = editor.getOnlySelectedShape()!.id
|
||||||
|
|
||||||
expect(editor.getShape(arrowId)).toMatchObject({
|
expect(arrow(arrowId).parentId).toBe(editor.getCurrentPageId())
|
||||||
props: {
|
|
||||||
start: { type: 'binding', boundShapeId: boxAid },
|
|
||||||
end: { type: 'binding', boundShapeId: frameId },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(editor.getShape(arrowId)?.parentId).toBe(editor.getCurrentPageId())
|
|
||||||
|
|
||||||
// move arrow to b
|
// move arrow to b
|
||||||
editor.pointerMove(15, 85)
|
editor.pointerMove(15, 85)
|
||||||
expect(editor.getShape(arrowId)?.parentId).toBe(frameId)
|
expect(arrow(arrowId).parentId).toBe(frameId)
|
||||||
expect(editor.getShape(arrowId)).toMatchObject({
|
expect(bindings(arrowId)).toMatchObject({
|
||||||
props: {
|
start: { toId: boxAid },
|
||||||
start: { type: 'binding', boundShapeId: boxAid },
|
end: { toId: boxBid },
|
||||||
end: { type: 'binding', boundShapeId: boxBid },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// move back to empty space
|
// move back to empty space
|
||||||
editor.pointerMove(50, 50)
|
editor.pointerMove(50, 50)
|
||||||
expect(editor.getShape(arrowId)?.parentId).toBe(editor.getCurrentPageId())
|
expect(arrow(arrowId).parentId).toBe(editor.getCurrentPageId())
|
||||||
expect(editor.getShape(arrowId)).toMatchObject({
|
expect(bindings(arrowId)).toMatchObject({
|
||||||
props: {
|
start: { toId: boxAid },
|
||||||
start: { type: 'binding', boundShapeId: boxAid },
|
end: { toId: frameId },
|
||||||
end: { type: 'binding', boundShapeId: frameId },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -568,21 +535,21 @@ describe("an arrow's parents", () => {
|
||||||
editor.pointerDown(15, 15).pointerMove(15, 85).pointerUp()
|
editor.pointerDown(15, 15).pointerMove(15, 85).pointerUp()
|
||||||
const arrowId = editor.getOnlySelectedShape()!.id
|
const arrowId = editor.getOnlySelectedShape()!.id
|
||||||
|
|
||||||
expect(editor.getShape(arrowId)).toMatchObject({
|
expect(arrow(arrowId)).toMatchObject({
|
||||||
parentId: frameId,
|
parentId: frameId,
|
||||||
props: {
|
})
|
||||||
start: { type: 'binding', boundShapeId: boxAid },
|
expect(bindings(arrowId)).toMatchObject({
|
||||||
end: { type: 'binding', boundShapeId: boxBid },
|
start: { toId: boxAid },
|
||||||
},
|
end: { toId: boxBid },
|
||||||
})
|
})
|
||||||
// move b outside of frame
|
// move b outside of frame
|
||||||
editor.select(boxBid).translateSelection(200, 0)
|
editor.select(boxBid).translateSelection(200, 0)
|
||||||
expect(editor.getShape(arrowId)).toMatchObject({
|
expect(arrow(arrowId)).toMatchObject({
|
||||||
parentId: editor.getCurrentPageId(),
|
parentId: editor.getCurrentPageId(),
|
||||||
props: {
|
})
|
||||||
start: { type: 'binding', boundShapeId: boxAid },
|
expect(bindings(arrowId)).toMatchObject({
|
||||||
end: { type: 'binding', boundShapeId: boxBid },
|
start: { toId: boxAid },
|
||||||
},
|
end: { toId: boxBid },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -591,12 +558,12 @@ describe("an arrow's parents", () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(15, 15).pointerMove(115, 15).pointerUp()
|
editor.pointerDown(15, 15).pointerMove(115, 15).pointerUp()
|
||||||
const arrowId = editor.getOnlySelectedShape()!.id
|
const arrowId = editor.getOnlySelectedShape()!.id
|
||||||
expect(editor.getShape(arrowId)).toMatchObject({
|
expect(arrow(arrowId)).toMatchObject({
|
||||||
parentId: editor.getCurrentPageId(),
|
parentId: editor.getCurrentPageId(),
|
||||||
props: {
|
})
|
||||||
start: { type: 'binding', boundShapeId: boxAid },
|
expect(bindings(arrowId)).toMatchObject({
|
||||||
end: { type: 'binding', boundShapeId: boxCid },
|
start: { toId: boxAid },
|
||||||
},
|
end: { toId: boxCid },
|
||||||
})
|
})
|
||||||
|
|
||||||
// move c inside of frame
|
// move c inside of frame
|
||||||
|
@ -604,10 +571,10 @@ describe("an arrow's parents", () => {
|
||||||
|
|
||||||
expect(editor.getShape(arrowId)).toMatchObject({
|
expect(editor.getShape(arrowId)).toMatchObject({
|
||||||
parentId: frameId,
|
parentId: frameId,
|
||||||
props: {
|
})
|
||||||
start: { type: 'binding', boundShapeId: boxAid },
|
expect(bindings(arrowId)).toMatchObject({
|
||||||
end: { type: 'binding', boundShapeId: boxCid },
|
start: { toId: boxAid },
|
||||||
},
|
end: { toId: boxCid },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,12 +9,14 @@ import {
|
||||||
SVGContainer,
|
SVGContainer,
|
||||||
ShapeUtil,
|
ShapeUtil,
|
||||||
SvgExportContext,
|
SvgExportContext,
|
||||||
|
TLArrowBinding,
|
||||||
|
TLArrowBindings,
|
||||||
TLArrowShape,
|
TLArrowShape,
|
||||||
TLArrowShapeProps,
|
|
||||||
TLHandle,
|
TLHandle,
|
||||||
TLOnEditEndHandler,
|
TLOnEditEndHandler,
|
||||||
TLOnHandleDragHandler,
|
TLOnHandleDragHandler,
|
||||||
TLOnResizeHandler,
|
TLOnResizeHandler,
|
||||||
|
TLOnResizeStartHandler,
|
||||||
TLOnTranslateHandler,
|
TLOnTranslateHandler,
|
||||||
TLOnTranslateStartHandler,
|
TLOnTranslateStartHandler,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
|
@ -23,10 +25,12 @@ import {
|
||||||
Vec,
|
Vec,
|
||||||
arrowShapeMigrations,
|
arrowShapeMigrations,
|
||||||
arrowShapeProps,
|
arrowShapeProps,
|
||||||
|
createOrUpdateArrowBinding,
|
||||||
|
getArrowBindings,
|
||||||
getArrowTerminalsInArrowSpace,
|
getArrowTerminalsInArrowSpace,
|
||||||
getDefaultColorTheme,
|
getDefaultColorTheme,
|
||||||
mapObjectMapValues,
|
mapObjectMapValues,
|
||||||
objectMapEntries,
|
removeArrowBinding,
|
||||||
structuredClone,
|
structuredClone,
|
||||||
toDomPrecision,
|
toDomPrecision,
|
||||||
track,
|
track,
|
||||||
|
@ -75,6 +79,11 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
override hideSelectionBoundsBg: TLShapeUtilFlag<TLArrowShape> = () => true
|
override hideSelectionBoundsBg: TLShapeUtilFlag<TLArrowShape> = () => true
|
||||||
override hideSelectionBoundsFg: TLShapeUtilFlag<TLArrowShape> = () => true
|
override hideSelectionBoundsFg: TLShapeUtilFlag<TLArrowShape> = () => true
|
||||||
|
|
||||||
|
override canBeLaidOut: TLShapeUtilFlag<TLArrowShape> = (shape) => {
|
||||||
|
const bindings = getArrowBindings(this.editor, shape)
|
||||||
|
return !bindings.start && !bindings.end
|
||||||
|
}
|
||||||
|
|
||||||
override getDefaultProps(): TLArrowShape['props'] {
|
override getDefaultProps(): TLArrowShape['props'] {
|
||||||
return {
|
return {
|
||||||
dash: 'draw',
|
dash: 'draw',
|
||||||
|
@ -83,8 +92,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
color: 'black',
|
color: 'black',
|
||||||
labelColor: 'black',
|
labelColor: 'black',
|
||||||
bend: 0,
|
bend: 0,
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
start: { x: 0, y: 0 },
|
||||||
end: { type: 'point', x: 2, y: 0 },
|
end: { x: 2, y: 0 },
|
||||||
arrowheadStart: 'none',
|
arrowheadStart: 'none',
|
||||||
arrowheadEnd: 'arrow',
|
arrowheadEnd: 'arrow',
|
||||||
text: '',
|
text: '',
|
||||||
|
@ -164,10 +173,11 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
|
||||||
override onHandleDrag: TLOnHandleDragHandler<TLArrowShape> = (shape, { handle, isPrecise }) => {
|
override onHandleDrag: TLOnHandleDragHandler<TLArrowShape> = (shape, { handle, isPrecise }) => {
|
||||||
const handleId = handle.id as ARROW_HANDLES
|
const handleId = handle.id as ARROW_HANDLES
|
||||||
|
const bindings = getArrowBindings(this.editor, shape)
|
||||||
|
|
||||||
if (handleId === ARROW_HANDLES.MIDDLE) {
|
if (handleId === ARROW_HANDLES.MIDDLE) {
|
||||||
// Bending the arrow...
|
// Bending the arrow...
|
||||||
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape)
|
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape, bindings)
|
||||||
|
|
||||||
const delta = Vec.Sub(end, start)
|
const delta = Vec.Sub(end, start)
|
||||||
const v = Vec.Per(delta)
|
const v = Vec.Per(delta)
|
||||||
|
@ -184,17 +194,23 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
|
||||||
// Start or end, pointing the arrow...
|
// Start or end, pointing the arrow...
|
||||||
|
|
||||||
const next = structuredClone(shape) as TLArrowShape
|
const update: TLShapePartial<TLArrowShape> = { id: shape.id, type: 'arrow', props: {} }
|
||||||
|
|
||||||
|
const currentBinding = bindings[handleId]
|
||||||
|
|
||||||
|
const otherHandleId = handleId === ARROW_HANDLES.START ? ARROW_HANDLES.END : ARROW_HANDLES.START
|
||||||
|
const otherBinding = bindings[otherHandleId]
|
||||||
|
|
||||||
if (this.editor.inputs.ctrlKey) {
|
if (this.editor.inputs.ctrlKey) {
|
||||||
// todo: maybe double check that this isn't equal to the other handle too?
|
// todo: maybe double check that this isn't equal to the other handle too?
|
||||||
// Skip binding
|
// Skip binding
|
||||||
next.props[handleId] = {
|
removeArrowBinding(this.editor, shape, handleId)
|
||||||
type: 'point',
|
|
||||||
|
update.props![handleId] = {
|
||||||
x: handle.x,
|
x: handle.x,
|
||||||
y: handle.y,
|
y: handle.y,
|
||||||
}
|
}
|
||||||
return next
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
const point = this.editor.getShapePageTransform(shape.id)!.applyToPoint(handle)
|
const point = this.editor.getShapePageTransform(shape.id)!.applyToPoint(handle)
|
||||||
|
@ -210,19 +226,20 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
// todo: maybe double check that this isn't equal to the other handle too?
|
// todo: maybe double check that this isn't equal to the other handle too?
|
||||||
next.props[handleId] = {
|
removeArrowBinding(this.editor, shape, handleId)
|
||||||
type: 'point',
|
|
||||||
|
update.props![handleId] = {
|
||||||
x: handle.x,
|
x: handle.x,
|
||||||
y: handle.y,
|
y: handle.y,
|
||||||
}
|
}
|
||||||
return next
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
// we've got a target! the handle is being dragged over a shape, bind to it
|
// we've got a target! the handle is being dragged over a shape, bind to it
|
||||||
|
|
||||||
const targetGeometry = this.editor.getShapeGeometry(target)
|
const targetGeometry = this.editor.getShapeGeometry(target)
|
||||||
const targetBounds = Box.ZeroFix(targetGeometry.bounds)
|
const targetBounds = Box.ZeroFix(targetGeometry.bounds)
|
||||||
const pageTransform = this.editor.getShapePageTransform(next.id)!
|
const pageTransform = this.editor.getShapePageTransform(update.id)!
|
||||||
const pointInPageSpace = pageTransform.applyToPoint(handle)
|
const pointInPageSpace = pageTransform.applyToPoint(handle)
|
||||||
const pointInTargetSpace = this.editor.getPointInShapeSpace(target, pointInPageSpace)
|
const pointInTargetSpace = this.editor.getPointInShapeSpace(target, pointInPageSpace)
|
||||||
|
|
||||||
|
@ -230,11 +247,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
|
||||||
if (!precise) {
|
if (!precise) {
|
||||||
// If we're switching to a new bound shape, then precise only if moving slowly
|
// If we're switching to a new bound shape, then precise only if moving slowly
|
||||||
const prevHandle = next.props[handleId]
|
if (!currentBinding || (currentBinding && target.id !== currentBinding.toId)) {
|
||||||
if (
|
|
||||||
prevHandle.type === 'point' ||
|
|
||||||
(prevHandle.type === 'binding' && target.id !== prevHandle.boundShapeId)
|
|
||||||
) {
|
|
||||||
precise = this.editor.inputs.pointerVelocity.len() < 0.5
|
precise = this.editor.inputs.pointerVelocity.len() < 0.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,13 +259,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
|
||||||
// Double check that we're not going to be doing an imprecise snap on
|
// Double check that we're not going to be doing an imprecise snap on
|
||||||
// the same shape twice, as this would result in a zero length line
|
// the same shape twice, as this would result in a zero length line
|
||||||
const otherHandle =
|
if (otherBinding && target.id === otherBinding.toId && otherBinding.props.isPrecise) {
|
||||||
next.props[handleId === ARROW_HANDLES.START ? ARROW_HANDLES.END : ARROW_HANDLES.START]
|
|
||||||
if (
|
|
||||||
otherHandle.type === 'binding' &&
|
|
||||||
target.id === otherHandle.boundShapeId &&
|
|
||||||
otherHandle.isPrecise
|
|
||||||
) {
|
|
||||||
precise = true
|
precise = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,64 +283,66 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next.props[handleId] = {
|
const b = {
|
||||||
type: 'binding',
|
terminal: handleId,
|
||||||
boundShapeId: target.id,
|
normalizedAnchor,
|
||||||
normalizedAnchor: normalizedAnchor,
|
|
||||||
isPrecise: precise,
|
isPrecise: precise,
|
||||||
isExact: this.editor.inputs.altKey,
|
isExact: this.editor.inputs.altKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next.props.start.type === 'binding' && next.props.end.type === 'binding') {
|
createOrUpdateArrowBinding(this.editor, shape, target.id, b)
|
||||||
if (next.props.start.boundShapeId === next.props.end.boundShapeId) {
|
|
||||||
if (Vec.Equals(next.props.start.normalizedAnchor, next.props.end.normalizedAnchor)) {
|
this.editor.setHintingShapes([target.id])
|
||||||
next.props.end.normalizedAnchor.x += 0.05
|
|
||||||
}
|
const newBindings = getArrowBindings(this.editor, shape)
|
||||||
|
if (newBindings.start && newBindings.end && newBindings.start.toId === newBindings.end.toId) {
|
||||||
|
if (
|
||||||
|
Vec.Equals(newBindings.start.props.normalizedAnchor, newBindings.end.props.normalizedAnchor)
|
||||||
|
) {
|
||||||
|
createOrUpdateArrowBinding(this.editor, shape, newBindings.end.toId, {
|
||||||
|
...newBindings.end.props,
|
||||||
|
normalizedAnchor: {
|
||||||
|
x: newBindings.end.props.normalizedAnchor.x + 0.05,
|
||||||
|
y: newBindings.end.props.normalizedAnchor.y,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
override onTranslateStart: TLOnTranslateStartHandler<TLArrowShape> = (shape) => {
|
override onTranslateStart: TLOnTranslateStartHandler<TLArrowShape> = (shape) => {
|
||||||
const startBindingId =
|
const bindings = getArrowBindings(this.editor, shape)
|
||||||
shape.props.start.type === 'binding' ? shape.props.start.boundShapeId : null
|
|
||||||
const endBindingId = shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : null
|
|
||||||
|
|
||||||
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(this.editor, shape)
|
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(this.editor, shape, bindings)
|
||||||
const shapePageTransform = this.editor.getShapePageTransform(shape.id)!
|
const shapePageTransform = this.editor.getShapePageTransform(shape.id)!
|
||||||
|
|
||||||
// If at least one bound shape is in the selection, do nothing;
|
// If at least one bound shape is in the selection, do nothing;
|
||||||
// If no bound shapes are in the selection, unbind any bound shapes
|
// If no bound shapes are in the selection, unbind any bound shapes
|
||||||
|
|
||||||
const selectedShapeIds = this.editor.getSelectedShapeIds()
|
const selectedShapeIds = this.editor.getSelectedShapeIds()
|
||||||
const shapesToCheck = new Set<string>()
|
|
||||||
if (startBindingId) {
|
|
||||||
// Add shape and all ancestors to set
|
|
||||||
shapesToCheck.add(startBindingId)
|
|
||||||
this.editor.getShapeAncestors(startBindingId).forEach((a) => shapesToCheck.add(a.id))
|
|
||||||
}
|
|
||||||
if (endBindingId) {
|
|
||||||
// Add shape and all ancestors to set
|
|
||||||
shapesToCheck.add(endBindingId)
|
|
||||||
this.editor.getShapeAncestors(endBindingId).forEach((a) => shapesToCheck.add(a.id))
|
|
||||||
}
|
|
||||||
// If any of the shapes are selected, return
|
|
||||||
for (const id of selectedShapeIds) {
|
|
||||||
if (shapesToCheck.has(id)) return
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = shape
|
if (
|
||||||
|
(bindings.start &&
|
||||||
|
(selectedShapeIds.includes(bindings.start.toId) ||
|
||||||
|
this.editor.isAncestorSelected(bindings.start.toId))) ||
|
||||||
|
(bindings.end &&
|
||||||
|
(selectedShapeIds.includes(bindings.end.toId) ||
|
||||||
|
this.editor.isAncestorSelected(bindings.end.toId)))
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// When we start translating shapes, record where their bindings were in page space so we
|
// When we start translating shapes, record where their bindings were in page space so we
|
||||||
// can maintain them as we translate the arrow
|
// can maintain them as we translate the arrow
|
||||||
shapeAtTranslationStart.set(shape, {
|
shapeAtTranslationStart.set(shape, {
|
||||||
pagePosition: shapePageTransform.applyToPoint(shape),
|
pagePosition: shapePageTransform.applyToPoint(shape),
|
||||||
terminalBindings: mapObjectMapValues(terminalsInArrowSpace, (terminalName, point) => {
|
terminalBindings: mapObjectMapValues(terminalsInArrowSpace, (terminalName, point) => {
|
||||||
const terminal = shape.props[terminalName]
|
const binding = bindings[terminalName]
|
||||||
if (terminal.type !== 'binding') return null
|
if (!binding) return null
|
||||||
return {
|
return {
|
||||||
binding: terminal,
|
binding,
|
||||||
shapePosition: point,
|
shapePosition: point,
|
||||||
pagePosition: shapePageTransform.applyToPoint(point),
|
pagePosition: shapePageTransform.applyToPoint(point),
|
||||||
}
|
}
|
||||||
|
@ -341,15 +350,16 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const handleName of [ARROW_HANDLES.START, ARROW_HANDLES.END] as const) {
|
for (const handleName of [ARROW_HANDLES.START, ARROW_HANDLES.END] as const) {
|
||||||
const terminal = shape.props[handleName]
|
const binding = bindings[handleName]
|
||||||
if (terminal.type !== 'binding') continue
|
if (!binding) continue
|
||||||
result = {
|
|
||||||
...shape,
|
this.editor.updateBinding({
|
||||||
props: { ...shape.props, [handleName]: { ...terminal, isPrecise: true } },
|
...binding,
|
||||||
}
|
props: { ...binding.props, isPrecise: true },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
override onTranslate?: TLOnTranslateHandler<TLArrowShape> = (initialShape, shape) => {
|
override onTranslate?: TLOnTranslateHandler<TLArrowShape> = (initialShape, shape) => {
|
||||||
|
@ -362,10 +372,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
atTranslationStart.pagePosition
|
atTranslationStart.pagePosition
|
||||||
)
|
)
|
||||||
|
|
||||||
let result = shape
|
for (const terminalBinding of Object.values(atTranslationStart.terminalBindings)) {
|
||||||
for (const [terminalName, terminalBinding] of objectMapEntries(
|
|
||||||
atTranslationStart.terminalBindings
|
|
||||||
)) {
|
|
||||||
if (!terminalBinding) continue
|
if (!terminalBinding) continue
|
||||||
|
|
||||||
const newPagePoint = Vec.Add(terminalBinding.pagePosition, Vec.Mul(pageDelta, 0.5))
|
const newPagePoint = Vec.Add(terminalBinding.pagePosition, Vec.Mul(pageDelta, 0.5))
|
||||||
|
@ -378,54 +385,46 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (newTarget?.id === terminalBinding.binding.boundShapeId) {
|
if (newTarget?.id === terminalBinding.binding.toId) {
|
||||||
const targetBounds = Box.ZeroFix(this.editor.getShapeGeometry(newTarget).bounds)
|
const targetBounds = Box.ZeroFix(this.editor.getShapeGeometry(newTarget).bounds)
|
||||||
const pointInTargetSpace = this.editor.getPointInShapeSpace(newTarget, newPagePoint)
|
const pointInTargetSpace = this.editor.getPointInShapeSpace(newTarget, newPagePoint)
|
||||||
const normalizedAnchor = {
|
const normalizedAnchor = {
|
||||||
x: (pointInTargetSpace.x - targetBounds.minX) / targetBounds.width,
|
x: (pointInTargetSpace.x - targetBounds.minX) / targetBounds.width,
|
||||||
y: (pointInTargetSpace.y - targetBounds.minY) / targetBounds.height,
|
y: (pointInTargetSpace.y - targetBounds.minY) / targetBounds.height,
|
||||||
}
|
}
|
||||||
result = {
|
createOrUpdateArrowBinding(this.editor, shape, newTarget.id, {
|
||||||
...result,
|
...terminalBinding.binding.props,
|
||||||
props: {
|
normalizedAnchor,
|
||||||
...result.props,
|
isPrecise: true,
|
||||||
[terminalName]: { ...terminalBinding.binding, isPrecise: true, normalizedAnchor },
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
result = {
|
removeArrowBinding(this.editor, shape, terminalBinding.binding.props.terminal)
|
||||||
...result,
|
|
||||||
props: {
|
|
||||||
...result.props,
|
|
||||||
[terminalName]: {
|
|
||||||
type: 'point',
|
|
||||||
x: terminalBinding.shapePosition.x,
|
|
||||||
y: terminalBinding.shapePosition.y,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// replace this with memo bag?
|
||||||
|
private _resizeInitialBindings: TLArrowBindings = { start: undefined, end: undefined }
|
||||||
|
override onResizeStart?: TLOnResizeStartHandler<TLArrowShape> = (shape) => {
|
||||||
|
this._resizeInitialBindings = getArrowBindings(this.editor, shape)
|
||||||
|
}
|
||||||
override onResize: TLOnResizeHandler<TLArrowShape> = (shape, info) => {
|
override onResize: TLOnResizeHandler<TLArrowShape> = (shape, info) => {
|
||||||
const { scaleX, scaleY } = info
|
const { scaleX, scaleY } = info
|
||||||
|
|
||||||
const terminals = getArrowTerminalsInArrowSpace(this.editor, shape)
|
const bindings = this._resizeInitialBindings
|
||||||
|
const terminals = getArrowTerminalsInArrowSpace(this.editor, shape, bindings)
|
||||||
|
|
||||||
const { start, end } = structuredClone<TLArrowShape['props']>(shape.props)
|
const { start, end } = structuredClone<TLArrowShape['props']>(shape.props)
|
||||||
let { bend } = shape.props
|
let { bend } = shape.props
|
||||||
|
|
||||||
// Rescale start handle if it's not bound to a shape
|
// Rescale start handle if it's not bound to a shape
|
||||||
if (start.type === 'point') {
|
if (!bindings.start) {
|
||||||
start.x = terminals.start.x * scaleX
|
start.x = terminals.start.x * scaleX
|
||||||
start.y = terminals.start.y * scaleY
|
start.y = terminals.start.y * scaleY
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rescale end handle if it's not bound to a shape
|
// Rescale end handle if it's not bound to a shape
|
||||||
if (end.type === 'point') {
|
if (!bindings.end) {
|
||||||
end.x = terminals.end.x * scaleX
|
end.x = terminals.end.x * scaleX
|
||||||
end.y = terminals.end.y * scaleY
|
end.y = terminals.end.y * scaleY
|
||||||
}
|
}
|
||||||
|
@ -436,18 +435,23 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
const mx = Math.abs(scaleX)
|
const mx = Math.abs(scaleX)
|
||||||
const my = Math.abs(scaleY)
|
const my = Math.abs(scaleY)
|
||||||
|
|
||||||
|
const startNormalizedAnchor = bindings?.start
|
||||||
|
? Vec.From(bindings.start.props.normalizedAnchor)
|
||||||
|
: null
|
||||||
|
const endNormalizedAnchor = bindings?.end ? Vec.From(bindings.end.props.normalizedAnchor) : null
|
||||||
|
|
||||||
if (scaleX < 0 && scaleY >= 0) {
|
if (scaleX < 0 && scaleY >= 0) {
|
||||||
if (bend !== 0) {
|
if (bend !== 0) {
|
||||||
bend *= -1
|
bend *= -1
|
||||||
bend *= Math.max(mx, my)
|
bend *= Math.max(mx, my)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start.type === 'binding') {
|
if (startNormalizedAnchor) {
|
||||||
start.normalizedAnchor.x = 1 - start.normalizedAnchor.x
|
startNormalizedAnchor.x = 1 - startNormalizedAnchor.x
|
||||||
}
|
}
|
||||||
|
|
||||||
if (end.type === 'binding') {
|
if (endNormalizedAnchor) {
|
||||||
end.normalizedAnchor.x = 1 - end.normalizedAnchor.x
|
endNormalizedAnchor.x = 1 - endNormalizedAnchor.x
|
||||||
}
|
}
|
||||||
} else if (scaleX >= 0 && scaleY < 0) {
|
} else if (scaleX >= 0 && scaleY < 0) {
|
||||||
if (bend !== 0) {
|
if (bend !== 0) {
|
||||||
|
@ -455,12 +459,12 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
bend *= Math.max(mx, my)
|
bend *= Math.max(mx, my)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start.type === 'binding') {
|
if (startNormalizedAnchor) {
|
||||||
start.normalizedAnchor.y = 1 - start.normalizedAnchor.y
|
startNormalizedAnchor.y = 1 - startNormalizedAnchor.y
|
||||||
}
|
}
|
||||||
|
|
||||||
if (end.type === 'binding') {
|
if (endNormalizedAnchor) {
|
||||||
end.normalizedAnchor.y = 1 - end.normalizedAnchor.y
|
endNormalizedAnchor.y = 1 - endNormalizedAnchor.y
|
||||||
}
|
}
|
||||||
} else if (scaleX >= 0 && scaleY >= 0) {
|
} else if (scaleX >= 0 && scaleY >= 0) {
|
||||||
if (bend !== 0) {
|
if (bend !== 0) {
|
||||||
|
@ -471,17 +475,30 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
bend *= Math.max(mx, my)
|
bend *= Math.max(mx, my)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start.type === 'binding') {
|
if (startNormalizedAnchor) {
|
||||||
start.normalizedAnchor.x = 1 - start.normalizedAnchor.x
|
startNormalizedAnchor.x = 1 - startNormalizedAnchor.x
|
||||||
start.normalizedAnchor.y = 1 - start.normalizedAnchor.y
|
startNormalizedAnchor.y = 1 - startNormalizedAnchor.y
|
||||||
}
|
}
|
||||||
|
|
||||||
if (end.type === 'binding') {
|
if (endNormalizedAnchor) {
|
||||||
end.normalizedAnchor.x = 1 - end.normalizedAnchor.x
|
endNormalizedAnchor.x = 1 - endNormalizedAnchor.x
|
||||||
end.normalizedAnchor.y = 1 - end.normalizedAnchor.y
|
endNormalizedAnchor.y = 1 - endNormalizedAnchor.y
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bindings.start && startNormalizedAnchor) {
|
||||||
|
createOrUpdateArrowBinding(this.editor, shape, bindings.start.toId, {
|
||||||
|
...bindings.start.props,
|
||||||
|
normalizedAnchor: startNormalizedAnchor.toJson(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (bindings.end && endNormalizedAnchor) {
|
||||||
|
createOrUpdateArrowBinding(this.editor, shape, bindings.end.toId, {
|
||||||
|
...bindings.end.props,
|
||||||
|
normalizedAnchor: endNormalizedAnchor.toJson(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const next = {
|
const next = {
|
||||||
props: {
|
props: {
|
||||||
start,
|
start,
|
||||||
|
@ -565,18 +582,18 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
indicator(shape: TLArrowShape) {
|
indicator(shape: TLArrowShape) {
|
||||||
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape)
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const isEditing = useIsEditing(shape.id)
|
||||||
|
|
||||||
const info = this.editor.getArrowInfo(shape)
|
const info = this.editor.getArrowInfo(shape)
|
||||||
|
if (!info) return null
|
||||||
|
|
||||||
|
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape, info?.bindings)
|
||||||
const geometry = this.editor.getShapeGeometry<Group2d>(shape)
|
const geometry = this.editor.getShapeGeometry<Group2d>(shape)
|
||||||
const bounds = geometry.bounds
|
const bounds = geometry.bounds
|
||||||
|
|
||||||
const labelGeometry = shape.props.text.trim() ? (geometry.children[1] as Rectangle2d) : null
|
const labelGeometry = shape.props.text.trim() ? (geometry.children[1] as Rectangle2d) : null
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const isEditing = useIsEditing(shape.id)
|
|
||||||
|
|
||||||
if (!info) return null
|
|
||||||
if (Vec.Equals(start, end)) return null
|
if (Vec.Equals(start, end)) return null
|
||||||
|
|
||||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
const strokeWidth = STROKE_SIZES[shape.props.size]
|
||||||
|
@ -753,6 +770,7 @@ const ArrowSvg = track(function ArrowSvg({
|
||||||
const theme = useDefaultColorTheme()
|
const theme = useDefaultColorTheme()
|
||||||
const info = editor.getArrowInfo(shape)
|
const info = editor.getArrowInfo(shape)
|
||||||
const bounds = Box.ZeroFix(editor.getShapeGeometry(shape).bounds)
|
const bounds = Box.ZeroFix(editor.getShapeGeometry(shape).bounds)
|
||||||
|
const bindings = getArrowBindings(editor, shape)
|
||||||
|
|
||||||
const changeIndex = React.useMemo<number>(() => {
|
const changeIndex = React.useMemo<number>(() => {
|
||||||
return editor.environment.isSafari ? (globalRenderIndex += 1) : 0
|
return editor.environment.isSafari ? (globalRenderIndex += 1) : 0
|
||||||
|
@ -783,7 +801,7 @@ const ArrowSvg = track(function ArrowSvg({
|
||||||
)
|
)
|
||||||
|
|
||||||
handlePath =
|
handlePath =
|
||||||
shape.props.start.type === 'binding' || shape.props.end.type === 'binding' ? (
|
bindings.start || bindings.end ? (
|
||||||
<path
|
<path
|
||||||
className="tl-arrow-hint"
|
className="tl-arrow-hint"
|
||||||
d={info.isStraight ? getStraightArrowHandlePath(info) : getCurvedArrowHandlePath(info)}
|
d={info.isStraight ? getStraightArrowHandlePath(info) : getCurvedArrowHandlePath(info)}
|
||||||
|
@ -791,19 +809,19 @@ const ArrowSvg = track(function ArrowSvg({
|
||||||
strokeDashoffset={strokeDashoffset}
|
strokeDashoffset={strokeDashoffset}
|
||||||
strokeWidth={sw}
|
strokeWidth={sw}
|
||||||
markerStart={
|
markerStart={
|
||||||
shape.props.start.type === 'binding'
|
bindings.start
|
||||||
? shape.props.start.isExact
|
? bindings.start.props.isExact
|
||||||
? ''
|
? ''
|
||||||
: shape.props.start.isPrecise
|
: bindings.start.props.isPrecise
|
||||||
? 'url(#arrowhead-cross)'
|
? 'url(#arrowhead-cross)'
|
||||||
: 'url(#arrowhead-dot)'
|
: 'url(#arrowhead-dot)'
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
markerEnd={
|
markerEnd={
|
||||||
shape.props.end.type === 'binding'
|
bindings.end
|
||||||
? shape.props.end.isExact
|
? bindings.end.props.isExact
|
||||||
? ''
|
? ''
|
||||||
: shape.props.end.isPrecise
|
: bindings.end.props.isPrecise
|
||||||
? 'url(#arrowhead-cross)'
|
? 'url(#arrowhead-cross)'
|
||||||
: 'url(#arrowhead-dot)'
|
: 'url(#arrowhead-dot)'
|
||||||
: ''
|
: ''
|
||||||
|
@ -903,7 +921,7 @@ const shapeAtTranslationStart = new WeakMap<
|
||||||
{
|
{
|
||||||
pagePosition: Vec
|
pagePosition: Vec
|
||||||
shapePosition: Vec
|
shapePosition: Vec
|
||||||
binding: Extract<TLArrowShapeProps['start'], { type: 'binding' }>
|
binding: TLArrowBinding
|
||||||
} | null
|
} | null
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
|
@ -268,8 +268,8 @@ export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
|
||||||
const debugGeom: Geometry2d[] = []
|
const debugGeom: Geometry2d[] = []
|
||||||
const info = editor.getArrowInfo(shape)!
|
const info = editor.getArrowInfo(shape)!
|
||||||
|
|
||||||
const hasStartBinding = shape.props.start.type === 'binding'
|
const hasStartBinding = !!info.bindings.start
|
||||||
const hasEndBinding = shape.props.end.type === 'binding'
|
const hasEndBinding = !!info.bindings.end
|
||||||
const hasStartArrowhead = info.start.arrowhead !== 'none'
|
const hasStartArrowhead = info.start.arrowhead !== 'none'
|
||||||
const hasEndArrowhead = info.end.arrowhead !== 'none'
|
const hasEndArrowhead = info.end.arrowhead !== 'none'
|
||||||
if (info.isStraight) {
|
if (info.isStraight) {
|
||||||
|
|
|
@ -42,11 +42,12 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
if (!this.shape) throw Error(`expected shape`)
|
if (!this.shape) throw Error(`expected shape`)
|
||||||
|
|
||||||
|
// const initialEndHandle = this.editor.getShapeHandles(this.shape)!.find((h) => h.id === 'end')!
|
||||||
this.updateArrowShapeEndHandle()
|
this.updateArrowShapeEndHandle()
|
||||||
|
|
||||||
this.editor.setCurrentTool('select.dragging_handle', {
|
this.editor.setCurrentTool('select.dragging_handle', {
|
||||||
shape: this.shape,
|
shape: this.shape,
|
||||||
handle: this.editor.getShapeHandles(this.shape)!.find((h) => h.id === 'end')!,
|
handle: { id: 'end', type: 'vertex', index: 'a3', x: 0, y: 0, canBind: true },
|
||||||
isCreating: true,
|
isCreating: true,
|
||||||
onInteractionEnd: 'arrow',
|
onInteractionEnd: 'arrow',
|
||||||
})
|
})
|
||||||
|
@ -111,10 +112,6 @@ export class Pointing extends StateNode {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (change) {
|
if (change) {
|
||||||
const startTerminal = change.props?.start
|
|
||||||
if (startTerminal?.type === 'binding') {
|
|
||||||
this.editor.setHintingShapes([startTerminal.boundShapeId])
|
|
||||||
}
|
|
||||||
this.editor.updateShapes([change])
|
this.editor.updateShapes([change])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,9 +127,20 @@ export class Pointing extends StateNode {
|
||||||
const handles = this.editor.getShapeHandles(shape)
|
const handles = this.editor.getShapeHandles(shape)
|
||||||
if (!handles) throw Error(`expected handles for arrow`)
|
if (!handles) throw Error(`expected handles for arrow`)
|
||||||
|
|
||||||
const shapeWithOutEndOffset = {
|
// start update
|
||||||
...shape,
|
{
|
||||||
props: { ...shape.props, end: { ...shape.props.end, x: 0, y: 0 } },
|
const util = this.editor.getShapeUtil<TLArrowShape>('arrow')
|
||||||
|
const initial = this.shape
|
||||||
|
const startHandle = handles.find((h) => h.id === 'start')!
|
||||||
|
const change = util.onHandleDrag?.(shape, {
|
||||||
|
handle: { ...startHandle, x: 0, y: 0 },
|
||||||
|
isPrecise: this.didTimeout, // sure about that?
|
||||||
|
initial: initial,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (change) {
|
||||||
|
this.editor.updateShapes([change])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// end update
|
// end update
|
||||||
|
@ -141,32 +149,12 @@ export class Pointing extends StateNode {
|
||||||
const initial = this.shape
|
const initial = this.shape
|
||||||
const point = this.editor.getPointInShapeSpace(shape, this.editor.inputs.currentPagePoint)
|
const point = this.editor.getPointInShapeSpace(shape, this.editor.inputs.currentPagePoint)
|
||||||
const endHandle = handles.find((h) => h.id === 'end')!
|
const endHandle = handles.find((h) => h.id === 'end')!
|
||||||
const change = util.onHandleDrag?.(shapeWithOutEndOffset, {
|
const change = util.onHandleDrag?.(this.editor.getShape(shape)!, {
|
||||||
handle: { ...endHandle, x: point.x, y: point.y },
|
handle: { ...endHandle, x: point.x, y: point.y },
|
||||||
isPrecise: false, // sure about that?
|
isPrecise: false, // sure about that?
|
||||||
initial: initial,
|
initial: initial,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (change) {
|
|
||||||
const endTerminal = change.props?.end
|
|
||||||
if (endTerminal?.type === 'binding') {
|
|
||||||
this.editor.setHintingShapes([endTerminal.boundShapeId])
|
|
||||||
}
|
|
||||||
this.editor.updateShapes([change])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start update
|
|
||||||
{
|
|
||||||
const util = this.editor.getShapeUtil<TLArrowShape>('arrow')
|
|
||||||
const initial = this.shape
|
|
||||||
const startHandle = handles.find((h) => h.id === 'start')!
|
|
||||||
const change = util.onHandleDrag?.(shapeWithOutEndOffset, {
|
|
||||||
handle: { ...startHandle, x: 0, y: 0 },
|
|
||||||
isPrecise: this.didTimeout, // sure about that?
|
|
||||||
initial: initial,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (change) {
|
if (change) {
|
||||||
this.editor.updateShapes([change])
|
this.editor.updateShapes([change])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
import {
|
||||||
StateNode,
|
StateNode,
|
||||||
TLArrowShape,
|
TLArrowShape,
|
||||||
TLArrowShapeTerminal,
|
|
||||||
TLCancelEvent,
|
TLCancelEvent,
|
||||||
TLEnterEventHandler,
|
TLEnterEventHandler,
|
||||||
TLEventHandlers,
|
TLEventHandlers,
|
||||||
|
@ -12,6 +11,7 @@ import {
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
Vec,
|
Vec,
|
||||||
|
getArrowBindings,
|
||||||
snapAngle,
|
snapAngle,
|
||||||
sortByIndex,
|
sortByIndex,
|
||||||
structuredClone,
|
structuredClone,
|
||||||
|
@ -112,16 +112,16 @@ export class DraggingHandle extends StateNode {
|
||||||
|
|
||||||
// <!-- Only relevant to arrows
|
// <!-- Only relevant to arrows
|
||||||
if (this.editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
|
if (this.editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
|
||||||
const initialTerminal = shape.props[info.handle.id as 'start' | 'end']
|
const initialBinding = getArrowBindings(this.editor, shape)[info.handle.id as 'start' | 'end']
|
||||||
|
|
||||||
this.isPrecise = false
|
this.isPrecise = false
|
||||||
|
|
||||||
if (initialTerminal?.type === 'binding') {
|
if (initialBinding) {
|
||||||
this.editor.setHintingShapes([initialTerminal.boundShapeId])
|
this.editor.setHintingShapes([initialBinding.toId])
|
||||||
|
|
||||||
this.isPrecise = initialTerminal.isPrecise
|
this.isPrecise = initialBinding.props.isPrecise
|
||||||
if (this.isPrecise) {
|
if (this.isPrecise) {
|
||||||
this.isPreciseId = initialTerminal.boundShapeId
|
this.isPreciseId = initialBinding.toId
|
||||||
} else {
|
} else {
|
||||||
this.resetExactTimeout()
|
this.resetExactTimeout()
|
||||||
}
|
}
|
||||||
|
@ -280,18 +280,18 @@ export class DraggingHandle extends StateNode {
|
||||||
initial: initial,
|
initial: initial,
|
||||||
})
|
})
|
||||||
|
|
||||||
const next: TLShapePartial<any> = { ...shape, ...changes }
|
const next: TLShapePartial<any> = { id: shape.id, type: shape.type, ...changes }
|
||||||
|
|
||||||
// Arrows
|
// Arrows
|
||||||
if (initialHandle.canBind) {
|
if (initialHandle.canBind && this.editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
|
||||||
const bindingAfter = (next.props as any)[initialHandle.id] as TLArrowShapeTerminal | undefined
|
const bindingAfter = getArrowBindings(editor, shape)[initialHandle.id as 'start' | 'end']
|
||||||
|
|
||||||
if (bindingAfter?.type === 'binding') {
|
if (bindingAfter) {
|
||||||
if (hintingShapeIds[0] !== bindingAfter.boundShapeId) {
|
if (hintingShapeIds[0] !== bindingAfter.toId) {
|
||||||
editor.setHintingShapes([bindingAfter.boundShapeId])
|
editor.setHintingShapes([bindingAfter.toId])
|
||||||
this.pointingId = bindingAfter.boundShapeId
|
this.pointingId = bindingAfter.toId
|
||||||
this.isPrecise = pointerVelocity.len() < 0.5 || altKey
|
this.isPrecise = pointerVelocity.len() < 0.5 || altKey
|
||||||
this.isPreciseId = this.isPrecise ? bindingAfter.boundShapeId : null
|
this.isPreciseId = this.isPrecise ? bindingAfter.toId : null
|
||||||
this.resetExactTimeout()
|
this.resetExactTimeout()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
TLNoteShape,
|
TLNoteShape,
|
||||||
TLPointerEventInfo,
|
TLPointerEventInfo,
|
||||||
Vec,
|
Vec,
|
||||||
|
getArrowBindings,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import {
|
import {
|
||||||
NOTE_CENTER_OFFSET,
|
NOTE_CENTER_OFFSET,
|
||||||
|
@ -25,10 +26,10 @@ export class PointingHandle extends StateNode {
|
||||||
|
|
||||||
const { shape } = info
|
const { shape } = info
|
||||||
if (this.editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
|
if (this.editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
|
||||||
const initialTerminal = shape.props[info.handle.id as 'start' | 'end']
|
const initialBinding = getArrowBindings(this.editor, shape)[info.handle.id as 'start' | 'end']
|
||||||
|
|
||||||
if (initialTerminal?.type === 'binding') {
|
if (initialBinding) {
|
||||||
this.editor.setHintingShapes([initialTerminal.boundShapeId])
|
this.editor.setHintingShapes([initialBinding.toId])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,8 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
|
||||||
|
|
||||||
const tldrawContent: TLContent = {
|
const tldrawContent: TLContent = {
|
||||||
shapes: [],
|
shapes: [],
|
||||||
|
// todo(alex) #write these properly
|
||||||
|
bindings: [],
|
||||||
rootShapeIds: [],
|
rootShapeIds: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
schema: editor.store.schema.serialize(),
|
schema: editor.store.schema.serialize(),
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
TLGroupShape,
|
TLGroupShape,
|
||||||
TLLineShape,
|
TLLineShape,
|
||||||
TLTextShape,
|
TLTextShape,
|
||||||
|
getArrowBindings,
|
||||||
useEditor,
|
useEditor,
|
||||||
useValue,
|
useValue,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
@ -17,14 +18,9 @@ function shapesWithUnboundArrows(editor: Editor) {
|
||||||
|
|
||||||
return selectedShapes.filter((shape) => {
|
return selectedShapes.filter((shape) => {
|
||||||
if (!shape) return false
|
if (!shape) return false
|
||||||
if (
|
if (editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
|
||||||
editor.isShapeOfType<TLArrowShape>(shape, 'arrow') &&
|
const bindings = getArrowBindings(editor, shape)
|
||||||
shape.props.start.type === 'binding'
|
if (bindings.start || bindings.end) return false
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (editor.isShapeOfType<TLArrowShape>(shape, 'arrow') && shape.props.end.type === 'binding') {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
@ -50,16 +46,16 @@ export const useAllowGroup = () => {
|
||||||
|
|
||||||
for (const shape of selectedShapes) {
|
for (const shape of selectedShapes) {
|
||||||
if (editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
|
if (editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
|
||||||
const { start, end } = shape.props
|
const bindings = getArrowBindings(editor, shape)
|
||||||
if (start.type === 'binding') {
|
if (bindings.start) {
|
||||||
// if the other shape is not among the selected shapes...
|
// if the other shape is not among the selected shapes...
|
||||||
if (!selectedShapes.some((s) => s.id === start.boundShapeId)) {
|
if (!selectedShapes.some((s) => s.id === bindings.start!.toId)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (end.type === 'binding') {
|
if (bindings.end) {
|
||||||
// if the other shape is not among the selected shapes...
|
// if the other shape is not among the selected shapes...
|
||||||
if (!selectedShapes.some((s) => s.id === end.boundShapeId)) {
|
if (!selectedShapes.some((s) => s.id === bindings.end!.toId)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,419 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`buildFromV1Document test fixtures arrow-binding.tldr 1`] = `
|
||||||
|
{
|
||||||
|
"binding:15": {
|
||||||
|
"fromId": "shape:13",
|
||||||
|
"id": "binding:15",
|
||||||
|
"meta": {},
|
||||||
|
"props": {
|
||||||
|
"isExact": false,
|
||||||
|
"isPrecise": true,
|
||||||
|
"normalizedAnchor": {
|
||||||
|
"x": 0.5,
|
||||||
|
"y": 0.5,
|
||||||
|
},
|
||||||
|
"terminal": "start",
|
||||||
|
},
|
||||||
|
"toId": "shape:11",
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "binding",
|
||||||
|
},
|
||||||
|
"binding:17": {
|
||||||
|
"fromId": "shape:13",
|
||||||
|
"id": "binding:17",
|
||||||
|
"meta": {},
|
||||||
|
"props": {
|
||||||
|
"isExact": false,
|
||||||
|
"isPrecise": true,
|
||||||
|
"normalizedAnchor": {
|
||||||
|
"x": 0.495,
|
||||||
|
"y": 0.655,
|
||||||
|
},
|
||||||
|
"terminal": "end",
|
||||||
|
},
|
||||||
|
"toId": "shape:9",
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "binding",
|
||||||
|
},
|
||||||
|
"document:document": {
|
||||||
|
"gridSize": 10,
|
||||||
|
"id": "document:document",
|
||||||
|
"meta": {},
|
||||||
|
"name": "",
|
||||||
|
"typeName": "document",
|
||||||
|
},
|
||||||
|
"page:page": {
|
||||||
|
"id": "page:page",
|
||||||
|
"index": "a1",
|
||||||
|
"meta": {},
|
||||||
|
"name": "Page 1",
|
||||||
|
"typeName": "page",
|
||||||
|
},
|
||||||
|
"shape:11": {
|
||||||
|
"id": "shape:11",
|
||||||
|
"index": "a2",
|
||||||
|
"isLocked": false,
|
||||||
|
"meta": {},
|
||||||
|
"opacity": 1,
|
||||||
|
"parentId": "page:page",
|
||||||
|
"props": {
|
||||||
|
"align": "middle",
|
||||||
|
"color": "red",
|
||||||
|
"dash": "draw",
|
||||||
|
"fill": "solid",
|
||||||
|
"font": "draw",
|
||||||
|
"geo": "rectangle",
|
||||||
|
"growY": 0,
|
||||||
|
"h": 114.39,
|
||||||
|
"labelColor": "red",
|
||||||
|
"size": "m",
|
||||||
|
"text": "",
|
||||||
|
"url": "",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"w": 240.17,
|
||||||
|
},
|
||||||
|
"rotation": 0,
|
||||||
|
"type": "geo",
|
||||||
|
"typeName": "shape",
|
||||||
|
"x": 1616.09,
|
||||||
|
"y": 584.28,
|
||||||
|
},
|
||||||
|
"shape:13": {
|
||||||
|
"id": "shape:13",
|
||||||
|
"index": "a3",
|
||||||
|
"isLocked": false,
|
||||||
|
"meta": {},
|
||||||
|
"opacity": 1,
|
||||||
|
"parentId": "page:page",
|
||||||
|
"props": {
|
||||||
|
"arrowheadEnd": "arrow",
|
||||||
|
"arrowheadStart": "none",
|
||||||
|
"bend": -74.62876127600846,
|
||||||
|
"color": "red",
|
||||||
|
"dash": "draw",
|
||||||
|
"end": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 173.3,
|
||||||
|
},
|
||||||
|
"fill": "none",
|
||||||
|
"font": "draw",
|
||||||
|
"labelColor": "red",
|
||||||
|
"labelPosition": 0.5,
|
||||||
|
"size": "m",
|
||||||
|
"start": {
|
||||||
|
"x": 146.32,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
"text": "",
|
||||||
|
},
|
||||||
|
"rotation": 0,
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "shape",
|
||||||
|
"x": 1541.56,
|
||||||
|
"y": 698.67,
|
||||||
|
},
|
||||||
|
"shape:9": {
|
||||||
|
"id": "shape:9",
|
||||||
|
"index": "a1",
|
||||||
|
"isLocked": false,
|
||||||
|
"meta": {},
|
||||||
|
"opacity": 1,
|
||||||
|
"parentId": "page:page",
|
||||||
|
"props": {
|
||||||
|
"align": "middle",
|
||||||
|
"color": "red",
|
||||||
|
"dash": "draw",
|
||||||
|
"fill": "solid",
|
||||||
|
"font": "draw",
|
||||||
|
"geo": "rectangle",
|
||||||
|
"growY": 0,
|
||||||
|
"h": 177.03,
|
||||||
|
"labelColor": "red",
|
||||||
|
"size": "m",
|
||||||
|
"text": "",
|
||||||
|
"url": "",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"w": 136.64,
|
||||||
|
},
|
||||||
|
"rotation": 0,
|
||||||
|
"type": "geo",
|
||||||
|
"typeName": "shape",
|
||||||
|
"x": 1388.92,
|
||||||
|
"y": 820.52,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`buildFromV1Document test fixtures exact-arrow-binding.tldr 1`] = `
|
||||||
|
{
|
||||||
|
"binding:14": {
|
||||||
|
"fromId": "shape:12",
|
||||||
|
"id": "binding:14",
|
||||||
|
"meta": {},
|
||||||
|
"props": {
|
||||||
|
"isExact": true,
|
||||||
|
"isPrecise": true,
|
||||||
|
"normalizedAnchor": {
|
||||||
|
"x": 0.6250000000000002,
|
||||||
|
"y": 0.5700000000000003,
|
||||||
|
},
|
||||||
|
"terminal": "start",
|
||||||
|
},
|
||||||
|
"toId": "shape:10",
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "binding",
|
||||||
|
},
|
||||||
|
"binding:16": {
|
||||||
|
"fromId": "shape:12",
|
||||||
|
"id": "binding:16",
|
||||||
|
"meta": {},
|
||||||
|
"props": {
|
||||||
|
"isExact": true,
|
||||||
|
"isPrecise": true,
|
||||||
|
"normalizedAnchor": {
|
||||||
|
"x": 0.5,
|
||||||
|
"y": 0.5,
|
||||||
|
},
|
||||||
|
"terminal": "end",
|
||||||
|
},
|
||||||
|
"toId": "shape:8",
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "binding",
|
||||||
|
},
|
||||||
|
"document:document": {
|
||||||
|
"gridSize": 10,
|
||||||
|
"id": "document:document",
|
||||||
|
"meta": {},
|
||||||
|
"name": "",
|
||||||
|
"typeName": "document",
|
||||||
|
},
|
||||||
|
"page:page": {
|
||||||
|
"id": "page:page",
|
||||||
|
"index": "a1",
|
||||||
|
"meta": {},
|
||||||
|
"name": "Page 1",
|
||||||
|
"typeName": "page",
|
||||||
|
},
|
||||||
|
"shape:10": {
|
||||||
|
"id": "shape:10",
|
||||||
|
"index": "a2",
|
||||||
|
"isLocked": false,
|
||||||
|
"meta": {},
|
||||||
|
"opacity": 1,
|
||||||
|
"parentId": "page:page",
|
||||||
|
"props": {
|
||||||
|
"align": "middle",
|
||||||
|
"color": "red",
|
||||||
|
"dash": "draw",
|
||||||
|
"fill": "solid",
|
||||||
|
"font": "draw",
|
||||||
|
"geo": "rectangle",
|
||||||
|
"growY": 0,
|
||||||
|
"h": 114.39,
|
||||||
|
"labelColor": "red",
|
||||||
|
"size": "m",
|
||||||
|
"text": "",
|
||||||
|
"url": "",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"w": 240.17,
|
||||||
|
},
|
||||||
|
"rotation": 0,
|
||||||
|
"type": "geo",
|
||||||
|
"typeName": "shape",
|
||||||
|
"x": 1616.09,
|
||||||
|
"y": 584.28,
|
||||||
|
},
|
||||||
|
"shape:12": {
|
||||||
|
"id": "shape:12",
|
||||||
|
"index": "a3",
|
||||||
|
"isLocked": false,
|
||||||
|
"meta": {},
|
||||||
|
"opacity": 1,
|
||||||
|
"parentId": "page:page",
|
||||||
|
"props": {
|
||||||
|
"arrowheadEnd": "arrow",
|
||||||
|
"arrowheadStart": "arrow",
|
||||||
|
"bend": -117.13359957478735,
|
||||||
|
"color": "red",
|
||||||
|
"dash": "draw",
|
||||||
|
"end": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 201.67,
|
||||||
|
},
|
||||||
|
"fill": "none",
|
||||||
|
"font": "draw",
|
||||||
|
"labelColor": "red",
|
||||||
|
"labelPosition": 0.5,
|
||||||
|
"size": "m",
|
||||||
|
"start": {
|
||||||
|
"x": 293.36,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
"text": "",
|
||||||
|
},
|
||||||
|
"rotation": 0,
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "shape",
|
||||||
|
"x": 1510.86,
|
||||||
|
"y": 661.97,
|
||||||
|
},
|
||||||
|
"shape:8": {
|
||||||
|
"id": "shape:8",
|
||||||
|
"index": "a1",
|
||||||
|
"isLocked": false,
|
||||||
|
"meta": {},
|
||||||
|
"opacity": 1,
|
||||||
|
"parentId": "page:page",
|
||||||
|
"props": {
|
||||||
|
"align": "middle",
|
||||||
|
"color": "red",
|
||||||
|
"dash": "draw",
|
||||||
|
"fill": "solid",
|
||||||
|
"font": "draw",
|
||||||
|
"geo": "rectangle",
|
||||||
|
"growY": 0,
|
||||||
|
"h": 177.03,
|
||||||
|
"labelColor": "red",
|
||||||
|
"size": "m",
|
||||||
|
"text": "",
|
||||||
|
"url": "",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"w": 136.64,
|
||||||
|
},
|
||||||
|
"rotation": 0,
|
||||||
|
"type": "geo",
|
||||||
|
"typeName": "shape",
|
||||||
|
"x": 1418.93,
|
||||||
|
"y": 779.31,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`buildFromV1Document test fixtures incorrect-arrow-binding.tldr 1`] = `
|
||||||
|
{
|
||||||
|
"binding:14": {
|
||||||
|
"fromId": "shape:12",
|
||||||
|
"id": "binding:14",
|
||||||
|
"meta": {},
|
||||||
|
"props": {
|
||||||
|
"isExact": false,
|
||||||
|
"isPrecise": true,
|
||||||
|
"normalizedAnchor": {
|
||||||
|
"x": 0.385,
|
||||||
|
"y": 0.425,
|
||||||
|
},
|
||||||
|
"terminal": "end",
|
||||||
|
},
|
||||||
|
"toId": "shape:8",
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "binding",
|
||||||
|
},
|
||||||
|
"document:document": {
|
||||||
|
"gridSize": 10,
|
||||||
|
"id": "document:document",
|
||||||
|
"meta": {},
|
||||||
|
"name": "",
|
||||||
|
"typeName": "document",
|
||||||
|
},
|
||||||
|
"page:page": {
|
||||||
|
"id": "page:page",
|
||||||
|
"index": "a1",
|
||||||
|
"meta": {},
|
||||||
|
"name": "Page 1",
|
||||||
|
"typeName": "page",
|
||||||
|
},
|
||||||
|
"shape:10": {
|
||||||
|
"id": "shape:10",
|
||||||
|
"index": "a2",
|
||||||
|
"isLocked": false,
|
||||||
|
"meta": {},
|
||||||
|
"opacity": 1,
|
||||||
|
"parentId": "page:page",
|
||||||
|
"props": {
|
||||||
|
"align": "middle",
|
||||||
|
"color": "red",
|
||||||
|
"dash": "draw",
|
||||||
|
"fill": "solid",
|
||||||
|
"font": "draw",
|
||||||
|
"geo": "rectangle",
|
||||||
|
"growY": 0,
|
||||||
|
"h": 114.39,
|
||||||
|
"labelColor": "red",
|
||||||
|
"size": "m",
|
||||||
|
"text": "",
|
||||||
|
"url": "",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"w": 240.17,
|
||||||
|
},
|
||||||
|
"rotation": 0,
|
||||||
|
"type": "geo",
|
||||||
|
"typeName": "shape",
|
||||||
|
"x": 1362.24,
|
||||||
|
"y": 784.23,
|
||||||
|
},
|
||||||
|
"shape:12": {
|
||||||
|
"id": "shape:12",
|
||||||
|
"index": "a1V",
|
||||||
|
"isLocked": false,
|
||||||
|
"meta": {},
|
||||||
|
"opacity": 1,
|
||||||
|
"parentId": "page:page",
|
||||||
|
"props": {
|
||||||
|
"arrowheadEnd": "arrow",
|
||||||
|
"arrowheadStart": "none",
|
||||||
|
"bend": -0.00010621283112062974,
|
||||||
|
"color": "red",
|
||||||
|
"dash": "draw",
|
||||||
|
"end": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 6.55,
|
||||||
|
},
|
||||||
|
"fill": "none",
|
||||||
|
"font": "draw",
|
||||||
|
"labelColor": "red",
|
||||||
|
"labelPosition": 0.5,
|
||||||
|
"size": "m",
|
||||||
|
"start": {
|
||||||
|
"x": 252.64,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
"text": "",
|
||||||
|
},
|
||||||
|
"rotation": 0,
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "shape",
|
||||||
|
"x": 1544.3,
|
||||||
|
"y": 817.34,
|
||||||
|
},
|
||||||
|
"shape:8": {
|
||||||
|
"id": "shape:8",
|
||||||
|
"index": "a1",
|
||||||
|
"isLocked": false,
|
||||||
|
"meta": {},
|
||||||
|
"opacity": 1,
|
||||||
|
"parentId": "page:page",
|
||||||
|
"props": {
|
||||||
|
"align": "middle",
|
||||||
|
"color": "red",
|
||||||
|
"dash": "draw",
|
||||||
|
"fill": "solid",
|
||||||
|
"font": "draw",
|
||||||
|
"geo": "rectangle",
|
||||||
|
"growY": 0,
|
||||||
|
"h": 177.03,
|
||||||
|
"labelColor": "red",
|
||||||
|
"size": "m",
|
||||||
|
"text": "",
|
||||||
|
"url": "",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
"w": 136.64,
|
||||||
|
},
|
||||||
|
"rotation": 0,
|
||||||
|
"type": "geo",
|
||||||
|
"typeName": "shape",
|
||||||
|
"x": 1391.66,
|
||||||
|
"y": 769.92,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { readdirSync } from 'fs'
|
||||||
|
import { readFile } from 'fs/promises'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { TestEditor } from '../../../test/TestEditor'
|
||||||
|
import { buildFromV1Document } from './buildFromV1Document'
|
||||||
|
|
||||||
|
jest.mock('nanoid', () => {
|
||||||
|
let nextNanoId = 0
|
||||||
|
|
||||||
|
const nanoid = () => {
|
||||||
|
nextNanoId++
|
||||||
|
return `${nextNanoId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nanoid,
|
||||||
|
default: nanoid,
|
||||||
|
__reset: () => {
|
||||||
|
nextNanoId = 0
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
require('nanoid').__reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('buildFromV1Document test fixtures', () => {
|
||||||
|
const files = readdirSync(join(__dirname, 'test-fixtures')).filter((fileName) =>
|
||||||
|
fileName.endsWith('.tldr')
|
||||||
|
)
|
||||||
|
|
||||||
|
test.each(files)('%s', async (fileName) => {
|
||||||
|
const filePath = join(__dirname, 'test-fixtures', fileName)
|
||||||
|
const fileContent = await readFile(filePath, 'utf-8')
|
||||||
|
const { document } = JSON.parse(fileContent)
|
||||||
|
|
||||||
|
const editor = new TestEditor()
|
||||||
|
buildFromV1Document(editor, document)
|
||||||
|
|
||||||
|
expect(editor.store.serialize()).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
|
@ -5,7 +5,6 @@ import {
|
||||||
PageRecordType,
|
PageRecordType,
|
||||||
TLArrowShape,
|
TLArrowShape,
|
||||||
TLArrowShapeArrowheadStyle,
|
TLArrowShapeArrowheadStyle,
|
||||||
TLArrowShapeTerminal,
|
|
||||||
TLAsset,
|
TLAsset,
|
||||||
TLAssetId,
|
TLAssetId,
|
||||||
TLDefaultColorStyle,
|
TLDefaultColorStyle,
|
||||||
|
@ -26,6 +25,8 @@ import {
|
||||||
VecModel,
|
VecModel,
|
||||||
clamp,
|
clamp,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
|
getArrowBindings,
|
||||||
|
structuredClone,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
|
||||||
const TLDRAW_V1_VERSION = 15.5
|
const TLDRAW_V1_VERSION = 15.5
|
||||||
|
@ -411,12 +412,10 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
|
||||||
arrowheadStart: getV2Arrowhead(v1Shape.decorations?.start),
|
arrowheadStart: getV2Arrowhead(v1Shape.decorations?.start),
|
||||||
arrowheadEnd: getV2Arrowhead(v1Shape.decorations?.end),
|
arrowheadEnd: getV2Arrowhead(v1Shape.decorations?.end),
|
||||||
start: {
|
start: {
|
||||||
type: 'point',
|
|
||||||
x: coerceNumber(v1Shape.handles.start.point[0]),
|
x: coerceNumber(v1Shape.handles.start.point[0]),
|
||||||
y: coerceNumber(v1Shape.handles.start.point[1]),
|
y: coerceNumber(v1Shape.handles.start.point[1]),
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
type: 'point',
|
|
||||||
x: coerceNumber(v1Shape.handles.end.point[0]),
|
x: coerceNumber(v1Shape.handles.end.point[0]),
|
||||||
y: coerceNumber(v1Shape.handles.end.point[1]),
|
y: coerceNumber(v1Shape.handles.end.point[1]),
|
||||||
},
|
},
|
||||||
|
@ -564,19 +563,24 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
|
||||||
})
|
})
|
||||||
|
|
||||||
if (change) {
|
if (change) {
|
||||||
if (change.props?.[handleId]) {
|
editor.updateShape(change)
|
||||||
const terminal = change.props?.[handleId] as TLArrowShapeTerminal
|
}
|
||||||
if (terminal.type === 'binding') {
|
|
||||||
terminal.isExact = binding.distance === 0
|
|
||||||
|
|
||||||
if (terminal.boundShapeId !== targetId) {
|
const freshBinding = getArrowBindings(
|
||||||
console.warn('Hit the wrong shape!')
|
editor,
|
||||||
terminal.boundShapeId = targetId
|
editor.getShape<TLArrowShape>(v2ShapeId)!
|
||||||
terminal.normalizedAnchor = { x: 0.5, y: 0.5 }
|
)[handleId]
|
||||||
}
|
if (freshBinding) {
|
||||||
}
|
const updatedFreshBinding = structuredClone(freshBinding)
|
||||||
|
if (binding.distance === 0) {
|
||||||
|
updatedFreshBinding.props.isExact = true
|
||||||
}
|
}
|
||||||
editor.updateShapes([change])
|
if (updatedFreshBinding.toId !== targetId) {
|
||||||
|
updatedFreshBinding.toId = targetId
|
||||||
|
updatedFreshBinding.props.normalizedAnchor = { x: nx, y: ny }
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.updateBinding(updatedFreshBinding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"New Document","fileHandle":null,"document":{"id":"doc","name":"New Document","version":15.5,"pages":{"page":{"id":"page","name":"Page 1","childIndex":1,"shapes":{"7c4f7706-b6c4-42e3-32c5-9c09e2b7e754":{"id":"7c4f7706-b6c4-42e3-32c5-9c09e2b7e754","type":"rectangle","name":"Rectangle","parentId":"page","childIndex":2.5,"point":[1616.09,584.28],"size":[240.17,114.39],"rotation":0,"style":{"color":"red","size":"small","isFilled":true,"dash":"draw","scale":1},"label":"","labelPoint":[0.5,0.5]},"fc27e6ec-38ed-4145-1f84-428494b23465":{"id":"fc27e6ec-38ed-4145-1f84-428494b23465","type":"rectangle","name":"Rectangle","parentId":"page","childIndex":2,"point":[1388.92,820.52],"size":[136.64,177.03],"rotation":0,"style":{"color":"red","size":"small","isFilled":true,"dash":"draw","scale":1},"label":"","labelPoint":[0.5,0.5]},"fbff7e81-6f86-4b51-0432-1abab3956bc5":{"id":"fbff7e81-6f86-4b51-0432-1abab3956bc5","type":"arrow","name":"Arrow","parentId":"page","childIndex":3,"point":[1541.56,698.67],"rotation":0,"bend":0.6580751340988311,"handles":{"start":{"id":"start","index":0,"point":[146.32,0],"canBind":true,"bindingId":"e19cf0fd-0033-49b5-11ca-894e6cdaedfe"},"end":{"id":"end","index":1,"point":[0,173.3],"canBind":true,"bindingId":"560685ee-c516-44c8-2af5-a9f605b0509d"},"bend":{"id":"bend","index":2,"point":[130.18,134.79]}},"decorations":{"end":"arrow"},"style":{"color":"red","size":"small","isFilled":true,"dash":"draw","scale":1},"label":"","labelPoint":[0.5,0.5]}},"bindings":{"e19cf0fd-0033-49b5-11ca-894e6cdaedfe":{"id":"e19cf0fd-0033-49b5-11ca-894e6cdaedfe","type":"arrow","fromId":"fbff7e81-6f86-4b51-0432-1abab3956bc5","toId":"7c4f7706-b6c4-42e3-32c5-9c09e2b7e754","handleId":"start","point":[0.5,0.5],"distance":16},"560685ee-c516-44c8-2af5-a9f605b0509d":{"id":"560685ee-c516-44c8-2af5-a9f605b0509d","type":"arrow","fromId":"fbff7e81-6f86-4b51-0432-1abab3956bc5","toId":"fc27e6ec-38ed-4145-1f84-428494b23465","handleId":"end","point":[0.49,0.81],"distance":16}}}},"pageStates":{"page":{"id":"page","selectedIds":[],"camera":{"point":[-314.5,-67.5],"zoom":1},"editingId":null}},"assets":{}},"assets":{}}
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"New Document","fileHandle":null,"document":{"id":"doc","name":"New Document","version":15.5,"pages":{"page":{"id":"page","name":"Page 1","childIndex":1,"shapes":{"7c4f7706-b6c4-42e3-32c5-9c09e2b7e754":{"id":"7c4f7706-b6c4-42e3-32c5-9c09e2b7e754","type":"rectangle","name":"Rectangle","parentId":"page","childIndex":2.5,"point":[1616.09,584.28],"size":[240.17,114.39],"rotation":0,"style":{"color":"red","size":"small","isFilled":true,"dash":"draw","scale":1},"label":"","labelPoint":[0.5,0.5]},"fc27e6ec-38ed-4145-1f84-428494b23465":{"id":"fc27e6ec-38ed-4145-1f84-428494b23465","type":"rectangle","name":"Rectangle","parentId":"page","childIndex":2,"point":[1418.93,779.31],"size":[136.64,177.03],"rotation":0,"style":{"color":"red","size":"small","isFilled":true,"dash":"draw","scale":1},"label":"","labelPoint":[0.5,0.5]},"fbff7e81-6f86-4b51-0432-1abab3956bc5":{"id":"fbff7e81-6f86-4b51-0432-1abab3956bc5","type":"arrow","name":"Arrow","parentId":"page","childIndex":3,"point":[1510.86,661.97],"rotation":0,"bend":0.6580672268331379,"handles":{"start":{"id":"start","index":0,"point":[293.36,0],"canBind":true,"bindingId":"9017a3ec-5fcf-4d50-1c79-689a36e75658"},"end":{"id":"end","index":1,"point":[0,201.67],"canBind":true,"bindingId":"b2a363e7-6203-4e41-20d4-c34caba366be"},"bend":{"id":"bend","index":2,"point":[213.04,197.36]}},"decorations":{"end":"arrow","start":"arrow"},"style":{"color":"red","size":"small","isFilled":true,"dash":"draw","scale":1},"label":"","labelPoint":[0.5,0.5]}},"bindings":{"b2a363e7-6203-4e41-20d4-c34caba366be":{"id":"b2a363e7-6203-4e41-20d4-c34caba366be","type":"arrow","fromId":"fbff7e81-6f86-4b51-0432-1abab3956bc5","toId":"fc27e6ec-38ed-4145-1f84-428494b23465","handleId":"end","point":[0.64,0.48],"distance":0},"9017a3ec-5fcf-4d50-1c79-689a36e75658":{"id":"9017a3ec-5fcf-4d50-1c79-689a36e75658","type":"arrow","fromId":"fbff7e81-6f86-4b51-0432-1abab3956bc5","toId":"7c4f7706-b6c4-42e3-32c5-9c09e2b7e754","handleId":"start","point":[0.75,0.64],"distance":0}}}},"pageStates":{"page":{"id":"page","selectedIds":[],"camera":{"point":[-314.5,-67.5],"zoom":1},"editingId":null}},"assets":{}},"assets":{}}
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"New Document","fileHandle":null,"document":{"id":"doc","name":"New Document","version":15.5,"pages":{"page":{"id":"page","name":"Page 1","childIndex":1,"shapes":{"7c4f7706-b6c4-42e3-32c5-9c09e2b7e754":{"id":"7c4f7706-b6c4-42e3-32c5-9c09e2b7e754","type":"rectangle","name":"Rectangle","parentId":"page","childIndex":2.5,"point":[1362.24,784.23],"size":[240.17,114.39],"rotation":0,"style":{"color":"red","size":"small","isFilled":true,"dash":"draw","scale":1},"label":"","labelPoint":[0.5,0.5]},"fc27e6ec-38ed-4145-1f84-428494b23465":{"id":"fc27e6ec-38ed-4145-1f84-428494b23465","type":"rectangle","name":"Rectangle","parentId":"page","childIndex":2,"point":[1391.66,769.92],"size":[136.64,177.03],"rotation":0,"style":{"color":"red","size":"small","isFilled":true,"dash":"draw","scale":1},"label":"","labelPoint":[0.5,0.5]},"fbff7e81-6f86-4b51-0432-1abab3956bc5":{"id":"fbff7e81-6f86-4b51-0432-1abab3956bc5","type":"arrow","name":"Arrow","parentId":"page","childIndex":3,"point":[1544.3,817.34],"rotation":0,"bend":8.405411069383627e-7,"handles":{"start":{"id":"start","index":0,"point":[252.64,0],"canBind":true},"end":{"id":"end","index":1,"point":[0,6.55],"canBind":true,"bindingId":"9e0fce85-6d95-468a-0dab-4c4b9b016d9e"},"bend":{"id":"bend","index":2,"point":[126.32,3.28]}},"decorations":{"end":"arrow"},"style":{"color":"red","size":"small","isFilled":true,"dash":"draw","scale":1},"label":"","labelPoint":[0.5,0.5]}},"bindings":{"9e0fce85-6d95-468a-0dab-4c4b9b016d9e":{"id":"9e0fce85-6d95-468a-0dab-4c4b9b016d9e","type":"arrow","fromId":"fbff7e81-6f86-4b51-0432-1abab3956bc5","toId":"fc27e6ec-38ed-4145-1f84-428494b23465","handleId":"end","point":[0.27,0.35],"distance":16}}}},"pageStates":{"page":{"id":"page","selectedIds":["7c4f7706-b6c4-42e3-32c5-9c09e2b7e754","fc27e6ec-38ed-4145-1f84-428494b23465","fbff7e81-6f86-4b51-0432-1abab3956bc5"],"camera":{"point":[-314.5,-67.5],"zoom":1},"editingId":null}},"assets":{}},"assets":{}}
|
|
@ -27,6 +27,7 @@ import {
|
||||||
createTLStore,
|
createTLStore,
|
||||||
rotateSelectionHandle,
|
rotateSelectionHandle,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
import { defaultBindingUtils } from '../lib/defaultBindingUtils'
|
||||||
import { defaultShapeTools } from '../lib/defaultShapeTools'
|
import { defaultShapeTools } from '../lib/defaultShapeTools'
|
||||||
import { defaultShapeUtils } from '../lib/defaultShapeUtils'
|
import { defaultShapeUtils } from '../lib/defaultShapeUtils'
|
||||||
import { defaultTools } from '../lib/defaultTools'
|
import { defaultTools } from '../lib/defaultTools'
|
||||||
|
@ -61,12 +62,17 @@ export class TestEditor extends Editor {
|
||||||
elm.tabIndex = 0
|
elm.tabIndex = 0
|
||||||
|
|
||||||
const shapeUtilsWithDefaults = [...defaultShapeUtils, ...(options.shapeUtils ?? [])]
|
const shapeUtilsWithDefaults = [...defaultShapeUtils, ...(options.shapeUtils ?? [])]
|
||||||
|
const bindingUtilsWithDefaults = [...defaultBindingUtils, ...(options.bindingUtils ?? [])]
|
||||||
|
|
||||||
super({
|
super({
|
||||||
...options,
|
...options,
|
||||||
shapeUtils: [...shapeUtilsWithDefaults],
|
shapeUtils: shapeUtilsWithDefaults,
|
||||||
|
bindingUtils: bindingUtilsWithDefaults,
|
||||||
tools: [...defaultTools, ...defaultShapeTools, ...(options.tools ?? [])],
|
tools: [...defaultTools, ...defaultShapeTools, ...(options.tools ?? [])],
|
||||||
store: createTLStore({ shapeUtils: [...shapeUtilsWithDefaults] }),
|
store: createTLStore({
|
||||||
|
shapeUtils: shapeUtilsWithDefaults,
|
||||||
|
bindingUtils: bindingUtilsWithDefaults,
|
||||||
|
}),
|
||||||
getContainer: () => elm,
|
getContainer: () => elm,
|
||||||
initialState: 'select',
|
initialState: 'select',
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,295 +0,0 @@
|
||||||
import { TLArrowShape, TLGeoShape, TLShapeId, createShapeId } from '@tldraw/editor'
|
|
||||||
import { TestEditor } from './TestEditor'
|
|
||||||
import { TL } from './test-jsx'
|
|
||||||
|
|
||||||
let editor: TestEditor
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
editor = new TestEditor()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('arrowBindingsIndex', () => {
|
|
||||||
it('keeps a mapping from bound shapes to the arrows that bind to them', () => {
|
|
||||||
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" />,
|
|
||||||
])
|
|
||||||
|
|
||||||
editor.selectNone()
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
editor.pointerDown(50, 50)
|
|
||||||
expect(editor.getOnlySelectedShape()).toBe(null)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([])
|
|
||||||
|
|
||||||
editor.pointerMove(50, 55)
|
|
||||||
expect(editor.getOnlySelectedShape()).not.toBe(null)
|
|
||||||
const arrow = editor.getOnlySelectedShape()!
|
|
||||||
expect(arrow.type).toBe('arrow')
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([
|
|
||||||
{ arrowId: arrow.id, handleId: 'start' },
|
|
||||||
{ arrowId: arrow.id, handleId: 'end' },
|
|
||||||
])
|
|
||||||
|
|
||||||
editor.pointerMove(250, 50)
|
|
||||||
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 = {
|
|
||||||
box1: createShapeId('box1'),
|
|
||||||
box2: createShapeId('box2'),
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.createShapes([
|
|
||||||
{ type: 'geo', id: ids.box1, x: 0, y: 0, props: { w: 100, h: 100 } },
|
|
||||||
{ type: 'geo', id: ids.box2, x: 200, y: 0, props: { w: 100, h: 100 } },
|
|
||||||
])
|
|
||||||
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
// start at box 1 and end on box 2
|
|
||||||
editor.pointerDown(50, 50)
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([])
|
|
||||||
|
|
||||||
editor.pointerMove(250, 50)
|
|
||||||
const arrow1 = editor.getOnlySelectedShape()!
|
|
||||||
expect(arrow1.type).toBe('arrow')
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([{ arrowId: arrow1.id, handleId: 'start' }])
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toEqual([{ arrowId: arrow1.id, handleId: 'end' }])
|
|
||||||
|
|
||||||
editor.pointerUp()
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([{ arrowId: arrow1.id, handleId: 'start' }])
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toEqual([{ arrowId: arrow1.id, handleId: 'end' }])
|
|
||||||
|
|
||||||
// start at box 1 and end on the page
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
editor.pointerMove(50, 50).pointerDown().pointerMove(50, -50).pointerUp()
|
|
||||||
const arrow2 = editor.getOnlySelectedShape()!
|
|
||||||
expect(arrow2.type).toBe('arrow')
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([
|
|
||||||
{ arrowId: arrow1.id, handleId: 'start' },
|
|
||||||
{ arrowId: arrow2.id, handleId: 'start' },
|
|
||||||
])
|
|
||||||
|
|
||||||
// start outside box 1 and end in box 1
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
editor.pointerDown(0, -50).pointerMove(50, 50).pointerUp(50, 50)
|
|
||||||
const arrow3 = editor.getOnlySelectedShape()!
|
|
||||||
expect(arrow3.type).toBe('arrow')
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([
|
|
||||||
{ arrowId: arrow1.id, handleId: 'start' },
|
|
||||||
{ arrowId: arrow2.id, handleId: 'start' },
|
|
||||||
{ arrowId: arrow3.id, handleId: 'end' },
|
|
||||||
])
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toEqual([{ arrowId: arrow1.id, handleId: 'end' }])
|
|
||||||
|
|
||||||
// start at box 2 and end on the page
|
|
||||||
editor.selectNone()
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
editor.pointerDown(250, 50)
|
|
||||||
editor.expectToBeIn('arrow.pointing')
|
|
||||||
editor.pointerMove(250, -50)
|
|
||||||
editor.expectToBeIn('select.dragging_handle')
|
|
||||||
const arrow4 = editor.getOnlySelectedShape()!
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toEqual([
|
|
||||||
{ arrowId: arrow1.id, handleId: 'end' },
|
|
||||||
{ arrowId: arrow4.id, handleId: 'start' },
|
|
||||||
])
|
|
||||||
|
|
||||||
editor.pointerUp(250, -50)
|
|
||||||
editor.expectToBeIn('select.idle')
|
|
||||||
expect(arrow4.type).toBe('arrow')
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toEqual([
|
|
||||||
{ arrowId: arrow1.id, handleId: 'end' },
|
|
||||||
{ arrowId: arrow4.id, handleId: 'start' },
|
|
||||||
])
|
|
||||||
|
|
||||||
// start outside box 2 and enter in box 2
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50)
|
|
||||||
const arrow5 = editor.getOnlySelectedShape()!
|
|
||||||
expect(arrow5.type).toBe('arrow')
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([
|
|
||||||
{ arrowId: arrow1.id, handleId: 'start' },
|
|
||||||
{ arrowId: arrow2.id, handleId: 'start' },
|
|
||||||
{ arrowId: arrow3.id, handleId: 'end' },
|
|
||||||
])
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toEqual([
|
|
||||||
{ arrowId: arrow1.id, handleId: 'end' },
|
|
||||||
{ arrowId: arrow4.id, handleId: 'start' },
|
|
||||||
{ arrowId: arrow5.id, handleId: 'end' },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('updating shapes', () => {
|
|
||||||
// ▲ │ │ ▲
|
|
||||||
// │ │ │ │
|
|
||||||
// b c e d
|
|
||||||
// ┌───┼─┴─┐ ┌──┴──┼─┐
|
|
||||||
// │ │ ▼ │ │ ▼ │ │
|
|
||||||
// │ └───┼─────a───┼───► │ │
|
|
||||||
// │ 1 │ │ 2 │
|
|
||||||
// └───────┘ └───────┘
|
|
||||||
let arrowAId: TLShapeId
|
|
||||||
let arrowBId: TLShapeId
|
|
||||||
let arrowCId: TLShapeId
|
|
||||||
let arrowDId: TLShapeId
|
|
||||||
let arrowEId: TLShapeId
|
|
||||||
let ids: Record<string, TLShapeId>
|
|
||||||
beforeEach(() => {
|
|
||||||
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
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
editor.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50)
|
|
||||||
arrowAId = editor.getOnlySelectedShape()!.id
|
|
||||||
// start at box 1 and leave
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
editor.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50)
|
|
||||||
arrowBId = editor.getOnlySelectedShape()!.id
|
|
||||||
// start outside box 1 and enter
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
editor.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50)
|
|
||||||
arrowCId = editor.getOnlySelectedShape()!.id
|
|
||||||
// start at box 2 and leave
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
editor.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50)
|
|
||||||
arrowDId = editor.getOnlySelectedShape()!.id
|
|
||||||
// start outside box 2 and enter
|
|
||||||
editor.setCurrentTool('arrow')
|
|
||||||
editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50)
|
|
||||||
arrowEId = editor.getOnlySelectedShape()!.id
|
|
||||||
})
|
|
||||||
it('deletes the entry if you delete the bound shapes', () => {
|
|
||||||
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(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
|
||||||
editor.deleteShapes([arrowEId])
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(2)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
|
||||||
|
|
||||||
editor.deleteShapes([arrowDId])
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
|
||||||
|
|
||||||
editor.deleteShapes([arrowCId])
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(2)
|
|
||||||
|
|
||||||
editor.deleteShapes([arrowBId])
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(1)
|
|
||||||
|
|
||||||
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', () => {
|
|
||||||
editor.deleteShapes([arrowAId, arrowBId, arrowCId, arrowDId, arrowEId])
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(0)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('adds new entries after initial creation', () => {
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
|
||||||
|
|
||||||
// draw from box 2 to box 1
|
|
||||||
editor.setCurrentTool('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 } = editor.createShapesFromJsx(
|
|
||||||
<TL.geo ref="box3" x={400} y={0} w={100} h={100} />
|
|
||||||
)
|
|
||||||
|
|
||||||
// draw from box 2 to box 3
|
|
||||||
|
|
||||||
editor.setCurrentTool('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(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
|
||||||
|
|
||||||
editor.selectAll()
|
|
||||||
editor.duplicateShapes(editor.getSelectedShapeIds())
|
|
||||||
|
|
||||||
const [box1Clone, box2Clone] = editor
|
|
||||||
.getSelectedShapes()
|
|
||||||
.filter((shape) => editor.isShapeOfType<TLGeoShape>(shape, 'geo'))
|
|
||||||
.sort((a, b) => a.x - b.x)
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(box2Clone.id)).toHaveLength(3)
|
|
||||||
expect(editor.getArrowsBoundTo(box1Clone.id)).toHaveLength(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows bound shapes to be moved', () => {
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
|
||||||
|
|
||||||
editor.nudgeShapes([ids.box2], { x: 0, y: -1 })
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows the arrows bound shape to change', () => {
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
|
||||||
|
|
||||||
// create another box
|
|
||||||
|
|
||||||
const { box3 } = editor.createShapesFromJsx(
|
|
||||||
<TL.geo ref="box3" x={400} y={0} w={100} h={100} />
|
|
||||||
)
|
|
||||||
|
|
||||||
// move arrowA from box2 to box3
|
|
||||||
editor.updateShapes<TLArrowShape>([
|
|
||||||
{
|
|
||||||
id: arrowAId,
|
|
||||||
type: 'arrow',
|
|
||||||
props: {
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
isExact: false,
|
|
||||||
boundShapeId: box3,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
||||||
isPrecise: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(2)
|
|
||||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
|
||||||
expect(editor.getArrowsBoundTo(box3)).toHaveLength(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TLArrowShape, TLShapeId, Vec, createShapeId } from '@tldraw/editor'
|
import { TLArrowShape, TLShapeId, Vec, createShapeId, getArrowBindings } from '@tldraw/editor'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
import { TL } from './test-jsx'
|
import { TL } from './test-jsx'
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ const ids = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrow = () => editor.getOnlySelectedShape() as TLArrowShape
|
const arrow = () => editor.getOnlySelectedShape() as TLArrowShape
|
||||||
|
const bindings = () => getArrowBindings(editor, arrow())
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
editor = new TestEditor()
|
editor = new TestEditor()
|
||||||
|
@ -83,16 +84,8 @@ describe('Making an arrow on the page', () => {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
props: {
|
props: {
|
||||||
start: {
|
start: { x: 0, y: 0 },
|
||||||
x: 0,
|
end: { x: 100, y: 0 },
|
||||||
y: 0,
|
|
||||||
type: 'point',
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
x: 100,
|
|
||||||
y: 0,
|
|
||||||
type: 'point',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(editor.getShapeUtil(arrow1).getHandles!(arrow1)).toMatchObject([
|
expect(editor.getShapeUtil(arrow1).getHandles!(arrow1)).toMatchObject([
|
||||||
|
@ -127,18 +120,17 @@ describe('When binding an arrow to a shape', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(0, 50)
|
editor.pointerDown(0, 50)
|
||||||
editor.pointerMove(99, 50)
|
editor.pointerMove(99, 50)
|
||||||
expect(arrow().props.start.type).toBe('point')
|
expect(bindings().start).toBeUndefined()
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('binds to the shape when dragged into the shape edge', () => {
|
it('binds to the shape when dragged into the shape edge', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(0, 50)
|
editor.pointerDown(0, 50)
|
||||||
editor.pointerMove(100, 50)
|
editor.pointerMove(100, 50)
|
||||||
expect(arrow().props.end).toMatchObject({
|
expect(bindings().end).toMatchObject({
|
||||||
type: 'binding',
|
toId: ids.box1,
|
||||||
boundShapeId: ids.box1,
|
props: { normalizedAnchor: { x: 0, y: 0.5 } },
|
||||||
normalizedAnchor: { x: 0, y: 0.5 },
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -146,21 +138,22 @@ describe('When binding an arrow to a shape', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(0, 50)
|
editor.pointerDown(0, 50)
|
||||||
editor.pointerMove(250, 50)
|
editor.pointerMove(250, 50)
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('binds and then unbinds when moved out', () => {
|
it('binds and then unbinds when moved out', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(0, 50)
|
editor.pointerDown(0, 50)
|
||||||
editor.pointerMove(150, 50)
|
editor.pointerMove(150, 50)
|
||||||
expect(arrow().props.end).toMatchObject({
|
expect(bindings().end).toMatchObject({
|
||||||
type: 'binding',
|
toId: ids.box1,
|
||||||
boundShapeId: ids.box1,
|
props: {
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: true, // enclosed
|
isPrecise: true, // enclosed
|
||||||
|
},
|
||||||
})
|
})
|
||||||
editor.pointerMove(250, 50)
|
editor.pointerMove(250, 50)
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not bind when control key is held', () => {
|
it('does not bind when control key is held', () => {
|
||||||
|
@ -168,7 +161,7 @@ describe('When binding an arrow to a shape', () => {
|
||||||
editor.keyDown('Control')
|
editor.keyDown('Control')
|
||||||
editor.pointerDown(0, 50)
|
editor.pointerDown(0, 50)
|
||||||
editor.pointerMove(100, 50)
|
editor.pointerMove(100, 50)
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not bind when the shape is locked', () => {
|
it('does not bind when the shape is locked', () => {
|
||||||
|
@ -176,7 +169,7 @@ describe('When binding an arrow to a shape', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(0, 50)
|
editor.pointerDown(0, 50)
|
||||||
editor.pointerMove(100, 50)
|
editor.pointerMove(100, 50)
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use timer on keyup when using control key to skip binding', () => {
|
it('should use timer on keyup when using control key to skip binding', () => {
|
||||||
|
@ -185,22 +178,22 @@ describe('When binding an arrow to a shape', () => {
|
||||||
editor.pointerMove(100, 50)
|
editor.pointerMove(100, 50)
|
||||||
|
|
||||||
// can press control while dragging to switch into no-binding mode
|
// can press control while dragging to switch into no-binding mode
|
||||||
expect(arrow().props.end.type).toBe('binding')
|
expect(bindings().end).toBeDefined()
|
||||||
editor.keyDown('Control')
|
editor.keyDown('Control')
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
|
|
||||||
editor.keyUp('Control')
|
editor.keyUp('Control')
|
||||||
expect(arrow().props.end.type).toBe('point') // there's a short delay here, it should still be a point
|
expect(bindings().end).toBeUndefined() // there's a short delay here, it should still be a point
|
||||||
jest.advanceTimersByTime(1000) // once the timer runs out...
|
jest.advanceTimersByTime(1000) // once the timer runs out...
|
||||||
expect(arrow().props.end.type).toBe('binding')
|
expect(bindings().end).toBeDefined()
|
||||||
|
|
||||||
editor.keyDown('Control') // no delay when pressing control again though
|
editor.keyDown('Control') // no delay when pressing control again though
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
|
|
||||||
editor.keyUp('Control')
|
editor.keyUp('Control')
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
jest.advanceTimersByTime(1000) // once the timer runs out...
|
jest.advanceTimersByTime(1000) // once the timer runs out...
|
||||||
expect(arrow().props.end.type).toBe('point') // still a point because interaction ended before timer ended
|
expect(bindings().end).toBeUndefined() // still a point because interaction ended before timer ended
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -218,13 +211,13 @@ describe('When shapes are overlapping', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(0, 50)
|
editor.pointerDown(0, 50)
|
||||||
editor.pointerMove(125, 50) // over box1 only
|
editor.pointerMove(125, 50) // over box1 only
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box1 })
|
expect(bindings().end).toMatchObject({ toId: ids.box1 })
|
||||||
editor.pointerMove(175, 50) // box2 is higher
|
editor.pointerMove(175, 50) // box2 is higher
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box2 })
|
expect(bindings().end).toMatchObject({ toId: ids.box2 })
|
||||||
editor.pointerMove(225, 50) // box3 is higher
|
editor.pointerMove(225, 50) // box3 is higher
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box3 })
|
expect(bindings().end).toMatchObject({ toId: ids.box3 })
|
||||||
editor.pointerMove(275, 50) // box4 is higher
|
editor.pointerMove(275, 50) // box4 is higher
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box4 })
|
expect(bindings().end).toMatchObject({ toId: ids.box4 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not bind when shapes are locked', () => {
|
it('does not bind when shapes are locked', () => {
|
||||||
|
@ -232,13 +225,13 @@ describe('When shapes are overlapping', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(0, 50)
|
editor.pointerDown(0, 50)
|
||||||
editor.pointerMove(125, 50) // over box1 only
|
editor.pointerMove(125, 50) // over box1 only
|
||||||
expect(arrow().props.end).toMatchObject({ type: 'point' }) // box 1 is locked!
|
expect(bindings().end).toBeUndefined() // box 1 is locked!
|
||||||
editor.pointerMove(175, 50) // box2 is higher
|
editor.pointerMove(175, 50) // box2 is higher
|
||||||
expect(arrow().props.end).toMatchObject({ type: 'point' }) // box 2 is locked! box1 is locked!
|
expect(bindings().end).toBeUndefined() // box 2 is locked! box1 is locked!
|
||||||
editor.pointerMove(225, 50) // box3 is higher
|
editor.pointerMove(225, 50) // box3 is higher
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box3 })
|
expect(bindings().end).toMatchObject({ toId: ids.box3 })
|
||||||
editor.pointerMove(275, 50) // box4 is higher
|
editor.pointerMove(275, 50) // box4 is higher
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box3 }) // box 4 is locked!
|
expect(bindings().end).toMatchObject({ toId: ids.box3 }) // box 4 is locked!
|
||||||
})
|
})
|
||||||
|
|
||||||
it('binds to the highest shape or to the first filled shape', () => {
|
it('binds to the highest shape or to the first filled shape', () => {
|
||||||
|
@ -249,13 +242,13 @@ describe('When shapes are overlapping', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(0, 50) // over nothing
|
editor.pointerDown(0, 50) // over nothing
|
||||||
editor.pointerMove(125, 50) // over box1 only
|
editor.pointerMove(125, 50) // over box1 only
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box1 })
|
expect(bindings().end).toMatchObject({ toId: ids.box1 })
|
||||||
editor.pointerMove(175, 50) // box2 is higher but box1 is filled?
|
editor.pointerMove(175, 50) // box2 is higher but box1 is filled?
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box1 })
|
expect(bindings().end).toMatchObject({ toId: ids.box1 })
|
||||||
editor.pointerMove(225, 50) // box3 is higher
|
editor.pointerMove(225, 50) // box3 is higher
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box3 })
|
expect(bindings().end).toMatchObject({ toId: ids.box3 })
|
||||||
editor.pointerMove(275, 50) // box4 is higher but box 3 is filled
|
editor.pointerMove(275, 50) // box4 is higher but box 3 is filled
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box3 })
|
expect(bindings().end).toMatchObject({ toId: ids.box3 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('binds to the smallest shape regardless of order', () => {
|
it('binds to the smallest shape regardless of order', () => {
|
||||||
|
@ -267,14 +260,14 @@ describe('When shapes are overlapping', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
editor.pointerDown(0, 50)
|
editor.pointerDown(0, 50)
|
||||||
editor.pointerMove(175, 50) // box1 is smaller even though it's behind box2
|
editor.pointerMove(175, 50) // box1 is smaller even though it's behind box2
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box1 })
|
expect(bindings().end).toMatchObject({ toId: ids.box1 })
|
||||||
editor.pointerMove(150, 90) // box3 is smaller and at the front
|
editor.pointerMove(150, 90) // box3 is smaller and at the front
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box3 })
|
expect(bindings().end).toMatchObject({ toId: ids.box3 })
|
||||||
editor.sendToBack([ids.box3])
|
editor.sendToBack([ids.box3])
|
||||||
editor.pointerMove(149, 90) // box3 is smaller, even when at the back
|
editor.pointerMove(149, 90) // box3 is smaller, even when at the back
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box3 })
|
expect(bindings().end).toMatchObject({ toId: ids.box3 })
|
||||||
editor.pointerMove(175, 50)
|
editor.pointerMove(175, 50)
|
||||||
expect(arrow().props.end).toMatchObject({ boundShapeId: ids.box1 })
|
expect(bindings().end).toMatchObject({ toId: ids.box1 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -305,21 +298,20 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(55, 50)
|
editor.pointerMove(55, 50)
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(2)
|
expect(editor.getCurrentPageShapes().length).toBe(2)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({ x: 50, y: 50 })
|
||||||
x: 50,
|
expect(bindings()).toMatchObject({
|
||||||
y: 50,
|
start: {
|
||||||
props: {
|
toId: ids.box1,
|
||||||
start: {
|
props: {
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box1,
|
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.5,
|
x: 0.5,
|
||||||
y: 0.5,
|
y: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
end: {
|
||||||
boundShapeId: ids.box1,
|
toId: ids.box1,
|
||||||
|
props: {
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.55,
|
x: 0.55,
|
||||||
y: 0.5,
|
y: 0.5,
|
||||||
|
@ -336,13 +328,11 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(25, 20)
|
editor.pointerMove(25, 20)
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(2)
|
expect(editor.getCurrentPageShapes().length).toBe(2)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({ x: 20, y: 20 })
|
||||||
x: 20,
|
expect(bindings()).toMatchObject({
|
||||||
y: 20,
|
start: {
|
||||||
props: {
|
toId: ids.box1,
|
||||||
start: {
|
props: {
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box1,
|
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
// bound to the center, imprecise!
|
// bound to the center, imprecise!
|
||||||
x: 0.2,
|
x: 0.2,
|
||||||
|
@ -350,9 +340,10 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
},
|
},
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
end: {
|
||||||
boundShapeId: ids.box1,
|
toId: ids.box1,
|
||||||
|
props: {
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.25,
|
x: 0.25,
|
||||||
y: 0.2,
|
y: 0.2,
|
||||||
|
@ -370,22 +361,21 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
editor.pointerMove(25, 20)
|
editor.pointerMove(25, 20)
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(2)
|
expect(editor.getCurrentPageShapes().length).toBe(2)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({ x: 20, y: 20 })
|
||||||
x: 20,
|
expect(bindings()).toMatchObject({
|
||||||
y: 20,
|
start: {
|
||||||
props: {
|
toId: ids.box1,
|
||||||
start: {
|
props: {
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box1,
|
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
// precise!
|
// precise!
|
||||||
x: 0.2,
|
x: 0.2,
|
||||||
y: 0.2,
|
y: 0.2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
end: {
|
||||||
boundShapeId: ids.box1,
|
toId: ids.box1,
|
||||||
|
props: {
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.25,
|
x: 0.25,
|
||||||
y: 0.2,
|
y: 0.2,
|
||||||
|
@ -412,21 +402,20 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(30, 30)
|
editor.pointerMove(30, 30)
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(3)
|
expect(editor.getCurrentPageShapes().length).toBe(3)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({ x: 25, y: 25 })
|
||||||
x: 25,
|
expect(bindings()).toMatchObject({
|
||||||
y: 25,
|
start: {
|
||||||
props: {
|
toId: ids.box2,
|
||||||
start: {
|
props: {
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box2,
|
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.5,
|
x: 0.5,
|
||||||
y: 0.5,
|
y: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
end: {
|
||||||
boundShapeId: ids.box2,
|
toId: ids.box2,
|
||||||
|
props: {
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.55,
|
x: 0.55,
|
||||||
y: 0.5,
|
y: 0.5,
|
||||||
|
@ -446,21 +435,20 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(30, 30)
|
editor.pointerMove(30, 30)
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(3)
|
expect(editor.getCurrentPageShapes().length).toBe(3)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({ x: 25, y: 25 })
|
||||||
x: 25,
|
expect(bindings()).toMatchObject({
|
||||||
y: 25,
|
start: {
|
||||||
props: {
|
toId: ids.box2,
|
||||||
start: {
|
props: {
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box2,
|
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.5,
|
x: 0.5,
|
||||||
y: 0.5,
|
y: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
end: {
|
||||||
boundShapeId: ids.box2,
|
toId: ids.box2,
|
||||||
|
props: {
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.55,
|
x: 0.55,
|
||||||
y: 0.5,
|
y: 0.5,
|
||||||
|
@ -481,14 +469,12 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(30, 30)
|
editor.pointerMove(30, 30)
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(3)
|
expect(editor.getCurrentPageShapes().length).toBe(3)
|
||||||
expect(arrow()).toMatchObject({
|
expect(bindings()).toMatchObject({
|
||||||
props: {
|
start: {
|
||||||
start: {
|
toId: ids.box1, // not box 2!
|
||||||
boundShapeId: ids.box1, // not box 2!
|
},
|
||||||
},
|
end: {
|
||||||
end: {
|
toId: ids.box1, // not box 2
|
||||||
boundShapeId: ids.box1, // not box 2
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -514,22 +500,21 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(30, 30)
|
editor.pointerMove(30, 30)
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(3)
|
expect(editor.getCurrentPageShapes().length).toBe(3)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({ x: 25, y: 25 })
|
||||||
x: 25,
|
expect(bindings()).toMatchObject({
|
||||||
y: 25,
|
start: {
|
||||||
props: {
|
toId: ids.box1,
|
||||||
start: {
|
props: {
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box1,
|
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.25,
|
x: 0.25,
|
||||||
y: 0.25,
|
y: 0.25,
|
||||||
},
|
},
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
end: {
|
||||||
boundShapeId: ids.box1,
|
toId: ids.box1,
|
||||||
|
props: {
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.3,
|
x: 0.3,
|
||||||
y: 0.3,
|
y: 0.3,
|
||||||
|
@ -551,21 +536,20 @@ describe('When starting an arrow inside of multiple shapes', () => {
|
||||||
expect(arrow()).toBe(null)
|
expect(arrow()).toBe(null)
|
||||||
editor.pointerMove(30, 30)
|
editor.pointerMove(30, 30)
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(3)
|
expect(editor.getCurrentPageShapes().length).toBe(3)
|
||||||
expect(arrow()).toMatchObject({
|
expect(arrow()).toMatchObject({ x: 25, y: 25 })
|
||||||
x: 25,
|
expect(bindings()).toMatchObject({
|
||||||
y: 25,
|
start: {
|
||||||
props: {
|
toId: ids.box2,
|
||||||
start: {
|
props: {
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box2,
|
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.5,
|
x: 0.5,
|
||||||
y: 0.5,
|
y: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
end: {
|
||||||
boundShapeId: ids.box2,
|
toId: ids.box2,
|
||||||
|
props: {
|
||||||
normalizedAnchor: {
|
normalizedAnchor: {
|
||||||
x: 0.55,
|
x: 0.55,
|
||||||
y: 0.5,
|
y: 0.5,
|
||||||
|
@ -607,13 +591,14 @@ describe('When binding an arrow to an ancestor', () => {
|
||||||
|
|
||||||
const arrow = editor.getCurrentPageShapes().find((s) => s.type === 'arrow') as TLArrowShape
|
const arrow = editor.getCurrentPageShapes().find((s) => s.type === 'arrow') as TLArrowShape
|
||||||
if (!arrow) throw Error('No arrow')
|
if (!arrow) throw Error('No arrow')
|
||||||
if (arrow.props.start.type !== 'binding') throw Error('no binding')
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
if (arrow.props.end.type !== 'binding') throw Error('no binding')
|
if (!bindings.start) throw Error('no binding')
|
||||||
|
if (!bindings.end) throw Error('no binding')
|
||||||
|
|
||||||
expect(arrow.props.start.boundShapeId).toBe(ids.box1)
|
expect(bindings.start.toId).toBe(ids.box1)
|
||||||
expect(arrow.props.end.boundShapeId).toBe(ids.frame)
|
expect(bindings.end.toId).toBe(ids.frame)
|
||||||
expect(arrow.props.start.isPrecise).toBe(false)
|
expect(bindings.start.props.isPrecise).toBe(false)
|
||||||
expect(arrow.props.end.isPrecise).toBe(true)
|
expect(bindings.end.props.isPrecise).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('binds precisely from parent to child', () => {
|
it('binds precisely from parent to child', () => {
|
||||||
|
@ -642,13 +627,14 @@ describe('When binding an arrow to an ancestor', () => {
|
||||||
|
|
||||||
const arrow = editor.getCurrentPageShapes().find((s) => s.type === 'arrow') as TLArrowShape
|
const arrow = editor.getCurrentPageShapes().find((s) => s.type === 'arrow') as TLArrowShape
|
||||||
if (!arrow) throw Error('No arrow')
|
if (!arrow) throw Error('No arrow')
|
||||||
if (arrow.props.start.type !== 'binding') throw Error('no binding')
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
if (arrow.props.end.type !== 'binding') throw Error('no binding')
|
if (!bindings.start) throw Error('no binding')
|
||||||
|
if (!bindings.end) throw Error('no binding')
|
||||||
|
|
||||||
expect(arrow.props.start.boundShapeId).toBe(ids.frame)
|
expect(bindings.start.toId).toBe(ids.frame)
|
||||||
expect(arrow.props.end.boundShapeId).toBe(ids.box1)
|
expect(bindings.end.toId).toBe(ids.box1)
|
||||||
expect(arrow.props.start.isPrecise).toBe(false)
|
expect(bindings.start.props.isPrecise).toBe(false)
|
||||||
expect(arrow.props.end.isPrecise).toBe(true)
|
expect(bindings.end.props.isPrecise).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -661,15 +647,13 @@ describe('Moving a bound arrow', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectBound(handle: 'start' | 'end', boundShapeId: TLShapeId) {
|
function expectBound(handle: 'start' | 'end', boundShapeId: TLShapeId) {
|
||||||
expect(editor.getOnlySelectedShape()).toMatchObject({
|
expect(bindings()[handle]).toMatchObject({
|
||||||
props: { [handle]: { type: 'binding', boundShapeId } },
|
toId: boundShapeId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectUnbound(handle: 'start' | 'end') {
|
function expectUnbound(handle: 'start' | 'end') {
|
||||||
expect(editor.getOnlySelectedShape()).toMatchObject({
|
expect(bindings()[handle]).toBeUndefined()
|
||||||
props: { [handle]: { type: 'point' } },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it('keeps the start of the arrow bound to the original shape as it moves', () => {
|
it('keeps the start of the arrow bound to the original shape as it moves', () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TLArrowShape, createShapeId } from '@tldraw/editor'
|
import { TLArrowShape, createShapeId, getArrowBindings } from '@tldraw/editor'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
|
|
||||||
let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
@ -26,6 +26,10 @@ function arrow() {
|
||||||
return editor.getCurrentPageShapes().find((s) => s.type === 'arrow') as TLArrowShape
|
return editor.getCurrentPageShapes().find((s) => s.type === 'arrow') as TLArrowShape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindings() {
|
||||||
|
return getArrowBindings(editor, arrow())
|
||||||
|
}
|
||||||
|
|
||||||
describe('restoring bound arrows', () => {
|
describe('restoring bound arrows', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
editor.createShapes([
|
editor.createShapes([
|
||||||
|
@ -44,29 +48,29 @@ describe('restoring bound arrows', () => {
|
||||||
it('removes bound arrows on delete, restores them on undo but only when change was done by user', () => {
|
it('removes bound arrows on delete, restores them on undo but only when change was done by user', () => {
|
||||||
editor.mark('deleting')
|
editor.mark('deleting')
|
||||||
editor.deleteShapes([ids.box2])
|
editor.deleteShapes([ids.box2])
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
editor.undo()
|
editor.undo()
|
||||||
expect(arrow().props.end.type).toBe('binding')
|
expect(bindings().end).toBeDefined()
|
||||||
editor.redo()
|
editor.redo()
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes / restores multiple bindings', () => {
|
it('removes / restores multiple bindings', () => {
|
||||||
editor.mark('deleting')
|
editor.mark('deleting')
|
||||||
expect(arrow().props.start.type).toBe('binding')
|
expect(bindings().start).toBeDefined()
|
||||||
expect(arrow().props.end.type).toBe('binding')
|
expect(bindings().end).toBeDefined()
|
||||||
|
|
||||||
editor.deleteShapes([ids.box1, ids.box2])
|
editor.deleteShapes([ids.box1, ids.box2])
|
||||||
expect(arrow().props.start.type).toBe('point')
|
expect(bindings().start).toBeUndefined()
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
|
|
||||||
editor.undo()
|
editor.undo()
|
||||||
expect(arrow().props.start.type).toBe('binding')
|
expect(bindings().start).toBeDefined()
|
||||||
expect(arrow().props.end.type).toBe('binding')
|
expect(bindings().end).toBeDefined()
|
||||||
|
|
||||||
editor.redo()
|
editor.redo()
|
||||||
expect(arrow().props.start.type).toBe('point')
|
expect(bindings().start).toBeUndefined()
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -77,8 +81,8 @@ describe('restoring bound arrows multiplayer', () => {
|
||||||
|
|
||||||
editor.setCurrentTool('arrow').pointerMove(0, 50).pointerDown().pointerMove(150, 50).pointerUp()
|
editor.setCurrentTool('arrow').pointerMove(0, 50).pointerDown().pointerMove(150, 50).pointerUp()
|
||||||
|
|
||||||
expect(arrow().props.start.type).toBe('point')
|
expect(bindings().start).toBeUndefined()
|
||||||
expect(arrow().props.end.type).toBe('binding')
|
expect(bindings().end).toBeDefined()
|
||||||
|
|
||||||
// Merge a change from a remote source that deletes box 2
|
// Merge a change from a remote source that deletes box 2
|
||||||
editor.store.mergeRemoteChanges(() => {
|
editor.store.mergeRemoteChanges(() => {
|
||||||
|
@ -89,8 +93,8 @@ describe('restoring bound arrows multiplayer', () => {
|
||||||
expect(editor.getShape(ids.box2)).toBeUndefined()
|
expect(editor.getShape(ids.box2)).toBeUndefined()
|
||||||
// arrow is still there, but without its binding
|
// arrow is still there, but without its binding
|
||||||
expect(arrow()).not.toBeUndefined()
|
expect(arrow()).not.toBeUndefined()
|
||||||
expect(arrow().props.start.type).toBe('point')
|
expect(bindings().start).toBeUndefined()
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
|
|
||||||
editor.undo() // undo creating the arrow
|
editor.undo() // undo creating the arrow
|
||||||
|
|
||||||
|
@ -101,8 +105,8 @@ describe('restoring bound arrows multiplayer', () => {
|
||||||
|
|
||||||
expect(editor.getShape(ids.box2)).toBeUndefined()
|
expect(editor.getShape(ids.box2)).toBeUndefined()
|
||||||
expect(arrow()).not.toBeUndefined()
|
expect(arrow()).not.toBeUndefined()
|
||||||
expect(arrow().props.start.type).toBe('point')
|
expect(bindings().start).toBeUndefined()
|
||||||
expect(arrow().props.end.type).toBe('point')
|
expect(bindings().end).toBeUndefined()
|
||||||
|
|
||||||
editor.undo() // undo creating arrow
|
editor.undo() // undo creating arrow
|
||||||
|
|
||||||
|
@ -121,7 +125,7 @@ describe('restoring bound arrows multiplayer', () => {
|
||||||
editor.redo() // redo creating arrow
|
editor.redo() // redo creating arrow
|
||||||
|
|
||||||
// box is back! arrow should be bound
|
// box is back! arrow should be bound
|
||||||
expect(arrow().props.start.type).toBe('point')
|
expect(bindings().start).toBeUndefined()
|
||||||
expect(arrow().props.end.type).toBe('binding')
|
expect(bindings().end).toBeDefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createShapeId, TLArrowShape } from '@tldraw/editor'
|
import { createShapeId, getArrowBindings, TLArrowShape } from '@tldraw/editor'
|
||||||
import { TestEditor } from '../TestEditor'
|
import { TestEditor } from '../TestEditor'
|
||||||
|
|
||||||
let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
@ -201,32 +201,36 @@ describe('When copying and pasting', () => {
|
||||||
it('creates new bindings for arrows when pasting', async () => {
|
it('creates new bindings for arrows when pasting', async () => {
|
||||||
const mockClipboard = doMockClipboard()
|
const mockClipboard = doMockClipboard()
|
||||||
|
|
||||||
editor.createShapes([
|
editor
|
||||||
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
.createShapes([
|
||||||
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
||||||
{
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
||||||
id: ids.arrow1,
|
{ id: ids.arrow1, type: 'arrow', x: 150, y: 150 },
|
||||||
type: 'arrow',
|
])
|
||||||
x: 150,
|
.createBindings([
|
||||||
y: 150,
|
{
|
||||||
props: {
|
fromId: ids.arrow1,
|
||||||
start: {
|
toId: ids.box1,
|
||||||
type: 'binding',
|
type: 'arrow',
|
||||||
boundShapeId: ids.box1,
|
props: {
|
||||||
isExact: false,
|
terminal: 'start',
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box2,
|
|
||||||
isExact: false,
|
isExact: false,
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
])
|
fromId: ids.arrow1,
|
||||||
|
toId: ids.box2,
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
const shapesBefore = editor.getCurrentPageShapes()
|
const shapesBefore = editor.getCurrentPageShapes()
|
||||||
editor.selectAll().copy()
|
editor.selectAll().copy()
|
||||||
|
@ -258,11 +262,10 @@ describe('When copying and pasting', () => {
|
||||||
...arrow1a,
|
...arrow1a,
|
||||||
id: arrow1b.id,
|
id: arrow1b.id,
|
||||||
index: 'a6',
|
index: 'a6',
|
||||||
props: {
|
})
|
||||||
...arrow1a.props,
|
expect(getArrowBindings(editor, arrow1b as TLArrowShape)).toMatchObject({
|
||||||
start: { ...arrow1a.props.start, boundShapeId: box1b.id },
|
start: { toId: box1b.id },
|
||||||
end: { ...arrow1a.props.end, boundShapeId: box2b.id },
|
end: { toId: box2b.id },
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -385,34 +388,38 @@ describe('When copying and pasting', () => {
|
||||||
it('creates new bindings for arrows when pasting', async () => {
|
it('creates new bindings for arrows when pasting', async () => {
|
||||||
const mockClipboard = doMockClipboard()
|
const mockClipboard = doMockClipboard()
|
||||||
|
|
||||||
editor.createShapes([
|
editor
|
||||||
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
.createShapes([
|
||||||
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
||||||
{
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
||||||
id: ids.arrow1,
|
{ id: ids.arrow1, type: 'arrow', x: 150, y: 150 },
|
||||||
type: 'arrow',
|
])
|
||||||
x: 150,
|
.createBindings([
|
||||||
y: 150,
|
{
|
||||||
props: {
|
fromId: ids.arrow1,
|
||||||
start: {
|
toId: ids.box1,
|
||||||
type: 'binding',
|
type: 'arrow',
|
||||||
boundShapeId: ids.box1,
|
props: {
|
||||||
isExact: false,
|
terminal: 'start',
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
boundShapeId: ids.box2,
|
|
||||||
isExact: false,
|
isExact: false,
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
])
|
fromId: ids.arrow1,
|
||||||
|
toId: ids.box2,
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
const shapesBefore = editor.getCurrentPageShapes()
|
const shapesBefore = editor.getCurrentPageShapesSorted()
|
||||||
|
|
||||||
editor.selectAll().cut()
|
editor.selectAll().cut()
|
||||||
|
|
||||||
|
@ -420,23 +427,16 @@ describe('When copying and pasting', () => {
|
||||||
await assertClipboardOfCorrectShape(mockClipboard.current)
|
await assertClipboardOfCorrectShape(mockClipboard.current)
|
||||||
|
|
||||||
editor.paste()
|
editor.paste()
|
||||||
const shapesAfter = editor.getCurrentPageShapes()
|
const shapesAfter = editor.getCurrentPageShapesSorted()
|
||||||
|
|
||||||
// The new shapes should match the old shapes, except for their id and the arrow's bindings!
|
// The new shapes should match the old shapes, except for their id and the arrow's bindings!
|
||||||
expect(shapesAfter.length).toBe(shapesBefore.length)
|
expect(shapesAfter.length).toBe(shapesBefore.length)
|
||||||
expect(shapesAfter[0]).toMatchObject({ ...shapesBefore[0], id: shapesAfter[0].id })
|
expect(shapesAfter[0]).toMatchObject({ ...shapesBefore[0], id: shapesAfter[0].id })
|
||||||
expect(shapesAfter[1]).toMatchObject({ ...shapesBefore[1], id: shapesAfter[1].id })
|
expect(shapesAfter[1]).toMatchObject({ ...shapesBefore[1], id: shapesAfter[1].id })
|
||||||
expect(shapesAfter[2]).toMatchObject({
|
expect(shapesAfter[2]).toMatchObject({ ...shapesBefore[2], id: shapesAfter[2].id })
|
||||||
...shapesBefore[2],
|
expect(getArrowBindings(editor, shapesAfter[2] as TLArrowShape)).toMatchObject({
|
||||||
id: shapesAfter[2].id,
|
start: { toId: shapesAfter[0].id },
|
||||||
props: {
|
end: { toId: shapesAfter[1].id },
|
||||||
...shapesBefore[2].props,
|
|
||||||
start: {
|
|
||||||
...(shapesBefore[2] as TLArrowShape).props.start,
|
|
||||||
boundShapeId: shapesAfter[0].id,
|
|
||||||
},
|
|
||||||
end: { ...(shapesBefore[2] as TLArrowShape).props.end, boundShapeId: shapesAfter[1].id },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createShapeId } from '@tldraw/editor'
|
import { createBindingId, createShapeId, getArrowBindings } from '@tldraw/editor'
|
||||||
import { TestEditor } from '../TestEditor'
|
import { TestEditor } from '../TestEditor'
|
||||||
|
|
||||||
let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
@ -29,20 +29,34 @@ beforeEach(() => {
|
||||||
x: 150,
|
x: 150,
|
||||||
y: 150,
|
y: 150,
|
||||||
props: {
|
props: {
|
||||||
start: {
|
start: { x: 0, y: 0 },
|
||||||
type: 'binding',
|
end: { x: 0, y: 0 },
|
||||||
isExact: false,
|
},
|
||||||
boundShapeId: ids.box1,
|
},
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
])
|
||||||
isPrecise: false,
|
.createBindings([
|
||||||
},
|
{
|
||||||
end: {
|
id: createBindingId(),
|
||||||
type: 'binding',
|
fromId: ids.arrow1,
|
||||||
isExact: false,
|
toId: ids.box1,
|
||||||
boundShapeId: ids.box2,
|
type: 'arrow',
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
props: {
|
||||||
isPrecise: false,
|
terminal: 'start',
|
||||||
},
|
isExact: false,
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
|
isPrecise: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
fromId: ids.arrow1,
|
||||||
|
toId: ids.box2,
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
isExact: false,
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
@ -90,24 +104,21 @@ describe('Editor.deleteShapes', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When deleting arrows', () => {
|
describe('When deleting arrows', () => {
|
||||||
|
function bindings() {
|
||||||
|
return getArrowBindings(editor, editor.getShape(ids.arrow1)!)
|
||||||
|
}
|
||||||
it('Restores any bindings on undo', () => {
|
it('Restores any bindings on undo', () => {
|
||||||
editor.select(ids.arrow1)
|
editor.select(ids.arrow1)
|
||||||
editor.mark('before deleting')
|
editor.mark('before deleting')
|
||||||
// @ts-expect-error
|
|
||||||
expect(editor._getArrowBindingsIndex().get()[ids.box1]).not.toBeUndefined()
|
expect(bindings().start).toBeDefined()
|
||||||
// @ts-expect-error
|
expect(bindings().end).toBeDefined()
|
||||||
expect(editor._getArrowBindingsIndex().get()[ids.box2]).not.toBeUndefined()
|
|
||||||
|
|
||||||
editor.deleteShapes(editor.getSelectedShapeIds()) // delete the selected shapes
|
editor.deleteShapes(editor.getSelectedShapeIds()) // delete the selected shapes
|
||||||
// @ts-expect-error
|
expect(editor.store.query.records('binding').get()).toHaveLength(0)
|
||||||
expect(editor._getArrowBindingsIndex().get()[ids.box1]).toBeUndefined()
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(editor._getArrowBindingsIndex().get()[ids.box2]).toBeUndefined()
|
|
||||||
|
|
||||||
editor.undo()
|
editor.undo()
|
||||||
// @ts-expect-error
|
expect(bindings().start).toBeDefined()
|
||||||
expect(editor._getArrowBindingsIndex().get()[ids.box1]).not.toBeUndefined()
|
expect(bindings().end).toBeDefined()
|
||||||
// @ts-expect-error
|
|
||||||
expect(editor._getArrowBindingsIndex().get()[ids.box2]).not.toBeUndefined()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -221,7 +221,7 @@ describe('arrows', () => {
|
||||||
expect(editor.getShapePageBounds(arrow)).toCloselyMatchObject({
|
expect(editor.getShapePageBounds(arrow)).toCloselyMatchObject({
|
||||||
x: 300,
|
x: 300,
|
||||||
y: 250,
|
y: 250,
|
||||||
w: 150,
|
w: 86.5,
|
||||||
h: 0,
|
h: 0,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { createShapeId, TLArrowShape, TLShapePartial } from '@tldraw/editor'
|
import {
|
||||||
|
createBindingId,
|
||||||
|
createShapeId,
|
||||||
|
getArrowBindings,
|
||||||
|
TLArrowShape,
|
||||||
|
TLBindingPartial,
|
||||||
|
TLShapePartial,
|
||||||
|
} from '@tldraw/editor'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
|
|
||||||
let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
@ -23,48 +30,50 @@ it('creates new bindings for arrows when pasting', async () => {
|
||||||
.createShapes([
|
.createShapes([
|
||||||
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
||||||
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
||||||
|
{ id: ids.arrow1, type: 'arrow', x: 150, y: 150 },
|
||||||
|
])
|
||||||
|
.createBindings([
|
||||||
{
|
{
|
||||||
id: ids.arrow1,
|
fromId: ids.arrow1,
|
||||||
|
toId: ids.box1,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
x: 150,
|
|
||||||
y: 150,
|
|
||||||
props: {
|
props: {
|
||||||
start: {
|
terminal: 'start',
|
||||||
type: 'binding',
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
boundShapeId: ids.box1,
|
isExact: false,
|
||||||
isExact: false,
|
isPrecise: false,
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
},
|
||||||
isPrecise: false,
|
},
|
||||||
},
|
{
|
||||||
end: {
|
fromId: ids.arrow1,
|
||||||
type: 'binding',
|
toId: ids.box2,
|
||||||
boundShapeId: ids.box2,
|
type: 'arrow',
|
||||||
isExact: false,
|
props: {
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
terminal: 'end',
|
||||||
isPrecise: false,
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
},
|
isExact: false,
|
||||||
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const shapesBefore = editor.getCurrentPageShapes()
|
const shapesBefore = editor.getCurrentPageShapesSorted()
|
||||||
|
|
||||||
editor.selectAll().duplicateShapes(editor.getSelectedShapeIds())
|
editor.selectAll().duplicateShapes(editor.getSelectedShapeIds())
|
||||||
|
|
||||||
const shapesAfter = editor.getCurrentPageShapes()
|
const shapesAfter = editor.getCurrentPageShapesSorted()
|
||||||
|
|
||||||
// We should not have changed the original shapes
|
// We should not have changed the original shapes
|
||||||
expect(shapesBefore[0]).toMatchObject(shapesAfter[0])
|
expect(shapesBefore[0]).toMatchObject(shapesAfter[0])
|
||||||
expect(shapesBefore[1]).toMatchObject(shapesAfter[1])
|
expect(shapesBefore[1]).toMatchObject(shapesAfter[2])
|
||||||
expect(shapesBefore[2]).toMatchObject(shapesAfter[2])
|
expect(shapesBefore[2]).toMatchObject(shapesAfter[4])
|
||||||
|
|
||||||
const box1a = shapesAfter[0]
|
const box1a = shapesAfter[0]
|
||||||
const box2a = shapesAfter[1]
|
const box2a = shapesAfter[2]
|
||||||
const arrow1a = shapesAfter[2] as TLArrowShape
|
|
||||||
|
|
||||||
const box1b = shapesAfter[3]
|
const box1b = shapesAfter[1]
|
||||||
const box2b = shapesAfter[4]
|
const box2b = shapesAfter[3]
|
||||||
const arrow1b = shapesAfter[5]
|
const arrow1b = shapesAfter[5] as TLArrowShape
|
||||||
|
|
||||||
// The new shapes should match the old shapes, except for their id and the arrow's bindings!
|
// The new shapes should match the old shapes, except for their id and the arrow's bindings!
|
||||||
expect(shapesAfter.length).toBe(shapesBefore.length * 2)
|
expect(shapesAfter.length).toBe(shapesBefore.length * 2)
|
||||||
|
@ -73,23 +82,27 @@ it('creates new bindings for arrows when pasting', async () => {
|
||||||
expect(arrow1b).toMatchObject({
|
expect(arrow1b).toMatchObject({
|
||||||
id: arrow1b.id,
|
id: arrow1b.id,
|
||||||
index: 'a4',
|
index: 'a4',
|
||||||
props: {
|
})
|
||||||
...arrow1a.props,
|
expect(getArrowBindings(editor, arrow1b)).toMatchObject({
|
||||||
start: { ...arrow1a.props.start, boundShapeId: box1b.id },
|
start: { toId: box1b.id },
|
||||||
end: { ...arrow1a.props.end, boundShapeId: box2b.id },
|
end: { toId: box2b.id },
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// blood moat incoming
|
// blood moat incoming
|
||||||
describe('When duplicating shapes that include arrows', () => {
|
describe('When duplicating shapes that include arrows', () => {
|
||||||
let shapes: TLShapePartial[]
|
let shapes: TLShapePartial[]
|
||||||
|
let bindings: TLBindingPartial[]
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const box1 = createShapeId()
|
const box1 = createShapeId()
|
||||||
const box2 = createShapeId()
|
const box2 = createShapeId()
|
||||||
const box3 = createShapeId()
|
const box3 = createShapeId()
|
||||||
|
|
||||||
|
const arrow1 = createShapeId()
|
||||||
|
const arrow2 = createShapeId()
|
||||||
|
const arrow3 = createShapeId()
|
||||||
|
|
||||||
shapes = [
|
shapes = [
|
||||||
{
|
{
|
||||||
id: box1,
|
id: box1,
|
||||||
|
@ -110,79 +123,125 @@ describe('When duplicating shapes that include arrows', () => {
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: createShapeId(),
|
id: arrow1,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 50,
|
y: 50,
|
||||||
props: {
|
props: {
|
||||||
bend: 200,
|
bend: 200,
|
||||||
start: {
|
start: { x: 0, y: 0 },
|
||||||
type: 'binding',
|
end: { x: 0, y: 0 },
|
||||||
normalizedAnchor: { x: 0.75, y: 0.75 },
|
|
||||||
boundShapeId: box1,
|
|
||||||
isExact: false,
|
|
||||||
isPrecise: true,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
normalizedAnchor: { x: 0.25, y: 0.25 },
|
|
||||||
boundShapeId: box1,
|
|
||||||
isExact: false,
|
|
||||||
isPrecise: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: createShapeId(),
|
id: arrow2,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 50,
|
y: 50,
|
||||||
props: {
|
props: {
|
||||||
bend: -200,
|
bend: -200,
|
||||||
start: {
|
start: { x: 0, y: 0 },
|
||||||
type: 'binding',
|
end: { x: 0, y: 0 },
|
||||||
normalizedAnchor: { x: 0.75, y: 0.75 },
|
|
||||||
boundShapeId: box1,
|
|
||||||
isExact: false,
|
|
||||||
isPrecise: true,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
normalizedAnchor: { x: 0.25, y: 0.25 },
|
|
||||||
boundShapeId: box1,
|
|
||||||
isExact: false,
|
|
||||||
isPrecise: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: createShapeId(),
|
id: arrow3,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 50,
|
y: 50,
|
||||||
props: {
|
props: {
|
||||||
bend: -200,
|
bend: -200,
|
||||||
start: {
|
start: { x: 0, y: 0 },
|
||||||
type: 'binding',
|
end: { x: 0, y: 0 },
|
||||||
normalizedAnchor: { x: 0.75, y: 0.75 },
|
},
|
||||||
boundShapeId: box1,
|
},
|
||||||
isExact: false,
|
]
|
||||||
isPrecise: true,
|
|
||||||
},
|
bindings = [
|
||||||
end: {
|
{
|
||||||
type: 'binding',
|
id: createBindingId(),
|
||||||
normalizedAnchor: { x: 0.25, y: 0.25 },
|
fromId: arrow1,
|
||||||
boundShapeId: box3,
|
toId: box1,
|
||||||
isExact: false,
|
type: 'arrow',
|
||||||
isPrecise: true,
|
props: {
|
||||||
},
|
terminal: 'start',
|
||||||
|
normalizedAnchor: { x: 0.75, y: 0.75 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
fromId: arrow1,
|
||||||
|
toId: box1,
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
normalizedAnchor: { x: 0.25, y: 0.25 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
fromId: arrow2,
|
||||||
|
toId: box1,
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
terminal: 'start',
|
||||||
|
normalizedAnchor: { x: 0.75, y: 0.75 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
fromId: arrow2,
|
||||||
|
toId: box1,
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
normalizedAnchor: { x: 0.25, y: 0.25 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
fromId: arrow3,
|
||||||
|
toId: box1,
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
terminal: 'start',
|
||||||
|
normalizedAnchor: { x: 0.75, y: 0.75 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
fromId: arrow3,
|
||||||
|
toId: box3,
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
normalizedAnchor: { x: 0.25, y: 0.25 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Preserves the same selection bounds', () => {
|
it('Preserves the same selection bounds', () => {
|
||||||
editor.selectAll().deleteShapes(editor.getSelectedShapeIds()).createShapes(shapes).selectAll()
|
editor
|
||||||
|
.selectAll()
|
||||||
|
.deleteShapes(editor.getSelectedShapeIds())
|
||||||
|
.createShapes(shapes)
|
||||||
|
.createBindings(bindings)
|
||||||
|
.selectAll()
|
||||||
|
|
||||||
const boundsBefore = editor.getSelectionRotatedPageBounds()!
|
const boundsBefore = editor.getSelectionRotatedPageBounds()!
|
||||||
editor.duplicateShapes(editor.getSelectedShapeIds())
|
editor.duplicateShapes(editor.getSelectedShapeIds())
|
||||||
|
@ -194,6 +253,7 @@ describe('When duplicating shapes that include arrows', () => {
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.deleteShapes(editor.getSelectedShapeIds())
|
.deleteShapes(editor.getSelectedShapeIds())
|
||||||
.createShapes(shapes)
|
.createShapes(shapes)
|
||||||
|
.createBindings(bindings)
|
||||||
.select(
|
.select(
|
||||||
...editor
|
...editor
|
||||||
.getCurrentPageShapes()
|
.getCurrentPageShapes()
|
||||||
|
|
|
@ -3,9 +3,12 @@ import {
|
||||||
PI,
|
PI,
|
||||||
TLArrowShape,
|
TLArrowShape,
|
||||||
TLArrowShapeProps,
|
TLArrowShapeProps,
|
||||||
|
TLBindingPartial,
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
|
createBindingId,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
|
getArrowBindings,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
|
|
||||||
|
@ -355,12 +358,10 @@ describe('flipping rotated shapes', () => {
|
||||||
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
|
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
|
||||||
const props: Partial<TLArrowShapeProps> = {
|
const props: Partial<TLArrowShapeProps> = {
|
||||||
start: {
|
start: {
|
||||||
type: 'point',
|
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
type: 'point',
|
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
|
@ -408,8 +409,8 @@ describe('flipping rotated shapes', () => {
|
||||||
const transform = editor.getShapePageTransform(id)
|
const transform = editor.getShapePageTransform(id)
|
||||||
if (!transform) throw new Error('no transform')
|
if (!transform) throw new Error('no transform')
|
||||||
const arrow = editor.getShape<TLArrowShape>(id)!
|
const arrow = editor.getShape<TLArrowShape>(id)!
|
||||||
if (arrow.props.start.type !== 'point' || arrow.props.end.type !== 'point')
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
throw new Error('not a point')
|
if (bindings.start || bindings.end) throw new Error('not a point')
|
||||||
const start = Mat.applyToPoint(transform, arrow.props.start)
|
const start = Mat.applyToPoint(transform, arrow.props.start)
|
||||||
const end = Mat.applyToPoint(transform, arrow.props.end)
|
const end = Mat.applyToPoint(transform, arrow.props.end)
|
||||||
return { start, end }
|
return { start, end }
|
||||||
|
@ -466,11 +467,15 @@ describe('flipping rotated shapes', () => {
|
||||||
|
|
||||||
describe('When flipping shapes that include arrows', () => {
|
describe('When flipping shapes that include arrows', () => {
|
||||||
let shapes: TLShapePartial[]
|
let shapes: TLShapePartial[]
|
||||||
|
let bindings: TLBindingPartial[]
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const box1 = createShapeId()
|
const box1 = createShapeId()
|
||||||
const box2 = createShapeId()
|
const box2 = createShapeId()
|
||||||
const box3 = createShapeId()
|
const box3 = createShapeId()
|
||||||
|
const arrow1 = createShapeId()
|
||||||
|
const arrow2 = createShapeId()
|
||||||
|
const arrow3 = createShapeId()
|
||||||
|
|
||||||
shapes = [
|
shapes = [
|
||||||
{
|
{
|
||||||
|
@ -492,79 +497,115 @@ describe('When flipping shapes that include arrows', () => {
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: createShapeId(),
|
id: arrow1,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 50,
|
y: 50,
|
||||||
props: {
|
props: {
|
||||||
bend: 200,
|
bend: 200,
|
||||||
start: {
|
|
||||||
type: 'binding',
|
|
||||||
normalizedAnchor: { x: 0.75, y: 0.75 },
|
|
||||||
boundShapeId: box1,
|
|
||||||
isExact: false,
|
|
||||||
isPrecise: true,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
normalizedAnchor: { x: 0.25, y: 0.25 },
|
|
||||||
boundShapeId: box1,
|
|
||||||
isExact: false,
|
|
||||||
isPrecise: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: createShapeId(),
|
id: arrow2,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 50,
|
y: 50,
|
||||||
props: {
|
props: {
|
||||||
bend: -200,
|
bend: -200,
|
||||||
start: {
|
|
||||||
type: 'binding',
|
|
||||||
normalizedAnchor: { x: 0.75, y: 0.75 },
|
|
||||||
boundShapeId: box1,
|
|
||||||
isExact: false,
|
|
||||||
isPrecise: true,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'binding',
|
|
||||||
normalizedAnchor: { x: 0.25, y: 0.25 },
|
|
||||||
boundShapeId: box1,
|
|
||||||
isExact: false,
|
|
||||||
isPrecise: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: createShapeId(),
|
id: arrow3,
|
||||||
type: 'arrow',
|
type: 'arrow',
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 50,
|
y: 50,
|
||||||
props: {
|
props: {
|
||||||
bend: -200,
|
bend: -200,
|
||||||
start: {
|
},
|
||||||
type: 'binding',
|
},
|
||||||
normalizedAnchor: { x: 0.75, y: 0.75 },
|
]
|
||||||
boundShapeId: box1,
|
bindings = [
|
||||||
isExact: false,
|
{
|
||||||
isPrecise: true,
|
id: createBindingId(),
|
||||||
},
|
type: 'arrow',
|
||||||
end: {
|
fromId: arrow1,
|
||||||
type: 'binding',
|
toId: box1,
|
||||||
normalizedAnchor: { x: 0.25, y: 0.25 },
|
props: {
|
||||||
boundShapeId: box3,
|
terminal: 'start',
|
||||||
isExact: false,
|
normalizedAnchor: { x: 0.75, y: 0.75 },
|
||||||
isPrecise: true,
|
isExact: false,
|
||||||
},
|
isPrecise: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrow1,
|
||||||
|
toId: box1,
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
normalizedAnchor: { x: 0.25, y: 0.25 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrow2,
|
||||||
|
toId: box1,
|
||||||
|
props: {
|
||||||
|
terminal: 'start',
|
||||||
|
normalizedAnchor: { x: 0.75, y: 0.75 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrow2,
|
||||||
|
toId: box1,
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
normalizedAnchor: { x: 0.25, y: 0.25 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrow3,
|
||||||
|
toId: box1,
|
||||||
|
props: {
|
||||||
|
terminal: 'start',
|
||||||
|
normalizedAnchor: { x: 0.75, y: 0.75 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createBindingId(),
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrow3,
|
||||||
|
toId: box3,
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
normalizedAnchor: { x: 0.25, y: 0.25 },
|
||||||
|
isExact: false,
|
||||||
|
isPrecise: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Flips horizontally', () => {
|
it('Flips horizontally', () => {
|
||||||
editor.selectAll().deleteShapes(editor.getSelectedShapeIds()).createShapes(shapes)
|
editor
|
||||||
|
.selectAll()
|
||||||
|
.deleteShapes(editor.getSelectedShapeIds())
|
||||||
|
.createShapes(shapes)
|
||||||
|
.createBindings(bindings)
|
||||||
|
|
||||||
const boundsBefore = editor.getSelectionRotatedPageBounds()!
|
const boundsBefore = editor.getSelectionRotatedPageBounds()!
|
||||||
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
|
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||||
|
@ -572,7 +613,11 @@ describe('When flipping shapes that include arrows', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Flips vertically', () => {
|
it('Flips vertically', () => {
|
||||||
editor.selectAll().deleteShapes(editor.getSelectedShapeIds()).createShapes(shapes)
|
editor
|
||||||
|
.selectAll()
|
||||||
|
.deleteShapes(editor.getSelectedShapeIds())
|
||||||
|
.createShapes(shapes)
|
||||||
|
.createBindings(bindings)
|
||||||
|
|
||||||
const boundsBefore = editor.getSelectionRotatedPageBounds()!
|
const boundsBefore = editor.getSelectionRotatedPageBounds()!
|
||||||
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
|
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
TLFrameShape,
|
TLFrameShape,
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
|
getArrowBindings,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { DEFAULT_FRAME_PADDING, fitFrameToContent, removeFrame } from '../lib/utils/frames/frames'
|
import { DEFAULT_FRAME_PADDING, fitFrameToContent, removeFrame } from '../lib/utils/frames/frames'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
|
@ -562,9 +563,10 @@ describe('frame shapes', () => {
|
||||||
editor.pointerDown(150, 150).pointerMove(250, 250).pointerUp(250, 250)
|
editor.pointerDown(150, 150).pointerMove(250, 250).pointerUp(250, 250)
|
||||||
|
|
||||||
const arrow = editor.getOnlySelectedShape()! as TLArrowShape
|
const arrow = editor.getOnlySelectedShape()! as TLArrowShape
|
||||||
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
|
|
||||||
expect(arrow.props.start).toMatchObject({ boundShapeId: frameId })
|
expect(bindings.start).toMatchObject({ toId: frameId })
|
||||||
expect(arrow.props.end).toMatchObject({ type: 'point' })
|
expect(bindings.end).toBeUndefined()
|
||||||
|
|
||||||
expect(arrow.parentId).toBe(editor.getCurrentPageId())
|
expect(arrow.parentId).toBe(editor.getCurrentPageId())
|
||||||
})
|
})
|
||||||
|
@ -587,9 +589,10 @@ describe('frame shapes', () => {
|
||||||
editor.pointerDown(150, 150).pointerMove(190, 190).pointerUp(190, 190)
|
editor.pointerDown(150, 150).pointerMove(190, 190).pointerUp(190, 190)
|
||||||
|
|
||||||
const arrow = editor.getOnlySelectedShape()! as TLArrowShape
|
const arrow = editor.getOnlySelectedShape()! as TLArrowShape
|
||||||
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
|
|
||||||
expect(arrow.props.start).toMatchObject({ boundShapeId: boxId })
|
expect(bindings.start).toMatchObject({ toId: boxId })
|
||||||
expect(arrow.props.end).toMatchObject({ boundShapeId: frameId })
|
expect(bindings.end).toMatchObject({ toId: frameId })
|
||||||
|
|
||||||
expect(arrow.parentId).toBe(editor.getCurrentPageId())
|
expect(arrow.parentId).toBe(editor.getCurrentPageId())
|
||||||
})
|
})
|
||||||
|
@ -703,23 +706,16 @@ describe('frame shapes', () => {
|
||||||
|
|
||||||
// Check if the arrow's handles remain points
|
// Check if the arrow's handles remain points
|
||||||
let arrow = editor.getOnlySelectedShape()! as TLArrowShape
|
let arrow = editor.getOnlySelectedShape()! as TLArrowShape
|
||||||
expect(arrow.props.start).toMatchObject({
|
expect(arrow.props.start).toMatchObject({ x: 0, y: 0 })
|
||||||
type: 'point',
|
expect(arrow.props.end).toMatchObject({ x: -125, y: -125 })
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
})
|
|
||||||
expect(arrow.props.end).toMatchObject({
|
|
||||||
type: 'point',
|
|
||||||
x: -125,
|
|
||||||
y: -125,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Move the end handle inside the frame
|
// Move the end handle inside the frame
|
||||||
editor.pointerMove(175, 175).pointerUp(175, 175)
|
editor.pointerMove(175, 175).pointerUp(175, 175)
|
||||||
|
|
||||||
// Check if arrow's end handle is bound to the inner box
|
// Check if arrow's end handle is bound to the inner box
|
||||||
arrow = editor.getOnlySelectedShape()! as TLArrowShape
|
arrow = editor.getOnlySelectedShape()! as TLArrowShape
|
||||||
expect(arrow.props.end).toMatchObject({ boundShapeId: innerBoxId })
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
|
expect(bindings.end).toMatchObject({ toId: innerBoxId })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('correctly fits to its content', () => {
|
it('correctly fits to its content', () => {
|
||||||
|
|
|
@ -141,42 +141,55 @@ it('works for shapes that are outside of the viewport, but are then moved inside
|
||||||
const box2Id = createShapeId()
|
const box2Id = createShapeId()
|
||||||
const arrowId = createShapeId()
|
const arrowId = createShapeId()
|
||||||
|
|
||||||
editor.createShapes([
|
editor
|
||||||
{
|
.createShapes([
|
||||||
id: box1Id,
|
{
|
||||||
props: { w: 100, h: 100, geo: 'rectangle' },
|
id: box1Id,
|
||||||
type: 'geo',
|
props: { w: 100, h: 100, geo: 'rectangle' },
|
||||||
x: -500,
|
type: 'geo',
|
||||||
y: 0,
|
x: -500,
|
||||||
},
|
y: 0,
|
||||||
{
|
},
|
||||||
id: box2Id,
|
{
|
||||||
type: 'geo',
|
id: box2Id,
|
||||||
x: -1000,
|
type: 'geo',
|
||||||
y: 200,
|
x: -1000,
|
||||||
props: { w: 100, h: 100, geo: 'rectangle' },
|
y: 200,
|
||||||
},
|
props: { w: 100, h: 100, geo: 'rectangle' },
|
||||||
{
|
},
|
||||||
id: arrowId,
|
{
|
||||||
type: 'arrow',
|
id: arrowId,
|
||||||
props: {
|
type: 'arrow',
|
||||||
start: {
|
props: {
|
||||||
type: 'binding',
|
start: { x: 0, y: 0 },
|
||||||
isExact: true,
|
end: { x: 0, y: 0 },
|
||||||
boundShapeId: box1Id,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
||||||
isPrecise: false,
|
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
])
|
||||||
|
.createBindings([
|
||||||
|
{
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrowId,
|
||||||
|
toId: box1Id,
|
||||||
|
props: {
|
||||||
|
terminal: 'start',
|
||||||
isExact: true,
|
isExact: true,
|
||||||
boundShapeId: box2Id,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
])
|
type: 'arrow',
|
||||||
|
fromId: arrowId,
|
||||||
|
toId: box2Id,
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
isExact: true,
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
|
isPrecise: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
expect(editor.getCulledShapes()).toEqual(new Set([box1Id, box2Id, arrowId]))
|
expect(editor.getCulledShapes()).toEqual(new Set([box1Id, box2Id, arrowId]))
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
assert,
|
assert,
|
||||||
compact,
|
compact,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
|
getArrowBindings,
|
||||||
sortByIndex,
|
sortByIndex,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
|
@ -57,16 +58,8 @@ const arrow = (id: TLShapeId, start: VecLike, end: VecLike): TLShapePartial => (
|
||||||
id,
|
id,
|
||||||
// index: bumpIndex(),
|
// index: bumpIndex(),
|
||||||
props: {
|
props: {
|
||||||
start: {
|
start: { x: start.x, y: start.y },
|
||||||
type: 'point',
|
end: { x: end.x, y: end.y },
|
||||||
x: start.x,
|
|
||||||
y: start.y,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: 'point',
|
|
||||||
x: end.x,
|
|
||||||
y: end.y,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const randomRotation = () => Math.random() * Math.PI * 2
|
const randomRotation = () => Math.random() * Math.PI * 2
|
||||||
|
@ -1562,8 +1555,9 @@ describe('binding bug', () => {
|
||||||
// go from A to group A
|
// go from A to group A
|
||||||
editor.pointerDown(5, 5).pointerMove(25, 5).pointerUp()
|
editor.pointerDown(5, 5).pointerMove(25, 5).pointerUp()
|
||||||
const arrow = onlySelectedShape() as TLArrowShape
|
const arrow = onlySelectedShape() as TLArrowShape
|
||||||
expect(arrow.props.start).toMatchObject({ boundShapeId: ids.boxA })
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
expect(arrow.props.end).toMatchObject({ type: 'point' })
|
expect(bindings.start).toMatchObject({ toId: ids.boxA })
|
||||||
|
expect(bindings.end).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1608,9 +1602,10 @@ describe('bindings', () => {
|
||||||
// go from E to group C (not hovering over a leaf box)
|
// go from E to group C (not hovering over a leaf box)
|
||||||
editor.pointerDown(5, 25).pointerMove(35, 5).pointerUp()
|
editor.pointerDown(5, 25).pointerMove(35, 5).pointerUp()
|
||||||
const arrow = onlySelectedShape() as TLArrowShape
|
const arrow = onlySelectedShape() as TLArrowShape
|
||||||
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
|
|
||||||
expect(arrow.props.start).toMatchObject({ boundShapeId: ids.boxE })
|
expect(bindings.start).toMatchObject({ toId: ids.boxE })
|
||||||
expect(arrow.props.end).toMatchObject({ type: 'point' })
|
expect(bindings.end).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can not be made from a group shape to some sibling shape', () => {
|
it('can not be made from a group shape to some sibling shape', () => {
|
||||||
|
@ -1619,20 +1614,22 @@ describe('bindings', () => {
|
||||||
editor.pointerDown(35, 5).pointerMove(5, 25).pointerUp()
|
editor.pointerDown(35, 5).pointerMove(5, 25).pointerUp()
|
||||||
|
|
||||||
const arrow = onlySelectedShape() as TLArrowShape
|
const arrow = onlySelectedShape() as TLArrowShape
|
||||||
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
|
|
||||||
expect(arrow.props.start).toMatchObject({ type: 'point' })
|
expect(bindings.start).toBeUndefined
|
||||||
expect(arrow.props.end).toMatchObject({ boundShapeId: ids.boxE })
|
expect(bindings.end).toMatchObject({ toId: ids.boxE })
|
||||||
})
|
})
|
||||||
it('can be made from a shape within a group to some shape outside of the group', () => {
|
it('can be made from a shape within a group to some shape outside of the group', () => {
|
||||||
editor.setCurrentTool('arrow')
|
editor.setCurrentTool('arrow')
|
||||||
// go from A to E
|
// go from A to E
|
||||||
editor.pointerDown(5, 5).pointerMove(5, 25).pointerUp()
|
editor.pointerDown(5, 5).pointerMove(5, 25).pointerUp()
|
||||||
const arrow = onlySelectedShape() as TLArrowShape
|
const arrow = onlySelectedShape() as TLArrowShape
|
||||||
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
|
|
||||||
expect(arrow.parentId).toBe(editor.getCurrentPageId())
|
expect(arrow.parentId).toBe(editor.getCurrentPageId())
|
||||||
|
|
||||||
expect(arrow.props.start).toMatchObject({ boundShapeId: ids.boxA })
|
expect(bindings.start).toMatchObject({ toId: ids.boxA })
|
||||||
expect(arrow.props.end).toMatchObject({ boundShapeId: ids.boxE })
|
expect(bindings.end).toMatchObject({ toId: ids.boxE })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can be made from a shape within a group to another shape within the group', () => {
|
it('can be made from a shape within a group to another shape within the group', () => {
|
||||||
|
@ -1640,10 +1637,11 @@ describe('bindings', () => {
|
||||||
// go from A to B
|
// go from A to B
|
||||||
editor.pointerDown(5, 5).pointerMove(25, 5).pointerUp()
|
editor.pointerDown(5, 5).pointerMove(25, 5).pointerUp()
|
||||||
const arrow = onlySelectedShape() as TLArrowShape
|
const arrow = onlySelectedShape() as TLArrowShape
|
||||||
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
|
|
||||||
expect(arrow.parentId).toBe(groupAId)
|
expect(arrow.parentId).toBe(groupAId)
|
||||||
expect(arrow.props.start).toMatchObject({ boundShapeId: ids.boxA })
|
expect(bindings.start).toMatchObject({ toId: ids.boxA })
|
||||||
expect(arrow.props.end).toMatchObject({ boundShapeId: ids.boxB })
|
expect(bindings.end).toMatchObject({ toId: ids.boxB })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can be made from a shape outside of a group to a shape within the group', () => {
|
it('can be made from a shape outside of a group to a shape within the group', () => {
|
||||||
|
@ -1651,10 +1649,11 @@ describe('bindings', () => {
|
||||||
// go from E to B
|
// go from E to B
|
||||||
editor.pointerDown(5, 25).pointerMove(27, 7).pointerMove(25, 5).pointerUp()
|
editor.pointerDown(5, 25).pointerMove(27, 7).pointerMove(25, 5).pointerUp()
|
||||||
const arrow = onlySelectedShape() as TLArrowShape
|
const arrow = onlySelectedShape() as TLArrowShape
|
||||||
|
const bindings = getArrowBindings(editor, arrow)
|
||||||
|
|
||||||
expect(arrow.parentId).toBe(editor.getCurrentPageId())
|
expect(arrow.parentId).toBe(editor.getCurrentPageId())
|
||||||
expect(arrow.props.start).toMatchObject({ boundShapeId: ids.boxE })
|
expect(bindings.start).toMatchObject({ toId: ids.boxE })
|
||||||
expect(arrow.props.end).toMatchObject({ boundShapeId: ids.boxB })
|
expect(bindings.end).toMatchObject({ toId: ids.boxB })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1723,20 +1722,17 @@ describe('moving handles within a group', () => {
|
||||||
editor.pointerDown(50, 50).pointerMove(60, 60).pointerUp(60, 60)
|
editor.pointerDown(50, 50).pointerMove(60, 60).pointerUp(60, 60)
|
||||||
|
|
||||||
let arrow = onlySelectedShape() as TLArrowShape
|
let arrow = onlySelectedShape() as TLArrowShape
|
||||||
|
let bindings = getArrowBindings(editor, arrow)
|
||||||
|
|
||||||
expect(arrow.parentId).toBe(groupA.id)
|
expect(arrow.parentId).toBe(groupA.id)
|
||||||
|
|
||||||
expect(arrow.props.start.type).toBe('point')
|
expect(bindings.start).toBeUndefined()
|
||||||
if (arrow.props.start.type === 'point') {
|
expect(arrow.props.start.x).toBe(0)
|
||||||
expect(arrow.props.start.x).toBe(0)
|
expect(arrow.props.start.y).toBe(0)
|
||||||
expect(arrow.props.start.y).toBe(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(arrow.props.end.type).toBe('point')
|
expect(bindings.end).toBeUndefined()
|
||||||
if (arrow.props.end.type === 'point') {
|
expect(arrow.props.end.x).toBe(10)
|
||||||
expect(arrow.props.end.x).toBe(10)
|
expect(arrow.props.end.y).toBe(10)
|
||||||
expect(arrow.props.end.y).toBe(10)
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.expectToBeIn('select.idle')
|
editor.expectToBeIn('select.idle')
|
||||||
|
|
||||||
|
@ -1759,20 +1755,17 @@ describe('moving handles within a group', () => {
|
||||||
editor.pointerMove(60, -10)
|
editor.pointerMove(60, -10)
|
||||||
|
|
||||||
arrow = editor.getShape(arrow.id)!
|
arrow = editor.getShape(arrow.id)!
|
||||||
|
bindings = getArrowBindings(editor, arrow)
|
||||||
|
|
||||||
expect(arrow.parentId).toBe(groupA.id)
|
expect(arrow.parentId).toBe(groupA.id)
|
||||||
|
|
||||||
expect(arrow.props.start.type).toBe('point')
|
expect(bindings.start).toBeUndefined()
|
||||||
if (arrow.props.start.type === 'point') {
|
expect(arrow.props.start.x).toBe(0)
|
||||||
expect(arrow.props.start.x).toBe(0)
|
expect(arrow.props.start.y).toBe(0)
|
||||||
expect(arrow.props.start.y).toBe(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(arrow.props.end.type).toBe('point')
|
expect(bindings.end).toBeUndefined()
|
||||||
if (arrow.props.end.type === 'point') {
|
expect(arrow.props.end.x).toBe(10)
|
||||||
expect(arrow.props.end.x).toBe(10)
|
expect(arrow.props.end.y).toBe(-60)
|
||||||
expect(arrow.props.end.y).toBe(-60)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
|
expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
|
||||||
x: 0,
|
x: 0,
|
||||||
|
|
|
@ -136,8 +136,8 @@ describe('When brushing arrows', () => {
|
||||||
ref="arrow1"
|
ref="arrow1"
|
||||||
x={0}
|
x={0}
|
||||||
y={0}
|
y={0}
|
||||||
start={{ type: 'point', x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ type: 'point', x: 100, y: 100 }}
|
end={{ x: 100, y: 100 }}
|
||||||
bend={0}
|
bend={0}
|
||||||
/>,
|
/>,
|
||||||
])
|
])
|
||||||
|
@ -158,8 +158,8 @@ describe('When brushing arrows', () => {
|
||||||
ref="arrow1"
|
ref="arrow1"
|
||||||
x={0}
|
x={0}
|
||||||
y={0}
|
y={0}
|
||||||
start={{ type: 'point', x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ type: 'point', x: 100, y: 100 }}
|
end={{ x: 100, y: 100 }}
|
||||||
bend={40}
|
bend={40}
|
||||||
/>,
|
/>,
|
||||||
])
|
])
|
||||||
|
|
|
@ -1490,8 +1490,8 @@ describe('When double clicking an editable shape', () => {
|
||||||
x: 200,
|
x: 200,
|
||||||
y: 50,
|
y: 50,
|
||||||
props: {
|
props: {
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
start: { x: 0, y: 0 },
|
||||||
end: { type: 'point', x: 100, y: 0 },
|
end: { x: 100, y: 0 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
Vec,
|
Vec,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
|
getArrowBindings,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
import { getSnapLines } from './getSnapLines'
|
import { getSnapLines } from './getSnapLines'
|
||||||
|
@ -1631,84 +1632,115 @@ describe('translating a shape with a bound shape', () => {
|
||||||
|
|
||||||
it('should preserve arrow bindings', () => {
|
it('should preserve arrow bindings', () => {
|
||||||
const arrow1 = createShapeId('arrow1')
|
const arrow1 = createShapeId('arrow1')
|
||||||
editor.createShapes([
|
editor
|
||||||
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
.createShapes([
|
||||||
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
||||||
{
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
||||||
id: arrow1,
|
{
|
||||||
type: 'arrow',
|
id: arrow1,
|
||||||
x: 150,
|
type: 'arrow',
|
||||||
y: 150,
|
x: 150,
|
||||||
props: {
|
y: 150,
|
||||||
start: {
|
props: {
|
||||||
type: 'binding',
|
start: { x: 0, y: 0 },
|
||||||
isExact: false,
|
end: { x: 0, y: 0 },
|
||||||
boundShapeId: ids.box1,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
||||||
isPrecise: false,
|
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
])
|
||||||
|
.createBindings([
|
||||||
|
{
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrow1,
|
||||||
|
toId: ids.box1,
|
||||||
|
props: {
|
||||||
|
terminal: 'start',
|
||||||
isExact: false,
|
isExact: false,
|
||||||
boundShapeId: ids.box2,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
])
|
type: 'arrow',
|
||||||
|
fromId: arrow1,
|
||||||
|
toId: ids.box2,
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
isExact: false,
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
|
isPrecise: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
editor.select(ids.box1, arrow1)
|
editor.select(ids.box1, arrow1)
|
||||||
editor.pointerDown(150, 150, ids.box1).pointerMove(0, 0)
|
editor.pointerDown(150, 150, ids.box1).pointerMove(0, 0)
|
||||||
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: -50, y: -50 })
|
expect(editor.getShape(ids.box1)).toMatchObject({ x: -50, y: -50 })
|
||||||
expect(editor.getShape(arrow1)).toMatchObject({
|
expect(getArrowBindings(editor, editor.getShape(arrow1) as TLArrowShape)).toMatchObject({
|
||||||
props: { start: { type: 'binding' }, end: { type: 'binding' } },
|
start: { type: 'arrow' },
|
||||||
|
end: { type: 'arrow' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('breaks arrow bindings when cloning', () => {
|
it('breaks arrow bindings when cloning', () => {
|
||||||
const arrow1 = createShapeId('arrow1')
|
const arrow1 = createShapeId('arrow1')
|
||||||
editor.createShapes([
|
editor
|
||||||
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
.createShapes([
|
||||||
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
||||||
{
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
||||||
id: arrow1,
|
{
|
||||||
type: 'arrow',
|
id: arrow1,
|
||||||
x: 150,
|
type: 'arrow',
|
||||||
y: 150,
|
x: 150,
|
||||||
props: {
|
y: 150,
|
||||||
start: {
|
props: {
|
||||||
type: 'binding',
|
start: { x: 0, y: 0 },
|
||||||
isExact: false,
|
end: { x: 0, y: 0 },
|
||||||
boundShapeId: ids.box1,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
||||||
isPrecise: false,
|
|
||||||
},
|
},
|
||||||
end: {
|
},
|
||||||
type: 'binding',
|
])
|
||||||
|
.createBindings([
|
||||||
|
{
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrow1,
|
||||||
|
toId: ids.box1,
|
||||||
|
props: {
|
||||||
|
terminal: 'start',
|
||||||
isExact: false,
|
isExact: false,
|
||||||
boundShapeId: ids.box2,
|
|
||||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
isPrecise: false,
|
isPrecise: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
])
|
type: 'arrow',
|
||||||
|
fromId: arrow1,
|
||||||
|
toId: ids.box2,
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
isExact: false,
|
||||||
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||||
|
isPrecise: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
editor.select(ids.box1, arrow1)
|
editor.select(ids.box1, arrow1)
|
||||||
editor.pointerDown(150, 150, ids.box1).pointerMove(0, 0, { altKey: true })
|
editor.pointerDown(150, 150, ids.box1).pointerMove(0, 0, { altKey: true })
|
||||||
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 100, y: 100 })
|
expect(editor.getShape(ids.box1)).toMatchObject({ x: 100, y: 100 })
|
||||||
expect(editor.getShape(arrow1)).toMatchObject({
|
expect(getArrowBindings(editor, editor.getShape(arrow1) as TLArrowShape)).toMatchObject({
|
||||||
props: { start: { type: 'binding' }, end: { type: 'binding' } },
|
start: { type: 'arrow' },
|
||||||
|
end: { type: 'arrow' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const newArrow = editor
|
const newArrow = editor
|
||||||
.getCurrentPageShapes()
|
.getCurrentPageShapes()
|
||||||
.find((s) => editor.isShapeOfType<TLArrowShape>(s, 'arrow') && s.id !== arrow1)
|
.find(
|
||||||
expect(newArrow).toMatchObject({
|
(s) => editor.isShapeOfType<TLArrowShape>(s, 'arrow') && s.id !== arrow1
|
||||||
props: { start: { type: 'binding' }, end: { type: 'point' } },
|
)! as TLArrowShape
|
||||||
|
expect(getArrowBindings(editor, newArrow)).toMatchObject({
|
||||||
|
start: { type: 'arrow' },
|
||||||
|
end: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,12 +15,19 @@ import { RecordId } from '@tldraw/store';
|
||||||
import { RecordType } from '@tldraw/store';
|
import { RecordType } from '@tldraw/store';
|
||||||
import { SerializedStore } from '@tldraw/store';
|
import { SerializedStore } from '@tldraw/store';
|
||||||
import { Signal } from '@tldraw/state';
|
import { Signal } from '@tldraw/state';
|
||||||
|
import { StandaloneDependsOn } from '@tldraw/store';
|
||||||
import { Store } from '@tldraw/store';
|
import { Store } from '@tldraw/store';
|
||||||
import { StoreSchema } from '@tldraw/store';
|
import { StoreSchema } from '@tldraw/store';
|
||||||
import { StoreSnapshot } from '@tldraw/store';
|
import { StoreSnapshot } from '@tldraw/store';
|
||||||
import { T } from '@tldraw/validate';
|
import { T } from '@tldraw/validate';
|
||||||
import { UnknownRecord } from '@tldraw/store';
|
import { UnknownRecord } from '@tldraw/store';
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const arrowBindingMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const arrowBindingProps: RecordProps<TLArrowBinding>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const ArrowShapeArrowheadEndStyle: EnumStyleProp<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">;
|
export const ArrowShapeArrowheadEndStyle: EnumStyleProp<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">;
|
||||||
|
|
||||||
|
@ -28,7 +35,7 @@ export const ArrowShapeArrowheadEndStyle: EnumStyleProp<"arrow" | "bar" | "diamo
|
||||||
export const ArrowShapeArrowheadStartStyle: EnumStyleProp<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">;
|
export const ArrowShapeArrowheadStartStyle: EnumStyleProp<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const arrowShapeMigrations: TLShapePropsMigrations;
|
export const arrowShapeMigrations: MigrationSequence;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const arrowShapeProps: {
|
export const arrowShapeProps: {
|
||||||
|
@ -37,39 +44,13 @@ export const arrowShapeProps: {
|
||||||
bend: T.Validator<number>;
|
bend: T.Validator<number>;
|
||||||
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
end: T.UnionValidator<"type", {
|
end: T.Validator<VecModel>;
|
||||||
binding: T.ObjectValidator<{
|
|
||||||
boundShapeId: TLShapeId;
|
|
||||||
isExact: boolean;
|
|
||||||
isPrecise: boolean;
|
|
||||||
normalizedAnchor: VecModel;
|
|
||||||
type: "binding";
|
|
||||||
} & {}>;
|
|
||||||
point: T.ObjectValidator<{
|
|
||||||
type: "point";
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
} & {}>;
|
|
||||||
}, never>;
|
|
||||||
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
||||||
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
||||||
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
labelPosition: T.Validator<number>;
|
labelPosition: T.Validator<number>;
|
||||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||||
start: T.UnionValidator<"type", {
|
start: T.Validator<VecModel>;
|
||||||
binding: T.ObjectValidator<{
|
|
||||||
boundShapeId: TLShapeId;
|
|
||||||
isExact: boolean;
|
|
||||||
isPrecise: boolean;
|
|
||||||
normalizedAnchor: VecModel;
|
|
||||||
type: "binding";
|
|
||||||
} & {}>;
|
|
||||||
point: T.ObjectValidator<{
|
|
||||||
type: "point";
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
} & {}>;
|
|
||||||
}, never>;
|
|
||||||
text: T.Validator<string>;
|
text: T.Validator<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -86,7 +67,10 @@ export const AssetRecordType: RecordType<TLAsset, "props" | "type">;
|
||||||
export const assetValidator: T.Validator<TLAsset>;
|
export const assetValidator: T.Validator<TLAsset>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const bookmarkShapeMigrations: TLShapePropsMigrations;
|
export const bindingIdValidator: T.Validator<TLBindingId>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const bookmarkShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const bookmarkShapeProps: {
|
export const bookmarkShapeProps: {
|
||||||
|
@ -132,6 +116,24 @@ export function createAssetValidator<Type extends string, Props extends JsonObje
|
||||||
typeName: 'asset';
|
typeName: 'asset';
|
||||||
}[P_1] | undefined; }>;
|
}[P_1] | undefined; }>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function createBindingId(id?: string): TLBindingId;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function createBindingPropsMigrationIds<S extends string, T extends Record<string, number>>(bindingType: S, ids: T): {
|
||||||
|
[k in keyof T]: `com.tldraw.binding.${S}/${T[k]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function createBindingPropsMigrationSequence(migrations: TLPropsMigrations): TLPropsMigrations;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function createBindingValidator<Type extends string, Props extends JsonObject, Meta extends JsonObject>(type: Type, props?: {
|
||||||
|
[K in keyof Props]: T.Validatable<Props[K]>;
|
||||||
|
}, meta?: {
|
||||||
|
[K in keyof Meta]: T.Validatable<Meta[K]>;
|
||||||
|
}): T.ObjectValidator<{ [P in "fromId" | "id" | "meta" | "toId" | "typeName" | (undefined extends Props ? never : "props") | (undefined extends Type ? never : "type")]: TLBaseBinding<Type, Props>[P]; } & { [P_1 in (undefined extends Props ? "props" : never) | (undefined extends Type ? "type" : never)]?: TLBaseBinding<Type, Props>[P_1] | undefined; }>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export const createPresenceStateDerivation: ($user: Signal<{
|
export const createPresenceStateDerivation: ($user: Signal<{
|
||||||
color: string;
|
color: string;
|
||||||
|
@ -143,12 +145,12 @@ export const createPresenceStateDerivation: ($user: Signal<{
|
||||||
export function createShapeId(id?: string): TLShapeId;
|
export function createShapeId(id?: string): TLShapeId;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function createShapePropsMigrationIds<S extends string, T extends Record<string, number>>(shapeType: S, ids: T): {
|
export function createShapePropsMigrationIds<const S extends string, const T extends Record<string, number>>(shapeType: S, ids: T): {
|
||||||
[k in keyof T]: `com.tldraw.shape.${S}/${T[k]}`;
|
[k in keyof T]: `com.tldraw.shape.${S}/${T[k]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function createShapePropsMigrationSequence(migrations: TLShapePropsMigrations): TLShapePropsMigrations;
|
export function createShapePropsMigrationSequence(migrations: TLPropsMigrations): TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function createShapeValidator<Type extends string, Props extends JsonObject, Meta extends JsonObject>(type: Type, props?: {
|
export function createShapeValidator<Type extends string, Props extends JsonObject, Meta extends JsonObject>(type: Type, props?: {
|
||||||
|
@ -158,9 +160,10 @@ export function createShapeValidator<Type extends string, Props extends JsonObje
|
||||||
}): T.ObjectValidator<{ [P in "id" | "index" | "isLocked" | "meta" | "opacity" | "parentId" | "rotation" | "typeName" | "x" | "y" | (undefined extends Props ? never : "props") | (undefined extends Type ? never : "type")]: TLBaseShape<Type, Props>[P]; } & { [P_1 in (undefined extends Props ? "props" : never) | (undefined extends Type ? "type" : never)]?: TLBaseShape<Type, Props>[P_1] | undefined; }>;
|
}): T.ObjectValidator<{ [P in "id" | "index" | "isLocked" | "meta" | "opacity" | "parentId" | "rotation" | "typeName" | "x" | "y" | (undefined extends Props ? never : "props") | (undefined extends Type ? never : "type")]: TLBaseShape<Type, Props>[P]; } & { [P_1 in (undefined extends Props ? "props" : never) | (undefined extends Type ? "type" : never)]?: TLBaseShape<Type, Props>[P_1] | undefined; }>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function createTLSchema({ shapes, migrations, }?: {
|
export function createTLSchema({ shapes, bindings, migrations, }?: {
|
||||||
|
bindings?: Record<string, SchemaPropsInfo>;
|
||||||
migrations?: readonly MigrationSequence[];
|
migrations?: readonly MigrationSequence[];
|
||||||
shapes?: Record<string, SchemaShapeInfo>;
|
shapes?: Record<string, SchemaPropsInfo>;
|
||||||
}): TLSchema;
|
}): TLSchema;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -194,7 +197,7 @@ export const DefaultHorizontalAlignStyle: EnumStyleProp<"end-legacy" | "end" | "
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const defaultShapeSchemas: {
|
export const defaultShapeSchemas: {
|
||||||
[T in TLDefaultShape['type']]: SchemaShapeInfo;
|
[T in TLDefaultShape['type']]: SchemaPropsInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -210,7 +213,7 @@ export const DefaultVerticalAlignStyle: EnumStyleProp<"end" | "middle" | "start"
|
||||||
export const DocumentRecordType: RecordType<TLDocument, never>;
|
export const DocumentRecordType: RecordType<TLDocument, never>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const drawShapeMigrations: TLShapePropsMigrations;
|
export const drawShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const drawShapeProps: {
|
export const drawShapeProps: {
|
||||||
|
@ -448,7 +451,7 @@ export type EmbedDefinition = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const embedShapeMigrations: TLShapePropsMigrations;
|
export const embedShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export const embedShapePermissionDefaults: {
|
export const embedShapePermissionDefaults: {
|
||||||
|
@ -484,7 +487,7 @@ export class EnumStyleProp<T> extends StyleProp<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const frameShapeMigrations: TLShapePropsMigrations;
|
export const frameShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const frameShapeProps: {
|
export const frameShapeProps: {
|
||||||
|
@ -497,7 +500,7 @@ export const frameShapeProps: {
|
||||||
export const GeoShapeGeoStyle: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
export const GeoShapeGeoStyle: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const geoShapeMigrations: TLShapePropsMigrations;
|
export const geoShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const geoShapeProps: {
|
export const geoShapeProps: {
|
||||||
|
@ -529,13 +532,13 @@ export function getDefaultTranslationLocale(): TLLanguage['locale'];
|
||||||
export function getShapePropKeysByStyle(props: Record<string, T.Validatable<any>>): Map<StyleProp<unknown>, string>;
|
export function getShapePropKeysByStyle(props: Record<string, T.Validatable<any>>): Map<StyleProp<unknown>, string>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const groupShapeMigrations: TLShapePropsMigrations;
|
export const groupShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const groupShapeProps: ShapeProps<TLGroupShape>;
|
export const groupShapeProps: RecordProps<TLGroupShape>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const highlightShapeMigrations: TLShapePropsMigrations;
|
export const highlightShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const highlightShapeProps: {
|
export const highlightShapeProps: {
|
||||||
|
@ -553,7 +556,7 @@ export const highlightShapeProps: {
|
||||||
export function idValidator<Id extends RecordId<UnknownRecord>>(prefix: Id['__type__']['typeName']): T.Validator<Id>;
|
export function idValidator<Id extends RecordId<UnknownRecord>>(prefix: Id['__type__']['typeName']): T.Validator<Id>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const imageShapeMigrations: TLShapePropsMigrations;
|
export const imageShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const imageShapeProps: {
|
export const imageShapeProps: {
|
||||||
|
@ -574,6 +577,12 @@ export const InstancePageStateRecordType: RecordType<TLInstancePageState, "pageI
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const InstancePresenceRecordType: RecordType<TLInstancePresence, "currentPageId" | "userId" | "userName">;
|
export const InstancePresenceRecordType: RecordType<TLInstancePresence, "currentPageId" | "userId" | "userName">;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function isBinding(record?: UnknownRecord): record is TLBinding;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function isBindingId(id?: string): id is TLBindingId;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function isPageId(id: string): id is TLPageId;
|
export function isPageId(id: string): id is TLPageId;
|
||||||
|
|
||||||
|
@ -698,7 +707,7 @@ export const LANGUAGES: readonly [{
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const lineShapeMigrations: TLShapePropsMigrations;
|
export const lineShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const lineShapeProps: {
|
export const lineShapeProps: {
|
||||||
|
@ -718,7 +727,7 @@ export const lineShapeProps: {
|
||||||
export const LineShapeSplineStyle: EnumStyleProp<"cubic" | "line">;
|
export const LineShapeSplineStyle: EnumStyleProp<"cubic" | "line">;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const noteShapeMigrations: TLShapePropsMigrations;
|
export const noteShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const noteShapeProps: {
|
export const noteShapeProps: {
|
||||||
|
@ -748,15 +757,33 @@ export const parentIdValidator: T.Validator<TLParentId>;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const PointerRecordType: RecordType<TLPointer, never>;
|
export const PointerRecordType: RecordType<TLPointer, never>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type RecordProps<R extends UnknownRecord & {
|
||||||
|
props: object;
|
||||||
|
}> = {
|
||||||
|
[K in keyof R['props']]: T.Validatable<R['props'][K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type RecordPropsType<Config extends Record<string, T.Validatable<any>>> = Expand<{
|
||||||
|
[K in keyof Config]: T.TypeOf<Config[K]>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const rootBindingMigrations: MigrationSequence;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const rootShapeMigrations: MigrationSequence;
|
export const rootShapeMigrations: MigrationSequence;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type SchemaShapeInfo = {
|
export interface SchemaPropsInfo {
|
||||||
|
// (undocumented)
|
||||||
meta?: Record<string, AnyValidator>;
|
meta?: Record<string, AnyValidator>;
|
||||||
migrations?: LegacyMigrations | MigrationSequence | TLShapePropsMigrations;
|
// (undocumented)
|
||||||
|
migrations?: LegacyMigrations | MigrationSequence | TLPropsMigrations;
|
||||||
|
// (undocumented)
|
||||||
props?: Record<string, AnyValidator>;
|
props?: Record<string, AnyValidator>;
|
||||||
};
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const scribbleValidator: T.Validator<TLScribble>;
|
export const scribbleValidator: T.Validator<TLScribble>;
|
||||||
|
@ -764,16 +791,6 @@ export const scribbleValidator: T.Validator<TLScribble>;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const shapeIdValidator: T.Validator<TLShapeId>;
|
export const shapeIdValidator: T.Validator<TLShapeId>;
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
|
|
||||||
[K in keyof Shape['props']]: T.Validatable<Shape['props'][K]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export type ShapePropsType<Config extends Record<string, T.Validatable<any>>> = Expand<{
|
|
||||||
[K in keyof Config]: T.TypeOf<Config[K]>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export class StyleProp<Type> implements T.Validatable<Type> {
|
export class StyleProp<Type> implements T.Validatable<Type> {
|
||||||
// @internal
|
// @internal
|
||||||
|
@ -802,7 +819,7 @@ export class StyleProp<Type> implements T.Validatable<Type> {
|
||||||
export type StylePropValue<T extends StyleProp<any>> = T extends StyleProp<infer U> ? U : never;
|
export type StylePropValue<T extends StyleProp<any>> = T extends StyleProp<infer U> ? U : never;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const textShapeMigrations: TLShapePropsMigrations;
|
export const textShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const textShapeProps: {
|
export const textShapeProps: {
|
||||||
|
@ -819,6 +836,19 @@ export const textShapeProps: {
|
||||||
// @public
|
// @public
|
||||||
export const TL_CANVAS_UI_COLOR_TYPES: Set<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
|
export const TL_CANVAS_UI_COLOR_TYPES: Set<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type TLArrowBinding = TLBaseBinding<'arrow', TLArrowBindingProps>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface TLArrowBindingProps {
|
||||||
|
isExact: boolean;
|
||||||
|
isPrecise: boolean;
|
||||||
|
// (undocumented)
|
||||||
|
normalizedAnchor: VecModel;
|
||||||
|
// (undocumented)
|
||||||
|
terminal: 'end' | 'start';
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>;
|
export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>;
|
||||||
|
|
||||||
|
@ -826,10 +856,7 @@ export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>;
|
||||||
export type TLArrowShapeArrowheadStyle = T.TypeOf<typeof ArrowShapeArrowheadStartStyle>;
|
export type TLArrowShapeArrowheadStyle = T.TypeOf<typeof ArrowShapeArrowheadStartStyle>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLArrowShapeProps = ShapePropsType<typeof arrowShapeProps>;
|
export type TLArrowShapeProps = RecordPropsType<typeof arrowShapeProps>;
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export type TLArrowShapeTerminal = T.TypeOf<typeof ArrowShapeTerminal>;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLAsset = TLBookmarkAsset | TLImageAsset | TLVideoAsset;
|
export type TLAsset = TLBookmarkAsset | TLImageAsset | TLVideoAsset;
|
||||||
|
@ -862,6 +889,20 @@ export interface TLBaseAsset<Type extends string, Props> extends BaseRecord<'ass
|
||||||
type: Type;
|
type: Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface TLBaseBinding<Type extends string, Props extends object> extends BaseRecord<'binding', TLBindingId> {
|
||||||
|
// (undocumented)
|
||||||
|
fromId: TLShapeId;
|
||||||
|
// (undocumented)
|
||||||
|
meta: JsonObject;
|
||||||
|
// (undocumented)
|
||||||
|
props: Props;
|
||||||
|
// (undocumented)
|
||||||
|
toId: TLShapeId;
|
||||||
|
// (undocumented)
|
||||||
|
type: Type;
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export interface TLBaseShape<Type extends string, Props extends object> extends BaseRecord<'shape', TLShapeId> {
|
export interface TLBaseShape<Type extends string, Props extends object> extends BaseRecord<'shape', TLShapeId> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -886,6 +927,20 @@ export interface TLBaseShape<Type extends string, Props extends object> extends
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export type TLBinding = TLDefaultBinding | TLUnknownBinding;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type TLBindingId = RecordId<TLUnknownBinding>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type TLBindingPartial<T extends TLBinding = TLBinding> = T extends T ? {
|
||||||
|
id: TLBindingId;
|
||||||
|
meta?: Partial<T['meta']>;
|
||||||
|
props?: Partial<T['props']>;
|
||||||
|
type: T['type'];
|
||||||
|
} & Partial<Omit<T, 'id' | 'meta' | 'props' | 'type'>> : never;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export type TLBookmarkAsset = TLBaseAsset<'bookmark', {
|
export type TLBookmarkAsset = TLBaseAsset<'bookmark', {
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -926,6 +981,9 @@ export interface TLCursor {
|
||||||
// @public
|
// @public
|
||||||
export type TLCursorType = SetValue<typeof TL_CURSOR_TYPES>;
|
export type TLCursorType = SetValue<typeof TL_CURSOR_TYPES>;
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export type TLDefaultBinding = TLArrowBinding;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLDefaultColorStyle = T.TypeOf<typeof DefaultColorStyle>;
|
export type TLDefaultColorStyle = T.TypeOf<typeof DefaultColorStyle>;
|
||||||
|
|
||||||
|
@ -1052,7 +1110,7 @@ export type TLImageShape = TLBaseShape<'image', TLImageShapeProps>;
|
||||||
export type TLImageShapeCrop = T.TypeOf<typeof ImageShapeCrop>;
|
export type TLImageShapeCrop = T.TypeOf<typeof ImageShapeCrop>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLImageShapeProps = ShapePropsType<typeof imageShapeProps>;
|
export type TLImageShapeProps = RecordPropsType<typeof imageShapeProps>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
||||||
|
@ -1219,7 +1277,25 @@ export type TLParentId = TLPageId | TLShapeId;
|
||||||
export const TLPOINTER_ID: TLPointerId;
|
export const TLPOINTER_ID: TLPointerId;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLRecord = TLAsset | TLCamera | TLDocument | TLInstance | TLInstancePageState | TLInstancePresence | TLPage | TLPointer | TLShape;
|
export interface TLPropsMigration {
|
||||||
|
// (undocumented)
|
||||||
|
readonly dependsOn?: MigrationId[];
|
||||||
|
// (undocumented)
|
||||||
|
readonly down?: ((props: any) => any) | typeof NO_DOWN_MIGRATION | typeof RETIRED_DOWN_MIGRATION;
|
||||||
|
// (undocumented)
|
||||||
|
readonly id: MigrationId;
|
||||||
|
// (undocumented)
|
||||||
|
readonly up: (props: any) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface TLPropsMigrations {
|
||||||
|
// (undocumented)
|
||||||
|
readonly sequence: Array<StandaloneDependsOn | TLPropsMigration>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type TLRecord = TLAsset | TLBinding | TLCamera | TLDocument | TLInstance | TLInstancePageState | TLInstancePresence | TLPage | TLPointer | TLShape;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLSchema = StoreSchema<TLRecord, TLStoreProps>;
|
export type TLSchema = StoreSchema<TLRecord, TLStoreProps>;
|
||||||
|
@ -1254,24 +1330,6 @@ export type TLShapePartial<T extends TLShape = TLShape> = T extends T ? {
|
||||||
type: T['type'];
|
type: T['type'];
|
||||||
} & Partial<Omit<T, 'id' | 'meta' | 'props' | 'type'>> : never;
|
} & Partial<Omit<T, 'id' | 'meta' | 'props' | 'type'>> : never;
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export type TLShapeProp = keyof TLShapeProps;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export type TLShapeProps = Identity<UnionToIntersection<TLDefaultShape['props']>>;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export type TLShapePropsMigrations = {
|
|
||||||
sequence: Array<{
|
|
||||||
readonly dependsOn: readonly MigrationId[];
|
|
||||||
} | {
|
|
||||||
readonly dependsOn?: MigrationId[];
|
|
||||||
readonly down?: ((props: any) => any) | typeof NO_DOWN_MIGRATION | typeof RETIRED_DOWN_MIGRATION;
|
|
||||||
readonly id: MigrationId;
|
|
||||||
readonly up: (props: any) => any;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLStore = Store<TLRecord, TLStoreProps>;
|
export type TLStore = Store<TLRecord, TLStoreProps>;
|
||||||
|
|
||||||
|
@ -1290,7 +1348,10 @@ export type TLStoreSnapshot = StoreSnapshot<TLRecord>;
|
||||||
export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>;
|
export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLTextShapeProps = ShapePropsType<typeof textShapeProps>;
|
export type TLTextShapeProps = RecordPropsType<typeof textShapeProps>;
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export type TLUnknownBinding = TLBaseBinding<string, object>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export type TLUnknownShape = TLBaseShape<string, object>;
|
export type TLUnknownShape = TLBaseShape<string, object>;
|
||||||
|
@ -1322,7 +1383,7 @@ export interface VecModel {
|
||||||
export const vecModelValidator: T.Validator<VecModel>;
|
export const vecModelValidator: T.Validator<VecModel>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const videoShapeMigrations: TLShapePropsMigrations;
|
export const videoShapeMigrations: TLPropsMigrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const videoShapeProps: {
|
export const videoShapeProps: {
|
||||||
|
|
|
@ -2,6 +2,18 @@ import { Migration, MigrationId, Store, UnknownRecord } from '@tldraw/store'
|
||||||
import { structuredClone } from '@tldraw/utils'
|
import { structuredClone } from '@tldraw/utils'
|
||||||
import { createTLSchema } from '../createTLSchema'
|
import { createTLSchema } from '../createTLSchema'
|
||||||
|
|
||||||
|
let nextNanoId = 0
|
||||||
|
jest.mock('nanoid', () => {
|
||||||
|
const nanoid = () => {
|
||||||
|
nextNanoId++
|
||||||
|
return `nanoid_${nextNanoId}`
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nanoid,
|
||||||
|
default: nanoid,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export const testSchema = createTLSchema()
|
export const testSchema = createTLSchema()
|
||||||
|
|
||||||
// mock all migrator fns
|
// mock all migrator fns
|
||||||
|
@ -43,10 +55,12 @@ export function getTestMigration(migrationId: MigrationId) {
|
||||||
return {
|
return {
|
||||||
id: migrationId,
|
id: migrationId,
|
||||||
up: (stuff: any) => {
|
up: (stuff: any) => {
|
||||||
|
nextNanoId = 0
|
||||||
const result = structuredClone(stuff)
|
const result = structuredClone(stuff)
|
||||||
return migration.up(result) ?? result
|
return migration.up(result) ?? result
|
||||||
},
|
},
|
||||||
down: (stuff: any) => {
|
down: (stuff: any) => {
|
||||||
|
nextNanoId = 0
|
||||||
if (typeof migration.down !== 'function') {
|
if (typeof migration.down !== 'function') {
|
||||||
throw new Error(`Migration ${migrationId} does not have a down function`)
|
throw new Error(`Migration ${migrationId} does not have a down function`)
|
||||||
}
|
}
|
||||||
|
|
39
packages/tlschema/src/bindings/TLArrowBinding.ts
Normal file
39
packages/tlschema/src/bindings/TLArrowBinding.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { T } from '@tldraw/validate'
|
||||||
|
import { VecModel, vecModelValidator } from '../misc/geometry-types'
|
||||||
|
import { createBindingPropsMigrationSequence } from '../records/TLBinding'
|
||||||
|
import { RecordProps } from '../recordsWithProps'
|
||||||
|
import { arrowShapeVersions } from '../shapes/TLArrowShape'
|
||||||
|
import { TLBaseBinding } from './TLBaseBinding'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export interface TLArrowBindingProps {
|
||||||
|
terminal: 'start' | 'end'
|
||||||
|
normalizedAnchor: VecModel
|
||||||
|
/**
|
||||||
|
* exact is whether the arrow head 'enters' the bound shape to point directly at the binding
|
||||||
|
* anchor point
|
||||||
|
*/
|
||||||
|
isExact: boolean
|
||||||
|
/**
|
||||||
|
* precise is whether to bind to the normalizedAnchor, or to the middle of the shape
|
||||||
|
*/
|
||||||
|
isPrecise: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const arrowBindingProps: RecordProps<TLArrowBinding> = {
|
||||||
|
terminal: T.literalEnum('start', 'end'),
|
||||||
|
normalizedAnchor: vecModelValidator,
|
||||||
|
isExact: T.boolean,
|
||||||
|
isPrecise: T.boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLArrowBinding = TLBaseBinding<'arrow', TLArrowBindingProps>
|
||||||
|
|
||||||
|
export const arrowBindingVersions = {} as const
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const arrowBindingMigrations = createBindingPropsMigrationSequence({
|
||||||
|
sequence: [{ dependsOn: [arrowShapeVersions.ExtractBindings] }],
|
||||||
|
})
|
41
packages/tlschema/src/bindings/TLBaseBinding.ts
Normal file
41
packages/tlschema/src/bindings/TLBaseBinding.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { BaseRecord } from '@tldraw/store'
|
||||||
|
import { JsonObject } from '@tldraw/utils'
|
||||||
|
import { T } from '@tldraw/validate'
|
||||||
|
import { idValidator } from '../misc/id-validator'
|
||||||
|
import { TLBindingId } from '../records/TLBinding'
|
||||||
|
import { TLShapeId } from '../records/TLShape'
|
||||||
|
import { shapeIdValidator } from '../shapes/TLBaseShape'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export interface TLBaseBinding<Type extends string, Props extends object>
|
||||||
|
extends BaseRecord<'binding', TLBindingId> {
|
||||||
|
type: Type
|
||||||
|
fromId: TLShapeId
|
||||||
|
toId: TLShapeId
|
||||||
|
props: Props
|
||||||
|
meta: JsonObject
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const bindingIdValidator = idValidator<TLBindingId>('binding')
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function createBindingValidator<
|
||||||
|
Type extends string,
|
||||||
|
Props extends JsonObject,
|
||||||
|
Meta extends JsonObject,
|
||||||
|
>(
|
||||||
|
type: Type,
|
||||||
|
props?: { [K in keyof Props]: T.Validatable<Props[K]> },
|
||||||
|
meta?: { [K in keyof Meta]: T.Validatable<Meta[K]> }
|
||||||
|
) {
|
||||||
|
return T.object<TLBaseBinding<Type, Props>>({
|
||||||
|
id: bindingIdValidator,
|
||||||
|
typeName: T.literal('binding'),
|
||||||
|
type: T.literal(type),
|
||||||
|
fromId: shapeIdValidator,
|
||||||
|
toId: shapeIdValidator,
|
||||||
|
props: props ? T.object(props) : (T.jsonValue as any),
|
||||||
|
meta: meta ? T.object(meta) : (T.jsonValue as any),
|
||||||
|
})
|
||||||
|
}
|
|
@ -4,7 +4,9 @@ import { TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLS
|
||||||
import { bookmarkAssetMigrations } from './assets/TLBookmarkAsset'
|
import { bookmarkAssetMigrations } from './assets/TLBookmarkAsset'
|
||||||
import { imageAssetMigrations } from './assets/TLImageAsset'
|
import { imageAssetMigrations } from './assets/TLImageAsset'
|
||||||
import { videoAssetMigrations } from './assets/TLVideoAsset'
|
import { videoAssetMigrations } from './assets/TLVideoAsset'
|
||||||
|
import { arrowBindingMigrations, arrowBindingProps } from './bindings/TLArrowBinding'
|
||||||
import { AssetRecordType, assetMigrations } from './records/TLAsset'
|
import { AssetRecordType, assetMigrations } from './records/TLAsset'
|
||||||
|
import { TLBinding, TLDefaultBinding, createBindingRecordType } from './records/TLBinding'
|
||||||
import { CameraRecordType, cameraMigrations } from './records/TLCamera'
|
import { CameraRecordType, cameraMigrations } from './records/TLCamera'
|
||||||
import { DocumentRecordType, documentMigrations } from './records/TLDocument'
|
import { DocumentRecordType, documentMigrations } from './records/TLDocument'
|
||||||
import { createInstanceRecordType, instanceMigrations } from './records/TLInstance'
|
import { createInstanceRecordType, instanceMigrations } from './records/TLInstance'
|
||||||
|
@ -15,12 +17,12 @@ import { InstancePresenceRecordType, instancePresenceMigrations } from './record
|
||||||
import { TLRecord } from './records/TLRecord'
|
import { TLRecord } from './records/TLRecord'
|
||||||
import {
|
import {
|
||||||
TLDefaultShape,
|
TLDefaultShape,
|
||||||
TLShapePropsMigrations,
|
TLShape,
|
||||||
createShapeRecordType,
|
createShapeRecordType,
|
||||||
getShapePropKeysByStyle,
|
getShapePropKeysByStyle,
|
||||||
processShapeMigrations,
|
|
||||||
rootShapeMigrations,
|
rootShapeMigrations,
|
||||||
} from './records/TLShape'
|
} from './records/TLShape'
|
||||||
|
import { TLPropsMigrations, processPropsMigrations } from './recordsWithProps'
|
||||||
import { arrowShapeMigrations, arrowShapeProps } from './shapes/TLArrowShape'
|
import { arrowShapeMigrations, arrowShapeProps } from './shapes/TLArrowShape'
|
||||||
import { bookmarkShapeMigrations, bookmarkShapeProps } from './shapes/TLBookmarkShape'
|
import { bookmarkShapeMigrations, bookmarkShapeProps } from './shapes/TLBookmarkShape'
|
||||||
import { drawShapeMigrations, drawShapeProps } from './shapes/TLDrawShape'
|
import { drawShapeMigrations, drawShapeProps } from './shapes/TLDrawShape'
|
||||||
|
@ -43,8 +45,8 @@ type AnyValidator = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type SchemaShapeInfo = {
|
export interface SchemaPropsInfo {
|
||||||
migrations?: LegacyMigrations | TLShapePropsMigrations | MigrationSequence
|
migrations?: LegacyMigrations | TLPropsMigrations | MigrationSequence
|
||||||
props?: Record<string, AnyValidator>
|
props?: Record<string, AnyValidator>
|
||||||
meta?: Record<string, AnyValidator>
|
meta?: Record<string, AnyValidator>
|
||||||
}
|
}
|
||||||
|
@ -53,7 +55,7 @@ export type SchemaShapeInfo = {
|
||||||
export type TLSchema = StoreSchema<TLRecord, TLStoreProps>
|
export type TLSchema = StoreSchema<TLRecord, TLStoreProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const defaultShapeSchemas: { [T in TLDefaultShape['type']]: SchemaShapeInfo } = {
|
export const defaultShapeSchemas: { [T in TLDefaultShape['type']]: SchemaPropsInfo } = {
|
||||||
arrow: { migrations: arrowShapeMigrations, props: arrowShapeProps },
|
arrow: { migrations: arrowShapeMigrations, props: arrowShapeProps },
|
||||||
bookmark: { migrations: bookmarkShapeMigrations, props: bookmarkShapeProps },
|
bookmark: { migrations: bookmarkShapeMigrations, props: bookmarkShapeProps },
|
||||||
draw: { migrations: drawShapeMigrations, props: drawShapeProps },
|
draw: { migrations: drawShapeMigrations, props: drawShapeProps },
|
||||||
|
@ -69,6 +71,11 @@ export const defaultShapeSchemas: { [T in TLDefaultShape['type']]: SchemaShapeIn
|
||||||
video: { migrations: videoShapeMigrations, props: videoShapeProps },
|
video: { migrations: videoShapeMigrations, props: videoShapeProps },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const defaultBindingSchemas: { [T in TLDefaultBinding['type']]: SchemaPropsInfo } = {
|
||||||
|
arrow: { migrations: arrowBindingMigrations, props: arrowBindingProps },
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a TLSchema with custom shapes. Custom shapes cannot override default shapes.
|
* Create a TLSchema with custom shapes. Custom shapes cannot override default shapes.
|
||||||
*
|
*
|
||||||
|
@ -77,9 +84,11 @@ export const defaultShapeSchemas: { [T in TLDefaultShape['type']]: SchemaShapeIn
|
||||||
* @public */
|
* @public */
|
||||||
export function createTLSchema({
|
export function createTLSchema({
|
||||||
shapes = defaultShapeSchemas,
|
shapes = defaultShapeSchemas,
|
||||||
|
bindings = defaultBindingSchemas,
|
||||||
migrations,
|
migrations,
|
||||||
}: {
|
}: {
|
||||||
shapes?: Record<string, SchemaShapeInfo>
|
shapes?: Record<string, SchemaPropsInfo>
|
||||||
|
bindings?: Record<string, SchemaPropsInfo>
|
||||||
migrations?: readonly MigrationSequence[]
|
migrations?: readonly MigrationSequence[]
|
||||||
} = {}): TLSchema {
|
} = {}): TLSchema {
|
||||||
const stylesById = new Map<string, StyleProp<unknown>>()
|
const stylesById = new Map<string, StyleProp<unknown>>()
|
||||||
|
@ -93,11 +102,13 @@ export function createTLSchema({
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShapeRecordType = createShapeRecordType(shapes)
|
const ShapeRecordType = createShapeRecordType(shapes)
|
||||||
|
const BindingRecordType = createBindingRecordType(bindings)
|
||||||
const InstanceRecordType = createInstanceRecordType(stylesById)
|
const InstanceRecordType = createInstanceRecordType(stylesById)
|
||||||
|
|
||||||
return StoreSchema.create(
|
return StoreSchema.create(
|
||||||
{
|
{
|
||||||
asset: AssetRecordType,
|
asset: AssetRecordType,
|
||||||
|
binding: BindingRecordType,
|
||||||
camera: CameraRecordType,
|
camera: CameraRecordType,
|
||||||
document: DocumentRecordType,
|
document: DocumentRecordType,
|
||||||
instance: InstanceRecordType,
|
instance: InstanceRecordType,
|
||||||
|
@ -124,7 +135,8 @@ export function createTLSchema({
|
||||||
imageAssetMigrations,
|
imageAssetMigrations,
|
||||||
videoAssetMigrations,
|
videoAssetMigrations,
|
||||||
|
|
||||||
...processShapeMigrations(shapes),
|
...processPropsMigrations<TLShape>('shape', shapes),
|
||||||
|
...processPropsMigrations<TLBinding>('binding', bindings),
|
||||||
|
|
||||||
...(migrations ?? []),
|
...(migrations ?? []),
|
||||||
],
|
],
|
||||||
|
|
|
@ -9,11 +9,22 @@ export { assetIdValidator, createAssetValidator, type TLBaseAsset } from './asse
|
||||||
export { type TLBookmarkAsset } from './assets/TLBookmarkAsset'
|
export { type TLBookmarkAsset } from './assets/TLBookmarkAsset'
|
||||||
export { type TLImageAsset } from './assets/TLImageAsset'
|
export { type TLImageAsset } from './assets/TLImageAsset'
|
||||||
export { type TLVideoAsset } from './assets/TLVideoAsset'
|
export { type TLVideoAsset } from './assets/TLVideoAsset'
|
||||||
|
export {
|
||||||
|
arrowBindingMigrations,
|
||||||
|
arrowBindingProps,
|
||||||
|
type TLArrowBinding,
|
||||||
|
type TLArrowBindingProps,
|
||||||
|
} from './bindings/TLArrowBinding'
|
||||||
|
export {
|
||||||
|
bindingIdValidator,
|
||||||
|
createBindingValidator,
|
||||||
|
type TLBaseBinding,
|
||||||
|
} from './bindings/TLBaseBinding'
|
||||||
export { createPresenceStateDerivation } from './createPresenceStateDerivation'
|
export { createPresenceStateDerivation } from './createPresenceStateDerivation'
|
||||||
export {
|
export {
|
||||||
createTLSchema,
|
createTLSchema,
|
||||||
defaultShapeSchemas,
|
defaultShapeSchemas,
|
||||||
type SchemaShapeInfo,
|
type SchemaPropsInfo,
|
||||||
type TLSchema,
|
type TLSchema,
|
||||||
} from './createTLSchema'
|
} from './createTLSchema'
|
||||||
export {
|
export {
|
||||||
|
@ -41,6 +52,19 @@ export {
|
||||||
type TLAssetPartial,
|
type TLAssetPartial,
|
||||||
type TLAssetShape,
|
type TLAssetShape,
|
||||||
} from './records/TLAsset'
|
} from './records/TLAsset'
|
||||||
|
export {
|
||||||
|
createBindingId,
|
||||||
|
createBindingPropsMigrationIds,
|
||||||
|
createBindingPropsMigrationSequence,
|
||||||
|
isBinding,
|
||||||
|
isBindingId,
|
||||||
|
rootBindingMigrations,
|
||||||
|
type TLBinding,
|
||||||
|
type TLBindingId,
|
||||||
|
type TLBindingPartial,
|
||||||
|
type TLDefaultBinding,
|
||||||
|
type TLUnknownBinding,
|
||||||
|
} from './records/TLBinding'
|
||||||
export { CameraRecordType, type TLCamera, type TLCameraId } from './records/TLCamera'
|
export { CameraRecordType, type TLCamera, type TLCameraId } from './records/TLCamera'
|
||||||
export { DocumentRecordType, TLDOCUMENT_ID, type TLDocument } from './records/TLDocument'
|
export { DocumentRecordType, TLDOCUMENT_ID, type TLDocument } from './records/TLDocument'
|
||||||
export { TLINSTANCE_ID, type TLInstance, type TLInstanceId } from './records/TLInstance'
|
export { TLINSTANCE_ID, type TLInstance, type TLInstanceId } from './records/TLInstance'
|
||||||
|
@ -68,11 +92,14 @@ export {
|
||||||
type TLShape,
|
type TLShape,
|
||||||
type TLShapeId,
|
type TLShapeId,
|
||||||
type TLShapePartial,
|
type TLShapePartial,
|
||||||
type TLShapeProp,
|
|
||||||
type TLShapeProps,
|
|
||||||
type TLShapePropsMigrations,
|
|
||||||
type TLUnknownShape,
|
type TLUnknownShape,
|
||||||
} from './records/TLShape'
|
} from './records/TLShape'
|
||||||
|
export {
|
||||||
|
type RecordProps,
|
||||||
|
type RecordPropsType,
|
||||||
|
type TLPropsMigration,
|
||||||
|
type TLPropsMigrations,
|
||||||
|
} from './recordsWithProps'
|
||||||
export {
|
export {
|
||||||
ArrowShapeArrowheadEndStyle,
|
ArrowShapeArrowheadEndStyle,
|
||||||
ArrowShapeArrowheadStartStyle,
|
ArrowShapeArrowheadStartStyle,
|
||||||
|
@ -81,14 +108,11 @@ export {
|
||||||
type TLArrowShape,
|
type TLArrowShape,
|
||||||
type TLArrowShapeArrowheadStyle,
|
type TLArrowShapeArrowheadStyle,
|
||||||
type TLArrowShapeProps,
|
type TLArrowShapeProps,
|
||||||
type TLArrowShapeTerminal,
|
|
||||||
} from './shapes/TLArrowShape'
|
} from './shapes/TLArrowShape'
|
||||||
export {
|
export {
|
||||||
createShapeValidator,
|
createShapeValidator,
|
||||||
parentIdValidator,
|
parentIdValidator,
|
||||||
shapeIdValidator,
|
shapeIdValidator,
|
||||||
type ShapeProps,
|
|
||||||
type ShapePropsType,
|
|
||||||
type TLBaseShape,
|
type TLBaseShape,
|
||||||
} from './shapes/TLBaseShape'
|
} from './shapes/TLBaseShape'
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -1573,6 +1573,236 @@ describe('Add text align to text shapes', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Extract bindings from arrows', () => {
|
||||||
|
const { up } = getTestMigration(arrowShapeVersions.ExtractBindings)
|
||||||
|
|
||||||
|
test('up works as expected', () => {
|
||||||
|
expect(
|
||||||
|
up({
|
||||||
|
'shape:arrow1': {
|
||||||
|
id: 'shape:arrow1',
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
start: {
|
||||||
|
type: 'binding',
|
||||||
|
boundShapeId: 'shape:box1',
|
||||||
|
normalizedAnchor: {
|
||||||
|
x: 0.4383437225516159,
|
||||||
|
y: 0.5065334673177019,
|
||||||
|
},
|
||||||
|
isPrecise: false,
|
||||||
|
isExact: false,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'binding',
|
||||||
|
boundShapeId: 'shape:box2',
|
||||||
|
normalizedAnchor: {
|
||||||
|
x: 0.5848167203201774,
|
||||||
|
y: 0.5766996080606552,
|
||||||
|
},
|
||||||
|
isPrecise: false,
|
||||||
|
isExact: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeName: 'shape',
|
||||||
|
},
|
||||||
|
'shape:arrow2': {
|
||||||
|
id: 'shape:arrow2',
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
start: {
|
||||||
|
type: 'binding',
|
||||||
|
boundShapeId: 'shape:arrow2',
|
||||||
|
normalizedAnchor: {
|
||||||
|
x: 0.4383437225516159,
|
||||||
|
y: 0.5065334673177019,
|
||||||
|
},
|
||||||
|
isPrecise: false,
|
||||||
|
isExact: false,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'point',
|
||||||
|
x: 174.75451263561803,
|
||||||
|
y: -1.4725753187527948,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeName: 'shape',
|
||||||
|
},
|
||||||
|
'shape:arrow3': {
|
||||||
|
id: 'shape:arrow3',
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
start: {
|
||||||
|
type: 'point',
|
||||||
|
x: 68.25440152898136,
|
||||||
|
y: -1.0404886613512332,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'binding',
|
||||||
|
boundShapeId: 'shape:box3',
|
||||||
|
normalizedAnchor: {
|
||||||
|
x: 0.5848167203201774,
|
||||||
|
y: 0.5766996080606552,
|
||||||
|
},
|
||||||
|
isPrecise: true,
|
||||||
|
isExact: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeName: 'shape',
|
||||||
|
},
|
||||||
|
'shape:arrow4': {
|
||||||
|
id: 'shape:arrow4',
|
||||||
|
type: 'arrow',
|
||||||
|
props: {
|
||||||
|
start: {
|
||||||
|
type: 'point',
|
||||||
|
x: 68.25440152898136,
|
||||||
|
y: -1.0404886613512758,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'point',
|
||||||
|
x: 174.75451263561803,
|
||||||
|
y: -1.4725753187527948,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeName: 'shape',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"binding:nanoid_1": {
|
||||||
|
"fromId": "shape:arrow1",
|
||||||
|
"id": "binding:nanoid_1",
|
||||||
|
"meta": {},
|
||||||
|
"props": {
|
||||||
|
"isExact": false,
|
||||||
|
"isPrecise": false,
|
||||||
|
"normalizedAnchor": {
|
||||||
|
"x": 0.4383437225516159,
|
||||||
|
"y": 0.5065334673177019,
|
||||||
|
},
|
||||||
|
"terminal": "start",
|
||||||
|
},
|
||||||
|
"toId": "shape:box1",
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "binding",
|
||||||
|
},
|
||||||
|
"binding:nanoid_2": {
|
||||||
|
"fromId": "shape:arrow1",
|
||||||
|
"id": "binding:nanoid_2",
|
||||||
|
"meta": {},
|
||||||
|
"props": {
|
||||||
|
"isExact": false,
|
||||||
|
"isPrecise": false,
|
||||||
|
"normalizedAnchor": {
|
||||||
|
"x": 0.5848167203201774,
|
||||||
|
"y": 0.5766996080606552,
|
||||||
|
},
|
||||||
|
"terminal": "end",
|
||||||
|
},
|
||||||
|
"toId": "shape:box2",
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "binding",
|
||||||
|
},
|
||||||
|
"binding:nanoid_3": {
|
||||||
|
"fromId": "shape:arrow2",
|
||||||
|
"id": "binding:nanoid_3",
|
||||||
|
"meta": {},
|
||||||
|
"props": {
|
||||||
|
"isExact": false,
|
||||||
|
"isPrecise": false,
|
||||||
|
"normalizedAnchor": {
|
||||||
|
"x": 0.4383437225516159,
|
||||||
|
"y": 0.5065334673177019,
|
||||||
|
},
|
||||||
|
"terminal": "start",
|
||||||
|
},
|
||||||
|
"toId": "shape:arrow2",
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "binding",
|
||||||
|
},
|
||||||
|
"binding:nanoid_4": {
|
||||||
|
"fromId": "shape:arrow3",
|
||||||
|
"id": "binding:nanoid_4",
|
||||||
|
"meta": {},
|
||||||
|
"props": {
|
||||||
|
"isExact": false,
|
||||||
|
"isPrecise": true,
|
||||||
|
"normalizedAnchor": {
|
||||||
|
"x": 0.5848167203201774,
|
||||||
|
"y": 0.5766996080606552,
|
||||||
|
},
|
||||||
|
"terminal": "end",
|
||||||
|
},
|
||||||
|
"toId": "shape:box3",
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "binding",
|
||||||
|
},
|
||||||
|
"shape:arrow1": {
|
||||||
|
"id": "shape:arrow1",
|
||||||
|
"props": {
|
||||||
|
"end": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "shape",
|
||||||
|
},
|
||||||
|
"shape:arrow2": {
|
||||||
|
"id": "shape:arrow2",
|
||||||
|
"props": {
|
||||||
|
"end": {
|
||||||
|
"x": 174.75451263561803,
|
||||||
|
"y": -1.4725753187527948,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "shape",
|
||||||
|
},
|
||||||
|
"shape:arrow3": {
|
||||||
|
"id": "shape:arrow3",
|
||||||
|
"props": {
|
||||||
|
"end": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"x": 68.25440152898136,
|
||||||
|
"y": -1.0404886613512332,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "shape",
|
||||||
|
},
|
||||||
|
"shape:arrow4": {
|
||||||
|
"id": "shape:arrow4",
|
||||||
|
"props": {
|
||||||
|
"end": {
|
||||||
|
"x": 174.75451263561803,
|
||||||
|
"y": -1.4725753187527948,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"x": 68.25440152898136,
|
||||||
|
"y": -1.0404886613512758,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "arrow",
|
||||||
|
"typeName": "shape",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||||
|
|
||||||
// check that all migrator fns were called at least once
|
// check that all migrator fns were called at least once
|
||||||
|
|
111
packages/tlschema/src/records/TLBinding.ts
Normal file
111
packages/tlschema/src/records/TLBinding.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import {
|
||||||
|
RecordId,
|
||||||
|
UnknownRecord,
|
||||||
|
createMigrationIds,
|
||||||
|
createRecordMigrationSequence,
|
||||||
|
createRecordType,
|
||||||
|
} from '@tldraw/store'
|
||||||
|
import { mapObjectMapValues } from '@tldraw/utils'
|
||||||
|
import { T } from '@tldraw/validate'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import { TLArrowBinding } from '../bindings/TLArrowBinding'
|
||||||
|
import { TLBaseBinding, createBindingValidator } from '../bindings/TLBaseBinding'
|
||||||
|
import { SchemaPropsInfo } from '../createTLSchema'
|
||||||
|
import { TLPropsMigrations } from '../recordsWithProps'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default set of bindings that are available in the editor.
|
||||||
|
*
|
||||||
|
* @public */
|
||||||
|
export type TLDefaultBinding = TLArrowBinding
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type for a binding that is available in the editor but whose type is
|
||||||
|
* unknown—either one of the editor's default bindings or else a custom binding.
|
||||||
|
*
|
||||||
|
* @public */
|
||||||
|
export type TLUnknownBinding = TLBaseBinding<string, object>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of all bindings that are available in the editor, including unknown bindings.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLBindingId = RecordId<TLUnknownBinding>
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const rootBindingVersions = createMigrationIds('com.tldraw.binding', {} as const)
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const rootBindingMigrations = createRecordMigrationSequence({
|
||||||
|
sequenceId: 'com.tldraw.binding',
|
||||||
|
recordType: 'binding',
|
||||||
|
sequence: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function isBinding(record?: UnknownRecord): record is TLBinding {
|
||||||
|
if (!record) return false
|
||||||
|
return record.typeName === 'binding'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function isBindingId(id?: string): id is TLBindingId {
|
||||||
|
if (!id) return false
|
||||||
|
return id.startsWith('binding:')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function createBindingId(id?: string): TLBindingId {
|
||||||
|
return `binding:${id ?? nanoid()}` as TLBindingId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function createBindingPropsMigrationSequence(
|
||||||
|
migrations: TLPropsMigrations
|
||||||
|
): TLPropsMigrations {
|
||||||
|
return migrations
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function createBindingPropsMigrationIds<S extends string, T extends Record<string, number>>(
|
||||||
|
bindingType: S,
|
||||||
|
ids: T
|
||||||
|
): { [k in keyof T]: `com.tldraw.binding.${S}/${T[k]}` } {
|
||||||
|
return mapObjectMapValues(ids, (_k, v) => `com.tldraw.binding.${bindingType}/${v}`) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export function createBindingRecordType(bindings: Record<string, SchemaPropsInfo>) {
|
||||||
|
return createRecordType<TLBinding>('binding', {
|
||||||
|
scope: 'document',
|
||||||
|
validator: T.model(
|
||||||
|
'binding',
|
||||||
|
T.union(
|
||||||
|
'type',
|
||||||
|
mapObjectMapValues(bindings, (type, { props, meta }) =>
|
||||||
|
createBindingValidator(type, props, meta)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}).withDefaultProperties(() => ({
|
||||||
|
meta: {},
|
||||||
|
}))
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { TLAsset } from './TLAsset'
|
import { TLAsset } from './TLAsset'
|
||||||
|
import { TLBinding } from './TLBinding'
|
||||||
import { TLCamera } from './TLCamera'
|
import { TLCamera } from './TLCamera'
|
||||||
import { TLDocument } from './TLDocument'
|
import { TLDocument } from './TLDocument'
|
||||||
import { TLInstance } from './TLInstance'
|
import { TLInstance } from './TLInstance'
|
||||||
|
@ -11,6 +12,7 @@ import { TLShape } from './TLShape'
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLRecord =
|
export type TLRecord =
|
||||||
| TLAsset
|
| TLAsset
|
||||||
|
| TLBinding
|
||||||
| TLCamera
|
| TLCamera
|
||||||
| TLDocument
|
| TLDocument
|
||||||
| TLInstance
|
| TLInstance
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import {
|
import {
|
||||||
Migration,
|
|
||||||
MigrationId,
|
|
||||||
MigrationSequence,
|
|
||||||
RecordId,
|
RecordId,
|
||||||
UnknownRecord,
|
UnknownRecord,
|
||||||
createMigrationIds,
|
createMigrationIds,
|
||||||
createMigrationSequence,
|
|
||||||
createRecordMigrationSequence,
|
createRecordMigrationSequence,
|
||||||
createRecordType,
|
createRecordType,
|
||||||
} from '@tldraw/store'
|
} from '@tldraw/store'
|
||||||
import { assert, mapObjectMapValues } from '@tldraw/utils'
|
import { mapObjectMapValues } from '@tldraw/utils'
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { SchemaShapeInfo } from '../createTLSchema'
|
import { SchemaPropsInfo } from '../createTLSchema'
|
||||||
|
import { TLPropsMigrations } from '../recordsWithProps'
|
||||||
import { TLArrowShape } from '../shapes/TLArrowShape'
|
import { TLArrowShape } from '../shapes/TLArrowShape'
|
||||||
import { TLBaseShape, createShapeValidator } from '../shapes/TLBaseShape'
|
import { TLBaseShape, createShapeValidator } from '../shapes/TLBaseShape'
|
||||||
import { TLBookmarkShape } from '../shapes/TLBookmarkShape'
|
import { TLBookmarkShape } from '../shapes/TLBookmarkShape'
|
||||||
|
@ -76,19 +73,6 @@ export type TLShapePartial<T extends TLShape = TLShape> = T extends T
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLShapeId = RecordId<TLUnknownShape>
|
export type TLShapeId = RecordId<TLUnknownShape>
|
||||||
|
|
||||||
// evil type shit that will get deleted in the next PR
|
|
||||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
|
|
||||||
? I
|
|
||||||
: never
|
|
||||||
|
|
||||||
type Identity<T> = { [K in keyof T]: T[K] }
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export type TLShapeProps = Identity<UnionToIntersection<TLDefaultShape['props']>>
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export type TLShapeProp = keyof TLShapeProps
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLParentId = TLPageId | TLShapeId
|
export type TLParentId = TLPageId | TLShapeId
|
||||||
|
|
||||||
|
@ -188,141 +172,27 @@ export function getShapePropKeysByStyle(props: Record<string, T.Validatable<any>
|
||||||
return propKeysByStyle
|
return propKeysByStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NO_DOWN_MIGRATION = 'none' as const
|
|
||||||
// If a down migration was deployed more than a couple of months ago it should be safe to retire it.
|
|
||||||
// We only really need them to smooth over the transition between versions, and some folks do keep
|
|
||||||
// browser tabs open for months without refreshing, but at a certain point that kind of behavior is
|
|
||||||
// on them. Plus anyway recently chrome has started to actually kill tabs that are open for too long rather
|
|
||||||
// than just suspending them, so if other browsers follow suit maybe it's less of a concern.
|
|
||||||
export const RETIRED_DOWN_MIGRATION = 'retired' as const
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export type TLShapePropsMigrations = {
|
|
||||||
sequence: Array<
|
|
||||||
| { readonly dependsOn: readonly MigrationId[] }
|
|
||||||
| {
|
|
||||||
readonly id: MigrationId
|
|
||||||
readonly dependsOn?: MigrationId[]
|
|
||||||
readonly up: (props: any) => any
|
|
||||||
readonly down?:
|
|
||||||
| typeof NO_DOWN_MIGRATION
|
|
||||||
| typeof RETIRED_DOWN_MIGRATION
|
|
||||||
| ((props: any) => any)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function createShapePropsMigrationSequence(
|
export function createShapePropsMigrationSequence(
|
||||||
migrations: TLShapePropsMigrations
|
migrations: TLPropsMigrations
|
||||||
): TLShapePropsMigrations {
|
): TLPropsMigrations {
|
||||||
return migrations
|
return migrations
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function createShapePropsMigrationIds<S extends string, T extends Record<string, number>>(
|
export function createShapePropsMigrationIds<
|
||||||
shapeType: S,
|
const S extends string,
|
||||||
ids: T
|
const T extends Record<string, number>,
|
||||||
): { [k in keyof T]: `com.tldraw.shape.${S}/${T[k]}` } {
|
>(shapeType: S, ids: T): { [k in keyof T]: `com.tldraw.shape.${S}/${T[k]}` } {
|
||||||
return mapObjectMapValues(ids, (_k, v) => `com.tldraw.shape.${shapeType}/${v}`) as any
|
return mapObjectMapValues(ids, (_k, v) => `com.tldraw.shape.${shapeType}/${v}`) as any
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processShapeMigrations(shapes: Record<string, SchemaShapeInfo>) {
|
|
||||||
const result: MigrationSequence[] = []
|
|
||||||
|
|
||||||
for (const [shapeType, { migrations }] of Object.entries(shapes)) {
|
|
||||||
const sequenceId = `com.tldraw.shape.${shapeType}`
|
|
||||||
if (!migrations) {
|
|
||||||
// provide empty migrations sequence to allow for future migrations
|
|
||||||
result.push(
|
|
||||||
createMigrationSequence({
|
|
||||||
sequenceId,
|
|
||||||
retroactive: false,
|
|
||||||
sequence: [],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else if ('sequenceId' in migrations) {
|
|
||||||
assert(
|
|
||||||
sequenceId === migrations.sequenceId,
|
|
||||||
`sequenceId mismatch for ${shapeType} shape migrations. Expected '${sequenceId}', got '${migrations.sequenceId}'`
|
|
||||||
)
|
|
||||||
result.push(migrations)
|
|
||||||
} else if ('sequence' in migrations) {
|
|
||||||
result.push(
|
|
||||||
createMigrationSequence({
|
|
||||||
sequenceId,
|
|
||||||
retroactive: false,
|
|
||||||
sequence: migrations.sequence.map((m) =>
|
|
||||||
'id' in m
|
|
||||||
? {
|
|
||||||
id: m.id,
|
|
||||||
scope: 'record',
|
|
||||||
filter: (r) => r.typeName === 'shape' && (r as TLShape).type === shapeType,
|
|
||||||
dependsOn: m.dependsOn,
|
|
||||||
up: (record: any) => {
|
|
||||||
const result = m.up(record.props)
|
|
||||||
if (result) {
|
|
||||||
record.props = result
|
|
||||||
}
|
|
||||||
},
|
|
||||||
down:
|
|
||||||
typeof m.down === 'function'
|
|
||||||
? (record: any) => {
|
|
||||||
const result = (m.down as (props: any) => any)(record.props)
|
|
||||||
if (result) {
|
|
||||||
record.props = result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
: m
|
|
||||||
),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// legacy migrations, will be removed in the future
|
|
||||||
result.push(
|
|
||||||
createMigrationSequence({
|
|
||||||
sequenceId,
|
|
||||||
retroactive: false,
|
|
||||||
sequence: Object.keys(migrations.migrators)
|
|
||||||
.map((k) => Number(k))
|
|
||||||
.sort((a: number, b: number) => a - b)
|
|
||||||
.map(
|
|
||||||
(version): Migration => ({
|
|
||||||
id: `${sequenceId}/${version}`,
|
|
||||||
scope: 'record',
|
|
||||||
filter: (r) => r.typeName === 'shape' && (r as TLShape).type === shapeType,
|
|
||||||
up: (record: any) => {
|
|
||||||
const result = migrations.migrators[version].up(record)
|
|
||||||
if (result) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
},
|
|
||||||
down: (record: any) => {
|
|
||||||
const result = migrations.migrators[version].down(record)
|
|
||||||
if (result) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export function createShapeRecordType(shapes: Record<string, SchemaShapeInfo>) {
|
export function createShapeRecordType(shapes: Record<string, SchemaPropsInfo>) {
|
||||||
return createRecordType<TLShape>('shape', {
|
return createRecordType<TLShape>('shape', {
|
||||||
scope: 'document',
|
scope: 'document',
|
||||||
validator: T.model(
|
validator: T.model(
|
||||||
|
|
147
packages/tlschema/src/recordsWithProps.ts
Normal file
147
packages/tlschema/src/recordsWithProps.ts
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import {
|
||||||
|
Migration,
|
||||||
|
MigrationId,
|
||||||
|
MigrationSequence,
|
||||||
|
RecordType,
|
||||||
|
StandaloneDependsOn,
|
||||||
|
UnknownRecord,
|
||||||
|
createMigrationSequence,
|
||||||
|
} from '@tldraw/store'
|
||||||
|
import { Expand, assert } from '@tldraw/utils'
|
||||||
|
import { T } from '@tldraw/validate'
|
||||||
|
import { SchemaPropsInfo } from './createTLSchema'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type RecordProps<R extends UnknownRecord & { props: object }> = {
|
||||||
|
[K in keyof R['props']]: T.Validatable<R['props'][K]>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type RecordPropsType<Config extends Record<string, T.Validatable<any>>> = Expand<{
|
||||||
|
[K in keyof Config]: T.TypeOf<Config[K]>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export const NO_DOWN_MIGRATION = 'none' as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a down migration was deployed more than a couple of months ago it should be safe to retire it.
|
||||||
|
* We only really need them to smooth over the transition between versions, and some folks do keep
|
||||||
|
* browser tabs open for months without refreshing, but at a certain point that kind of behavior is
|
||||||
|
* on them. Plus anyway recently chrome has started to actually kill tabs that are open for too long
|
||||||
|
* rather than just suspending them, so if other browsers follow suit maybe it's less of a concern.
|
||||||
|
*/
|
||||||
|
export const RETIRED_DOWN_MIGRATION = 'retired' as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface TLPropsMigration {
|
||||||
|
readonly id: MigrationId
|
||||||
|
readonly dependsOn?: MigrationId[]
|
||||||
|
readonly up: (props: any) => any
|
||||||
|
readonly down?: typeof NO_DOWN_MIGRATION | typeof RETIRED_DOWN_MIGRATION | ((props: any) => any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface TLPropsMigrations {
|
||||||
|
readonly sequence: Array<StandaloneDependsOn | TLPropsMigration>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processPropsMigrations<R extends UnknownRecord & { type: string; props: object }>(
|
||||||
|
typeName: R['typeName'],
|
||||||
|
records: Record<string, SchemaPropsInfo>
|
||||||
|
) {
|
||||||
|
const result: MigrationSequence[] = []
|
||||||
|
|
||||||
|
for (const [subType, { migrations }] of Object.entries(records)) {
|
||||||
|
const sequenceId = `com.tldraw.${typeName}.${subType}`
|
||||||
|
if (!migrations) {
|
||||||
|
// provide empty migrations sequence to allow for future migrations
|
||||||
|
result.push(
|
||||||
|
createMigrationSequence({
|
||||||
|
sequenceId,
|
||||||
|
retroactive: false,
|
||||||
|
sequence: [],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else if ('sequenceId' in migrations) {
|
||||||
|
assert(
|
||||||
|
sequenceId === migrations.sequenceId,
|
||||||
|
`sequenceId mismatch for ${subType} ${RecordType} migrations. Expected '${sequenceId}', got '${migrations.sequenceId}'`
|
||||||
|
)
|
||||||
|
result.push(migrations)
|
||||||
|
} else if ('sequence' in migrations) {
|
||||||
|
result.push(
|
||||||
|
createMigrationSequence({
|
||||||
|
sequenceId,
|
||||||
|
retroactive: false,
|
||||||
|
sequence: migrations.sequence.map((m) =>
|
||||||
|
'id' in m ? createPropsMigration(typeName, subType, m) : m
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// legacy migrations, will be removed in the future
|
||||||
|
result.push(
|
||||||
|
createMigrationSequence({
|
||||||
|
sequenceId,
|
||||||
|
retroactive: false,
|
||||||
|
sequence: Object.keys(migrations.migrators)
|
||||||
|
.map((k) => Number(k))
|
||||||
|
.sort((a: number, b: number) => a - b)
|
||||||
|
.map(
|
||||||
|
(version): Migration => ({
|
||||||
|
id: `${sequenceId}/${version}`,
|
||||||
|
scope: 'record',
|
||||||
|
filter: (r) => r.typeName === typeName && (r as R).type === subType,
|
||||||
|
up: (record: any) => {
|
||||||
|
const result = migrations.migrators[version].up(record)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: (record: any) => {
|
||||||
|
const result = migrations.migrators[version].down(record)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPropsMigration<R extends UnknownRecord & { type: string; props: object }>(
|
||||||
|
typeName: R['typeName'],
|
||||||
|
subType: R['type'],
|
||||||
|
m: TLPropsMigration
|
||||||
|
): Migration {
|
||||||
|
return {
|
||||||
|
id: m.id,
|
||||||
|
dependsOn: m.dependsOn,
|
||||||
|
scope: 'record',
|
||||||
|
filter: (r) => r.typeName === typeName && (r as R).type === subType,
|
||||||
|
up: (record: any) => {
|
||||||
|
const result = m.up(record.props)
|
||||||
|
if (result) {
|
||||||
|
record.props = result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down:
|
||||||
|
typeof m.down === 'function'
|
||||||
|
? (record: any) => {
|
||||||
|
const result = (m.down as (props: any) => any)(record.props)
|
||||||
|
if (result) {
|
||||||
|
record.props = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,22 @@
|
||||||
|
import { createMigrationSequence } from '@tldraw/store'
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { vecModelValidator } from '../misc/geometry-types'
|
import { TLArrowBinding } from '../bindings/TLArrowBinding'
|
||||||
|
import { VecModel, vecModelValidator } from '../misc/geometry-types'
|
||||||
|
import { createBindingId } from '../records/TLBinding'
|
||||||
|
import { TLShapeId, createShapePropsMigrationIds } from '../records/TLShape'
|
||||||
import {
|
import {
|
||||||
RETIRED_DOWN_MIGRATION,
|
RETIRED_DOWN_MIGRATION,
|
||||||
createShapePropsMigrationIds,
|
RecordPropsType,
|
||||||
createShapePropsMigrationSequence,
|
TLPropsMigration,
|
||||||
} from '../records/TLShape'
|
createPropsMigration,
|
||||||
|
} from '../recordsWithProps'
|
||||||
import { StyleProp } from '../styles/StyleProp'
|
import { StyleProp } from '../styles/StyleProp'
|
||||||
import { DefaultColorStyle, DefaultLabelColorStyle } from '../styles/TLColorStyle'
|
import { DefaultColorStyle, DefaultLabelColorStyle } from '../styles/TLColorStyle'
|
||||||
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
||||||
import { DefaultFillStyle } from '../styles/TLFillStyle'
|
import { DefaultFillStyle } from '../styles/TLFillStyle'
|
||||||
import { DefaultFontStyle } from '../styles/TLFontStyle'
|
import { DefaultFontStyle } from '../styles/TLFontStyle'
|
||||||
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
||||||
import { ShapePropsType, TLBaseShape, shapeIdValidator } from './TLBaseShape'
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
|
|
||||||
const arrowheadTypes = [
|
const arrowheadTypes = [
|
||||||
'arrow',
|
'arrow',
|
||||||
|
@ -40,25 +45,6 @@ export const ArrowShapeArrowheadEndStyle = StyleProp.defineEnum('tldraw:arrowhea
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLArrowShapeArrowheadStyle = T.TypeOf<typeof ArrowShapeArrowheadStartStyle>
|
export type TLArrowShapeArrowheadStyle = T.TypeOf<typeof ArrowShapeArrowheadStartStyle>
|
||||||
|
|
||||||
/** @public */
|
|
||||||
const ArrowShapeTerminal = T.union('type', {
|
|
||||||
binding: T.object({
|
|
||||||
type: T.literal('binding'),
|
|
||||||
boundShapeId: shapeIdValidator,
|
|
||||||
normalizedAnchor: vecModelValidator,
|
|
||||||
isExact: T.boolean,
|
|
||||||
isPrecise: T.boolean,
|
|
||||||
}),
|
|
||||||
point: T.object({
|
|
||||||
type: T.literal('point'),
|
|
||||||
x: T.number,
|
|
||||||
y: T.number,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export type TLArrowShapeTerminal = T.TypeOf<typeof ArrowShapeTerminal>
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const arrowShapeProps = {
|
export const arrowShapeProps = {
|
||||||
labelColor: DefaultLabelColorStyle,
|
labelColor: DefaultLabelColorStyle,
|
||||||
|
@ -69,15 +55,15 @@ export const arrowShapeProps = {
|
||||||
arrowheadStart: ArrowShapeArrowheadStartStyle,
|
arrowheadStart: ArrowShapeArrowheadStartStyle,
|
||||||
arrowheadEnd: ArrowShapeArrowheadEndStyle,
|
arrowheadEnd: ArrowShapeArrowheadEndStyle,
|
||||||
font: DefaultFontStyle,
|
font: DefaultFontStyle,
|
||||||
start: ArrowShapeTerminal,
|
start: vecModelValidator,
|
||||||
end: ArrowShapeTerminal,
|
end: vecModelValidator,
|
||||||
bend: T.number,
|
bend: T.number,
|
||||||
text: T.string,
|
text: T.string,
|
||||||
labelPosition: T.number,
|
labelPosition: T.number,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLArrowShapeProps = ShapePropsType<typeof arrowShapeProps>
|
export type TLArrowShapeProps = RecordPropsType<typeof arrowShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>
|
export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>
|
||||||
|
@ -86,20 +72,26 @@ export const arrowShapeVersions = createShapePropsMigrationIds('arrow', {
|
||||||
AddLabelColor: 1,
|
AddLabelColor: 1,
|
||||||
AddIsPrecise: 2,
|
AddIsPrecise: 2,
|
||||||
AddLabelPosition: 3,
|
AddLabelPosition: 3,
|
||||||
|
ExtractBindings: 4,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function propsMigration(migration: TLPropsMigration) {
|
||||||
|
return createPropsMigration<TLArrowShape>('shape', 'arrow', migration)
|
||||||
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const arrowShapeMigrations = createShapePropsMigrationSequence({
|
export const arrowShapeMigrations = createMigrationSequence({
|
||||||
|
sequenceId: 'com.tldraw.shape.arrow',
|
||||||
sequence: [
|
sequence: [
|
||||||
{
|
propsMigration({
|
||||||
id: arrowShapeVersions.AddLabelColor,
|
id: arrowShapeVersions.AddLabelColor,
|
||||||
up: (props) => {
|
up: (props) => {
|
||||||
props.labelColor = 'black'
|
props.labelColor = 'black'
|
||||||
},
|
},
|
||||||
down: RETIRED_DOWN_MIGRATION,
|
down: RETIRED_DOWN_MIGRATION,
|
||||||
},
|
}),
|
||||||
|
|
||||||
{
|
propsMigration({
|
||||||
id: arrowShapeVersions.AddIsPrecise,
|
id: arrowShapeVersions.AddIsPrecise,
|
||||||
up: ({ start, end }) => {
|
up: ({ start, end }) => {
|
||||||
if (start.type === 'binding') {
|
if (start.type === 'binding') {
|
||||||
|
@ -123,9 +115,9 @@ export const arrowShapeMigrations = createShapePropsMigrationSequence({
|
||||||
delete end.isPrecise
|
delete end.isPrecise
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
|
|
||||||
{
|
propsMigration({
|
||||||
id: arrowShapeVersions.AddLabelPosition,
|
id: arrowShapeVersions.AddLabelPosition,
|
||||||
up: (props) => {
|
up: (props) => {
|
||||||
props.labelPosition = 0.5
|
props.labelPosition = 0.5
|
||||||
|
@ -133,6 +125,82 @@ export const arrowShapeMigrations = createShapePropsMigrationSequence({
|
||||||
down: (props) => {
|
down: (props) => {
|
||||||
delete props.labelPosition
|
delete props.labelPosition
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
{
|
||||||
|
id: arrowShapeVersions.ExtractBindings,
|
||||||
|
scope: 'store',
|
||||||
|
up: (oldStore) => {
|
||||||
|
type OldArrowTerminal =
|
||||||
|
| {
|
||||||
|
type: 'point'
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'binding'
|
||||||
|
boundShapeId: TLShapeId
|
||||||
|
normalizedAnchor: VecModel
|
||||||
|
isExact: boolean
|
||||||
|
isPrecise: boolean
|
||||||
|
}
|
||||||
|
// new type:
|
||||||
|
| { type?: undefined; x: number; y: number }
|
||||||
|
|
||||||
|
type OldArrow = TLBaseShape<'arrow', { start: OldArrowTerminal; end: OldArrowTerminal }>
|
||||||
|
|
||||||
|
const arrows = Object.values(oldStore).filter(
|
||||||
|
(r: any): r is OldArrow => r.typeName === 'shape' && r.type === 'arrow'
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const arrow of arrows) {
|
||||||
|
const { start, end } = arrow.props
|
||||||
|
if (start.type === 'binding') {
|
||||||
|
const id = createBindingId()
|
||||||
|
const binding: TLArrowBinding = {
|
||||||
|
typeName: 'binding',
|
||||||
|
id,
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrow.id,
|
||||||
|
toId: start.boundShapeId,
|
||||||
|
meta: {},
|
||||||
|
props: {
|
||||||
|
terminal: 'start',
|
||||||
|
normalizedAnchor: start.normalizedAnchor,
|
||||||
|
isExact: start.isExact,
|
||||||
|
isPrecise: start.isPrecise,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
oldStore[id] = binding
|
||||||
|
arrow.props.start = { x: 0, y: 0 }
|
||||||
|
} else {
|
||||||
|
delete arrow.props.start.type
|
||||||
|
}
|
||||||
|
if (end.type === 'binding') {
|
||||||
|
const id = createBindingId()
|
||||||
|
const binding: TLArrowBinding = {
|
||||||
|
typeName: 'binding',
|
||||||
|
id,
|
||||||
|
type: 'arrow',
|
||||||
|
fromId: arrow.id,
|
||||||
|
toId: end.boundShapeId,
|
||||||
|
meta: {},
|
||||||
|
props: {
|
||||||
|
terminal: 'end',
|
||||||
|
normalizedAnchor: end.normalizedAnchor,
|
||||||
|
isExact: end.isExact,
|
||||||
|
isPrecise: end.isPrecise,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
oldStore[id] = binding
|
||||||
|
arrow.props.end = { x: 0, y: 0 }
|
||||||
|
} else {
|
||||||
|
delete arrow.props.end.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { BaseRecord } from '@tldraw/store'
|
import { BaseRecord } from '@tldraw/store'
|
||||||
import { Expand, IndexKey, JsonObject } from '@tldraw/utils'
|
import { IndexKey, JsonObject } from '@tldraw/utils'
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { TLOpacityType, opacityValidator } from '../misc/TLOpacity'
|
import { TLOpacityType, opacityValidator } from '../misc/TLOpacity'
|
||||||
import { idValidator } from '../misc/id-validator'
|
import { idValidator } from '../misc/id-validator'
|
||||||
|
@ -56,13 +56,3 @@ export function createShapeValidator<
|
||||||
meta: meta ? T.object(meta) : (T.jsonValue as any),
|
meta: meta ? T.object(meta) : (T.jsonValue as any),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
|
|
||||||
[K in keyof Shape['props']]: T.Validatable<Shape['props'][K]>
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export type ShapePropsType<Config extends Record<string, T.Validatable<any>>> = Expand<{
|
|
||||||
[K in keyof Config]: T.TypeOf<Config[K]>
|
|
||||||
}>
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { assetIdValidator } from '../assets/TLBaseAsset'
|
import { assetIdValidator } from '../assets/TLBaseAsset'
|
||||||
import {
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
RETIRED_DOWN_MIGRATION,
|
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
|
||||||
createShapePropsMigrationIds,
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
createShapePropsMigrationSequence,
|
|
||||||
} from '../records/TLShape'
|
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const bookmarkShapeProps = {
|
export const bookmarkShapeProps = {
|
||||||
|
@ -16,7 +13,7 @@ export const bookmarkShapeProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLBookmarkShapeProps = ShapePropsType<typeof bookmarkShapeProps>
|
export type TLBookmarkShapeProps = RecordPropsType<typeof bookmarkShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLBookmarkShape = TLBaseShape<'bookmark', TLBookmarkShapeProps>
|
export type TLBookmarkShape = TLBaseShape<'bookmark', TLBookmarkShapeProps>
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { vecModelValidator } from '../misc/geometry-types'
|
import { vecModelValidator } from '../misc/geometry-types'
|
||||||
import {
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
RETIRED_DOWN_MIGRATION,
|
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
|
||||||
createShapePropsMigrationIds,
|
|
||||||
createShapePropsMigrationSequence,
|
|
||||||
} from '../records/TLShape'
|
|
||||||
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
||||||
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
||||||
import { DefaultFillStyle } from '../styles/TLFillStyle'
|
import { DefaultFillStyle } from '../styles/TLFillStyle'
|
||||||
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
|
|
||||||
export const DrawShapeSegment = T.object({
|
export const DrawShapeSegment = T.object({
|
||||||
type: T.literalEnum('free', 'straight'),
|
type: T.literalEnum('free', 'straight'),
|
||||||
|
@ -32,7 +29,7 @@ export const drawShapeProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLDrawShapeProps = ShapePropsType<typeof drawShapeProps>
|
export type TLDrawShapeProps = RecordPropsType<typeof drawShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>
|
export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import {
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
RETIRED_DOWN_MIGRATION,
|
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
|
||||||
createShapePropsMigrationIds,
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
createShapePropsMigrationSequence,
|
|
||||||
} from '../records/TLShape'
|
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
|
||||||
|
|
||||||
// Only allow multiplayer embeds. If we add additional routes later for example '/help' this won't match
|
// Only allow multiplayer embeds. If we add additional routes later for example '/help' this won't match
|
||||||
const TLDRAW_APP_RE = /(^\/r\/[^/]+\/?$)/
|
const TLDRAW_APP_RE = /(^\/r\/[^/]+\/?$)/
|
||||||
|
@ -635,7 +632,7 @@ export const embedShapeProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLEmbedShapeProps = ShapePropsType<typeof embedShapeProps>
|
export type TLEmbedShapeProps = RecordPropsType<typeof embedShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLEmbedShape = TLBaseShape<'embed', TLEmbedShapeProps>
|
export type TLEmbedShape = TLBaseShape<'embed', TLEmbedShapeProps>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { createShapePropsMigrationSequence } from '../records/TLShape'
|
import { createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
import { RecordPropsType } from '../recordsWithProps'
|
||||||
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const frameShapeProps = {
|
export const frameShapeProps = {
|
||||||
|
@ -9,7 +10,7 @@ export const frameShapeProps = {
|
||||||
name: T.string,
|
name: T.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
type TLFrameShapeProps = ShapePropsType<typeof frameShapeProps>
|
type TLFrameShapeProps = RecordPropsType<typeof frameShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLFrameShape = TLBaseShape<'frame', TLFrameShapeProps>
|
export type TLFrameShape = TLBaseShape<'frame', TLFrameShapeProps>
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import {
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
RETIRED_DOWN_MIGRATION,
|
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
|
||||||
createShapePropsMigrationIds,
|
|
||||||
createShapePropsMigrationSequence,
|
|
||||||
} from '../records/TLShape'
|
|
||||||
import { StyleProp } from '../styles/StyleProp'
|
import { StyleProp } from '../styles/StyleProp'
|
||||||
import { DefaultColorStyle, DefaultLabelColorStyle } from '../styles/TLColorStyle'
|
import { DefaultColorStyle, DefaultLabelColorStyle } from '../styles/TLColorStyle'
|
||||||
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
||||||
|
@ -15,7 +12,7 @@ import {
|
||||||
} from '../styles/TLHorizontalAlignStyle'
|
} from '../styles/TLHorizontalAlignStyle'
|
||||||
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
||||||
import { DefaultVerticalAlignStyle } from '../styles/TLVerticalAlignStyle'
|
import { DefaultVerticalAlignStyle } from '../styles/TLVerticalAlignStyle'
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const GeoShapeGeoStyle = StyleProp.defineEnum('tldraw:geo', {
|
export const GeoShapeGeoStyle = StyleProp.defineEnum('tldraw:geo', {
|
||||||
|
@ -65,7 +62,7 @@ export const geoShapeProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLGeoShapeProps = ShapePropsType<typeof geoShapeProps>
|
export type TLGeoShapeProps = RecordPropsType<typeof geoShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLGeoShape = TLBaseShape<'geo', TLGeoShapeProps>
|
export type TLGeoShape = TLBaseShape<'geo', TLGeoShapeProps>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { createShapePropsMigrationSequence } from '../records/TLShape'
|
import { createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
import { ShapeProps, TLBaseShape } from './TLBaseShape'
|
import { RecordProps } from '../recordsWithProps'
|
||||||
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLGroupShapeProps = { [key in never]: undefined }
|
export type TLGroupShapeProps = { [key in never]: undefined }
|
||||||
|
@ -8,7 +9,7 @@ export type TLGroupShapeProps = { [key in never]: undefined }
|
||||||
export type TLGroupShape = TLBaseShape<'group', TLGroupShapeProps>
|
export type TLGroupShape = TLBaseShape<'group', TLGroupShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const groupShapeProps: ShapeProps<TLGroupShape> = {}
|
export const groupShapeProps: RecordProps<TLGroupShape> = {}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const groupShapeMigrations = createShapePropsMigrationSequence({ sequence: [] })
|
export const groupShapeMigrations = createShapePropsMigrationSequence({ sequence: [] })
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { createShapePropsMigrationSequence } from '../records/TLShape'
|
import { createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
|
import { RecordPropsType } from '../recordsWithProps'
|
||||||
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
||||||
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
import { DrawShapeSegment } from './TLDrawShape'
|
import { DrawShapeSegment } from './TLDrawShape'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -15,7 +16,7 @@ export const highlightShapeProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLHighlightShapeProps = ShapePropsType<typeof highlightShapeProps>
|
export type TLHighlightShapeProps = RecordPropsType<typeof highlightShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
|
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { assetIdValidator } from '../assets/TLBaseAsset'
|
import { assetIdValidator } from '../assets/TLBaseAsset'
|
||||||
import { vecModelValidator } from '../misc/geometry-types'
|
import { vecModelValidator } from '../misc/geometry-types'
|
||||||
import {
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
RETIRED_DOWN_MIGRATION,
|
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
|
||||||
createShapePropsMigrationIds,
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
createShapePropsMigrationSequence,
|
|
||||||
} from '../records/TLShape'
|
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const ImageShapeCrop = T.object({
|
export const ImageShapeCrop = T.object({
|
||||||
|
@ -27,7 +24,7 @@ export const imageShapeProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLImageShapeProps = ShapePropsType<typeof imageShapeProps>
|
export type TLImageShapeProps = RecordPropsType<typeof imageShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLImageShape = TLBaseShape<'image', TLImageShapeProps>
|
export type TLImageShape = TLBaseShape<'image', TLImageShapeProps>
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import { IndexKey, getIndices, objectMapFromEntries, sortByIndex } from '@tldraw/utils'
|
import { IndexKey, getIndices, objectMapFromEntries, sortByIndex } from '@tldraw/utils'
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import {
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
RETIRED_DOWN_MIGRATION,
|
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
|
||||||
createShapePropsMigrationIds,
|
|
||||||
createShapePropsMigrationSequence,
|
|
||||||
} from '../records/TLShape'
|
|
||||||
import { StyleProp } from '../styles/StyleProp'
|
import { StyleProp } from '../styles/StyleProp'
|
||||||
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
||||||
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
||||||
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const LineShapeSplineStyle = StyleProp.defineEnum('tldraw:spline', {
|
export const LineShapeSplineStyle = StyleProp.defineEnum('tldraw:spline', {
|
||||||
|
@ -37,7 +34,7 @@ export const lineShapeProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLLineShapeProps = ShapePropsType<typeof lineShapeProps>
|
export type TLLineShapeProps = RecordPropsType<typeof lineShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLLineShape = TLBaseShape<'line', TLLineShapeProps>
|
export type TLLineShape = TLBaseShape<'line', TLLineShapeProps>
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import {
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
RETIRED_DOWN_MIGRATION,
|
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
|
||||||
createShapePropsMigrationIds,
|
|
||||||
createShapePropsMigrationSequence,
|
|
||||||
} from '../records/TLShape'
|
|
||||||
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
||||||
import { DefaultFontStyle } from '../styles/TLFontStyle'
|
import { DefaultFontStyle } from '../styles/TLFontStyle'
|
||||||
import { DefaultHorizontalAlignStyle } from '../styles/TLHorizontalAlignStyle'
|
import { DefaultHorizontalAlignStyle } from '../styles/TLHorizontalAlignStyle'
|
||||||
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
||||||
import { DefaultVerticalAlignStyle } from '../styles/TLVerticalAlignStyle'
|
import { DefaultVerticalAlignStyle } from '../styles/TLVerticalAlignStyle'
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const noteShapeProps = {
|
export const noteShapeProps = {
|
||||||
|
@ -25,7 +22,7 @@ export const noteShapeProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLNoteShapeProps = ShapePropsType<typeof noteShapeProps>
|
export type TLNoteShapeProps = RecordPropsType<typeof noteShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps>
|
export type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps>
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import {
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
RETIRED_DOWN_MIGRATION,
|
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
|
||||||
createShapePropsMigrationIds,
|
|
||||||
createShapePropsMigrationSequence,
|
|
||||||
} from '../records/TLShape'
|
|
||||||
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
||||||
import { DefaultFontStyle } from '../styles/TLFontStyle'
|
import { DefaultFontStyle } from '../styles/TLFontStyle'
|
||||||
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
||||||
import { DefaultTextAlignStyle } from '../styles/TLTextAlignStyle'
|
import { DefaultTextAlignStyle } from '../styles/TLTextAlignStyle'
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const textShapeProps = {
|
export const textShapeProps = {
|
||||||
|
@ -23,7 +20,7 @@ export const textShapeProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLTextShapeProps = ShapePropsType<typeof textShapeProps>
|
export type TLTextShapeProps = RecordPropsType<typeof textShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>
|
export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { assetIdValidator } from '../assets/TLBaseAsset'
|
import { assetIdValidator } from '../assets/TLBaseAsset'
|
||||||
import {
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
RETIRED_DOWN_MIGRATION,
|
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
|
||||||
createShapePropsMigrationIds,
|
import { TLBaseShape } from './TLBaseShape'
|
||||||
createShapePropsMigrationSequence,
|
|
||||||
} from '../records/TLShape'
|
|
||||||
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const videoShapeProps = {
|
export const videoShapeProps = {
|
||||||
|
@ -18,7 +15,7 @@ export const videoShapeProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLVideoShapeProps = ShapePropsType<typeof videoShapeProps>
|
export type TLVideoShapeProps = RecordPropsType<typeof videoShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLVideoShape = TLBaseShape<'video', TLVideoShapeProps>
|
export type TLVideoShape = TLBaseShape<'video', TLVideoShapeProps>
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import {
|
import {
|
||||||
Editor,
|
Editor,
|
||||||
PageRecordType,
|
PageRecordType,
|
||||||
TLArrowShapeTerminal,
|
TLArrowBinding,
|
||||||
TLPage,
|
TLPage,
|
||||||
TLPageId,
|
TLPageId,
|
||||||
TLShape,
|
TLShape,
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
TLStore,
|
TLStore,
|
||||||
|
VecModel,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
|
defaultBindingUtils,
|
||||||
defaultShapeUtils,
|
defaultShapeUtils,
|
||||||
defaultTools,
|
defaultTools,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
|
@ -37,8 +39,8 @@ export type Op =
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'create-arrow'
|
type: 'create-arrow'
|
||||||
start: TLArrowShapeTerminal
|
start: TLArrowBinding | VecModel
|
||||||
end: TLArrowShapeTerminal
|
end: TLArrowBinding | VecModel
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'delete-shape'
|
type: 'delete-shape'
|
||||||
|
@ -97,6 +99,7 @@ export class FuzzEditor extends RandomSource {
|
||||||
super(_seed)
|
super(_seed)
|
||||||
this.editor = new Editor({
|
this.editor = new Editor({
|
||||||
shapeUtils: defaultShapeUtils,
|
shapeUtils: defaultShapeUtils,
|
||||||
|
bindingUtils: defaultBindingUtils,
|
||||||
tools: defaultTools,
|
tools: defaultTools,
|
||||||
initialState: 'select',
|
initialState: 'select',
|
||||||
store,
|
store,
|
||||||
|
|
|
@ -50,8 +50,8 @@ const oldArrow: TLBaseShape<'arrow', Omit<TLArrowShapeProps, 'labelColor'>> = {
|
||||||
fill: 'none',
|
fill: 'none',
|
||||||
color: 'black',
|
color: 'black',
|
||||||
bend: 0,
|
bend: 0,
|
||||||
start: { type: 'point', x: 0, y: 0 },
|
start: { x: 0, y: 0 },
|
||||||
end: { type: 'point', x: 0, y: 0 },
|
end: { x: 0, y: 0 },
|
||||||
arrowheadStart: 'none',
|
arrowheadStart: 'none',
|
||||||
arrowheadEnd: 'arrow',
|
arrowheadEnd: 'arrow',
|
||||||
text: '',
|
text: '',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import {
|
import {
|
||||||
Editor,
|
Editor,
|
||||||
|
TLArrowBinding,
|
||||||
TLArrowShape,
|
TLArrowShape,
|
||||||
TLRecord,
|
TLRecord,
|
||||||
TLStore,
|
TLStore,
|
||||||
|
@ -117,10 +118,18 @@ let totalNumShapes = 0
|
||||||
let totalNumPages = 0
|
let totalNumPages = 0
|
||||||
|
|
||||||
function arrowsAreSound(editor: Editor) {
|
function arrowsAreSound(editor: Editor) {
|
||||||
const arrows = editor.getCurrentPageShapes().filter((s) => s.type === 'arrow') as TLArrowShape[]
|
const arrows = editor.getCurrentPageShapes().filter((s): s is TLArrowShape => s.type === 'arrow')
|
||||||
for (const arrow of arrows) {
|
for (const arrow of arrows) {
|
||||||
for (const terminal of [arrow.props.start, arrow.props.end]) {
|
const bindings = editor.getBindingsFromShape<TLArrowBinding>(arrow, 'arrow')
|
||||||
if (terminal.type === 'binding' && !editor.store.has(terminal.boundShapeId)) {
|
const terminalsSeen = new Set()
|
||||||
|
for (const binding of bindings) {
|
||||||
|
if (terminalsSeen.has(binding.props.terminal)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
terminalsSeen.add(binding.props.terminal)
|
||||||
|
|
||||||
|
if (!editor.store.has(binding.toId)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue