alex 2024-04-27 00:01:53 +00:00 zatwierdzone przez GitHub
commit e3788dc431
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
73 zmienionych plików z 1918 dodań i 1524 usunięć

Wyświetl plik

@ -2,8 +2,8 @@ import {
BaseBoxShapeUtil,
BoundsSnapGeometry,
HTMLContainer,
RecordProps,
Rectangle2d,
ShapeProps,
T,
TLBaseShape,
} from 'tldraw'
@ -23,7 +23,7 @@ type IPlayingCard = TLBaseShape<
export class PlayingCardUtil extends BaseBoxShapeUtil<IPlayingCard> {
// [2]
static override type = 'PlayingCard' as const
static override props: ShapeProps<IPlayingCard> = {
static override props: RecordProps<IPlayingCard> = {
w: T.number,
h: T.number,
suit: T.string,

Wyświetl plik

@ -1,8 +1,8 @@
import { DefaultColorStyle, ShapeProps, T } from 'tldraw'
import { DefaultColorStyle, RecordProps, T } from 'tldraw'
import { ICardShape } from './card-shape-types'
// 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,
h: T.number,
color: DefaultColorStyle,

Wyświetl plik

@ -1,8 +1,8 @@
import {
Geometry2d,
HTMLContainer,
RecordProps,
Rectangle2d,
ShapeProps,
ShapeUtil,
T,
TLBaseShape,
@ -28,7 +28,7 @@ type ICustomShape = TLBaseShape<
export class MyShapeUtil extends ShapeUtil<ICustomShape> {
// [a]
static override type = 'my-custom-shape' as const
static override props: ShapeProps<ICustomShape> = {
static override props: RecordProps<ICustomShape> = {
w: T.number,
h: T.number,
text: T.string,

Wyświetl plik

@ -1,7 +1,7 @@
import {
BaseBoxShapeUtil,
HTMLContainer,
ShapeProps,
RecordProps,
T,
TLBaseShape,
TLOnEditEndHandler,
@ -23,7 +23,7 @@ type IMyEditableShape = TLBaseShape<
export class EditableShapeUtil extends BaseBoxShapeUtil<IMyEditableShape> {
static override type = 'my-editable-shape' as const
static override props: ShapeProps<IMyEditableShape> = {
static override props: RecordProps<IMyEditableShape> = {
w: T.number,
h: T.number,
animal: T.number,

Wyświetl plik

@ -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!
@ -14,7 +14,7 @@ type IMyInteractiveShape = TLBaseShape<
export class myInteractiveShape extends BaseBoxShapeUtil<IMyInteractiveShape> {
static override type = 'my-interactive-shape' as const
static override props: ShapeProps<IMyInteractiveShape> = {
static override props: RecordProps<IMyInteractiveShape> = {
w: T.number,
h: T.number,
checked: T.boolean,

Wyświetl plik

@ -1,9 +1,9 @@
import { useCallback } from 'react'
import {
Geometry2d,
RecordProps,
Rectangle2d,
SVGContainer,
ShapeProps,
ShapeUtil,
T,
TLBaseShape,
@ -24,7 +24,7 @@ export type SlideShape = TLBaseShape<
export class SlideShapeUtil extends ShapeUtil<SlideShape> {
static override type = 'slide' as const
static override props: ShapeProps<SlideShape> = {
static override props: RecordProps<SlideShape> = {
w: T.number,
h: T.number,
}

Wyświetl plik

@ -8,7 +8,7 @@ import {
Geometry2d,
LABEL_FONT_SIZES,
Polygon2d,
ShapePropsType,
RecordPropsType,
ShapeUtil,
T,
TEXT_PROPS,
@ -52,7 +52,7 @@ export const speechBubbleShapeProps = {
tail: vecModelValidator,
}
export type SpeechBubbleShapeProps = ShapePropsType<typeof speechBubbleShapeProps>
export type SpeechBubbleShapeProps = RecordPropsType<typeof speechBubbleShapeProps>
export type SpeechBubbleShape = TLBaseShape<'speech-bubble', SpeechBubbleShapeProps>
export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {

Wyświetl plik

@ -29,22 +29,27 @@ import { default as React_2 } from 'react';
import * as React_3 from 'react';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { RecordProps } from '@tldraw/tlschema';
import { RecordsDiff } from '@tldraw/store';
import { SerializedSchema } from '@tldraw/store';
import { SerializedStore } from '@tldraw/store';
import { ShapeProps } from '@tldraw/tlschema';
import { Signal } from '@tldraw/state';
import { Store } from '@tldraw/store';
import { StoreSchema } from '@tldraw/store';
import { StoreSnapshot } from '@tldraw/store';
import { StyleProp } from '@tldraw/tlschema';
import { StylePropValue } from '@tldraw/tlschema';
import { TLArrowBinding } from '@tldraw/tlschema';
import { TLArrowBindingProps } from '@tldraw/tlschema';
import { TLArrowShape } from '@tldraw/tlschema';
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
import { TLAsset } from '@tldraw/tlschema';
import { TLAssetId } from '@tldraw/tlschema';
import { TLAssetPartial } 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 { TLCamera } from '@tldraw/tlschema';
import { TLCursor } from '@tldraw/tlschema';
@ -60,14 +65,15 @@ import { TLInstancePresence } from '@tldraw/tlschema';
import { TLPage } from '@tldraw/tlschema';
import { TLPageId } from '@tldraw/tlschema';
import { TLParentId } from '@tldraw/tlschema';
import { TLPropsMigrations } from '@tldraw/tlschema';
import { TLRecord } from '@tldraw/tlschema';
import { TLScribble } from '@tldraw/tlschema';
import { TLShape } from '@tldraw/tlschema';
import { TLShapeId } from '@tldraw/tlschema';
import { TLShapePartial } from '@tldraw/tlschema';
import { TLShapePropsMigrations } from '@tldraw/tlschema';
import { TLStore } from '@tldraw/tlschema';
import { TLStoreProps } from '@tldraw/tlschema';
import { TLUnknownBinding } from '@tldraw/tlschema';
import { TLUnknownShape } from '@tldraw/tlschema';
import { TLVideoAsset } from '@tldraw/tlschema';
import { track } from '@tldraw/state';
@ -138,6 +144,12 @@ export class Arc2d extends Geometry2d {
// @public
export function areAnglesCompatible(a: number, b: number): boolean;
// @internal
export function arrowBindingMakeItNotSo(editor: Editor, arrow: TLArrowShape, terminal: 'end' | 'start'): void;
// @internal
export function arrowBindingMakeItSo(editor: Editor, arrow: TLArrowShape | TLShapeId, target: TLShape | TLShapeId, props: TLArrowBindingProps): void;
export { Atom }
export { atom }
@ -169,6 +181,23 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
onResize: TLOnResizeHandler<any>;
}
// @public (undocumented)
export abstract class BindingUtil<Binding extends TLUnknownBinding = TLUnknownBinding> {
constructor(editor: Editor);
// (undocumented)
editor: Editor;
abstract getDefaultProps(): Binding['props'];
// (undocumented)
static migrations?: TLPropsMigrations;
// (undocumented)
onAfterShapeChange?(binding: Binding, direction: 'from' | 'to', prev: TLShape, next: TLShape): void;
// (undocumented)
onBeforeShapeDelete?(binding: Binding, direction: 'from' | 'to', shape: TLShape): void;
// (undocumented)
static props?: RecordProps<TLUnknownBinding>;
static type: string;
}
// @public
export interface BoundsSnapGeometry {
points?: VecModel[];
@ -585,7 +614,7 @@ export class Edge2d extends Geometry2d {
// @public (undocumented)
export class Editor extends EventEmitter<TLEventMap> {
constructor({ store, user, shapeUtils, tools, getContainer, initialState, inferDarkMode, }: TLEditorOptions);
constructor({ store, user, shapeUtils, bindingUtils, tools, getContainer, initialState, inferDarkMode, }: TLEditorOptions);
addOpenMenu(id: string): this;
alignShapes(shapes: TLShape[] | TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
animateShape(partial: null | TLShapePartial | undefined, animationOptions?: TLAnimationOptions): this;
@ -605,6 +634,9 @@ export class Editor extends EventEmitter<TLEventMap> {
bail(): this;
bailToMark(id: string): this;
batch(fn: () => void, opts?: TLHistoryBatchOptions): this;
bindingUtils: {
readonly [K in string]?: BindingUtil<TLUnknownBinding>;
};
bringForward(shapes: TLShape[] | TLShapeId[]): this;
bringToFront(shapes: TLShape[] | TLShapeId[]): this;
cancel(): this;
@ -619,6 +651,10 @@ export class Editor extends EventEmitter<TLEventMap> {
// @internal (undocumented)
crash(error: unknown): this;
createAssets(assets: TLAsset[]): this;
// (undocumented)
createBinding(partial: RequiredKeys<TLBindingPartial, 'fromId' | 'toId' | 'type'>): void;
// (undocumented)
createBindings(partials: RequiredKeys<TLBindingPartial, 'fromId' | 'toId' | 'type'>[]): void;
// @internal (undocumented)
createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): {
extras: {
@ -636,6 +672,10 @@ export class Editor extends EventEmitter<TLEventMap> {
createShape<T extends TLUnknownShape>(shape: OptionalKeys<TLShapePartial<T>, 'id'>): this;
createShapes<T extends TLUnknownShape>(shapes: OptionalKeys<TLShapePartial<T>, 'id'>[]): this;
deleteAssets(assets: TLAsset[] | TLAssetId[]): this;
// (undocumented)
deleteBinding(binding: TLBinding | TLBindingId): void;
// (undocumented)
deleteBindings(bindings: (TLBinding | TLBindingId)[]): void;
deleteOpenMenu(id: string): this;
deletePage(page: TLPage | TLPageId): this;
deleteShape(id: TLShapeId): this;
@ -671,15 +711,25 @@ export class Editor extends EventEmitter<TLEventMap> {
findCommonAncestor(shapes: TLShape[] | TLShapeId[], predicate?: (shape: TLShape) => boolean): TLShapeId | undefined;
findShapeAncestor(shape: TLShape | TLShapeId, predicate: (parent: TLShape) => boolean): TLShape | undefined;
flipShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
// (undocumented)
getAllBindingsForShape(shape: TLShape | TLShapeId): TLBinding[];
getAncestorPageId(shape?: TLShape | TLShapeId): TLPageId | undefined;
getArrowInfo(shape: TLArrowShape | TLShapeId): TLArrowInfo | undefined;
getArrowsBoundTo(shapeId: TLShapeId): {
arrowId: TLShapeId;
handleId: "end" | "start";
}[];
getArrowsBoundTo(shapeId: TLShapeId): TLArrowShape[];
getAsset(asset: TLAsset | TLAssetId): TLAsset | undefined;
getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined>;
getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[];
// (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;
getCameraState(): "idle" | "moving";
getCanRedo(): boolean;
@ -940,6 +990,10 @@ export class Editor extends EventEmitter<TLEventMap> {
// (undocumented)
ungroupShapes(ids: TLShape[]): this;
updateAssets(assets: TLAssetPartial[]): this;
// (undocumented)
updateBinding(partial: TLBindingPartial): void;
// (undocumented)
updateBindings(partials: (null | TLBindingPartial | undefined)[]): void;
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions): this;
// (undocumented)
_updateCurrentPageState: (partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions) => void;
@ -1085,8 +1139,11 @@ export abstract class Geometry2d {
// @public
export function getArcMeasure(A: number, B: number, sweepFlag: number, largeArcFlag: number): number;
// @internal (undocumented)
export function getArrowBindings(editor: Editor, shape: TLArrowShape): TLArrowBindings;
// @public (undocumented)
export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape): {
export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape, bindings: TLArrowBindings): {
end: Vec;
start: Vec;
};
@ -1182,11 +1239,11 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
// (undocumented)
indicator(shape: TLGroupShape): JSX_2.Element;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onChildrenChange: TLOnChildrenChangeHandler<TLGroupShape>;
// (undocumented)
static props: ShapeProps<TLGroupShape>;
static props: RecordProps<TLGroupShape>;
// (undocumented)
static type: "group";
}
@ -1715,7 +1772,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
abstract indicator(shape: Shape): any;
isAspectRatioLocked: TLShapeUtilFlag<Shape>;
// (undocumented)
static migrations?: LegacyMigrations | TLShapePropsMigrations;
static migrations?: LegacyMigrations | MigrationSequence | TLPropsMigrations;
onBeforeCreate?: TLOnBeforeCreateHandler<Shape>;
onBeforeUpdate?: TLOnBeforeUpdateHandler<Shape>;
// @internal
@ -1740,7 +1797,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
onTranslateEnd?: TLOnTranslateEndHandler<Shape>;
onTranslateStart?: TLOnTranslateStartHandler<Shape>;
// (undocumented)
static props?: ShapeProps<TLUnknownShape>;
static props?: RecordProps<TLUnknownShape>;
// @internal
providesBackgroundForChildren(shape: Shape): boolean;
toBackgroundSvg?(shape: Shape, ctx: SvgExportContext): null | Promise<null | ReactElement> | ReactElement;
@ -1985,6 +2042,9 @@ export type TLAnimationOptions = Partial<{
easing: (t: number) => number;
}>;
// @public (undocumented)
export type TLAnyBindingUtilConstructor = TLBindingUtilConstructor<any>;
// @public (undocumented)
export type TLAnyShapeUtilConstructor = TLShapeUtilConstructor<any>;
@ -2006,6 +2066,7 @@ export interface TLArcInfo {
// @public (undocumented)
export type TLArrowInfo = {
bindings: TLArrowBindings;
bodyArc: TLArcInfo;
end: TLArrowPoint;
handleArc: TLArcInfo;
@ -2014,6 +2075,7 @@ export type TLArrowInfo = {
middle: VecLike;
start: TLArrowPoint;
} | {
bindings: TLArrowBindings;
end: TLArrowPoint;
isStraight: true;
isValid: boolean;
@ -2059,6 +2121,18 @@ export type TLBeforeCreateHandler<R extends TLRecord> = (record: R, source: 'rem
// @public (undocumented)
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)
export type TLBrushProps = {
brush: BoxModel;
@ -2139,6 +2213,7 @@ export const TldrawEditor: React_2.NamedExoticComponent<TldrawEditorProps>;
// @public
export interface TldrawEditorBaseProps {
autoFocus?: boolean;
bindingUtils?: readonly TLAnyBindingUtilConstructor[];
children?: ReactNode;
className?: string;
components?: TLEditorComponents;
@ -2170,6 +2245,7 @@ export type TLEditorComponents = Partial<{
// @public (undocumented)
export interface TLEditorOptions {
bindingUtils: readonly TLBindingUtilConstructor<TLUnknownBinding>[];
getContainer: () => HTMLElement;
inferDarkMode?: boolean;
initialState?: string;
@ -2595,9 +2671,9 @@ export interface TLShapeUtilConstructor<T extends TLUnknownShape, U extends Shap
// (undocumented)
new (editor: Editor): U;
// (undocumented)
migrations?: LegacyMigrations | MigrationSequence | TLShapePropsMigrations;
migrations?: LegacyMigrations | MigrationSequence | TLPropsMigrations;
// (undocumented)
props?: ShapeProps<T>;
props?: RecordProps<T>;
// (undocumented)
type: T['type'];
}
@ -2633,6 +2709,7 @@ export type TLStoreOptions = {
id?: string;
initialData?: SerializedStore<TLRecord>;
} & ({
bindingUtils?: readonly TLAnyBindingUtilConstructor[];
migrations?: readonly MigrationSequence[];
shapeUtils?: readonly TLAnyShapeUtilConstructor[];
} | {

Wyświetl plik

@ -104,6 +104,7 @@ export {
type TLStoreOptions,
} from './lib/config/createTLStore'
export { createTLUser } from './lib/config/createTLUser'
export { type TLAnyBindingUtilConstructor } from './lib/config/defaultBindings'
export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
export {
ANIMATION_MEDIUM_MS,
@ -130,6 +131,7 @@ export {
type TLEditorOptions,
type TLResizeShapeOptions,
} from './lib/editor/Editor'
export { BindingUtil, type TLBindingUtilConstructor } from './lib/editor/bindings/BindingUtil'
export { HistoryManager } from './lib/editor/managers/HistoryManager'
export type {
SideEffectManager,
@ -186,7 +188,12 @@ export {
type TLArrowInfo,
type TLArrowPoint,
} from './lib/editor/shapes/shared/arrow/arrow-types'
export { getArrowTerminalsInArrowSpace } from './lib/editor/shapes/shared/arrow/shared'
export {
arrowBindingMakeItNotSo,
arrowBindingMakeItSo,
getArrowBindings,
getArrowTerminalsInArrowSpace,
} from './lib/editor/shapes/shared/arrow/shared'
export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox'
export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode'

Wyświetl plik

@ -15,6 +15,7 @@ import classNames from 'classnames'
import { OptionalErrorBoundary } from './components/ErrorBoundary'
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
import { TLUser, createTLUser } from './config/createTLUser'
import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
import { Editor } from './editor/Editor'
import { TLStateNodeConstructor } from './editor/tools/StateNode'
@ -75,6 +76,11 @@ export interface TldrawEditorBaseProps {
*/
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.
*/
@ -135,6 +141,7 @@ declare global {
}
const EMPTY_SHAPE_UTILS_ARRAY = [] as const
const EMPTY_BINDING_UTILS_ARRAY = [] as const
const EMPTY_TOOLS_ARRAY = [] as const
/** @public */
@ -157,6 +164,7 @@ export const TldrawEditor = memo(function TldrawEditor({
const withDefaults = {
...rest,
shapeUtils: rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY,
bindingUtils: rest.bindingUtils ?? EMPTY_BINDING_UTILS_ARRAY,
tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
components,
}
@ -197,12 +205,25 @@ export const TldrawEditor = memo(function TldrawEditor({
})
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({
shapeUtils,
bindingUtils,
initialData,
persistenceKey,
sessionId,
@ -219,7 +240,7 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
...rest
}: Required<
TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser },
'shapeUtils' | 'tools'
'shapeUtils' | 'bindingUtils' | 'tools'
>) {
const container = useContainer()
@ -262,6 +283,7 @@ function TldrawEditorWithReadyStore({
store,
tools,
shapeUtils,
bindingUtils,
user,
initialState,
autoFocus = true,
@ -271,7 +293,7 @@ function TldrawEditorWithReadyStore({
store: TLStore
user: TLUser
},
'shapeUtils' | 'tools'
'shapeUtils' | 'bindingUtils' | 'tools'
>) {
const { ErrorFallback } = useEditorComponents()
const container = useContainer()
@ -281,6 +303,7 @@ function TldrawEditorWithReadyStore({
const editor = new Editor({
store,
shapeUtils,
bindingUtils,
tools,
getContainer: () => container,
user,
@ -292,7 +315,7 @@ function TldrawEditorWithReadyStore({
return () => {
editor.dispose()
}
}, [container, shapeUtils, tools, store, user, initialState, inferDarkMode])
}, [container, shapeUtils, bindingUtils, tools, store, user, initialState, inferDarkMode])
const crashingError = useSyncExternalStore(
useCallback(

Wyświetl plik

@ -1,13 +1,6 @@
import { HistoryEntry, MigrationSequence, SerializedStore, Store, StoreSchema } from '@tldraw/store'
import {
SchemaShapeInfo,
TLRecord,
TLStore,
TLStoreProps,
TLUnknownShape,
createTLSchema,
} from '@tldraw/tlschema'
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
import { SchemaPropsInfo, TLRecord, TLStore, TLStoreProps, createTLSchema } from '@tldraw/tlschema'
import { TLAnyBindingUtilConstructor, checkBindings } from './defaultBindings'
import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShapes'
/** @public */
@ -16,7 +9,11 @@ export type TLStoreOptions = {
defaultName?: string
id?: string
} & (
| { shapeUtils?: readonly TLAnyShapeUtilConstructor[]; migrations?: readonly MigrationSequence[] }
| {
shapeUtils?: readonly TLAnyShapeUtilConstructor[]
migrations?: readonly MigrationSequence[]
bindingUtils?: readonly TLAnyBindingUtilConstructor[]
}
| { schema?: StoreSchema<TLRecord, TLStoreProps> }
)
@ -41,9 +38,12 @@ export function createTLStore({
rest.schema
: // we need a schema
createTLSchema({
shapes: currentPageShapesToShapeMap(
shapes: utilsToMap(
checkShapesAndAddCore('shapeUtils' in rest && rest.shapeUtils ? rest.shapeUtils : [])
),
bindings: utilsToMap(
checkBindings('bindingUtils' in rest && rest.bindingUtils ? rest.bindingUtils : [])
),
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(
shapeUtils.map((s): [string, SchemaShapeInfo] => [
utils.map((s): [string, SchemaPropsInfo] => [
s.type,
{
props: s.props,

Wyświetl plik

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

Wyświetl plik

@ -6,10 +6,14 @@ import {
PageRecordType,
StyleProp,
StylePropValue,
TLArrowBinding,
TLArrowShape,
TLAsset,
TLAssetId,
TLAssetPartial,
TLBinding,
TLBindingId,
TLBindingPartial,
TLCursor,
TLCursorType,
TLDOCUMENT_ID,
@ -31,8 +35,10 @@ import {
TLShapeId,
TLShapePartial,
TLStore,
TLUnknownBinding,
TLUnknownShape,
TLVideoAsset,
createBindingId,
createShapeId,
getShapePropKeysByStyle,
isPageId,
@ -60,6 +66,7 @@ import { EventEmitter } from 'eventemitter3'
import { flushSync } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { TLUser, createTLUser } from '../config/createTLUser'
import { checkBindings } from '../config/defaultBindings'
import { checkShapesAndAddCore } from '../config/defaultShapes'
import {
ANIMATION_MEDIUM_MS,
@ -98,7 +105,7 @@ import { getIncrementedName } from '../utils/getIncrementedName'
import { getReorderingShapesChanges } from '../utils/reorderShapes'
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
import { uniqueId } from '../utils/uniqueId'
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
import { BindingUtil, TLBindingUtilConstructor } from './bindings/BindingUtil'
import { notVisibleShapes } from './derivations/notVisibleShapes'
import { parentsToChildren } from './derivations/parentsToChildren'
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
@ -115,7 +122,7 @@ import { UserPreferencesManager } from './managers/UserPreferencesManager'
import { ShapeUtil, TLResizeMode, TLShapeUtilConstructor } from './shapes/ShapeUtil'
import { TLArrowInfo } from './shapes/shared/arrow/arrow-types'
import { getCurvedArrowInfo } from './shapes/shared/arrow/curved-arrow'
import { getArrowTerminalsInArrowSpace, getIsArrowStraight } from './shapes/shared/arrow/shared'
import { getArrowBindings, getIsArrowStraight } from './shapes/shared/arrow/shared'
import { getStraightArrowInfo } from './shapes/shared/arrow/straight-arrow'
import { RootState } from './tools/RootState'
import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
@ -160,6 +167,10 @@ export interface TLEditorOptions {
* An array of shapes to use in the editor. These will be used to create and manage shapes in the editor.
*/
shapeUtils: readonly TLShapeUtilConstructor<TLUnknownShape>[]
/**
* An array of bindings to use in the editor. These will be used to create and manage bindings in the editor.
*/
bindingUtils: readonly TLBindingUtilConstructor<TLUnknownBinding>[]
/**
* An array of tools to use in the editor. These will be used to handle events and manage user interactions in the editor.
*/
@ -189,6 +200,7 @@ export class Editor extends EventEmitter<TLEventMap> {
store,
user,
shapeUtils,
bindingUtils,
tools,
getContainer,
initialState,
@ -248,6 +260,14 @@ export class Editor extends EventEmitter<TLEventMap> {
this.shapeUtils = _shapeUtils
this.styleProps = _styleProps
const allBindingUtils = checkBindings(bindingUtils)
const _bindingUtils = {} as Record<string, BindingUtil<any>>
for (const Util of allBindingUtils) {
const util = new Util(this)
_bindingUtils[Util.type] = util
}
this.bindingUtils = _bindingUtils
// Tools.
// Accept tools from constructor parameters which may not conflict with the root note's default or
// "baked in" tools, select and zoom.
@ -265,118 +285,118 @@ export class Editor extends EventEmitter<TLEventMap> {
const invalidParents = new Set<TLShapeId>()
const reparentArrow = (arrowId: TLArrowShape['id']) => {
const arrow = this.getShape<TLArrowShape>(arrowId)
if (!arrow) return
const { start, end } = arrow.props
const startShape = start.type === 'binding' ? this.getShape(start.boundShapeId) : undefined
const endShape = end.type === 'binding' ? this.getShape(end.boundShapeId) : undefined
// const reparentArrow = (arrowId: TLArrowShape['id']) => {
// const arrow = this.getShape<TLArrowShape>(arrowId)
// if (!arrow) return
// const { start, end } = arrow.props
// const startShape = start.type === 'binding' ? this.getShape(start.boundShapeId) : undefined
// const endShape = end.type === 'binding' ? this.getShape(end.boundShapeId) : undefined
const parentPageId = this.getAncestorPageId(arrow)
if (!parentPageId) return
// const parentPageId = this.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 = this.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
}
// let nextParentId: TLParentId
// if (startShape && endShape) {
// // if arrow has two bindings, always parent arrow to closest common ancestor of the bindings
// nextParentId = this.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) {
this.reparentShapes([arrowId], nextParentId)
}
// if (nextParentId && nextParentId !== arrow.parentId) {
// this.reparentShapes([arrowId], nextParentId)
// }
const reparentedArrow = this.getShape<TLArrowShape>(arrowId)
if (!reparentedArrow) throw Error('no reparented arrow')
// const reparentedArrow = this.getShape<TLArrowShape>(arrowId)
// if (!reparentedArrow) throw Error('no reparented arrow')
const startSibling = this.getShapeNearestSibling(reparentedArrow, startShape)
const endSibling = this.getShapeNearestSibling(reparentedArrow, endShape)
// const startSibling = this.getShapeNearestSibling(reparentedArrow, startShape)
// const endSibling = this.getShapeNearestSibling(reparentedArrow, endShape)
let highestSibling: TLShape | undefined
// 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
}
// 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
// let finalIndex: IndexKey
const higherSiblings = this.getSortedChildIdsForParent(highestSibling.parentId)
.map((id) => this.getShape(id)!)
.filter((sibling) => sibling.index > highestSibling!.index)
// const higherSiblings = this.getSortedChildIdsForParent(highestSibling.parentId)
// .map((id) => this.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 (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 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
}
// 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)
}
// // 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) {
this.updateShapes<TLArrowShape>([{ id: arrowId, type: 'arrow', index: finalIndex }])
}
}
// if (finalIndex !== reparentedArrow.index) {
// this.updateShapes<TLArrowShape>([{ id: arrowId, type: 'arrow', index: finalIndex }])
// }
// }
const unbindArrowTerminal = (arrow: TLArrowShape, handleId: 'start' | 'end') => {
const { x, y } = getArrowTerminalsInArrowSpace(this, arrow)[handleId]
this.store.put([{ ...arrow, props: { ...arrow.props, [handleId]: { type: 'point', x, y } } }])
}
// const unbindArrowTerminal = (arrow: TLArrowShape, handleId: 'start' | 'end') => {
// const { x, y } = getArrowTerminalsInArrowSpace(this, arrow)[handleId]
// this.store.put([{ ...arrow, props: { ...arrow.props, [handleId]: { type: 'point', x, y } } }])
// }
const arrowDidUpdate = (arrow: TLArrowShape) => {
// 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 terminal = arrow.props[handle]
if (terminal.type !== 'binding') continue
const boundShape = this.getShape(terminal.boundShapeId)
const isShapeInSamePageAsArrow =
this.getAncestorPageId(arrow) === this.getAncestorPageId(boundShape)
if (!boundShape || !isShapeInSamePageAsArrow) {
unbindArrowTerminal(arrow, handle)
}
}
// const arrowDidUpdate = (arrow: TLArrowShape) => {
// // 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 terminal = arrow.props[handle]
// if (terminal.type !== 'binding') continue
// const boundShape = this.getShape(terminal.boundShapeId)
// const isShapeInSamePageAsArrow =
// this.getAncestorPageId(arrow) === this.getAncestorPageId(boundShape)
// if (!boundShape || !isShapeInSamePageAsArrow) {
// unbindArrowTerminal(arrow, handle)
// }
// }
// always check the arrow parents
reparentArrow(arrow.id)
}
// // always check the arrow parents
// reparentArrow(arrow.id)
// }
const cleanupInstancePageState = (
prevPageState: TLInstancePageState,
@ -450,28 +470,28 @@ export class Editor extends EventEmitter<TLEventMap> {
this.sideEffects.register({
shape: {
afterCreate: (record) => {
if (this.isShapeOfType<TLArrowShape>(record, 'arrow')) {
arrowDidUpdate(record)
}
// if (this.isShapeOfType<TLArrowShape>(record, 'arrow')) {
// arrowDidUpdate(record)
// }
},
afterChange: (prev, next) => {
if (this.isShapeOfType<TLArrowShape>(next, 'arrow')) {
arrowDidUpdate(next)
}
// if (this.isShapeOfType<TLArrowShape>(next, 'arrow')) {
// arrowDidUpdate(next)
// }
// if the shape's parent changed and it is bound to an arrow, update the arrow's parent
if (prev.parentId !== next.parentId) {
const reparentBoundArrows = (id: TLShapeId) => {
const boundArrows = this._getArrowBindingsIndex().get()[id]
if (boundArrows?.length) {
for (const arrow of boundArrows) {
reparentArrow(arrow.arrowId)
}
}
}
reparentBoundArrows(next.id)
this.visitDescendants(next.id, reparentBoundArrows)
}
// if (prev.parentId !== next.parentId) {
// const reparentBoundArrows = (id: TLShapeId) => {
// const boundArrows = this._getArrowBindingsIndex().get()[id]
// if (boundArrows?.length) {
// for (const arrow of boundArrows) {
// reparentArrow(arrow.arrowId)
// }
// }
// }
// reparentBoundArrows(next.id)
// this.visitDescendants(next.id, reparentBoundArrows)
// }
// if this shape moved to a new page, clean up any previous page's instance state
if (prev.parentId !== next.parentId && isPageId(next.parentId)) {
@ -504,14 +524,14 @@ export class Editor extends EventEmitter<TLEventMap> {
invalidParents.add(record.parentId)
}
// clean up any arrows bound to this shape
const bindings = this._getArrowBindingsIndex().get()[record.id]
if (bindings?.length) {
for (const { arrowId, handleId } of bindings) {
const arrow = this.getShape<TLArrowShape>(arrowId)
if (!arrow) continue
unbindArrowTerminal(arrow, handleId)
}
}
// const bindings = this._getArrowBindingsIndex().get()[record.id]
// if (bindings?.length) {
// for (const { arrowId, handleId } of bindings) {
// const arrow = this.getShape<TLArrowShape>(arrowId)
// if (!arrow) continue
// unbindArrowTerminal(arrow, handleId)
// }
// }
const deletedIds = new Set([record.id])
const updates = compact(
this.getPageStates().map((pageState) => {
@ -792,6 +812,41 @@ export class Editor extends EventEmitter<TLEventMap> {
return shapeUtil
}
/* ------------------- Binding Utils ------------------ */
/**
* A map of shape utility classes (TLShapeUtils) by shape type.
*
* @public
*/
bindingUtils: { readonly [K in string]?: BindingUtil<TLUnknownBinding> }
/**
* Get a binding util from a binding itself.
*
* @example
* ```ts
* const util = editor.getBindingUtil(myArrowBinding)
* const util = editor.getBindingUtil('arrow')
* const util = editor.getBindingUtil<TLArrowBinding>(myArrowBinding)
* const util = editor.getBindingUtil(TLArrowBinding)('arrow')
* ```
*
* @param binding - A binding, binding partial, or binding type.
*
* @public
*/
getBindingUtil<S extends TLUnknownBinding>(binding: S | TLBindingPartial<S>): BindingUtil<S>
getBindingUtil<S extends TLUnknownBinding>(type: S['type']): BindingUtil<S>
getBindingUtil<T extends BindingUtil>(
type: T extends BindingUtil<infer R> ? R['type'] : string
): T
getBindingUtil(arg: string | { type: string }) {
const type = typeof arg === 'string' ? arg : arg.type
const bindingUtil = getOwnProperty(this.bindingUtils, type)
assert(bindingUtil, `No binding util found for type "${type}"`)
return bindingUtil
}
/* --------------------- History -------------------- */
/**
@ -913,12 +968,6 @@ export class Editor extends EventEmitter<TLEventMap> {
/* --------------------- Arrows --------------------- */
// todo: move these to tldraw or replace with a bindings API
/** @internal */
@computed
private _getArrowBindingsIndex() {
return arrowBindingsIndex(this)
}
/**
* Get all arrows bound to a shape.
*
@ -927,15 +976,19 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
getArrowsBoundTo(shapeId: TLShapeId) {
return this._getArrowBindingsIndex().get()[shapeId] || EMPTY_ARRAY
const ids = new Set(
this.getBindingsToShape<TLArrowBinding>(shapeId, 'arrow').map((b) => b.fromId)
)
return compact(Array.from(ids, (id) => this.getShape<TLArrowShape>(id)))
}
@computed
private getArrowInfoCache() {
return this.store.createComputedCache<TLArrowInfo, TLArrowShape>('arrow infoCache', (shape) => {
const bindings = getArrowBindings(this, shape)
return getIsArrowStraight(shape)
? getStraightArrowInfo(this, shape)
: getCurvedArrowInfo(this, shape)
? getStraightArrowInfo(this, shape, bindings)
: getCurvedArrowInfo(this, shape, bindings)
})
}
@ -4836,6 +4889,99 @@ export class Editor extends EventEmitter<TLEventMap> {
return match
}
/* -------------------- Bindings -------------------- */
getBinding(id: TLBindingId): TLBinding | undefined {
return this.store.get(id) as TLBinding | undefined
}
// TODO: maintain these indexes more pro-actively
getBindingsFromShape<Binding extends TLUnknownBinding = TLBinding>(
shape: TLShape | TLShapeId,
type: Binding['type']
): Binding[] {
const id = typeof shape === 'string' ? shape : shape.id
return this.store.query.exec('binding', {
fromId: { eq: id },
type: { eq: type },
}) as Binding[]
}
getBindingsToShape<Binding extends TLUnknownBinding = TLBinding>(
shape: TLShape | TLShapeId,
type: Binding['type']
): Binding[] {
const id = typeof shape === 'string' ? shape : shape.id
return this.store.query.exec('binding', {
toId: { eq: id },
type: { eq: type },
}) as Binding[]
}
getAllBindingsForShape(shape: TLShape | TLShapeId): TLBinding[] {
const id = typeof shape === 'string' ? shape : shape.id
const from = this.store.query.index('binding', 'fromId').get().get(id) ?? new Set()
const to = this.store.query.index('binding', 'toId').get().get(id)
const shapes = []
for (const id of from) {
shapes.push(this.store.get(id) as TLBinding)
}
if (to) {
for (const id of to) {
if (from.has(id)) continue
shapes.push(this.store.get(id) as TLBinding)
}
}
return shapes
}
createBindings(partials: RequiredKeys<TLBindingPartial, 'type' | 'toId' | 'fromId'>[]) {
const bindings = partials.map((partial) => {
const util = this.getBindingUtil<TLUnknownBinding>(partial.type)
const defaultProps = util.getDefaultProps()
return this.store.schema.types.binding.create({
...partial,
id: partial.id ?? createBindingId(),
props: {
...defaultProps,
...partial.props,
},
})
})
this.store.put(bindings)
}
createBinding(partial: RequiredKeys<TLBindingPartial, 'type' | 'fromId' | 'toId'>) {
return this.createBindings([partial])
}
updateBindings(partials: (TLBindingPartial | null | undefined)[]) {
const updated: TLBinding[] = []
for (const partial of partials) {
if (!partial) continue
const current = this.getBinding(partial.id)
if (!current) continue
const updatedBinding = applyPartialToBinding(current, partial)
if (updatedBinding === current) continue
updated.push(updatedBinding)
}
this.store.put(updated)
}
updateBinding(partial: TLBindingPartial) {
return this.updateBindings([partial])
}
deleteBindings(bindings: (TLBinding | TLBindingId)[]) {
const ids = bindings.map((binding) => (typeof binding === 'string' ? binding : binding.id))
this.store.remove(ids)
}
deleteBinding(binding: TLBinding | TLBindingId) {
return this.deleteBindings([binding])
}
/* -------------------- Commands -------------------- */
/**
@ -4999,80 +5145,80 @@ export class Editor extends EventEmitter<TLEventMap> {
let newShape: TLShape = structuredClone(shape)
if (
this.isShapeOfType<TLArrowShape>(shape, 'arrow') &&
this.isShapeOfType<TLArrowShape>(newShape, 'arrow')
) {
const info = this.getArrowInfo(shape)
let newStartShapeId: TLShapeId | undefined = undefined
let newEndShapeId: TLShapeId | undefined = undefined
// if (
// this.isShapeOfType<TLArrowShape>(shape, 'arrow') &&
// this.isShapeOfType<TLArrowShape>(newShape, 'arrow')
// ) {
// const info = this.getArrowInfo(shape)
// let newStartShapeId: TLShapeId | undefined = undefined
// let newEndShapeId: TLShapeId | undefined = undefined
if (shape.props.start.type === 'binding') {
newStartShapeId = idsMap.get(shape.props.start.boundShapeId)
// if (shape.props.start.type === 'binding') {
// newStartShapeId = idsMap.get(shape.props.start.boundShapeId)
if (!newStartShapeId) {
if (info?.isValid) {
const { x, y } = info.start.point
newShape.props.start = {
type: 'point',
x,
y,
}
} else {
const { start } = getArrowTerminalsInArrowSpace(this, shape)
newShape.props.start = {
type: 'point',
x: start.x,
y: start.y,
}
}
}
}
// if (!newStartShapeId) {
// if (info?.isValid) {
// const { x, y } = info.start.point
// newShape.props.start = {
// type: 'point',
// x,
// y,
// }
// } else {
// const { start } = getArrowTerminalsInArrowSpace(this, shape)
// newShape.props.start = {
// type: 'point',
// x: start.x,
// y: start.y,
// }
// }
// }
// }
if (shape.props.end.type === 'binding') {
newEndShapeId = idsMap.get(shape.props.end.boundShapeId)
if (!newEndShapeId) {
if (info?.isValid) {
const { x, y } = info.end.point
newShape.props.end = {
type: 'point',
x,
y,
}
} else {
const { end } = getArrowTerminalsInArrowSpace(this, shape)
newShape.props.start = {
type: 'point',
x: end.x,
y: end.y,
}
}
}
}
// if (shape.props.end.type === 'binding') {
// newEndShapeId = idsMap.get(shape.props.end.boundShapeId)
// if (!newEndShapeId) {
// if (info?.isValid) {
// const { x, y } = info.end.point
// newShape.props.end = {
// type: 'point',
// x,
// y,
// }
// } else {
// const { end } = getArrowTerminalsInArrowSpace(this, shape)
// newShape.props.start = {
// type: 'point',
// x: end.x,
// y: end.y,
// }
// }
// }
// }
const infoAfter = getIsArrowStraight(newShape)
? getStraightArrowInfo(this, newShape)
: getCurvedArrowInfo(this, newShape)
// const infoAfter = getIsArrowStraight(newShape)
// ? getStraightArrowInfo(this, newShape)
// : getCurvedArrowInfo(this, newShape)
if (info?.isValid && infoAfter?.isValid && !getIsArrowStraight(shape)) {
const mpA = Vec.Med(info.start.handle, info.end.handle)
const distA = Vec.Dist(info.middle, mpA)
const distB = Vec.Dist(infoAfter.middle, mpA)
if (newShape.props.bend < 0) {
newShape.props.bend += distB - distA
} else {
newShape.props.bend -= distB - distA
}
}
// if (info?.isValid && infoAfter?.isValid && !getIsArrowStraight(shape)) {
// const mpA = Vec.Med(info.start.handle, info.end.handle)
// const distA = Vec.Dist(info.middle, mpA)
// const distB = Vec.Dist(infoAfter.middle, mpA)
// if (newShape.props.bend < 0) {
// newShape.props.bend += distB - distA
// } else {
// newShape.props.bend -= distB - distA
// }
// }
if (newShape.props.start.type === 'binding' && newStartShapeId) {
newShape.props.start.boundShapeId = newStartShapeId
}
// if (newShape.props.start.type === 'binding' && newStartShapeId) {
// newShape.props.start.boundShapeId = newStartShapeId
// }
if (newShape.props.end.type === 'binding' && newEndShapeId) {
newShape.props.end.boundShapeId = newEndShapeId
}
}
// if (newShape.props.end.type === 'binding' && newEndShapeId) {
// newShape.props.end.boundShapeId = newEndShapeId
// }
// }
newShape = { ...newShape, id: createId, x: shape.x + ox, y: shape.y + oy, index }
@ -5430,11 +5576,11 @@ export class Editor extends EventEmitter<TLEventMap> {
.filter((shape) => {
if (!shape) return false
if (this.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') {
return false
}
}
// if (this.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
// if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') {
// return false
// }
// }
return true
})
@ -5576,11 +5722,11 @@ export class Editor extends EventEmitter<TLEventMap> {
.filter((shape) => {
if (!shape) return false
if (this.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') {
return false
}
}
// if (this.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
// if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') {
// return false
// }
// }
return true
})
@ -7270,75 +7416,75 @@ export class Editor extends EventEmitter<TLEventMap> {
shape = structuredClone(shape) as typeof shape
if (this.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
const startBindingId =
shape.props.start.type === 'binding' ? shape.props.start.boundShapeId : undefined
// if (this.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
// const startBindingId =
// shape.props.start.type === 'binding' ? shape.props.start.boundShapeId : undefined
const endBindingId =
shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : undefined
// const endBindingId =
// shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : undefined
const info = this.getArrowInfo(shape)
// const info = this.getArrowInfo(shape)
if (shape.props.start.type === 'binding') {
if (!shapesForContent.some((s) => s.id === startBindingId)) {
// Uh oh, the arrow's bound-to shape isn't among the shapes
// that we're getting the content for. We should try to adjust
// the arrow so that it appears in the place it would be
if (info?.isValid) {
const { x, y } = info.start.point
shape.props.start = {
type: 'point',
x,
y,
}
} else {
const { start } = getArrowTerminalsInArrowSpace(this, shape)
shape.props.start = {
type: 'point',
x: start.x,
y: start.y,
}
}
}
}
// if (shape.props.start.type === 'binding') {
// if (!shapesForContent.some((s) => s.id === startBindingId)) {
// // Uh oh, the arrow's bound-to shape isn't among the shapes
// // that we're getting the content for. We should try to adjust
// // the arrow so that it appears in the place it would be
// if (info?.isValid) {
// const { x, y } = info.start.point
// shape.props.start = {
// type: 'point',
// x,
// y,
// }
// } else {
// const { start } = getArrowTerminalsInArrowSpace(this, shape)
// shape.props.start = {
// type: 'point',
// x: start.x,
// y: start.y,
// }
// }
// }
// }
if (shape.props.end.type === 'binding') {
if (!shapesForContent.some((s) => s.id === endBindingId)) {
if (info?.isValid) {
const { x, y } = info.end.point
shape.props.end = {
type: 'point',
x,
y,
}
} else {
const { end } = getArrowTerminalsInArrowSpace(this, shape)
shape.props.end = {
type: 'point',
x: end.x,
y: end.y,
}
}
}
}
// if (shape.props.end.type === 'binding') {
// if (!shapesForContent.some((s) => s.id === endBindingId)) {
// if (info?.isValid) {
// const { x, y } = info.end.point
// shape.props.end = {
// type: 'point',
// x,
// y,
// }
// } else {
// const { end } = getArrowTerminalsInArrowSpace(this, shape)
// shape.props.end = {
// type: 'point',
// x: end.x,
// y: end.y,
// }
// }
// }
// }
const infoAfter = getIsArrowStraight(shape)
? getStraightArrowInfo(this, shape)
: getCurvedArrowInfo(this, shape)
// const infoAfter = getIsArrowStraight(shape)
// ? getStraightArrowInfo(this, shape)
// : getCurvedArrowInfo(this, shape)
if (info?.isValid && infoAfter?.isValid && !getIsArrowStraight(shape)) {
const mpA = Vec.Med(info.start.handle, info.end.handle)
const distA = Vec.Dist(info.middle, mpA)
const distB = Vec.Dist(infoAfter.middle, mpA)
if (shape.props.bend < 0) {
shape.props.bend += distB - distA
} else {
shape.props.bend -= distB - distA
}
}
// if (info?.isValid && infoAfter?.isValid && !getIsArrowStraight(shape)) {
// const mpA = Vec.Med(info.start.handle, info.end.handle)
// const distA = Vec.Dist(info.middle, mpA)
// const distB = Vec.Dist(infoAfter.middle, mpA)
// if (shape.props.bend < 0) {
// shape.props.bend += distB - distA
// } else {
// shape.props.bend -= distB - distA
// }
// }
return shape
}
// return shape
// }
return shape
})
@ -7550,24 +7696,24 @@ export class Editor extends EventEmitter<TLEventMap> {
index = getIndexAbove(index)
}
if (this.isShapeOfType<TLArrowShape>(newShape, 'arrow')) {
if (newShape.props.start.type === 'binding') {
const mappedId = idMap.get(newShape.props.start.boundShapeId)
newShape.props.start = mappedId
? { ...newShape.props.start, boundShapeId: mappedId }
: // this shouldn't happen, if you copy an arrow but not it's bound shape it should
// convert the binding to a point at the time of copying
{ type: 'point', x: 0, y: 0 }
}
if (newShape.props.end.type === 'binding') {
const mappedId = idMap.get(newShape.props.end.boundShapeId)
newShape.props.end = mappedId
? { ...newShape.props.end, boundShapeId: mappedId }
: // this shouldn't happen, if you copy an arrow but not it's bound shape it should
// convert the binding to a point at the time of copying
{ type: 'point', x: 0, y: 0 }
}
}
// if (this.isShapeOfType<TLArrowShape>(newShape, 'arrow')) {
// if (newShape.props.start.type === 'binding') {
// const mappedId = idMap.get(newShape.props.start.boundShapeId)
// newShape.props.start = mappedId
// ? { ...newShape.props.start, boundShapeId: mappedId }
// : // this shouldn't happen, if you copy an arrow but not it's bound shape it should
// // convert the binding to a point at the time of copying
// { type: 'point', x: 0, y: 0 }
// }
// if (newShape.props.end.type === 'binding') {
// const mappedId = idMap.get(newShape.props.end.boundShapeId)
// newShape.props.end = mappedId
// ? { ...newShape.props.end, boundShapeId: mappedId }
// : // this shouldn't happen, if you copy an arrow but not it's bound shape it should
// // convert the binding to a point at the time of copying
// { type: 'point', x: 0, y: 0 }
// }
// }
return newShape
})
@ -8547,6 +8693,41 @@ function applyPartialToShape<T extends TLShape>(prev: T, partial?: TLShapePartia
return next
}
function applyPartialToBinding<T extends TLBinding>(prev: T, partial?: TLBindingPartial<T>): T {
if (!partial) return prev
let next = null as null | T
const entries = Object.entries(partial)
for (let i = 0, n = entries.length; i < n; i++) {
const [k, v] = entries[i]
if (v === undefined) continue
// Is the key a special key? We don't update those
if (k === 'id' || k === 'type' || k === 'typeName') continue
// Is the value the same as it was before?
if (v === (prev as any)[k]) continue
// There's a new value, so create the new shape if we haven't already (should we be cloning this?)
if (!next) next = { ...prev }
// for props / meta properties, we support updates with partials of this object
if (k === 'props' || k === 'meta') {
next[k] = { ...prev[k] } as JsonObject
for (const [nextKey, nextValue] of Object.entries(v as object)) {
if (nextValue !== undefined) {
;(next[k] as JsonObject)[nextKey] = nextValue
}
}
continue
}
// base property
;(next as any)[k] = v
}
if (!next) return prev
return next
}
function pushShapeWithDescendants(editor: Editor, id: TLShapeId, result: TLShape[]): void {
const shape = editor.getShape(id)
if (!shape) return

Wyświetl plik

@ -0,0 +1,43 @@
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 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(): Binding['props']
onAfterShapeChange?(
binding: Binding,
direction: 'from' | 'to',
prev: TLShape,
next: TLShape
): void
onBeforeShapeDelete?(binding: Binding, direction: 'from' | 'to', shape: TLShape): void
}

Wyświetl plik

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

Wyświetl plik

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { LegacyMigrations, MigrationSequence } from '@tldraw/store'
import {
ShapeProps,
RecordProps,
TLHandle,
TLPropsMigrations,
TLShape,
TLShapePartial,
TLShapePropsMigrations,
TLUnknownShape,
} from '@tldraw/tlschema'
import { ReactElement } from 'react'
@ -25,8 +25,8 @@ export interface TLShapeUtilConstructor<
> {
new (editor: Editor): U
type: T['type']
props?: ShapeProps<T>
migrations?: LegacyMigrations | TLShapePropsMigrations | MigrationSequence
props?: RecordProps<T>
migrations?: LegacyMigrations | TLPropsMigrations | MigrationSequence
}
/** @public */
@ -41,8 +41,8 @@ export interface TLShapeUtilCanvasSvgDef {
/** @public */
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
constructor(public editor: Editor) {}
static props?: ShapeProps<TLUnknownShape>
static migrations?: LegacyMigrations | TLShapePropsMigrations
static props?: RecordProps<TLUnknownShape>
static migrations?: LegacyMigrations | TLPropsMigrations | MigrationSequence
/**
* The type of the shape util, which should match the shape's type.

Wyświetl plik

@ -1,5 +1,6 @@
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema'
import { VecLike } from '../../../../primitives/Vec'
import { TLArrowBindings } from './shared'
/** @public */
export type TLArrowPoint = {
@ -21,6 +22,7 @@ export interface TLArcInfo {
/** @public */
export type TLArrowInfo =
| {
bindings: TLArrowBindings
isStraight: false
start: TLArrowPoint
end: TLArrowPoint
@ -30,6 +32,7 @@ export type TLArrowInfo =
isValid: boolean
}
| {
bindings: TLArrowBindings
isStraight: true
start: TLArrowPoint
end: TLArrowPoint

Wyświetl plik

@ -15,6 +15,7 @@ import {
BOUND_ARROW_OFFSET,
MIN_ARROW_LENGTH,
STROKE_SIZES,
TLArrowBindings,
WAY_TOO_BIG_ARROW_BEND_FACTOR,
getArrowTerminalsInArrowSpace,
getBoundShapeInfoForTerminal,
@ -25,16 +26,16 @@ import { getStraightArrowInfo } from './straight-arrow'
export function getCurvedArrowInfo(
editor: Editor,
shape: TLArrowShape,
extraBend = 0
bindings: TLArrowBindings
): TLArrowInfo {
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)) {
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 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 middle = Vec.Add(med, u.per().mul(-bend)) // middle handle
const startShapeInfo = getBoundShapeInfoForTerminal(editor, shape.props.start)
const endShapeInfo = getBoundShapeInfoForTerminal(editor, shape.props.end)
const startShapeInfo = getBoundShapeInfoForTerminal(editor, shape, 'start')
const endShapeInfo = getBoundShapeInfoForTerminal(editor, shape, 'end')
// The positions of the body of the arrow, which may be different
// than the arrow's start / end points if the arrow is bound to shapes
@ -53,6 +54,7 @@ export function getCurvedArrowInfo(
if (Vec.Equals(a, b)) {
return {
bindings,
isStraight: true,
start: {
handle: a,
@ -84,7 +86,7 @@ export function getCurvedArrowInfo(
!isSafeFloat(handleArc.length) ||
!isSafeFloat(handleArc.size)
) {
return getStraightArrowInfo(editor, shape)
return getStraightArrowInfo(editor, shape, bindings)
}
const tempA = a.clone()
@ -341,6 +343,7 @@ export function getCurvedArrowInfo(
const bodyArc = getArcInfo(a, b, c)
return {
bindings,
isStraight: false,
start: {
point: a,

Wyświetl plik

@ -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 { Vec } from '../../../../primitives/Vec'
import { Group2d } from '../../../../primitives/geometry/Group2d'
@ -19,15 +25,17 @@ export type BoundShapeInfo<T extends TLShape = TLShape> = {
export function getBoundShapeInfoForTerminal(
editor: Editor,
terminal: TLArrowShapeTerminal
arrow: TLArrowShape,
terminalName: 'start' | 'end'
): BoundShapeInfo | undefined {
if (terminal.type === 'point') {
return
}
const binding = editor
.getBindingsFromShape<TLArrowBinding>(arrow, 'arrow')
.find((b) => b.props.terminal === terminalName)
if (!binding) return
const shape = editor.getShape(terminal.boundShapeId)!
const transform = editor.getShapePageTransform(shape)!
const geometry = editor.getShapeGeometry(shape)
const boundShape = editor.getShape(binding.toId)!
const transform = editor.getShapePageTransform(boundShape)!
const geometry = editor.getShapeGeometry(boundShape)
// 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
@ -36,10 +44,10 @@ export function getBoundShapeInfoForTerminal(
const outline = geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
return {
shape,
shape: boundShape,
transform,
isClosed: geometry.isClosed,
isExact: terminal.isExact,
isExact: binding.props.isExact,
didIntersect: false,
outline,
}
@ -48,14 +56,10 @@ export function getBoundShapeInfoForTerminal(
function getArrowTerminalInArrowSpace(
editor: Editor,
arrowPageTransform: Mat,
terminal: TLArrowShapeTerminal,
binding: TLArrowBinding,
forceImprecise: boolean
) {
if (terminal.type === 'point') {
return Vec.From(terminal)
}
const boundShape = editor.getShape(terminal.boundShapeId)
const boundShape = editor.getShape(binding.toId)
if (!boundShape) {
// this can happen in multiplayer contexts where the shape is being deleted
@ -69,7 +73,9 @@ function getArrowTerminalInArrowSpace(
point,
Vec.MulV(
// 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
)
)
@ -79,41 +85,113 @@ function getArrowTerminalInArrowSpace(
}
}
/** @public */
export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape) {
const arrowPageTransform = editor.getShapePageTransform(shape)!
export interface TLArrowBindings {
start: TLArrowBinding | undefined
end: TLArrowBinding | undefined
}
let startBoundShapeId: TLShapeId | undefined
let endBoundShapeId: TLShapeId | undefined
if (shape.props.start.type === 'binding' && shape.props.end.type === 'binding') {
startBoundShapeId = shape.props.start.boundShapeId
endBoundShapeId = shape.props.end.boundShapeId
/** @internal */
export function getArrowBindings(editor: Editor, shape: TLArrowShape): TLArrowBindings {
const bindings = editor.getBindingsFromShape<TLArrowBinding>(shape, 'arrow')
return {
start: bindings.find((b) => b.props.terminal === 'start'),
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(
editor,
startBoundShapeId,
endBoundShapeId
bindings.start?.toId,
bindings.end?.toId
)
const start = getArrowTerminalInArrowSpace(
editor,
arrowPageTransform,
shape.props.start,
boundShapeRelationships === 'double-bound' || boundShapeRelationships === 'start-contains-end'
)
const start = bindings.start
? getArrowTerminalInArrowSpace(
editor,
arrowPageTransform,
bindings.start,
boundShapeRelationships === 'double-bound' ||
boundShapeRelationships === 'start-contains-end'
)
: Vec.From(shape.props.start)
const end = getArrowTerminalInArrowSpace(
editor,
arrowPageTransform,
shape.props.end,
boundShapeRelationships === 'double-bound' || boundShapeRelationships === 'end-contains-start'
)
const end = bindings.end
? getArrowTerminalInArrowSpace(
editor,
arrowPageTransform,
bindings.end,
boundShapeRelationships === 'double-bound' ||
boundShapeRelationships === 'end-contains-start'
)
: Vec.From(shape.props.end)
return { start, end }
}
/**
* Create or update the arrow binding for a particular arrow terminal. Will clear up if needed.
* TODO(alex): find a better name for this
* @internal
*/
export function arrowBindingMakeItSo(
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 arrowBindingMakeItNotSo(
editor: Editor,
arrow: TLArrowShape,
terminal: 'start' | 'end'
) {
const existing = editor
.getBindingsFromShape<TLArrowBinding>(arrow, 'arrow')
.filter((b) => b.props.terminal === terminal)
editor.deleteBindings(existing)
}
/** @internal */
export const MIN_ARROW_LENGTH = 10
/** @internal */

Wyświetl plik

@ -12,15 +12,20 @@ import {
BoundShapeInfo,
MIN_ARROW_LENGTH,
STROKE_SIZES,
TLArrowBindings,
getArrowTerminalsInArrowSpace,
getBoundShapeInfoForTerminal,
getBoundShapeRelationships,
} from './shared'
export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArrowInfo {
const { start, end, arrowheadStart, arrowheadEnd } = shape.props
export function getStraightArrowInfo(
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 b = terminalsInArrowSpace.end.clone()
@ -28,6 +33,7 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
if (Vec.Equals(a, b)) {
return {
bindings,
isStraight: true,
start: {
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.
const startShapeInfo = getBoundShapeInfoForTerminal(editor, start)
const endShapeInfo = getBoundShapeInfoForTerminal(editor, end)
const startShapeInfo = getBoundShapeInfoForTerminal(editor, shape, 'start')
const endShapeInfo = getBoundShapeInfoForTerminal(editor, shape, 'end')
const arrowPageTransform = editor.getShapePageTransform(shape)!
@ -189,6 +195,7 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
const length = Vec.Dist(a, b)
return {
bindings,
isStraight: true,
start: {
handle: terminalsInArrowSpace.start,

Wyświetl plik

@ -24,6 +24,7 @@ beforeEach(() => {
editor = new Editor({
initialState: 'A',
shapeUtils: [],
bindingUtils: [],
tools: [A, B, C],
store: createTLStore({ shapeUtils: [] }),
getContainer: () => document.body,

Wyświetl plik

@ -6,6 +6,7 @@ let editor: Editor
beforeEach(() => {
editor = new Editor({
shapeUtils: [],
bindingUtils: [],
tools: [],
store: createTLStore({ shapeUtils: [] }),
getContainer: () => document.body,

Wyświetl plik

@ -37,7 +37,7 @@ export type ComputedCache<Data, R extends UnknownRecord> = {
export function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R>;
// @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]}`;
};
@ -265,6 +265,11 @@ export function squashRecordDiffs<T extends UnknownRecord>(diffs: RecordsDiff<T>
// @internal
export function squashRecordDiffsMutable<T extends UnknownRecord>(target: RecordsDiff<T>, diffs: RecordsDiff<T>[]): void;
// @public (undocumented)
export type StandaloneDependsOn = {
readonly dependsOn: readonly MigrationId[];
};
// @public
export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
constructor(config: {

Wyświetl plik

@ -43,5 +43,6 @@ export {
type MigrationId,
type MigrationResult,
type MigrationSequence,
type StandaloneDependsOn,
} from './lib/migrate'
export type { AllRecords } from './lib/type-utils'

Wyświetl plik

@ -121,7 +121,12 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
if (!migration.dependsOn?.length) continue
for (const dep of migration.dependsOn) {
const depMigration = allMigrations.find((m) => m.id === dep)
assert(depMigration, `Migration '${migration.id}' depends on missing migration '${dep}'`)
// TODO: we can't assert here because the store migrations depend on the arrow
// migrations, but the arrow migrations might not be present if we're using the
// editor without arrows :/
if (!depMigration) {
console.warn(`Migration '${migration.id}' depends on missing migration '${dep}'`)
}
}
}
}

Wyświetl plik

@ -91,10 +91,10 @@ export function createMigrationSequence({
* @public
* @public
*/
export function createMigrationIds<ID extends string, Versions extends Record<string, number>>(
sequenceId: ID,
versions: Versions
): { [K in keyof Versions]: `${ID}/${Versions[K]}` } {
export function createMigrationIds<
const ID extends string,
const Versions extends Record<string, number>,
>(sequenceId: ID, versions: Versions): { [K in keyof Versions]: `${ID}/${Versions[K]}` } {
return Object.fromEntries(
objectMapEntries(versions).map(([key, version]) => [key, `${sequenceId}/${version}`] as const)
) as any
@ -136,6 +136,7 @@ export type LegacyMigration<Before = any, After = any> = {
/** @public */
export type MigrationId = `${string}/${number}`
/** @public */
export type StandaloneDependsOn = {
readonly dependsOn: readonly MigrationId[]
}

Wyświetl plik

@ -33,7 +33,6 @@ import { MemoExoticComponent } from 'react';
import { MigrationFailureReason } from '@tldraw/editor';
import { MigrationSequence } from '@tldraw/editor';
import { NamedExoticComponent } from 'react';
import { ObjectValidator } from '@tldraw/editor';
import { Polygon2d } from '@tldraw/editor';
import { Polyline2d } from '@tldraw/editor';
import { default as React_2 } from 'react';
@ -54,6 +53,7 @@ import { StoreSnapshot } from '@tldraw/editor';
import { StyleProp } from '@tldraw/editor';
import { SvgExportContext } from '@tldraw/editor';
import { T } from '@tldraw/editor';
import { TLAnyBindingUtilConstructor } from '@tldraw/editor';
import { TLAnyShapeUtilConstructor } from '@tldraw/editor';
import { TLArrowShape } from '@tldraw/editor';
import { TLAssetId } from '@tldraw/editor';
@ -102,6 +102,7 @@ import { TLParentId } from '@tldraw/editor';
import { TLPointerEvent } from '@tldraw/editor';
import { TLPointerEventInfo } from '@tldraw/editor';
import { TLPointerEventName } from '@tldraw/editor';
import { TLPropsMigrations } from '@tldraw/editor';
import { TLRecord } from '@tldraw/editor';
import { TLRotationSnapshot } from '@tldraw/editor';
import { TLSchema } from '@tldraw/editor';
@ -112,7 +113,6 @@ import { TLSelectionHandle } from '@tldraw/editor';
import { TLShape } from '@tldraw/editor';
import { TLShapeId } from '@tldraw/editor';
import { TLShapePartial } from '@tldraw/editor';
import { TLShapePropsMigrations } from '@tldraw/editor';
import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
import { TLShapeUtilFlag } from '@tldraw/editor';
import { TLStore } from '@tldraw/editor';
@ -121,7 +121,6 @@ import { TLSvgOptions } from '@tldraw/editor';
import { TLTextShape } from '@tldraw/editor';
import { TLUnknownShape } from '@tldraw/editor';
import { TLVideoShape } from '@tldraw/editor';
import { UnionValidator } from '@tldraw/editor';
import { UnknownRecord } from '@tldraw/editor';
import { Validator } from '@tldraw/editor';
import { Vec } from '@tldraw/editor';
@ -192,7 +191,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// (undocumented)
indicator(shape: TLArrowShape): JSX_2.Element | null;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: MigrationSequence;
// (undocumented)
onDoubleClickHandle: (shape: TLArrowShape, handle: TLHandle) => TLShapePartial<TLArrowShape> | void;
// (undocumented)
@ -212,39 +211,13 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
bend: Validator<number>;
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">;
end: UnionValidator<"type", {
binding: ObjectValidator< {
boundShapeId: TLShapeId;
isExact: boolean;
isPrecise: boolean;
normalizedAnchor: VecModel;
type: "binding";
}>;
point: ObjectValidator< {
type: "point";
x: number;
y: number;
}>;
}, never>;
end: Validator<VecModel>;
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
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">;
labelPosition: Validator<number>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
start: UnionValidator<"type", {
binding: ObjectValidator< {
boundShapeId: TLShapeId;
isExact: boolean;
isPrecise: boolean;
normalizedAnchor: VecModel;
type: "binding";
}>;
point: ObjectValidator< {
type: "point";
x: number;
y: number;
}>;
}, never>;
start: Validator<VecModel>;
text: Validator<string>;
};
// (undocumented)
@ -281,7 +254,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
// (undocumented)
indicator(shape: TLBookmarkShape): JSX_2.Element;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onBeforeCreate?: TLOnBeforeCreateHandler<TLBookmarkShape>;
// (undocumented)
@ -492,7 +465,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
// (undocumented)
indicator(shape: TLDrawShape): JSX_2.Element;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onResize: TLOnResizeHandler<TLDrawShape>;
// (undocumented)
@ -549,7 +522,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
// (undocumented)
isAspectRatioLocked: TLShapeUtilFlag<TLEmbedShape>;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onResize: TLOnResizeHandler<TLEmbedShape>;
// (undocumented)
@ -656,7 +629,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
// (undocumented)
indicator(shape: TLFrameShape): JSX_2.Element;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onDragShapesOut: (_shape: TLFrameShape, shapes: TLShape[]) => void;
// (undocumented)
@ -709,7 +682,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
// (undocumented)
indicator(shape: TLGeoShape): JSX_2.Element;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onBeforeCreate: (shape: TLGeoShape) => {
id: TLShapeId;
@ -923,7 +896,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
// (undocumented)
indicator(shape: TLHighlightShape): JSX_2.Element;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onResize: TLOnResizeHandler<TLHighlightShape>;
// (undocumented)
@ -961,7 +934,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
// (undocumented)
isAspectRatioLocked: () => boolean;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onDoubleClick: (shape: TLImageShape) => void;
// (undocumented)
@ -1065,7 +1038,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
// (undocumented)
indicator(shape: TLLineShape): JSX_2.Element;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onHandleDrag: TLOnHandleDragHandler<TLLineShape>;
// (undocumented)
@ -1129,7 +1102,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
// (undocumented)
indicator(shape: TLNoteShape): JSX_2.Element;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onBeforeCreate: (next: TLNoteShape) => {
id: TLShapeId;
@ -1376,7 +1349,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
// (undocumented)
isAspectRatioLocked: TLShapeUtilFlag<TLTextShape>;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
onBeforeCreate: (shape: TLTextShape) => {
id: TLShapeId;
@ -1496,6 +1469,7 @@ export function TldrawHandles({ children }: TLHandlesProps): JSX_2.Element | nul
// @public
export const TldrawImage: NamedExoticComponent< {
background?: boolean | undefined;
bindingUtils?: readonly TLAnyBindingUtilConstructor[] | undefined;
bounds?: Box | undefined;
darkMode?: boolean | undefined;
format?: "png" | "svg" | undefined;
@ -1509,6 +1483,7 @@ snapshot: StoreSnapshot<TLRecord>;
// @public
export type TldrawImageProps = Expand<{
bindingUtils?: readonly TLAnyBindingUtilConstructor[];
shapeUtils?: readonly TLAnyShapeUtilConstructor[];
format?: 'png' | 'svg';
pageId?: TLPageId;
@ -2668,7 +2643,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
// (undocumented)
isAspectRatioLocked: () => boolean;
// (undocumented)
static migrations: TLShapePropsMigrations;
static migrations: TLPropsMigrations;
// (undocumented)
static props: {
assetId: Validator<TLAssetId | null>;

Wyświetl plik

@ -23,6 +23,7 @@ import { TldrawHandles } from './canvas/TldrawHandles'
import { TldrawScribble } from './canvas/TldrawScribble'
import { TldrawSelectionBackground } from './canvas/TldrawSelectionBackground'
import { TldrawSelectionForeground } from './canvas/TldrawSelectionForeground'
import { defaultBindingUtils } from './defaultBindingUtils'
import {
TLExternalContentProps,
registerDefaultExternalContentHandlers,
@ -79,6 +80,7 @@ export function Tldraw(props: TldrawProps) {
onMount,
components = {},
shapeUtils = [],
bindingUtils = [],
tools = [],
...rest
} = props
@ -102,6 +104,12 @@ export function Tldraw(props: TldrawProps) {
[_shapeUtils]
)
const _bindingUtils = useShallowArrayIdentity(bindingUtils)
const bindingUtilsWithDefaults = useMemo(
() => [...defaultBindingUtils, ..._bindingUtils],
[_bindingUtils]
)
const _tools = useShallowArrayIdentity(tools)
const toolsWithDefaults = useMemo(
() => [...defaultTools, ...defaultShapeTools, ..._tools],
@ -123,6 +131,7 @@ export function Tldraw(props: TldrawProps) {
{...rest}
components={componentsWithDefault}
shapeUtils={shapeUtilsWithDefaults}
bindingUtils={bindingUtilsWithDefaults}
tools={toolsWithDefaults}
>
<TldrawUi {...rest} components={componentsWithDefault}>

Wyświetl plik

@ -4,6 +4,7 @@ import {
Expand,
LoadingScreen,
StoreSnapshot,
TLAnyBindingUtilConstructor,
TLAnyShapeUtilConstructor,
TLPageId,
TLRecord,
@ -12,6 +13,7 @@ import {
useTLStore,
} from '@tldraw/editor'
import { memo, useLayoutEffect, useMemo, useState } from 'react'
import { defaultBindingUtils } from './defaultBindingUtils'
import { defaultShapeUtils } from './defaultShapeUtils'
import { usePreloadAssets } from './ui/hooks/usePreloadAssets'
import { getSvgAsImage } from './utils/export/export'
@ -43,6 +45,10 @@ export type TldrawImageProps = Expand<
* Additional shape utils to use.
*/
shapeUtils?: readonly TLAnyShapeUtilConstructor[]
/**
* Additional binding utils to use.
*/
bindingUtils?: readonly TLAnyBindingUtilConstructor[]
} & Partial<TLSvgOptions>
>
@ -69,6 +75,11 @@ export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
const shapeUtils = useShallowArrayIdentity(props.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 assets = useDefaultEditorAssetsWithOverrides()
@ -98,7 +109,8 @@ export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
const editor = new Editor({
store,
shapeUtils: shapeUtilsWithDefaults ?? [],
shapeUtils: shapeUtilsWithDefaults,
bindingUtils: bindingUtilsWithDefaults,
tools: [],
getContainer: () => tempElm,
})
@ -152,6 +164,7 @@ export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
container,
store,
shapeUtilsWithDefaults,
bindingUtilsWithDefaults,
pageId,
bounds,
scale,

Wyświetl plik

@ -0,0 +1,21 @@
import {
BindingUtil,
TLArrowBindingProps,
arrowBindingMigrations,
arrowBindingProps,
} from '@tldraw/editor'
export class ArrowBindingUtil extends BindingUtil {
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 },
}
}
}

Wyświetl plik

@ -0,0 +1,4 @@
import { TLAnyBindingUtilConstructor } from '@tldraw/editor'
import { ArrowBindingUtil } from './bindings/arrow/ArrowBindingUtil'
export const defaultBindingUtils: TLAnyBindingUtilConstructor[] = [ArrowBindingUtil]

Wyświetl plik

@ -1,4 +1,4 @@
import { IndexKey, TLArrowShape, Vec, createShapeId } from '@tldraw/editor'
import { IndexKey, TLArrowShape, Vec, createShapeId, getArrowBindings } from '@tldraw/editor'
import { TestEditor } from '../../../test/TestEditor'
let editor: TestEditor
@ -530,8 +530,8 @@ describe('line bug', () => {
.keyUp('Shift')
expect(editor.getCurrentPageShapes().length).toBe(2)
const arrow = editor.getCurrentPageShapes()[1] as TLArrowShape
expect(arrow.props.end.type).toBe('binding')
const bindings = getArrowBindings(editor, editor.getCurrentPageShapes()[1] as TLArrowShape)
expect(bindings.end).toBeDefined()
})
it('works as expected when binding to a straight horizontal line', () => {
@ -552,7 +552,7 @@ describe('line bug', () => {
.pointerUp()
expect(editor.getCurrentPageShapes().length).toBe(2)
const arrow = editor.getCurrentPageShapes()[1] as TLArrowShape
expect(arrow.props.end.type).toBe('binding')
const bindings = getArrowBindings(editor, editor.getCurrentPageShapes()[1] as TLArrowShape)
expect(bindings.end).toBeDefined()
})
})

Wyświetl plik

@ -1,11 +1,13 @@
import {
assert,
createShapeId,
HALF_PI,
TLArrowShape,
TLArrowShapeTerminal,
TLShapeId,
arrowBindingMakeItSo,
assert,
createShapeId,
getArrowBindings,
} from '@tldraw/editor'
import { describe } from 'node:test'
import { TestEditor } from '../../../test/TestEditor'
let editor: TestEditor
@ -42,23 +44,25 @@ beforeEach(() => {
x: 150,
y: 150,
props: {
start: {
type: 'binding',
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,
},
start: { x: 0, y: 0 },
end: { x: 0, y: 0 },
},
},
])
arrowBindingMakeItSo(editor, ids.arrow1, ids.box1, {
terminal: 'start',
isExact: false,
isPrecise: false,
normalizedAnchor: { x: 0.5, y: 0.5 },
})
arrowBindingMakeItSo(editor, ids.arrow1, ids.box2, {
terminal: 'end',
isExact: false,
isPrecise: false,
normalizedAnchor: { x: 0.5, y: 0.5 },
})
})
describe('When translating a bound shape', () => {
@ -93,6 +97,11 @@ describe('When translating a bound shape', () => {
},
},
})
expect(getArrowBindings(editor, editor.getShape(ids.arrow1)!)).toMatchObject({
start: {
toId: ids.box1,
},
})
})
it('updates the arrow when curved', () => {
@ -300,8 +309,9 @@ describe('Other cases when arrow are moved', () => {
editor.setCurrentTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350)
let arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]
assert(editor.isShapeOfType<TLArrowShape>(arrow, 'arrow'))
assert(arrow.props.end.type === 'binding')
expect(arrow.props.end.boundShapeId).toBe(ids.box3)
let bindings = getArrowBindings(editor, arrow)
assert(bindings.end)
expect(bindings.end.toId).toBe(ids.box3)
// translate:
editor.selectAll().nudgeShapes(editor.getSelectedShapeIds(), { x: 0, y: 1 })
@ -309,8 +319,9 @@ describe('Other cases when arrow are moved', () => {
// arrow should still be bound to box3
arrow = editor.getShape(arrow.id)!
assert(editor.isShapeOfType<TLArrowShape>(arrow, 'arrow'))
assert(arrow.props.end.type === 'binding')
expect(arrow.props.end.boundShapeId).toBe(ids.box3)
bindings = getArrowBindings(editor, arrow)
assert(bindings.end)
expect(bindings.end.toId).toBe(ids.box3)
})
})
@ -342,11 +353,7 @@ describe('When a shape is rotated', () => {
},
})
const anchor = (
editor.getShape<TLArrowShape>(arrow.id)!.props.end as TLArrowShapeTerminal & {
type: 'binding'
}
).normalizedAnchor
const anchor = getArrowBindings(editor, editor.getShape(arrow.id)!).end!.props.normalizedAnchor
expect(anchor.x).toBeCloseTo(0.5)
expect(anchor.y).toBeCloseTo(0.75)
})

Wyświetl plik

@ -9,8 +9,8 @@ import {
SVGContainer,
ShapeUtil,
SvgExportContext,
TLArrowBinding,
TLArrowShape,
TLArrowShapeProps,
TLHandle,
TLOnEditEndHandler,
TLOnHandleDragHandler,
@ -21,12 +21,14 @@ import {
TLShapeUtilCanvasSvgDef,
TLShapeUtilFlag,
Vec,
arrowBindingMakeItNotSo,
arrowBindingMakeItSo,
arrowShapeMigrations,
arrowShapeProps,
getArrowBindings,
getArrowTerminalsInArrowSpace,
getDefaultColorTheme,
mapObjectMapValues,
objectMapEntries,
structuredClone,
toDomPrecision,
track,
@ -83,8 +85,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
color: 'black',
labelColor: 'black',
bend: 0,
start: { type: 'point', x: 0, y: 0 },
end: { type: 'point', x: 2, y: 0 },
start: { x: 0, y: 0 },
end: { x: 2, y: 0 },
arrowheadStart: 'none',
arrowheadEnd: 'arrow',
text: '',
@ -164,10 +166,11 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
override onHandleDrag: TLOnHandleDragHandler<TLArrowShape> = (shape, { handle, isPrecise }) => {
const handleId = handle.id as ARROW_HANDLES
const bindings = getArrowBindings(this.editor, shape)
if (handleId === ARROW_HANDLES.MIDDLE) {
// 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 v = Vec.Per(delta)
@ -186,11 +189,17 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
const next = structuredClone(shape) as TLArrowShape
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) {
// todo: maybe double check that this isn't equal to the other handle too?
// Skip binding
arrowBindingMakeItNotSo(this.editor, shape, handleId)
next.props[handleId] = {
type: 'point',
x: handle.x,
y: handle.y,
}
@ -210,8 +219,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
if (!target) {
// todo: maybe double check that this isn't equal to the other handle too?
arrowBindingMakeItNotSo(this.editor, shape, handleId)
next.props[handleId] = {
type: 'point',
x: handle.x,
y: handle.y,
}
@ -230,11 +240,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
if (!precise) {
// If we're switching to a new bound shape, then precise only if moving slowly
const prevHandle = next.props[handleId]
if (
prevHandle.type === 'point' ||
(prevHandle.type === 'binding' && target.id !== prevHandle.boundShapeId)
) {
if (!currentBinding || (currentBinding && target.id !== currentBinding.toId)) {
precise = this.editor.inputs.pointerVelocity.len() < 0.5
}
}
@ -246,13 +252,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// 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
const otherHandle =
next.props[handleId === ARROW_HANDLES.START ? ARROW_HANDLES.END : ARROW_HANDLES.START]
if (
otherHandle.type === 'binding' &&
target.id === otherHandle.boundShapeId &&
otherHandle.isPrecise
) {
if (otherBinding && target.id === otherBinding.toId && otherBinding.props.isPrecise) {
precise = true
}
}
@ -276,64 +276,58 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
}
}
next.props[handleId] = {
type: 'binding',
boundShapeId: target.id,
normalizedAnchor: normalizedAnchor,
arrowBindingMakeItSo(this.editor, shape, target.id, {
terminal: handleId,
normalizedAnchor,
isPrecise: precise,
isExact: this.editor.inputs.altKey,
}
})
if (next.props.start.type === 'binding' && next.props.end.type === 'binding') {
if (next.props.start.boundShapeId === next.props.end.boundShapeId) {
if (Vec.Equals(next.props.start.normalizedAnchor, next.props.end.normalizedAnchor)) {
next.props.end.normalizedAnchor.x += 0.05
}
}
}
this.editor.setHintingShapes([target.id])
// TODO(alex): restore this if we can
// if (next.props.start.type === 'binding' && next.props.end.type === 'binding') {
// if (next.props.start.boundShapeId === next.props.end.boundShapeId) {
// if (Vec.Equals(next.props.start.normalizedAnchor, next.props.end.normalizedAnchor)) {
// next.props.end.normalizedAnchor.x += 0.05
// }
// }
// }
return next
}
override onTranslateStart: TLOnTranslateStartHandler<TLArrowShape> = (shape) => {
const startBindingId =
shape.props.start.type === 'binding' ? shape.props.start.boundShapeId : null
const endBindingId = shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : null
const bindings = getArrowBindings(this.editor, shape)
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(this.editor, shape)
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(this.editor, shape, bindings)
const shapePageTransform = this.editor.getShapePageTransform(shape.id)!
// If at least one bound shape is in the selection, do nothing;
// If no bound shapes are in the selection, unbind any bound shapes
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
// can maintain them as we translate the arrow
shapeAtTranslationStart.set(shape, {
pagePosition: shapePageTransform.applyToPoint(shape),
terminalBindings: mapObjectMapValues(terminalsInArrowSpace, (terminalName, point) => {
const terminal = shape.props[terminalName]
if (terminal.type !== 'binding') return null
const binding = bindings[terminalName]
if (!binding) return null
return {
binding: terminal,
binding,
shapePosition: point,
pagePosition: shapePageTransform.applyToPoint(point),
}
@ -341,15 +335,16 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
})
for (const handleName of [ARROW_HANDLES.START, ARROW_HANDLES.END] as const) {
const terminal = shape.props[handleName]
if (terminal.type !== 'binding') continue
result = {
...shape,
props: { ...shape.props, [handleName]: { ...terminal, isPrecise: true } },
}
const binding = bindings[handleName]
if (!binding) continue
this.editor.updateBinding({
...binding,
props: { ...binding.props, isPrecise: true },
})
}
return result
return
}
override onTranslate?: TLOnTranslateHandler<TLArrowShape> = (initialShape, shape) => {
@ -362,10 +357,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
atTranslationStart.pagePosition
)
let result = shape
for (const [terminalName, terminalBinding] of objectMapEntries(
atTranslationStart.terminalBindings
)) {
for (const terminalBinding of Object.values(atTranslationStart.terminalBindings)) {
if (!terminalBinding) continue
const newPagePoint = Vec.Add(terminalBinding.pagePosition, Vec.Mul(pageDelta, 0.5))
@ -378,54 +370,41 @@ 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 pointInTargetSpace = this.editor.getPointInShapeSpace(newTarget, newPagePoint)
const normalizedAnchor = {
x: (pointInTargetSpace.x - targetBounds.minX) / targetBounds.width,
y: (pointInTargetSpace.y - targetBounds.minY) / targetBounds.height,
}
result = {
...result,
props: {
...result.props,
[terminalName]: { ...terminalBinding.binding, isPrecise: true, normalizedAnchor },
},
}
arrowBindingMakeItSo(this.editor, shape, newTarget.id, {
...terminalBinding.binding.props,
normalizedAnchor,
isPrecise: true,
})
} else {
result = {
...result,
props: {
...result.props,
[terminalName]: {
type: 'point',
x: terminalBinding.shapePosition.x,
y: terminalBinding.shapePosition.y,
},
},
}
arrowBindingMakeItNotSo(this.editor, shape, terminalBinding.binding.props.terminal)
}
}
return result
}
override onResize: TLOnResizeHandler<TLArrowShape> = (shape, info) => {
const { scaleX, scaleY } = info
const terminals = getArrowTerminalsInArrowSpace(this.editor, shape)
const bindings = getArrowBindings(this.editor, shape)
const terminals = getArrowTerminalsInArrowSpace(this.editor, shape, bindings)
const { start, end } = structuredClone<TLArrowShape['props']>(shape.props)
let { bend } = shape.props
// 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.y = terminals.start.y * scaleY
}
// 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.y = terminals.end.y * scaleY
}
@ -436,18 +415,23 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
const mx = Math.abs(scaleX)
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 (bend !== 0) {
bend *= -1
bend *= Math.max(mx, my)
}
if (start.type === 'binding') {
start.normalizedAnchor.x = 1 - start.normalizedAnchor.x
if (startNormalizedAnchor) {
startNormalizedAnchor.x = 1 - startNormalizedAnchor.x
}
if (end.type === 'binding') {
end.normalizedAnchor.x = 1 - end.normalizedAnchor.x
if (endNormalizedAnchor) {
endNormalizedAnchor.x = 1 - endNormalizedAnchor.x
}
} else if (scaleX >= 0 && scaleY < 0) {
if (bend !== 0) {
@ -455,12 +439,12 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
bend *= Math.max(mx, my)
}
if (start.type === 'binding') {
start.normalizedAnchor.y = 1 - start.normalizedAnchor.y
if (startNormalizedAnchor) {
startNormalizedAnchor.y = 1 - startNormalizedAnchor.y
}
if (end.type === 'binding') {
end.normalizedAnchor.y = 1 - end.normalizedAnchor.y
if (endNormalizedAnchor) {
endNormalizedAnchor.y = 1 - endNormalizedAnchor.y
}
} else if (scaleX >= 0 && scaleY >= 0) {
if (bend !== 0) {
@ -471,17 +455,30 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
bend *= Math.max(mx, my)
}
if (start.type === 'binding') {
start.normalizedAnchor.x = 1 - start.normalizedAnchor.x
start.normalizedAnchor.y = 1 - start.normalizedAnchor.y
if (startNormalizedAnchor) {
startNormalizedAnchor.x = 1 - startNormalizedAnchor.x
startNormalizedAnchor.y = 1 - startNormalizedAnchor.y
}
if (end.type === 'binding') {
end.normalizedAnchor.x = 1 - end.normalizedAnchor.x
end.normalizedAnchor.y = 1 - end.normalizedAnchor.y
if (endNormalizedAnchor) {
endNormalizedAnchor.x = 1 - endNormalizedAnchor.x
endNormalizedAnchor.y = 1 - endNormalizedAnchor.y
}
}
if (bindings.start && startNormalizedAnchor) {
arrowBindingMakeItSo(this.editor, shape, bindings.start.toId, {
...bindings.start.props,
normalizedAnchor: startNormalizedAnchor,
})
}
if (bindings.end && endNormalizedAnchor) {
arrowBindingMakeItSo(this.editor, shape, bindings.end.toId, {
...bindings.end.props,
normalizedAnchor: endNormalizedAnchor,
})
}
const next = {
props: {
start,
@ -565,18 +562,18 @@ export class ArrowShapeUtil extends ShapeUtil<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)
if (!info) return null
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape, info?.bindings)
const geometry = this.editor.getShapeGeometry<Group2d>(shape)
const bounds = geometry.bounds
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
const strokeWidth = STROKE_SIZES[shape.props.size]
@ -753,6 +750,7 @@ const ArrowSvg = track(function ArrowSvg({
const theme = useDefaultColorTheme()
const info = editor.getArrowInfo(shape)
const bounds = Box.ZeroFix(editor.getShapeGeometry(shape).bounds)
const bindings = getArrowBindings(editor, shape)
const changeIndex = React.useMemo<number>(() => {
return editor.environment.isSafari ? (globalRenderIndex += 1) : 0
@ -783,7 +781,7 @@ const ArrowSvg = track(function ArrowSvg({
)
handlePath =
shape.props.start.type === 'binding' || shape.props.end.type === 'binding' ? (
bindings.start || bindings.end ? (
<path
className="tl-arrow-hint"
d={info.isStraight ? getStraightArrowHandlePath(info) : getCurvedArrowHandlePath(info)}
@ -791,19 +789,19 @@ const ArrowSvg = track(function ArrowSvg({
strokeDashoffset={strokeDashoffset}
strokeWidth={sw}
markerStart={
shape.props.start.type === 'binding'
? shape.props.start.isExact
bindings.start
? bindings.start.props.isExact
? ''
: shape.props.start.isPrecise
: bindings.start.props.isPrecise
? 'url(#arrowhead-cross)'
: 'url(#arrowhead-dot)'
: ''
}
markerEnd={
shape.props.end.type === 'binding'
? shape.props.end.isExact
bindings.end
? bindings.end.props.isExact
? ''
: shape.props.end.isPrecise
: bindings.end.props.isPrecise
? 'url(#arrowhead-cross)'
: 'url(#arrowhead-dot)'
: ''
@ -903,7 +901,7 @@ const shapeAtTranslationStart = new WeakMap<
{
pagePosition: Vec
shapePosition: Vec
binding: Extract<TLArrowShapeProps['start'], { type: 'binding' }>
binding: TLArrowBinding
} | null
>
}

Wyświetl plik

@ -268,8 +268,8 @@ export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
const debugGeom: Geometry2d[] = []
const info = editor.getArrowInfo(shape)!
const hasStartBinding = shape.props.start.type === 'binding'
const hasEndBinding = shape.props.end.type === 'binding'
const hasStartBinding = !!info.bindings.start
const hasEndBinding = !!info.bindings.end
const hasStartArrowhead = info.start.arrowhead !== 'none'
const hasEndArrowhead = info.end.arrowhead !== 'none'
if (info.isStraight) {

Wyświetl plik

@ -111,10 +111,6 @@ export class Pointing extends StateNode {
})
if (change) {
const startTerminal = change.props?.start
if (startTerminal?.type === 'binding') {
this.editor.setHintingShapes([startTerminal.boundShapeId])
}
this.editor.updateShapes([change])
}
@ -148,10 +144,6 @@ export class Pointing extends StateNode {
})
if (change) {
const endTerminal = change.props?.end
if (endTerminal?.type === 'binding') {
this.editor.setHintingShapes([endTerminal.boundShapeId])
}
this.editor.updateShapes([change])
}
}

Wyświetl plik

@ -1,7 +1,6 @@
import {
StateNode,
TLArrowShape,
TLArrowShapeTerminal,
TLCancelEvent,
TLEnterEventHandler,
TLEventHandlers,
@ -12,6 +11,7 @@ import {
TLShapeId,
TLShapePartial,
Vec,
getArrowBindings,
snapAngle,
sortByIndex,
structuredClone,
@ -112,16 +112,16 @@ export class DraggingHandle extends StateNode {
// <!-- Only relevant to arrows
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
if (initialTerminal?.type === 'binding') {
this.editor.setHintingShapes([initialTerminal.boundShapeId])
if (initialBinding) {
this.editor.setHintingShapes([initialBinding.toId])
this.isPrecise = initialTerminal.isPrecise
this.isPrecise = initialBinding.props.isPrecise
if (this.isPrecise) {
this.isPreciseId = initialTerminal.boundShapeId
this.isPreciseId = initialBinding.toId
} else {
this.resetExactTimeout()
}
@ -283,15 +283,15 @@ export class DraggingHandle extends StateNode {
const next: TLShapePartial<any> = { ...shape, ...changes }
// Arrows
if (initialHandle.canBind) {
const bindingAfter = (next.props as any)[initialHandle.id] as TLArrowShapeTerminal | undefined
if (initialHandle.canBind && this.editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
const bindingAfter = getArrowBindings(editor, shape)[initialHandle.id as 'start' | 'end']
if (bindingAfter?.type === 'binding') {
if (hintingShapeIds[0] !== bindingAfter.boundShapeId) {
editor.setHintingShapes([bindingAfter.boundShapeId])
this.pointingId = bindingAfter.boundShapeId
if (bindingAfter) {
if (hintingShapeIds[0] !== bindingAfter.toId) {
editor.setHintingShapes([bindingAfter.toId])
this.pointingId = bindingAfter.toId
this.isPrecise = pointerVelocity.len() < 0.5 || altKey
this.isPreciseId = this.isPrecise ? bindingAfter.boundShapeId : null
this.isPreciseId = this.isPrecise ? bindingAfter.toId : null
this.resetExactTimeout()
}
} else {

Wyświetl plik

@ -7,6 +7,7 @@ import {
TLNoteShape,
TLPointerEventInfo,
Vec,
getArrowBindings,
} from '@tldraw/editor'
import {
NOTE_CENTER_OFFSET,
@ -25,10 +26,10 @@ export class PointingHandle extends StateNode {
const { shape } = info
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') {
this.editor.setHintingShapes([initialTerminal.boundShapeId])
if (initialBinding) {
this.editor.setHintingShapes([initialBinding.toId])
}
}

Wyświetl plik

@ -5,6 +5,7 @@ import {
TLGroupShape,
TLLineShape,
TLTextShape,
getArrowBindings,
useEditor,
useValue,
} from '@tldraw/editor'
@ -17,14 +18,9 @@ function shapesWithUnboundArrows(editor: Editor) {
return selectedShapes.filter((shape) => {
if (!shape) return false
if (
editor.isShapeOfType<TLArrowShape>(shape, 'arrow') &&
shape.props.start.type === 'binding'
) {
return false
}
if (editor.isShapeOfType<TLArrowShape>(shape, 'arrow') && shape.props.end.type === 'binding') {
return false
if (editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
const bindings = getArrowBindings(editor, shape)
if (bindings.start || bindings.end) return false
}
return true
})
@ -50,16 +46,16 @@ export const useAllowGroup = () => {
for (const shape of selectedShapes) {
if (editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
const { start, end } = shape.props
if (start.type === 'binding') {
const bindings = getArrowBindings(editor, shape)
if (bindings.start) {
// 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
}
}
if (end.type === 'binding') {
if (bindings.end) {
// 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
}
}

Wyświetl plik

@ -5,7 +5,6 @@ import {
PageRecordType,
TLArrowShape,
TLArrowShapeArrowheadStyle,
TLArrowShapeTerminal,
TLAsset,
TLAssetId,
TLDefaultColorStyle,
@ -410,12 +409,10 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
arrowheadStart: getV2Arrowhead(v1Shape.decorations?.start),
arrowheadEnd: getV2Arrowhead(v1Shape.decorations?.end),
start: {
type: 'point',
x: coerceNumber(v1Shape.handles.start.point[0]),
y: coerceNumber(v1Shape.handles.start.point[1]),
},
end: {
type: 'point',
x: coerceNumber(v1Shape.handles.end.point[0]),
y: coerceNumber(v1Shape.handles.end.point[1]),
},
@ -563,18 +560,19 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
})
if (change) {
if (change.props?.[handleId]) {
const terminal = change.props?.[handleId] as TLArrowShapeTerminal
if (terminal.type === 'binding') {
terminal.isExact = binding.distance === 0
// TODO(alex): can we delete this?
// if (change.props?.[handleId]) {
// const terminal = change.props?.[handleId] as TLArrowShapeTerminal
// if (terminal.type === 'binding') {
// terminal.isExact = binding.distance === 0
if (terminal.boundShapeId !== targetId) {
console.warn('Hit the wrong shape!')
terminal.boundShapeId = targetId
terminal.normalizedAnchor = { x: 0.5, y: 0.5 }
}
}
}
// if (terminal.boundShapeId !== targetId) {
// console.warn('Hit the wrong shape!')
// terminal.boundShapeId = targetId
// terminal.normalizedAnchor = { x: 0.5, y: 0.5 }
// }
// }
// }
editor.updateShapes([change])
}
}

Wyświetl plik

@ -26,6 +26,7 @@ import {
createTLStore,
rotateSelectionHandle,
} from '@tldraw/editor'
import { defaultBindingUtils } from '../lib/defaultBindingUtils'
import { defaultShapeTools } from '../lib/defaultShapeTools'
import { defaultShapeUtils } from '../lib/defaultShapeUtils'
import { defaultTools } from '../lib/defaultTools'
@ -60,12 +61,17 @@ export class TestEditor extends Editor {
elm.tabIndex = 0
const shapeUtilsWithDefaults = [...defaultShapeUtils, ...(options.shapeUtils ?? [])]
const bindingUtilsWithDefaults = [...defaultBindingUtils, ...(options.bindingUtils ?? [])]
super({
...options,
shapeUtils: [...shapeUtilsWithDefaults],
shapeUtils: shapeUtilsWithDefaults,
bindingUtils: bindingUtilsWithDefaults,
tools: [...defaultTools, ...defaultShapeTools, ...(options.tools ?? [])],
store: createTLStore({ shapeUtils: [...shapeUtilsWithDefaults] }),
store: createTLStore({
shapeUtils: shapeUtilsWithDefaults,
bindingUtils: bindingUtilsWithDefaults,
}),
getContainer: () => elm,
initialState: 'select',
})

Wyświetl plik

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

Wyświetl plik

@ -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 { TL } from './test-jsx'
@ -20,6 +20,7 @@ const ids = {
}
const arrow = () => editor.getOnlySelectedShape() as TLArrowShape
const bindings = () => getArrowBindings(editor, arrow())
beforeEach(() => {
editor = new TestEditor()
@ -127,18 +128,17 @@ describe('When binding an arrow to a shape', () => {
editor.setCurrentTool('arrow')
editor.pointerDown(0, 50)
editor.pointerMove(99, 50)
expect(arrow().props.start.type).toBe('point')
expect(arrow().props.end.type).toBe('point')
expect(bindings().start).toBeUndefined()
expect(bindings().end).toBeUndefined()
})
it('binds to the shape when dragged into the shape edge', () => {
editor.setCurrentTool('arrow')
editor.pointerDown(0, 50)
editor.pointerMove(100, 50)
expect(arrow().props.end).toMatchObject({
type: 'binding',
boundShapeId: ids.box1,
normalizedAnchor: { x: 0, y: 0.5 },
expect(bindings().end).toMatchObject({
toId: ids.box1,
props: { normalizedAnchor: { x: 0, y: 0.5 } },
})
})
@ -146,21 +146,22 @@ describe('When binding an arrow to a shape', () => {
editor.setCurrentTool('arrow')
editor.pointerDown(0, 50)
editor.pointerMove(250, 50)
expect(arrow().props.end.type).toBe('point')
expect(bindings().end).toBeUndefined()
})
it('binds and then unbinds when moved out', () => {
editor.setCurrentTool('arrow')
editor.pointerDown(0, 50)
editor.pointerMove(150, 50)
expect(arrow().props.end).toMatchObject({
type: 'binding',
boundShapeId: ids.box1,
normalizedAnchor: { x: 0.5, y: 0.5 },
isPrecise: true, // enclosed
expect(bindings().end).toMatchObject({
toId: ids.box1,
props: {
normalizedAnchor: { x: 0.5, y: 0.5 },
isPrecise: true, // enclosed
},
})
editor.pointerMove(250, 50)
expect(arrow().props.end.type).toBe('point')
expect(bindings().end).toBeUndefined()
})
it('does not bind when control key is held', () => {
@ -168,7 +169,7 @@ describe('When binding an arrow to a shape', () => {
editor.keyDown('Control')
editor.pointerDown(0, 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', () => {
@ -176,7 +177,7 @@ describe('When binding an arrow to a shape', () => {
editor.setCurrentTool('arrow')
editor.pointerDown(0, 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', () => {
@ -185,22 +186,22 @@ describe('When binding an arrow to a shape', () => {
editor.pointerMove(100, 50)
// 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')
expect(arrow().props.end.type).toBe('point')
expect(bindings().end).toBeUndefined()
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...
expect(arrow().props.end.type).toBe('binding')
expect(bindings().end).toBeDefined()
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.pointerUp()
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
})
})
@ -607,13 +608,14 @@ describe('When binding an arrow to an ancestor', () => {
const arrow = editor.getCurrentPageShapes().find((s) => s.type === 'arrow') as TLArrowShape
if (!arrow) throw Error('No arrow')
if (arrow.props.start.type !== 'binding') throw Error('no binding')
if (arrow.props.end.type !== 'binding') throw Error('no binding')
const bindings = getArrowBindings(editor, arrow)
if (!bindings.start) throw Error('no binding')
if (!bindings.end) throw Error('no binding')
expect(arrow.props.start.boundShapeId).toBe(ids.box1)
expect(arrow.props.end.boundShapeId).toBe(ids.frame)
expect(arrow.props.start.isPrecise).toBe(false)
expect(arrow.props.end.isPrecise).toBe(true)
expect(bindings.start.toId).toBe(ids.box1)
expect(bindings.end.toId).toBe(ids.frame)
expect(bindings.start.props.isPrecise).toBe(false)
expect(bindings.end.props.isPrecise).toBe(true)
})
it('binds precisely from parent to child', () => {
@ -642,13 +644,14 @@ describe('When binding an arrow to an ancestor', () => {
const arrow = editor.getCurrentPageShapes().find((s) => s.type === 'arrow') as TLArrowShape
if (!arrow) throw Error('No arrow')
if (arrow.props.start.type !== 'binding') throw Error('no binding')
if (arrow.props.end.type !== 'binding') throw Error('no binding')
const bindings = getArrowBindings(editor, arrow)
if (!bindings.start) throw Error('no binding')
if (!bindings.end) throw Error('no binding')
expect(arrow.props.start.boundShapeId).toBe(ids.frame)
expect(arrow.props.end.boundShapeId).toBe(ids.box1)
expect(arrow.props.start.isPrecise).toBe(false)
expect(arrow.props.end.isPrecise).toBe(true)
expect(bindings.start.toId).toBe(ids.frame)
expect(bindings.end.toId).toBe(ids.box1)
expect(bindings.start.props.isPrecise).toBe(false)
expect(bindings.end.props.isPrecise).toBe(true)
})
})

Wyświetl plik

@ -1,4 +1,4 @@
import { TLArrowShape, createShapeId } from '@tldraw/editor'
import { TLArrowShape, createShapeId, getArrowBindings } from '@tldraw/editor'
import { TestEditor } from './TestEditor'
let editor: TestEditor
@ -26,6 +26,10 @@ function arrow() {
return editor.getCurrentPageShapes().find((s) => s.type === 'arrow') as TLArrowShape
}
function bindings() {
return getArrowBindings(editor, arrow())
}
describe('restoring bound arrows', () => {
beforeEach(() => {
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', () => {
editor.mark('deleting')
editor.deleteShapes([ids.box2])
expect(arrow().props.end.type).toBe('point')
expect(bindings().end).toBeUndefined()
editor.undo()
expect(arrow().props.end.type).toBe('binding')
expect(bindings().end).toBeDefined()
editor.redo()
expect(arrow().props.end.type).toBe('point')
expect(bindings().end).toBeUndefined()
})
it('removes / restores multiple bindings', () => {
editor.mark('deleting')
expect(arrow().props.start.type).toBe('binding')
expect(arrow().props.end.type).toBe('binding')
expect(bindings().start).toBeDefined()
expect(bindings().end).toBeDefined()
editor.deleteShapes([ids.box1, ids.box2])
expect(arrow().props.start.type).toBe('point')
expect(arrow().props.end.type).toBe('point')
expect(bindings().start).toBeUndefined()
expect(bindings().end).toBeUndefined()
editor.undo()
expect(arrow().props.start.type).toBe('binding')
expect(arrow().props.end.type).toBe('binding')
expect(bindings().start).toBeDefined()
expect(bindings().end).toBeDefined()
editor.redo()
expect(arrow().props.start.type).toBe('point')
expect(arrow().props.end.type).toBe('point')
expect(bindings().start).toBeUndefined()
expect(bindings().end).toBeUndefined()
})
})
@ -77,8 +81,8 @@ describe('restoring bound arrows multiplayer', () => {
editor.setCurrentTool('arrow').pointerMove(0, 50).pointerDown().pointerMove(150, 50).pointerUp()
expect(arrow().props.start.type).toBe('point')
expect(arrow().props.end.type).toBe('binding')
expect(bindings().start).toBeUndefined()
expect(bindings().end).toBeDefined()
// Merge a change from a remote source that deletes box 2
editor.store.mergeRemoteChanges(() => {
@ -89,8 +93,8 @@ describe('restoring bound arrows multiplayer', () => {
expect(editor.getShape(ids.box2)).toBeUndefined()
// arrow is still there, but without its binding
expect(arrow()).not.toBeUndefined()
expect(arrow().props.start.type).toBe('point')
expect(arrow().props.end.type).toBe('point')
expect(bindings().start).toBeUndefined()
expect(bindings().end).toBeUndefined()
editor.undo() // undo creating the arrow
@ -101,8 +105,8 @@ describe('restoring bound arrows multiplayer', () => {
expect(editor.getShape(ids.box2)).toBeUndefined()
expect(arrow()).not.toBeUndefined()
expect(arrow().props.start.type).toBe('point')
expect(arrow().props.end.type).toBe('point')
expect(bindings().start).toBeUndefined()
expect(bindings().end).toBeUndefined()
editor.undo() // undo creating arrow
@ -121,7 +125,7 @@ describe('restoring bound arrows multiplayer', () => {
editor.redo() // redo creating arrow
// box is back! arrow should be bound
expect(arrow().props.start.type).toBe('point')
expect(arrow().props.end.type).toBe('binding')
expect(bindings().start).toBeUndefined()
expect(bindings().end).toBeDefined()
})
})

Wyświetl plik

@ -6,6 +6,7 @@ import {
TLShapeId,
TLShapePartial,
createShapeId,
getArrowBindings,
} from '@tldraw/editor'
import { TestEditor } from './TestEditor'
@ -355,12 +356,10 @@ describe('flipping rotated shapes', () => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
const props: Partial<TLArrowShapeProps> = {
start: {
type: 'point',
x: 0,
y: 0,
},
end: {
type: 'point',
x: 100,
y: 0,
},
@ -408,8 +407,8 @@ describe('flipping rotated shapes', () => {
const transform = editor.getShapePageTransform(id)
if (!transform) throw new Error('no transform')
const arrow = editor.getShape<TLArrowShape>(id)!
if (arrow.props.start.type !== 'point' || arrow.props.end.type !== 'point')
throw new Error('not a point')
const bindings = getArrowBindings(editor, arrow)
if (bindings.start || bindings.end) throw new Error('not a point')
const start = Mat.applyToPoint(transform, arrow.props.start)
const end = Mat.applyToPoint(transform, arrow.props.end)
return { start, end }

Wyświetl plik

@ -12,6 +12,7 @@ import {
assert,
compact,
createShapeId,
getArrowBindings,
sortByIndex,
} from '@tldraw/editor'
import { TestEditor } from './TestEditor'
@ -1723,20 +1724,17 @@ describe('moving handles within a group', () => {
editor.pointerDown(50, 50).pointerMove(60, 60).pointerUp(60, 60)
let arrow = onlySelectedShape() as TLArrowShape
let bindings = getArrowBindings(editor, arrow)
expect(arrow.parentId).toBe(groupA.id)
expect(arrow.props.start.type).toBe('point')
if (arrow.props.start.type === 'point') {
expect(arrow.props.start.x).toBe(0)
expect(arrow.props.start.y).toBe(0)
}
expect(bindings.start).toBeUndefined()
expect(arrow.props.start.x).toBe(0)
expect(arrow.props.start.y).toBe(0)
expect(arrow.props.end.type).toBe('point')
if (arrow.props.end.type === 'point') {
expect(arrow.props.end.x).toBe(10)
expect(arrow.props.end.y).toBe(10)
}
expect(bindings.end).toBeUndefined()
expect(arrow.props.end.x).toBe(10)
expect(arrow.props.end.y).toBe(10)
editor.expectToBeIn('select.idle')
@ -1759,20 +1757,17 @@ describe('moving handles within a group', () => {
editor.pointerMove(60, -10)
arrow = editor.getShape(arrow.id)!
bindings = getArrowBindings(editor, arrow)
expect(arrow.parentId).toBe(groupA.id)
expect(arrow.props.start.type).toBe('point')
if (arrow.props.start.type === 'point') {
expect(arrow.props.start.x).toBe(0)
expect(arrow.props.start.y).toBe(0)
}
expect(bindings.start).toBeUndefined()
expect(arrow.props.start.x).toBe(0)
expect(arrow.props.start.y).toBe(0)
expect(arrow.props.end.type).toBe('point')
if (arrow.props.end.type === 'point') {
expect(arrow.props.end.x).toBe(10)
expect(arrow.props.end.y).toBe(-60)
}
expect(bindings.end).toBeUndefined()
expect(arrow.props.end.x).toBe(10)
expect(arrow.props.end.y).toBe(-60)
expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
x: 0,

Wyświetl plik

@ -136,8 +136,8 @@ describe('When brushing arrows', () => {
ref="arrow1"
x={0}
y={0}
start={{ type: 'point', x: 0, y: 0 }}
end={{ type: 'point', x: 100, y: 100 }}
start={{ x: 0, y: 0 }}
end={{ x: 100, y: 100 }}
bend={0}
/>,
])
@ -158,8 +158,8 @@ describe('When brushing arrows', () => {
ref="arrow1"
x={0}
y={0}
start={{ type: 'point', x: 0, y: 0 }}
end={{ type: 'point', x: 100, y: 100 }}
start={{ x: 0, y: 0 }}
end={{ x: 100, y: 100 }}
bend={40}
/>,
])

Wyświetl plik

@ -15,12 +15,19 @@ import { RecordId } from '@tldraw/store';
import { RecordType } from '@tldraw/store';
import { SerializedStore } from '@tldraw/store';
import { Signal } from '@tldraw/state';
import { StandaloneDependsOn } from '@tldraw/store';
import { Store } from '@tldraw/store';
import { StoreSchema } from '@tldraw/store';
import { StoreSnapshot } from '@tldraw/store';
import { T } from '@tldraw/validate';
import { UnknownRecord } from '@tldraw/store';
// @public (undocumented)
export const arrowBindingMigrations: TLPropsMigrations;
// @public (undocumented)
export const arrowBindingProps: RecordProps<TLArrowBinding>;
// @public (undocumented)
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">;
// @public (undocumented)
export const arrowShapeMigrations: TLShapePropsMigrations;
export const arrowShapeMigrations: MigrationSequence;
// @public (undocumented)
export const arrowShapeProps: {
@ -37,39 +44,13 @@ export const arrowShapeProps: {
bend: T.Validator<number>;
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">;
end: T.UnionValidator<"type", {
binding: T.ObjectValidator<{
boundShapeId: TLShapeId;
isExact: boolean;
isPrecise: boolean;
normalizedAnchor: VecModel;
type: "binding";
} & {}>;
point: T.ObjectValidator<{
type: "point";
x: number;
y: number;
} & {}>;
}, never>;
end: T.Validator<VecModel>;
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
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">;
labelPosition: T.Validator<number>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
start: T.UnionValidator<"type", {
binding: T.ObjectValidator<{
boundShapeId: TLShapeId;
isExact: boolean;
isPrecise: boolean;
normalizedAnchor: VecModel;
type: "binding";
} & {}>;
point: T.ObjectValidator<{
type: "point";
x: number;
y: number;
} & {}>;
}, never>;
start: T.Validator<VecModel>;
text: T.Validator<string>;
};
@ -86,7 +67,10 @@ export const AssetRecordType: RecordType<TLAsset, "props" | "type">;
export const assetValidator: T.Validator<TLAsset>;
// @public (undocumented)
export const bookmarkShapeMigrations: TLShapePropsMigrations;
export const bindingIdValidator: T.Validator<TLBindingId>;
// @public (undocumented)
export const bookmarkShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const bookmarkShapeProps: {
@ -132,6 +116,24 @@ export function createAssetValidator<Type extends string, Props extends JsonObje
typeName: 'asset';
}[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
export const createPresenceStateDerivation: ($user: Signal<{
color: string;
@ -143,12 +145,12 @@ export const createPresenceStateDerivation: ($user: Signal<{
export function createShapeId(id?: string): TLShapeId;
// @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]}`;
};
// @public (undocumented)
export function createShapePropsMigrationSequence(migrations: TLShapePropsMigrations): TLShapePropsMigrations;
export function createShapePropsMigrationSequence(migrations: TLPropsMigrations): TLPropsMigrations;
// @public (undocumented)
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; }>;
// @public
export function createTLSchema({ shapes, migrations, }?: {
export function createTLSchema({ shapes, bindings, migrations, }?: {
bindings?: Record<string, SchemaPropsInfo>;
migrations?: readonly MigrationSequence[];
shapes?: Record<string, SchemaShapeInfo>;
shapes?: Record<string, SchemaPropsInfo>;
}): TLSchema;
// @public (undocumented)
@ -194,7 +197,7 @@ export const DefaultHorizontalAlignStyle: EnumStyleProp<"end-legacy" | "end" | "
// @public (undocumented)
export const defaultShapeSchemas: {
[T in TLDefaultShape['type']]: SchemaShapeInfo;
[T in TLDefaultShape['type']]: SchemaPropsInfo;
};
// @public (undocumented)
@ -207,7 +210,7 @@ export const DefaultVerticalAlignStyle: EnumStyleProp<"end" | "middle" | "start"
export const DocumentRecordType: RecordType<TLDocument, never>;
// @public (undocumented)
export const drawShapeMigrations: TLShapePropsMigrations;
export const drawShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const drawShapeProps: {
@ -426,7 +429,7 @@ export type EmbedDefinition = {
};
// @public (undocumented)
export const embedShapeMigrations: TLShapePropsMigrations;
export const embedShapeMigrations: TLPropsMigrations;
// @public
export const embedShapePermissionDefaults: {
@ -462,7 +465,7 @@ export class EnumStyleProp<T> extends StyleProp<T> {
}
// @public (undocumented)
export const frameShapeMigrations: TLShapePropsMigrations;
export const frameShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const frameShapeProps: {
@ -475,7 +478,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">;
// @public (undocumented)
export const geoShapeMigrations: TLShapePropsMigrations;
export const geoShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const geoShapeProps: {
@ -507,13 +510,13 @@ export function getDefaultTranslationLocale(): TLLanguage['locale'];
export function getShapePropKeysByStyle(props: Record<string, T.Validatable<any>>): Map<StyleProp<unknown>, string>;
// @public (undocumented)
export const groupShapeMigrations: TLShapePropsMigrations;
export const groupShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const groupShapeProps: ShapeProps<TLGroupShape>;
export const groupShapeProps: RecordProps<TLGroupShape>;
// @public (undocumented)
export const highlightShapeMigrations: TLShapePropsMigrations;
export const highlightShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const highlightShapeProps: {
@ -531,7 +534,7 @@ export const highlightShapeProps: {
export function idValidator<Id extends RecordId<UnknownRecord>>(prefix: Id['__type__']['typeName']): T.Validator<Id>;
// @public (undocumented)
export const imageShapeMigrations: TLShapePropsMigrations;
export const imageShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const imageShapeProps: {
@ -552,6 +555,12 @@ export const InstancePageStateRecordType: RecordType<TLInstancePageState, "pageI
// @public (undocumented)
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)
export function isPageId(id: string): id is TLPageId;
@ -673,7 +682,7 @@ export const LANGUAGES: readonly [{
}];
// @public (undocumented)
export const lineShapeMigrations: TLShapePropsMigrations;
export const lineShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const lineShapeProps: {
@ -693,7 +702,7 @@ export const lineShapeProps: {
export const LineShapeSplineStyle: EnumStyleProp<"cubic" | "line">;
// @public (undocumented)
export const noteShapeMigrations: TLShapePropsMigrations;
export const noteShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const noteShapeProps: {
@ -723,15 +732,33 @@ export const parentIdValidator: T.Validator<TLParentId>;
// @public (undocumented)
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)
export const rootShapeMigrations: MigrationSequence;
// @public (undocumented)
export type SchemaShapeInfo = {
export interface SchemaPropsInfo {
// (undocumented)
meta?: Record<string, AnyValidator>;
migrations?: LegacyMigrations | MigrationSequence | TLShapePropsMigrations;
// (undocumented)
migrations?: LegacyMigrations | MigrationSequence | TLPropsMigrations;
// (undocumented)
props?: Record<string, AnyValidator>;
};
}
// @public (undocumented)
export const scribbleValidator: T.Validator<TLScribble>;
@ -739,16 +766,6 @@ export const scribbleValidator: T.Validator<TLScribble>;
// @public (undocumented)
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
export class StyleProp<Type> implements T.Validatable<Type> {
// @internal
@ -777,7 +794,7 @@ export class StyleProp<Type> implements T.Validatable<Type> {
export type StylePropValue<T extends StyleProp<any>> = T extends StyleProp<infer U> ? U : never;
// @public (undocumented)
export const textShapeMigrations: TLShapePropsMigrations;
export const textShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const textShapeProps: {
@ -794,6 +811,19 @@ export const textShapeProps: {
// @public
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)
export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>;
@ -801,10 +831,7 @@ export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>;
export type TLArrowShapeArrowheadStyle = T.TypeOf<typeof ArrowShapeArrowheadStartStyle>;
// @public (undocumented)
export type TLArrowShapeProps = ShapePropsType<typeof arrowShapeProps>;
// @public (undocumented)
export type TLArrowShapeTerminal = T.TypeOf<typeof ArrowShapeTerminal>;
export type TLArrowShapeProps = RecordPropsType<typeof arrowShapeProps>;
// @public (undocumented)
export type TLAsset = TLBookmarkAsset | TLImageAsset | TLVideoAsset;
@ -837,6 +864,20 @@ export interface TLBaseAsset<Type extends string, Props> extends BaseRecord<'ass
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)
export interface TLBaseShape<Type extends string, Props extends object> extends BaseRecord<'shape', TLShapeId> {
// (undocumented)
@ -861,6 +902,20 @@ export interface TLBaseShape<Type extends string, Props extends object> extends
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
export type TLBookmarkAsset = TLBaseAsset<'bookmark', {
description: string;
@ -901,6 +956,9 @@ export interface TLCursor {
// @public
export type TLCursorType = SetValue<typeof TL_CURSOR_TYPES>;
// @public
export type TLDefaultBinding = TLArrowBinding;
// @public (undocumented)
export type TLDefaultColorStyle = T.TypeOf<typeof DefaultColorStyle>;
@ -1024,7 +1082,7 @@ export type TLImageShape = TLBaseShape<'image', TLImageShapeProps>;
export type TLImageShapeCrop = T.TypeOf<typeof ImageShapeCrop>;
// @public (undocumented)
export type TLImageShapeProps = ShapePropsType<typeof imageShapeProps>;
export type TLImageShapeProps = RecordPropsType<typeof imageShapeProps>;
// @public
export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
@ -1193,7 +1251,25 @@ export type TLParentId = TLPageId | TLShapeId;
export const TLPOINTER_ID: TLPointerId;
// @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)
export type TLSchema = StoreSchema<TLRecord, TLStoreProps>;
@ -1228,24 +1304,6 @@ export type TLShapePartial<T extends TLShape = TLShape> = T extends T ? {
type: T['type'];
} & 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)
export type TLStore = Store<TLRecord, TLStoreProps>;
@ -1264,7 +1322,10 @@ export type TLStoreSnapshot = StoreSnapshot<TLRecord>;
export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>;
// @public (undocumented)
export type TLTextShapeProps = ShapePropsType<typeof textShapeProps>;
export type TLTextShapeProps = RecordPropsType<typeof textShapeProps>;
// @public
export type TLUnknownBinding = TLBaseBinding<string, object>;
// @public
export type TLUnknownShape = TLBaseShape<string, object>;
@ -1296,7 +1357,7 @@ export interface VecModel {
export const vecModelValidator: T.Validator<VecModel>;
// @public (undocumented)
export const videoShapeMigrations: TLShapePropsMigrations;
export const videoShapeMigrations: TLPropsMigrations;
// @public (undocumented)
export const videoShapeProps: {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -4,7 +4,9 @@ import { TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLS
import { bookmarkAssetMigrations } from './assets/TLBookmarkAsset'
import { imageAssetMigrations } from './assets/TLImageAsset'
import { videoAssetMigrations } from './assets/TLVideoAsset'
import { arrowBindingMigrations, arrowBindingProps } from './bindings/TLArrowBinding'
import { AssetRecordType, assetMigrations } from './records/TLAsset'
import { TLBinding, TLDefaultBinding, createBindingRecordType } from './records/TLBinding'
import { CameraRecordType, cameraMigrations } from './records/TLCamera'
import { DocumentRecordType, documentMigrations } from './records/TLDocument'
import { createInstanceRecordType, instanceMigrations } from './records/TLInstance'
@ -15,12 +17,12 @@ import { InstancePresenceRecordType, instancePresenceMigrations } from './record
import { TLRecord } from './records/TLRecord'
import {
TLDefaultShape,
TLShapePropsMigrations,
TLShape,
createShapeRecordType,
getShapePropKeysByStyle,
processShapeMigrations,
rootShapeMigrations,
} from './records/TLShape'
import { TLPropsMigrations, processPropsMigrations } from './recordsWithProps'
import { arrowShapeMigrations, arrowShapeProps } from './shapes/TLArrowShape'
import { bookmarkShapeMigrations, bookmarkShapeProps } from './shapes/TLBookmarkShape'
import { drawShapeMigrations, drawShapeProps } from './shapes/TLDrawShape'
@ -43,8 +45,8 @@ type AnyValidator = {
}
/** @public */
export type SchemaShapeInfo = {
migrations?: LegacyMigrations | TLShapePropsMigrations | MigrationSequence
export interface SchemaPropsInfo {
migrations?: LegacyMigrations | TLPropsMigrations | MigrationSequence
props?: Record<string, AnyValidator>
meta?: Record<string, AnyValidator>
}
@ -53,7 +55,7 @@ export type SchemaShapeInfo = {
export type TLSchema = StoreSchema<TLRecord, TLStoreProps>
/** @public */
export const defaultShapeSchemas: { [T in TLDefaultShape['type']]: SchemaShapeInfo } = {
export const defaultShapeSchemas: { [T in TLDefaultShape['type']]: SchemaPropsInfo } = {
arrow: { migrations: arrowShapeMigrations, props: arrowShapeProps },
bookmark: { migrations: bookmarkShapeMigrations, props: bookmarkShapeProps },
draw: { migrations: drawShapeMigrations, props: drawShapeProps },
@ -69,6 +71,11 @@ export const defaultShapeSchemas: { [T in TLDefaultShape['type']]: SchemaShapeIn
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.
*
@ -77,9 +84,11 @@ export const defaultShapeSchemas: { [T in TLDefaultShape['type']]: SchemaShapeIn
* @public */
export function createTLSchema({
shapes = defaultShapeSchemas,
bindings = defaultBindingSchemas,
migrations,
}: {
shapes?: Record<string, SchemaShapeInfo>
shapes?: Record<string, SchemaPropsInfo>
bindings?: Record<string, SchemaPropsInfo>
migrations?: readonly MigrationSequence[]
} = {}): TLSchema {
const stylesById = new Map<string, StyleProp<unknown>>()
@ -93,11 +102,13 @@ export function createTLSchema({
}
const ShapeRecordType = createShapeRecordType(shapes)
const BindingRecordType = createBindingRecordType(bindings)
const InstanceRecordType = createInstanceRecordType(stylesById)
return StoreSchema.create(
{
asset: AssetRecordType,
binding: BindingRecordType,
camera: CameraRecordType,
document: DocumentRecordType,
instance: InstanceRecordType,
@ -124,7 +135,8 @@ export function createTLSchema({
imageAssetMigrations,
videoAssetMigrations,
...processShapeMigrations(shapes),
...processPropsMigrations<TLShape>('shape', shapes),
...processPropsMigrations<TLBinding>('binding', bindings),
...(migrations ?? []),
],

Wyświetl plik

@ -9,11 +9,22 @@ export { assetIdValidator, createAssetValidator, type TLBaseAsset } from './asse
export { type TLBookmarkAsset } from './assets/TLBookmarkAsset'
export { type TLImageAsset } from './assets/TLImageAsset'
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 {
createTLSchema,
defaultShapeSchemas,
type SchemaShapeInfo,
type SchemaPropsInfo,
type TLSchema,
} from './createTLSchema'
export {
@ -41,6 +52,19 @@ export {
type TLAssetPartial,
type TLAssetShape,
} 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 { DocumentRecordType, TLDOCUMENT_ID, type TLDocument } from './records/TLDocument'
export { TLINSTANCE_ID, type TLInstance, type TLInstanceId } from './records/TLInstance'
@ -68,11 +92,14 @@ export {
type TLShape,
type TLShapeId,
type TLShapePartial,
type TLShapeProp,
type TLShapeProps,
type TLShapePropsMigrations,
type TLUnknownShape,
} from './records/TLShape'
export {
type RecordProps,
type RecordPropsType,
type TLPropsMigration,
type TLPropsMigrations,
} from './recordsWithProps'
export {
ArrowShapeArrowheadEndStyle,
ArrowShapeArrowheadStartStyle,
@ -81,14 +108,11 @@ export {
type TLArrowShape,
type TLArrowShapeArrowheadStyle,
type TLArrowShapeProps,
type TLArrowShapeTerminal,
} from './shapes/TLArrowShape'
export {
createShapeValidator,
parentIdValidator,
shapeIdValidator,
type ShapeProps,
type ShapePropsType,
type TLBaseShape,
} from './shapes/TLBaseShape'
export {

Wyświetl plik

@ -0,0 +1,116 @@
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
* unknowneither 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(() => ({
x: 0,
y: 0,
rotation: 0,
isLocked: false,
opacity: 1,
meta: {},
}))
}

Wyświetl plik

@ -1,4 +1,5 @@
import { TLAsset } from './TLAsset'
import { TLBinding } from './TLBinding'
import { TLCamera } from './TLCamera'
import { TLDocument } from './TLDocument'
import { TLInstance } from './TLInstance'
@ -11,6 +12,7 @@ import { TLShape } from './TLShape'
/** @public */
export type TLRecord =
| TLAsset
| TLBinding
| TLCamera
| TLDocument
| TLInstance

Wyświetl plik

@ -1,18 +1,15 @@
import {
Migration,
MigrationId,
MigrationSequence,
RecordId,
UnknownRecord,
createMigrationIds,
createMigrationSequence,
createRecordMigrationSequence,
createRecordType,
} from '@tldraw/store'
import { assert, mapObjectMapValues } from '@tldraw/utils'
import { mapObjectMapValues } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { nanoid } from 'nanoid'
import { SchemaShapeInfo } from '../createTLSchema'
import { SchemaPropsInfo } from '../createTLSchema'
import { TLPropsMigrations } from '../recordsWithProps'
import { TLArrowShape } from '../shapes/TLArrowShape'
import { TLBaseShape, createShapeValidator } from '../shapes/TLBaseShape'
import { TLBookmarkShape } from '../shapes/TLBookmarkShape'
@ -76,19 +73,6 @@ export type TLShapePartial<T extends TLShape = TLShape> = T extends T
/** @public */
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 */
export type TLParentId = TLPageId | TLShapeId
@ -188,141 +172,27 @@ export function getShapePropKeysByStyle(props: Record<string, T.Validatable<any>
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
*/
export function createShapePropsMigrationSequence(
migrations: TLShapePropsMigrations
): TLShapePropsMigrations {
migrations: TLPropsMigrations
): TLPropsMigrations {
return migrations
}
/**
* @public
*/
export function createShapePropsMigrationIds<S extends string, T extends Record<string, number>>(
shapeType: S,
ids: T
): { [k in keyof T]: `com.tldraw.shape.${S}/${T[k]}` } {
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]}` } {
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 */
export function createShapeRecordType(shapes: Record<string, SchemaShapeInfo>) {
export function createShapeRecordType(shapes: Record<string, SchemaPropsInfo>) {
return createRecordType<TLShape>('shape', {
scope: 'document',
validator: T.model(

Wyświetl plik

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

Wyświetl plik

@ -1,17 +1,22 @@
import { createMigrationSequence } from '@tldraw/store'
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 {
RETIRED_DOWN_MIGRATION,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
} from '../records/TLShape'
RecordPropsType,
TLPropsMigration,
createPropsMigration,
} from '../recordsWithProps'
import { StyleProp } from '../styles/StyleProp'
import { DefaultColorStyle, DefaultLabelColorStyle } from '../styles/TLColorStyle'
import { DefaultDashStyle } from '../styles/TLDashStyle'
import { DefaultFillStyle } from '../styles/TLFillStyle'
import { DefaultFontStyle } from '../styles/TLFontStyle'
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
import { ShapePropsType, TLBaseShape, shapeIdValidator } from './TLBaseShape'
import { TLBaseShape } from './TLBaseShape'
const arrowheadTypes = [
'arrow',
@ -40,25 +45,6 @@ export const ArrowShapeArrowheadEndStyle = StyleProp.defineEnum('tldraw:arrowhea
/** @public */
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 */
export const arrowShapeProps = {
labelColor: DefaultLabelColorStyle,
@ -69,15 +55,15 @@ export const arrowShapeProps = {
arrowheadStart: ArrowShapeArrowheadStartStyle,
arrowheadEnd: ArrowShapeArrowheadEndStyle,
font: DefaultFontStyle,
start: ArrowShapeTerminal,
end: ArrowShapeTerminal,
start: vecModelValidator,
end: vecModelValidator,
bend: T.number,
text: T.string,
labelPosition: T.number,
}
/** @public */
export type TLArrowShapeProps = ShapePropsType<typeof arrowShapeProps>
export type TLArrowShapeProps = RecordPropsType<typeof arrowShapeProps>
/** @public */
export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>
@ -86,20 +72,26 @@ export const arrowShapeVersions = createShapePropsMigrationIds('arrow', {
AddLabelColor: 1,
AddIsPrecise: 2,
AddLabelPosition: 3,
ExtractBindings: 4,
})
function propsMigration(migration: TLPropsMigration) {
return createPropsMigration<TLArrowShape>('shape', 'arrow', migration)
}
/** @public */
export const arrowShapeMigrations = createShapePropsMigrationSequence({
export const arrowShapeMigrations = createMigrationSequence({
sequenceId: 'com.tldraw.shape.arrow',
sequence: [
{
propsMigration({
id: arrowShapeVersions.AddLabelColor,
up: (props) => {
props.labelColor = 'black'
},
down: RETIRED_DOWN_MIGRATION,
},
}),
{
propsMigration({
id: arrowShapeVersions.AddIsPrecise,
up: ({ start, end }) => {
if (start.type === 'binding') {
@ -123,9 +115,9 @@ export const arrowShapeMigrations = createShapePropsMigrationSequence({
delete end.isPrecise
}
},
},
}),
{
propsMigration({
id: arrowShapeVersions.AddLabelPosition,
up: (props) => {
props.labelPosition = 0.5
@ -133,6 +125,78 @@ export const arrowShapeMigrations = createShapePropsMigrationSequence({
down: (props) => {
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 }
}
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 }
}
}
},
},
],
})

Wyświetl plik

@ -1,5 +1,5 @@
import { BaseRecord } from '@tldraw/store'
import { Expand, IndexKey, JsonObject } from '@tldraw/utils'
import { IndexKey, JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { TLOpacityType, opacityValidator } from '../misc/TLOpacity'
import { idValidator } from '../misc/id-validator'
@ -56,13 +56,3 @@ export function createShapeValidator<
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]>
}>

Wyświetl plik

@ -1,11 +1,8 @@
import { T } from '@tldraw/validate'
import { assetIdValidator } from '../assets/TLBaseAsset'
import {
RETIRED_DOWN_MIGRATION,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
} from '../records/TLShape'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
import { TLBaseShape } from './TLBaseShape'
/** @public */
export const bookmarkShapeProps = {
@ -16,7 +13,7 @@ export const bookmarkShapeProps = {
}
/** @public */
export type TLBookmarkShapeProps = ShapePropsType<typeof bookmarkShapeProps>
export type TLBookmarkShapeProps = RecordPropsType<typeof bookmarkShapeProps>
/** @public */
export type TLBookmarkShape = TLBaseShape<'bookmark', TLBookmarkShapeProps>

Wyświetl plik

@ -1,15 +1,12 @@
import { T } from '@tldraw/validate'
import { vecModelValidator } from '../misc/geometry-types'
import {
RETIRED_DOWN_MIGRATION,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
} from '../records/TLShape'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
import { DefaultColorStyle } from '../styles/TLColorStyle'
import { DefaultDashStyle } from '../styles/TLDashStyle'
import { DefaultFillStyle } from '../styles/TLFillStyle'
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { TLBaseShape } from './TLBaseShape'
export const DrawShapeSegment = T.object({
type: T.literalEnum('free', 'straight'),
@ -32,7 +29,7 @@ export const drawShapeProps = {
}
/** @public */
export type TLDrawShapeProps = ShapePropsType<typeof drawShapeProps>
export type TLDrawShapeProps = RecordPropsType<typeof drawShapeProps>
/** @public */
export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>

Wyświetl plik

@ -1,10 +1,7 @@
import { T } from '@tldraw/validate'
import {
RETIRED_DOWN_MIGRATION,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
} from '../records/TLShape'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
import { TLBaseShape } from './TLBaseShape'
// Only allow multiplayer embeds. If we add additional routes later for example '/help' this won't match
const TLDRAW_APP_RE = /(^\/r\/[^/]+\/?$)/
@ -591,7 +588,7 @@ export const embedShapeProps = {
}
/** @public */
export type TLEmbedShapeProps = ShapePropsType<typeof embedShapeProps>
export type TLEmbedShapeProps = RecordPropsType<typeof embedShapeProps>
/** @public */
export type TLEmbedShape = TLBaseShape<'embed', TLEmbedShapeProps>

Wyświetl plik

@ -1,6 +1,7 @@
import { T } from '@tldraw/validate'
import { createShapePropsMigrationSequence } from '../records/TLShape'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { RecordPropsType } from '../recordsWithProps'
import { TLBaseShape } from './TLBaseShape'
/** @public */
export const frameShapeProps = {
@ -9,7 +10,7 @@ export const frameShapeProps = {
name: T.string,
}
type TLFrameShapeProps = ShapePropsType<typeof frameShapeProps>
type TLFrameShapeProps = RecordPropsType<typeof frameShapeProps>
/** @public */
export type TLFrameShape = TLBaseShape<'frame', TLFrameShapeProps>

Wyświetl plik

@ -1,9 +1,6 @@
import { T } from '@tldraw/validate'
import {
RETIRED_DOWN_MIGRATION,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
} from '../records/TLShape'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
import { StyleProp } from '../styles/StyleProp'
import { DefaultColorStyle, DefaultLabelColorStyle } from '../styles/TLColorStyle'
import { DefaultDashStyle } from '../styles/TLDashStyle'
@ -15,7 +12,7 @@ import {
} from '../styles/TLHorizontalAlignStyle'
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
import { DefaultVerticalAlignStyle } from '../styles/TLVerticalAlignStyle'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { TLBaseShape } from './TLBaseShape'
/** @public */
export const GeoShapeGeoStyle = StyleProp.defineEnum('tldraw:geo', {
@ -65,7 +62,7 @@ export const geoShapeProps = {
}
/** @public */
export type TLGeoShapeProps = ShapePropsType<typeof geoShapeProps>
export type TLGeoShapeProps = RecordPropsType<typeof geoShapeProps>
/** @public */
export type TLGeoShape = TLBaseShape<'geo', TLGeoShapeProps>

Wyświetl plik

@ -1,5 +1,6 @@
import { createShapePropsMigrationSequence } from '../records/TLShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
import { RecordProps } from '../recordsWithProps'
import { TLBaseShape } from './TLBaseShape'
/** @public */
export type TLGroupShapeProps = { [key in never]: undefined }
@ -8,7 +9,7 @@ export type TLGroupShapeProps = { [key in never]: undefined }
export type TLGroupShape = TLBaseShape<'group', TLGroupShapeProps>
/** @public */
export const groupShapeProps: ShapeProps<TLGroupShape> = {}
export const groupShapeProps: RecordProps<TLGroupShape> = {}
/** @public */
export const groupShapeMigrations = createShapePropsMigrationSequence({ sequence: [] })

Wyświetl plik

@ -1,8 +1,9 @@
import { T } from '@tldraw/validate'
import { createShapePropsMigrationSequence } from '../records/TLShape'
import { RecordPropsType } from '../recordsWithProps'
import { DefaultColorStyle } from '../styles/TLColorStyle'
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { TLBaseShape } from './TLBaseShape'
import { DrawShapeSegment } from './TLDrawShape'
/** @public */
@ -15,7 +16,7 @@ export const highlightShapeProps = {
}
/** @public */
export type TLHighlightShapeProps = ShapePropsType<typeof highlightShapeProps>
export type TLHighlightShapeProps = RecordPropsType<typeof highlightShapeProps>
/** @public */
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>

Wyświetl plik

@ -1,12 +1,9 @@
import { T } from '@tldraw/validate'
import { assetIdValidator } from '../assets/TLBaseAsset'
import { vecModelValidator } from '../misc/geometry-types'
import {
RETIRED_DOWN_MIGRATION,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
} from '../records/TLShape'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
import { TLBaseShape } from './TLBaseShape'
/** @public */
export const ImageShapeCrop = T.object({
@ -27,7 +24,7 @@ export const imageShapeProps = {
}
/** @public */
export type TLImageShapeProps = ShapePropsType<typeof imageShapeProps>
export type TLImageShapeProps = RecordPropsType<typeof imageShapeProps>
/** @public */
export type TLImageShape = TLBaseShape<'image', TLImageShapeProps>

Wyświetl plik

@ -1,15 +1,12 @@
import { IndexKey, getIndices, objectMapFromEntries, sortByIndex } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import {
RETIRED_DOWN_MIGRATION,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
} from '../records/TLShape'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
import { StyleProp } from '../styles/StyleProp'
import { DefaultColorStyle } from '../styles/TLColorStyle'
import { DefaultDashStyle } from '../styles/TLDashStyle'
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { TLBaseShape } from './TLBaseShape'
/** @public */
export const LineShapeSplineStyle = StyleProp.defineEnum('tldraw:spline', {
@ -37,7 +34,7 @@ export const lineShapeProps = {
}
/** @public */
export type TLLineShapeProps = ShapePropsType<typeof lineShapeProps>
export type TLLineShapeProps = RecordPropsType<typeof lineShapeProps>
/** @public */
export type TLLineShape = TLBaseShape<'line', TLLineShapeProps>

Wyświetl plik

@ -1,15 +1,12 @@
import { T } from '@tldraw/validate'
import {
RETIRED_DOWN_MIGRATION,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
} from '../records/TLShape'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
import { DefaultColorStyle } from '../styles/TLColorStyle'
import { DefaultFontStyle } from '../styles/TLFontStyle'
import { DefaultHorizontalAlignStyle } from '../styles/TLHorizontalAlignStyle'
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
import { DefaultVerticalAlignStyle } from '../styles/TLVerticalAlignStyle'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { TLBaseShape } from './TLBaseShape'
/** @public */
export const noteShapeProps = {
@ -25,7 +22,7 @@ export const noteShapeProps = {
}
/** @public */
export type TLNoteShapeProps = ShapePropsType<typeof noteShapeProps>
export type TLNoteShapeProps = RecordPropsType<typeof noteShapeProps>
/** @public */
export type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps>

Wyświetl plik

@ -1,14 +1,11 @@
import { T } from '@tldraw/validate'
import {
RETIRED_DOWN_MIGRATION,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
} from '../records/TLShape'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
import { DefaultColorStyle } from '../styles/TLColorStyle'
import { DefaultFontStyle } from '../styles/TLFontStyle'
import { DefaultHorizontalAlignStyle } from '../styles/TLHorizontalAlignStyle'
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { TLBaseShape } from './TLBaseShape'
/** @public */
export const textShapeProps = {
@ -23,7 +20,7 @@ export const textShapeProps = {
}
/** @public */
export type TLTextShapeProps = ShapePropsType<typeof textShapeProps>
export type TLTextShapeProps = RecordPropsType<typeof textShapeProps>
/** @public */
export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>

Wyświetl plik

@ -1,11 +1,8 @@
import { T } from '@tldraw/validate'
import { assetIdValidator } from '../assets/TLBaseAsset'
import {
RETIRED_DOWN_MIGRATION,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
} from '../records/TLShape'
import { ShapePropsType, TLBaseShape } from './TLBaseShape'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RETIRED_DOWN_MIGRATION, RecordPropsType } from '../recordsWithProps'
import { TLBaseShape } from './TLBaseShape'
/** @public */
export const videoShapeProps = {
@ -18,7 +15,7 @@ export const videoShapeProps = {
}
/** @public */
export type TLVideoShapeProps = ShapePropsType<typeof videoShapeProps>
export type TLVideoShapeProps = RecordPropsType<typeof videoShapeProps>
/** @public */
export type TLVideoShape = TLBaseShape<'video', TLVideoShapeProps>

Wyświetl plik

@ -1,16 +1,18 @@
import {
Editor,
PageRecordType,
TLArrowShapeTerminal,
TLArrowBinding,
TLPage,
TLPageId,
TLShape,
TLShapeId,
TLStore,
VecModel,
createShapeId,
defaultShapeUtils,
defaultTools,
} from 'tldraw'
import { defaultBindingUtils } from 'tldraw/src/lib/defaultBindingUtils'
import { RandomSource } from './RandomSource'
export type Op =
@ -37,8 +39,8 @@ export type Op =
}
| {
type: 'create-arrow'
start: TLArrowShapeTerminal
end: TLArrowShapeTerminal
start: TLArrowBinding | VecModel
end: TLArrowBinding | VecModel
}
| {
type: 'delete-shape'
@ -97,6 +99,7 @@ export class FuzzEditor extends RandomSource {
super(_seed)
this.editor = new Editor({
shapeUtils: defaultShapeUtils,
bindingUtils: defaultBindingUtils,
tools: defaultTools,
initialState: 'select',
store,

Wyświetl plik

@ -50,8 +50,8 @@ const oldArrow: TLBaseShape<'arrow', Omit<TLArrowShapeProps, 'labelColor'>> = {
fill: 'none',
color: 'black',
bend: 0,
start: { type: 'point', x: 0, y: 0 },
end: { type: 'point', x: 0, y: 0 },
start: { x: 0, y: 0 },
end: { x: 0, y: 0 },
arrowheadStart: 'none',
arrowheadEnd: 'arrow',
text: '',

Wyświetl plik

@ -1,6 +1,7 @@
import { nanoid } from 'nanoid'
import {
Editor,
TLArrowBinding,
TLArrowShape,
TLRecord,
TLStore,
@ -117,10 +118,18 @@ let totalNumShapes = 0
let totalNumPages = 0
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 terminal of [arrow.props.start, arrow.props.end]) {
if (terminal.type === 'binding' && !editor.store.has(terminal.boundShapeId)) {
const bindings = editor.getBindingsFromShape<TLArrowBinding>(arrow, 'arrow')
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
}
}