[refactor] Remove TLShapeDef, getShapeUtilByType. (#1432)

This PR removes `TLShapeDef` and associated helpers / references.

It purposely loosens the configuration and typings to better support
customization.

### Change Type

- [x] `major` — Breaking Change

### Test Plan

1. Use the app!

### Release Notes

- [tlschema] Update props of `createTLSchema`
- [editor] Update props of `TldrawEditorConfig`
- [editor] Remove `App.getShapeUtilByType`
- [editor] Update `App.getShapeUtil` to take a type rather than a shape

---------

Co-authored-by: alex <alex@dytry.ch>
This commit is contained in:
Steve Ruiz 2023-05-23 13:32:42 +01:00 committed by GitHub
parent 3ce18c0c31
commit 649125cdad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 527 additions and 690 deletions

View file

@ -1,5 +1,4 @@
import { import {
defineShape,
HTMLContainer, HTMLContainer,
MenuGroup, MenuGroup,
menuItem, menuItem,
@ -30,23 +29,13 @@ type CardShape = TLBaseShape<
} }
> >
// Shape Definition
// ----------------
// The shape definition is used to tell TypeScript about the shape
// and to register the shape with the app.
export const CardShape = defineShape<CardShape>({
type: 'card',
getShapeUtil: () => CardUtil,
// validator: createShapeValidator({ ... })
})
// Shape Util // Shape Util
// ---------- // ----------
// The CardUtil class is used by the app to answer questions about a // The CardUtil class is used by the app to answer questions about a
// shape of the 'card' type. For example, what is the default props // shape of the 'card' type. For example, what is the default props
// for this shape? What should we render for it, or for its indicator? // for this shape? What should we render for it, or for its indicator?
class CardUtil extends TLBoxUtil<CardShape> { class CardUtil extends TLBoxUtil<CardShape> {
static type = 'card' static override type = 'card' as const
// There are a LOT of other things we could add here, like these flags // There are a LOT of other things we could add here, like these flags
override isAspectRatioLocked = (_shape: CardShape) => false override isAspectRatioLocked = (_shape: CardShape) => false
@ -105,8 +94,11 @@ export class CardTool extends TLBoxTool {
// Finally, collect the custom tools and shapes into a config object // Finally, collect the custom tools and shapes into a config object
const customTldrawConfig = new TldrawEditorConfig({ const customTldrawConfig = new TldrawEditorConfig({
tools: [CardTool], tools: [CardTool],
shapes: [CardShape], shapes: {
allowUnknownShapes: true, card: {
util: CardUtil,
},
},
}) })
// ... and we can make our custom shape example! // ... and we can make our custom shape example!

View file

@ -1,11 +1,4 @@
import { import { createShapeId, TLBaseShape, TLBoxUtil, Tldraw, TldrawEditorConfig } from '@tldraw/tldraw'
createShapeId,
defineShape,
TLBaseShape,
TLBoxUtil,
Tldraw,
TldrawEditorConfig,
} from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css' import '@tldraw/tldraw/ui.css'
@ -13,7 +6,6 @@ export default function ErrorBoundaryExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw <Tldraw
persistenceKey="error-boundary-example"
components={{ components={{
// disable app-level error boundaries: // disable app-level error boundaries:
ErrorFallback: null, ErrorFallback: null,
@ -34,6 +26,10 @@ export default function ErrorBoundaryExample() {
props: { message: 'Something has gone wrong' }, props: { message: 'Something has gone wrong' },
}, },
]) ])
// center the camera on the error shape
app.zoomToFit()
app.resetZoom()
}} }}
/> />
</div> </div>
@ -44,24 +40,25 @@ export default function ErrorBoundaryExample() {
// shape type that always throws an error. See CustomConfigExample for more info // shape type that always throws an error. See CustomConfigExample for more info
// on creating custom shapes. // on creating custom shapes.
type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }> type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }>
const ErrorShape = defineShape<ErrorShape>({
type: 'error', class ErrorUtil extends TLBoxUtil<ErrorShape> {
getShapeUtil: () => override type = 'error' as const
class ErrorShapeUtil extends TLBoxUtil<ErrorShape> {
static type = 'error' defaultProps() {
defaultProps() { return { message: 'Error!', w: 100, h: 100 }
return { message: 'Error!', w: 100, h: 100 } }
} render(shape: ErrorShape) {
render(shape: ErrorShape) { throw new Error(shape.props.message)
throw new Error(shape.props.message) }
} indicator() {
indicator() { throw new Error(`Error shape indicator!`)
throw new Error(`Error shape indicator!`) }
} }
},
})
const customConfigWithErrorShape = new TldrawEditorConfig({ const customConfigWithErrorShape = new TldrawEditorConfig({
shapes: [ErrorShape], shapes: {
allowUnknownShapes: true, error: {
util: ErrorUtil,
},
},
}) })

View file

@ -30,6 +30,7 @@ import { MatLike } from '@tldraw/primitives';
import { Matrix2d } from '@tldraw/primitives'; import { Matrix2d } from '@tldraw/primitives';
import { Matrix2dModel } from '@tldraw/primitives'; import { Matrix2dModel } from '@tldraw/primitives';
import { Migrations } from '@tldraw/tlstore'; import { Migrations } from '@tldraw/tlstore';
import { MigrationsForShapes } from '@tldraw/tlschema';
import { Polyline2d } from '@tldraw/primitives'; import { Polyline2d } from '@tldraw/primitives';
import * as React_2 from 'react'; import * as React_2 from 'react';
import { default as React_3 } from 'react'; import { default as React_3 } from 'react';
@ -43,7 +44,6 @@ import { Signal } from 'signia';
import { sortByIndex } from '@tldraw/indices'; import { sortByIndex } from '@tldraw/indices';
import { StoreSchema } from '@tldraw/tlstore'; import { StoreSchema } from '@tldraw/tlstore';
import { StoreSnapshot } from '@tldraw/tlstore'; import { StoreSnapshot } from '@tldraw/tlstore';
import { StoreValidator } from '@tldraw/tlstore';
import { StrokePoint } from '@tldraw/primitives'; import { StrokePoint } from '@tldraw/primitives';
import { TLAlignType } from '@tldraw/tlschema'; import { TLAlignType } from '@tldraw/tlschema';
import { TLArrowheadType } from '@tldraw/tlschema'; import { TLArrowheadType } from '@tldraw/tlschema';
@ -103,6 +103,7 @@ import { TLUserId } from '@tldraw/tlschema';
import { TLUserPresence } from '@tldraw/tlschema'; import { TLUserPresence } from '@tldraw/tlschema';
import { TLVideoAsset } from '@tldraw/tlschema'; import { TLVideoAsset } from '@tldraw/tlschema';
import { TLVideoShape } from '@tldraw/tlschema'; import { TLVideoShape } from '@tldraw/tlschema';
import { ValidatorsForShapes } from '@tldraw/tlschema';
import { Vec2d } from '@tldraw/primitives'; import { Vec2d } from '@tldraw/primitives';
import { Vec2dModel } from '@tldraw/tlschema'; import { Vec2dModel } from '@tldraw/tlschema';
import { VecLike } from '@tldraw/primitives'; import { VecLike } from '@tldraw/primitives';
@ -286,8 +287,11 @@ export class App extends EventEmitter<TLEventMap> {
getShapesAndDescendantsInOrder(ids: TLShapeId[]): TLShape[]; getShapesAndDescendantsInOrder(ids: TLShapeId[]): TLShape[];
getShapesAtPoint(point: VecLike): TLShape[]; getShapesAtPoint(point: VecLike): TLShape[];
getShapesInPage(pageId: TLPageId): TLShape[]; getShapesInPage(pageId: TLPageId): TLShape[];
getShapeUtil<T extends TLShape = TLShape>(shape: T): TLShapeUtil<T>; getShapeUtil<C extends {
getShapeUtilByDef<Def extends TLShapeDef<any, any>>(def: Def): ReturnType<Def['createShapeUtils']>; new (...args: any[]): TLShapeUtil<any>;
type: string;
}>(util: C): InstanceType<C>;
getShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): TLShapeUtil<S>;
getSortedChildIds(parentId: TLParentId): TLShapeId[]; getSortedChildIds(parentId: TLParentId): TLShapeId[];
getStateDescendant(path: string): StateNode | undefined; getStateDescendant(path: string): StateNode | undefined;
getStrokeWidth(id: TLSizeStyle['id']): number; getStrokeWidth(id: TLSizeStyle['id']): number;
@ -359,6 +363,10 @@ export class App extends EventEmitter<TLEventMap> {
isSelected(id: TLShapeId): boolean; isSelected(id: TLShapeId): boolean;
isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean; isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean;
isShapeInViewport(id: TLShapeId): boolean; isShapeInViewport(id: TLShapeId): boolean;
isShapeOfType<T extends TLUnknownShape>(shape: TLUnknownShape, util: {
new (...args: any): TLShapeUtil<T>;
type: string;
}): shape is T;
// (undocumented) // (undocumented)
get isSnapMode(): boolean; get isSnapMode(): boolean;
// (undocumented) // (undocumented)
@ -375,7 +383,7 @@ export class App extends EventEmitter<TLEventMap> {
title: string; title: string;
description: string; description: string;
}>; }>;
get onlySelectedShape(): TLBaseShape<any, any> | null; get onlySelectedShape(): null | TLShape;
get openMenus(): string[]; get openMenus(): string[];
packShapes(ids?: TLShapeId[], padding?: number): this; packShapes(ids?: TLShapeId[], padding?: number): this;
get pages(): TLPage[]; get pages(): TLPage[];
@ -436,7 +444,7 @@ export class App extends EventEmitter<TLEventMap> {
get selectedIds(): TLShapeId[]; get selectedIds(): TLShapeId[];
get selectedIdsSet(): ReadonlySet<TLShapeId>; get selectedIdsSet(): ReadonlySet<TLShapeId>;
get selectedPageBounds(): Box2d | null; get selectedPageBounds(): Box2d | null;
get selectedShapes(): TLBaseShape<any, any>[]; get selectedShapes(): TLShape[];
// (undocumented) // (undocumented)
get selectionBounds(): Box2d | undefined; get selectionBounds(): Box2d | undefined;
// (undocumented) // (undocumented)
@ -652,14 +660,6 @@ export function defaultEmptyAs(str: string, dflt: string): string;
// @internal (undocumented) // @internal (undocumented)
export const DefaultErrorFallback: TLErrorFallback; export const DefaultErrorFallback: TLErrorFallback;
// @public (undocumented)
export function defineShape<ShapeType extends TLUnknownShape, ShapeUtil extends TLShapeUtil<ShapeType> = TLShapeUtil<ShapeType>>({ type, getShapeUtil, validator, migrations, }: {
type: ShapeType['type'];
getShapeUtil: () => TLShapeUtilConstructor<ShapeType, ShapeUtil>;
validator?: StoreValidator<ShapeType>;
migrations?: Migrations;
}): TLShapeDef<ShapeType, ShapeUtil>;
// @internal (undocumented) // @internal (undocumented)
export const DOUBLE_CLICK_DURATION = 450; export const DOUBLE_CLICK_DURATION = 450;
@ -805,7 +805,7 @@ export function getRotationSnapshot({ app }: {
initialCursorAngle: number; initialCursorAngle: number;
initialSelectionRotation: number; initialSelectionRotation: number;
shapeSnapshots: { shapeSnapshots: {
shape: TLBaseShape<any, any>; shape: TLShape;
initialPagePoint: Vec2d; initialPagePoint: Vec2d;
}[]; }[];
}; };
@ -1580,9 +1580,6 @@ export const TEXT_PROPS: {
maxWidth: string; maxWidth: string;
}; };
// @public (undocumented)
export const TLArrowShapeDef: TLShapeDef<TLArrowShape, TLArrowUtil>;
// @public (undocumented) // @public (undocumented)
export class TLArrowUtil extends TLShapeUtil<TLArrowShape> { export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
// (undocumented) // (undocumented)
@ -1657,9 +1654,6 @@ export interface TLBaseEventInfo {
type: UiEventType; type: UiEventType;
} }
// @public (undocumented)
export const TLBookmarkShapeDef: TLShapeDef<TLBookmarkShape, TLBookmarkUtil>;
// @public (undocumented) // @public (undocumented)
export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> { export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
// (undocumented) // (undocumented)
@ -1799,12 +1793,7 @@ export function TldrawEditor(props: TldrawEditorProps): JSX.Element;
// @public (undocumented) // @public (undocumented)
export class TldrawEditorConfig { export class TldrawEditorConfig {
constructor(args: { constructor(opts: TldrawEditorConfigOptions);
shapes?: readonly TLShapeDef<any, any>[];
tools?: readonly StateNodeConstructor[];
allowUnknownShapes?: boolean;
derivePresenceState?: (store: TLStore) => Signal<null | TLInstancePresence>;
});
// (undocumented) // (undocumented)
createStore(config: { createStore(config: {
initialData?: StoreSnapshot<TLRecord>; initialData?: StoreSnapshot<TLRecord>;
@ -1814,7 +1803,11 @@ export class TldrawEditorConfig {
// (undocumented) // (undocumented)
static readonly default: TldrawEditorConfig; static readonly default: TldrawEditorConfig;
// (undocumented) // (undocumented)
readonly shapes: readonly TLUnknownShapeDef[]; readonly shapeMigrations: MigrationsForShapes<TLShape>;
// (undocumented)
readonly shapeUtils: UtilsForShapes<TLShape>;
// (undocumented)
readonly shapeValidators: ValidatorsForShapes<TLShape>;
// (undocumented) // (undocumented)
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>; readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>;
// (undocumented) // (undocumented)
@ -1844,9 +1837,6 @@ export interface TldrawEditorProps {
userId?: TLUserId; userId?: TLUserId;
} }
// @public (undocumented)
export const TLDrawShapeDef: TLShapeDef<TLDrawShape, TLDrawUtil>;
// @public (undocumented) // @public (undocumented)
export class TLDrawUtil extends TLShapeUtil<TLDrawShape> { export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
// (undocumented) // (undocumented)
@ -1928,9 +1918,6 @@ export interface TLEditorComponents {
ZoomBrush: null | TLBrushComponent; ZoomBrush: null | TLBrushComponent;
} }
// @public (undocumented)
export const TLEmbedShapeDef: TLShapeDef<TLEmbedShape, TLEmbedUtil>;
// @public (undocumented) // @public (undocumented)
export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> { export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
// (undocumented) // (undocumented)
@ -2048,9 +2035,6 @@ export type TLEventName = 'cancel' | 'complete' | 'interrupt' | 'wheel' | TLCLic
// @public (undocumented) // @public (undocumented)
export type TLExportType = 'jpeg' | 'json' | 'png' | 'svg' | 'webp'; export type TLExportType = 'jpeg' | 'json' | 'png' | 'svg' | 'webp';
// @public (undocumented)
export const TLFrameShapeDef: TLShapeDef<TLFrameShape, TLFrameUtil>;
// @public (undocumented) // @public (undocumented)
export class TLFrameUtil extends TLBoxUtil<TLFrameShape> { export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
// (undocumented) // (undocumented)
@ -2081,9 +2065,6 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
static type: string; static type: string;
} }
// @public (undocumented)
export const TLGeoShapeDef: TLShapeDef<TLGeoShape, TLGeoUtil>;
// @public (undocumented) // @public (undocumented)
export class TLGeoUtil extends TLBoxUtil<TLGeoShape> { export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
// (undocumented) // (undocumented)
@ -2200,9 +2181,6 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
static type: string; static type: string;
} }
// @public (undocumented)
export const TLGroupShapeDef: TLShapeDef<TLGroupShape, TLGroupUtil>;
// @public (undocumented) // @public (undocumented)
export class TLGroupUtil extends TLShapeUtil<TLGroupShape> { export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
// (undocumented) // (undocumented)
@ -2232,9 +2210,6 @@ export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
// @public (undocumented) // @public (undocumented)
export type TLHistoryEntry = TLCommand | TLMark; export type TLHistoryEntry = TLCommand | TLMark;
// @public (undocumented)
export const TLImageShapeDef: TLShapeDef<TLImageShape, TLImageUtil>;
// @public (undocumented) // @public (undocumented)
export class TLImageUtil extends TLBoxUtil<TLImageShape> { export class TLImageUtil extends TLBoxUtil<TLImageShape> {
// (undocumented) // (undocumented)
@ -2280,9 +2255,6 @@ export type TLKeyboardEventInfo = TLBaseEventInfo & {
// @public (undocumented) // @public (undocumented)
export type TLKeyboardEventName = 'key_down' | 'key_repeat' | 'key_up'; export type TLKeyboardEventName = 'key_down' | 'key_repeat' | 'key_up';
// @public (undocumented)
export const TLLineShapeDef: TLShapeDef<TLLineShape, TLLineUtil>;
// @public (undocumented) // @public (undocumented)
export class TLLineUtil extends TLShapeUtil<TLLineShape> { export class TLLineUtil extends TLShapeUtil<TLLineShape> {
// (undocumented) // (undocumented)
@ -2331,9 +2303,6 @@ export type TLMark = {
onRedo: boolean; onRedo: boolean;
}; };
// @public (undocumented)
export const TLNoteShapeDef: TLShapeDef<TLNoteShape, TLNoteUtil>;
// @public (undocumented) // @public (undocumented)
export class TLNoteUtil extends TLShapeUtil<TLNoteShape> { export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
// (undocumented) // (undocumented)
@ -2478,21 +2447,7 @@ export type TLResizeMode = 'resize_bounds' | 'scale_shape';
export type TLSelectionHandle = RotateCorner | SelectionCorner | SelectionEdge; export type TLSelectionHandle = RotateCorner | SelectionCorner | SelectionEdge;
// @public (undocumented) // @public (undocumented)
export interface TLShapeDef<ShapeType extends TLUnknownShape, ShapeUtil extends TLShapeUtil<ShapeType> = TLShapeUtil<ShapeType>> { export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
// (undocumented)
readonly createShapeUtils: (app: App) => ShapeUtil;
// (undocumented)
readonly is: (shape: TLUnknownShape) => shape is ShapeType;
// (undocumented)
readonly migrations: Migrations;
// (undocumented)
readonly type: ShapeType['type'];
// (undocumented)
readonly validator?: StoreValidator<ShapeType>;
}
// @public (undocumented)
export abstract class TLShapeUtil<T extends TLUnknownShape> {
constructor(app: App, type: T['type']); constructor(app: App, type: T['type']);
// (undocumented) // (undocumented)
app: App; app: App;
@ -2558,6 +2513,8 @@ export abstract class TLShapeUtil<T extends TLUnknownShape> {
transform(shape: T): Matrix2d; transform(shape: T): Matrix2d;
// (undocumented) // (undocumented)
readonly type: T['type']; readonly type: T['type'];
// (undocumented)
static type: string;
} }
// @public (undocumented) // @public (undocumented)
@ -2569,9 +2526,6 @@ export interface TLShapeUtilConstructor<T extends TLUnknownShape, ShapeUtil exte
// @public (undocumented) // @public (undocumented)
export type TLShapeUtilFlag<T> = (shape: T) => boolean; export type TLShapeUtilFlag<T> = (shape: T) => boolean;
// @public (undocumented)
export const TLTextShapeDef: TLShapeDef<TLTextShape, TLTextUtil>;
// @public (undocumented) // @public (undocumented)
export class TLTextUtil extends TLShapeUtil<TLTextShape> { export class TLTextUtil extends TLShapeUtil<TLTextShape> {
// (undocumented) // (undocumented)
@ -2660,12 +2614,6 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
// @public (undocumented) // @public (undocumented)
export type TLTickEvent = (elapsed: number) => void; export type TLTickEvent = (elapsed: number) => void;
// @public (undocumented)
export type TLUnknownShapeDef = TLShapeDef<TLUnknownShape, TLShapeUtil<TLUnknownShape>>;
// @public (undocumented)
export const TLVideoShapeDef: TLShapeDef<TLVideoShape, TLVideoUtil>;
// @public (undocumented) // @public (undocumented)
export class TLVideoUtil extends TLBoxUtil<TLVideoShape> { export class TLVideoUtil extends TLBoxUtil<TLVideoShape> {
// (undocumented) // (undocumented)

View file

@ -27,24 +27,17 @@ export {
type AppOptions, type AppOptions,
type TLChange, type TLChange,
} from './lib/app/App' } from './lib/app/App'
export { TLArrowShapeDef, TLArrowUtil } from './lib/app/shapeutils/TLArrowUtil/TLArrowUtil' export { TLArrowUtil } from './lib/app/shapeutils/TLArrowUtil/TLArrowUtil'
export { export { TLBookmarkUtil } from './lib/app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
TLBookmarkShapeDef,
TLBookmarkUtil,
} from './lib/app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
export { TLBoxUtil } from './lib/app/shapeutils/TLBoxUtil' export { TLBoxUtil } from './lib/app/shapeutils/TLBoxUtil'
export { TLDrawShapeDef, TLDrawUtil } from './lib/app/shapeutils/TLDrawUtil/TLDrawUtil' export { TLDrawUtil } from './lib/app/shapeutils/TLDrawUtil/TLDrawUtil'
export { TLEmbedShapeDef, TLEmbedUtil } from './lib/app/shapeutils/TLEmbedUtil/TLEmbedUtil' export { TLEmbedUtil } from './lib/app/shapeutils/TLEmbedUtil/TLEmbedUtil'
export { TLFrameShapeDef, TLFrameUtil } from './lib/app/shapeutils/TLFrameUtil/TLFrameUtil' export { TLFrameUtil } from './lib/app/shapeutils/TLFrameUtil/TLFrameUtil'
export { TLGeoShapeDef, TLGeoUtil } from './lib/app/shapeutils/TLGeoUtil/TLGeoUtil' export { TLGeoUtil } from './lib/app/shapeutils/TLGeoUtil/TLGeoUtil'
export { TLGroupShapeDef, TLGroupUtil } from './lib/app/shapeutils/TLGroupUtil/TLGroupUtil' export { TLGroupUtil } from './lib/app/shapeutils/TLGroupUtil/TLGroupUtil'
export { TLImageShapeDef, TLImageUtil } from './lib/app/shapeutils/TLImageUtil/TLImageUtil' export { TLImageUtil } from './lib/app/shapeutils/TLImageUtil/TLImageUtil'
export { export { TLLineUtil, getSplineForLineShape } from './lib/app/shapeutils/TLLineUtil/TLLineUtil'
TLLineShapeDef, export { TLNoteUtil } from './lib/app/shapeutils/TLNoteUtil/TLNoteUtil'
TLLineUtil,
getSplineForLineShape,
} from './lib/app/shapeutils/TLLineUtil/TLLineUtil'
export { TLNoteShapeDef, TLNoteUtil } from './lib/app/shapeutils/TLNoteUtil/TLNoteUtil'
export { export {
TLShapeUtil, TLShapeUtil,
type OnBeforeCreateHandler, type OnBeforeCreateHandler,
@ -71,8 +64,8 @@ export {
type TLShapeUtilConstructor, type TLShapeUtilConstructor,
type TLShapeUtilFlag, type TLShapeUtilFlag,
} from './lib/app/shapeutils/TLShapeUtil' } from './lib/app/shapeutils/TLShapeUtil'
export { INDENT, TLTextShapeDef, TLTextUtil } from './lib/app/shapeutils/TLTextUtil/TLTextUtil' export { INDENT, TLTextUtil } from './lib/app/shapeutils/TLTextUtil/TLTextUtil'
export { TLVideoShapeDef, TLVideoUtil } from './lib/app/shapeutils/TLVideoUtil/TLVideoUtil' export { TLVideoUtil } from './lib/app/shapeutils/TLVideoUtil/TLVideoUtil'
export { StateNode, type StateNodeConstructor } from './lib/app/statechart/StateNode' export { StateNode, type StateNodeConstructor } from './lib/app/statechart/StateNode'
export { TLBoxTool, type TLBoxLike } from './lib/app/statechart/TLBoxTool/TLBoxTool' export { TLBoxTool, type TLBoxLike } from './lib/app/statechart/TLBoxTool/TLBoxTool'
export { type ClipboardPayload, type TLClipboardModel } from './lib/app/types/clipboard-types' export { type ClipboardPayload, type TLClipboardModel } from './lib/app/types/clipboard-types'
@ -138,11 +131,6 @@ export {
type ReadySyncedStore, type ReadySyncedStore,
type SyncedStore, type SyncedStore,
} from './lib/config/SyncedStore' } from './lib/config/SyncedStore'
export {
defineShape,
type TLShapeDef,
type TLUnknownShapeDef,
} from './lib/config/TLShapeDefinition'
export { TldrawEditorConfig } from './lib/config/TldrawEditorConfig' export { TldrawEditorConfig } from './lib/config/TldrawEditorConfig'
export { export {
ANIMATION_MEDIUM_MS, ANIMATION_MEDIUM_MS,

View file

@ -76,7 +76,6 @@ import {
import { EventEmitter } from 'eventemitter3' import { EventEmitter } from 'eventemitter3'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { EMPTY_ARRAY, atom, computed, transact } from 'signia' import { EMPTY_ARRAY, atom, computed, transact } from 'signia'
import { TLShapeDef } from '../config/TLShapeDefinition'
import { TldrawEditorConfig } from '../config/TldrawEditorConfig' import { TldrawEditorConfig } from '../config/TldrawEditorConfig'
import { import {
ANIMATION_MEDIUM_MS, ANIMATION_MEDIUM_MS,
@ -118,17 +117,17 @@ import { HistoryManager } from './managers/HistoryManager'
import { SnapManager } from './managers/SnapManager' import { SnapManager } from './managers/SnapManager'
import { TextManager } from './managers/TextManager' import { TextManager } from './managers/TextManager'
import { TickManager } from './managers/TickManager' import { TickManager } from './managers/TickManager'
import { TLArrowShapeDef } from './shapeutils/TLArrowUtil/TLArrowUtil' import { TLArrowUtil } from './shapeutils/TLArrowUtil/TLArrowUtil'
import { getCurvedArrowInfo } from './shapeutils/TLArrowUtil/arrow/curved-arrow' import { getCurvedArrowInfo } from './shapeutils/TLArrowUtil/arrow/curved-arrow'
import { import {
getArrowTerminalsInArrowSpace, getArrowTerminalsInArrowSpace,
getIsArrowStraight, getIsArrowStraight,
} from './shapeutils/TLArrowUtil/arrow/shared' } from './shapeutils/TLArrowUtil/arrow/shared'
import { getStraightArrowInfo } from './shapeutils/TLArrowUtil/arrow/straight-arrow' import { getStraightArrowInfo } from './shapeutils/TLArrowUtil/arrow/straight-arrow'
import { TLFrameShapeDef } from './shapeutils/TLFrameUtil/TLFrameUtil' import { TLFrameUtil } from './shapeutils/TLFrameUtil/TLFrameUtil'
import { TLGroupShapeDef } from './shapeutils/TLGroupUtil/TLGroupUtil' import { TLGroupUtil } from './shapeutils/TLGroupUtil/TLGroupUtil'
import { TLResizeMode, TLShapeUtil } from './shapeutils/TLShapeUtil' import { TLResizeMode, TLShapeUtil } from './shapeutils/TLShapeUtil'
import { TLTextShapeDef } from './shapeutils/TLTextUtil/TLTextUtil' import { TLTextUtil } from './shapeutils/TLTextUtil/TLTextUtil'
import { TLExportColors } from './shapeutils/shared/TLExportColors' import { TLExportColors } from './shapeutils/shared/TLExportColors'
import { RootState } from './statechart/RootState' import { RootState } from './statechart/RootState'
import { StateNode } from './statechart/StateNode' import { StateNode } from './statechart/StateNode'
@ -192,10 +191,7 @@ export class App extends EventEmitter<TLEventMap> {
// Set the shape utils // Set the shape utils
this.shapeUtils = Object.fromEntries( this.shapeUtils = Object.fromEntries(
config.shapes.map((def) => [ Object.entries(config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)])
def.type,
def.createShapeUtils(this) as TLShapeUtil<TLUnknownShape>,
])
) )
if (typeof window !== 'undefined' && 'navigator' in window) { if (typeof window !== 'undefined' && 'navigator' in window) {
@ -243,7 +239,7 @@ export class App extends EventEmitter<TLEventMap> {
this._updateDepth-- this._updateDepth--
} }
this.store.onAfterCreate = (record) => { this.store.onAfterCreate = (record) => {
if (record.typeName === 'shape' && TLArrowShapeDef.is(record)) { if (record.typeName === 'shape' && this.isShapeOfType(record, TLArrowUtil)) {
this._arrowDidUpdate(record) this._arrowDidUpdate(record)
} }
} }
@ -934,38 +930,42 @@ export class App extends EventEmitter<TLEventMap> {
*/ */
shapeUtils: { readonly [K in string]?: TLShapeUtil<TLUnknownShape> } shapeUtils: { readonly [K in string]?: TLShapeUtil<TLUnknownShape> }
/**
* Get a shape util for a given shape or shape type.
*
* @example
*
* ```ts
* app.getShapeUtil(myBoxShape)
* ```
*
* @param type - The shape type.
* @public
*/
getShapeUtil<T extends TLShape = TLShape>(shape: T): TLShapeUtil<T> {
return this.shapeUtils[shape.type] as any as TLShapeUtil<T>
}
/** /**
* Get a shape util by its definition. * Get a shape util by its definition.
* *
* @example * @example
* *
* ```ts * ```ts
* app.getShapeUtilByDef(TLDrawShapeDef) * app.getShapeUtil(TLArrowUtil)
* ``` * ```
* *
* @param def - The shape definition. * @param util - The shape util.
* @public * @public
*/ */
getShapeUtilByDef<Def extends TLShapeDef<any, any>>( getShapeUtil<C extends { new (...args: any[]): TLShapeUtil<any>; type: string }>(
def: Def util: C
): ReturnType<Def['createShapeUtils']> { ): InstanceType<C>
return this.shapeUtils[def.type] as ReturnType<Def['createShapeUtils']> /**
* Get a shape util from a shape itself.
*
* @example
*
* ```ts
* const util = app.getShapeUtil(myShape)
* const util = app.getShapeUtil<TLArrowUtil>(myShape)
* const util = app.getShapeUtil(TLArrowUtil)
* ```
*
* @param shape - A shape or shape partial.
* @public
*/
getShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): TLShapeUtil<S>
getShapeUtil<T extends TLShapeUtil>({
type,
}: {
type: T extends TLShapeUtil<infer R> ? R['type'] : string
}): T {
return this.shapeUtils[type] as T
} }
/** /**
@ -1183,6 +1183,7 @@ export class App extends EventEmitter<TLEventMap> {
}, },
]) ])
} }
for (const parentId of this._invalidParents) { for (const parentId of this._invalidParents) {
this._invalidParents.delete(parentId) this._invalidParents.delete(parentId)
const parent = this.getShapeById(parentId) const parent = this.getShapeById(parentId)
@ -1213,7 +1214,7 @@ export class App extends EventEmitter<TLEventMap> {
/** @internal */ /** @internal */
private _reparentArrow(arrowId: TLShapeId) { private _reparentArrow(arrowId: TLShapeId) {
const arrow = this.getShapeById(arrowId) as TLArrowShape | undefined const arrow = this.getShapeById<TLArrowShape>(arrowId)
if (!arrow) return if (!arrow) return
const { start, end } = arrow.props const { start, end } = arrow.props
const startShape = start.type === 'binding' ? this.getShapeById(start.boundShapeId) : undefined const startShape = start.type === 'binding' ? this.getShapeById(start.boundShapeId) : undefined
@ -1237,7 +1238,8 @@ export class App extends EventEmitter<TLEventMap> {
this.reparentShapesById([arrowId], nextParentId) this.reparentShapesById([arrowId], nextParentId)
} }
const reparentedArrow = this.getShapeById(arrowId) as TLArrowShape const reparentedArrow = this.getShapeById<TLArrowShape>(arrowId)
if (!reparentedArrow) throw Error('no reparented arrow')
const startSibling = this.getNearestSiblingShape(reparentedArrow, startShape) const startSibling = this.getNearestSiblingShape(reparentedArrow, startShape)
const endSibling = this.getNearestSiblingShape(reparentedArrow, endShape) const endSibling = this.getNearestSiblingShape(reparentedArrow, endShape)
@ -1303,6 +1305,7 @@ export class App extends EventEmitter<TLEventMap> {
// const update = this.getShapeUtil(next).onUpdate?.(prev, next) // const update = this.getShapeUtil(next).onUpdate?.(prev, next)
// return update ?? next // return update ?? next
// } // }
@computed @computed
private get _allPageStates() { private get _allPageStates() {
return this.store.query.records('instance_page_state') return this.store.query.records('instance_page_state')
@ -1400,7 +1403,7 @@ export class App extends EventEmitter<TLEventMap> {
/** @internal */ /** @internal */
private _shapeDidChange(prev: TLShape, next: TLShape) { private _shapeDidChange(prev: TLShape, next: TLShape) {
if (TLArrowShapeDef.is(next)) { if (this.isShapeOfType(next, TLArrowUtil)) {
this._arrowDidUpdate(next) this._arrowDidUpdate(next)
} }
@ -3019,8 +3022,8 @@ export class App extends EventEmitter<TLEventMap> {
* @readonly * @readonly
* @public * @public
*/ */
@computed get shapesArray() { @computed get shapesArray(): TLShape[] {
return Array.from(this.shapeIds).map((id) => this.store.get(id)! as TLShape) return Array.from(this.shapeIds).map((id) => this.store.get(id)!)
} }
/** /**
@ -3074,7 +3077,7 @@ export class App extends EventEmitter<TLEventMap> {
* @public * @public
* @readonly * @readonly
*/ */
@computed get selectedShapes() { @computed get selectedShapes(): TLShape[] {
const { selectedIds } = this.pageState const { selectedIds } = this.pageState
return compact(selectedIds.map((id) => this.store.get(id))) return compact(selectedIds.map((id) => this.store.get(id)))
} }
@ -3093,11 +3096,32 @@ export class App extends EventEmitter<TLEventMap> {
* @public * @public
* @readonly * @readonly
*/ */
@computed get onlySelectedShape() { @computed get onlySelectedShape(): TLShape | null {
const { selectedShapes } = this const { selectedShapes } = this
return selectedShapes.length === 1 ? selectedShapes[0] : null return selectedShapes.length === 1 ? selectedShapes[0] : null
} }
/**
* Get whether a shape matches the type of a TLShapeUtil.
*
* @example
*
* ```ts
* const isArrowShape = isShapeOfType(someShape, TLArrowUtil)
* ```
*
* @param util - the TLShapeUtil constructor to test against
* @param shape - the shape to test
*
* @public
*/
isShapeOfType<T extends TLUnknownShape>(
shape: TLUnknownShape,
util: { new (...args: any): TLShapeUtil<T>; type: string }
): shape is T {
return shape.type === util.type
}
/** /**
* Get a shape by its id. * Get a shape by its id.
* *
@ -4024,12 +4048,12 @@ export class App extends EventEmitter<TLEventMap> {
let shapes = dedupe( let shapes = dedupe(
ids ids
.map((id) => this.getShapeById(id) as TLShape) .map((id) => this.getShapeById(id)!)
.sort(sortByIndex) .sort(sortByIndex)
.flatMap((shape) => { .flatMap((shape) => {
const allShapes = [shape] const allShapes = [shape]
this.visitDescendants(shape.id, (descendant) => { this.visitDescendants(shape.id, (descendant) => {
allShapes.push(this.getShapeById(descendant) as TLShape) allShapes.push(this.getShapeById(descendant)!)
}) })
return allShapes return allShapes
}) })
@ -4040,14 +4064,14 @@ export class App extends EventEmitter<TLEventMap> {
shape = structuredClone(shape) as typeof shape shape = structuredClone(shape) as typeof shape
if (TLArrowShapeDef.is(shape)) { if (this.isShapeOfType(shape, TLArrowUtil)) {
const startBindingId = const startBindingId =
shape.props.start.type === 'binding' ? shape.props.start.boundShapeId : undefined shape.props.start.type === 'binding' ? shape.props.start.boundShapeId : undefined
const endBindingId = const endBindingId =
shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : undefined shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : undefined
const info = this.getShapeUtilByDef(TLArrowShapeDef).getArrowInfo(shape) const info = this.getShapeUtil(TLArrowUtil).getArrowInfo(shape)
if (shape.props.start.type === 'binding') { if (shape.props.start.type === 'binding') {
if (!shapes.some((s) => s.id === startBindingId)) { if (!shapes.some((s) => s.id === startBindingId)) {
@ -4223,8 +4247,8 @@ export class App extends EventEmitter<TLEventMap> {
if (rootShapeIds.length === 1) { if (rootShapeIds.length === 1) {
const rootShape = shapes.find((s) => s.id === rootShapeIds[0])! const rootShape = shapes.find((s) => s.id === rootShapeIds[0])!
if ( if (
TLFrameShapeDef.is(parent) && this.isShapeOfType(parent, TLFrameUtil) &&
TLFrameShapeDef.is(rootShape) && this.isShapeOfType(rootShape, TLFrameUtil) &&
rootShape.props.w === parent?.props.w && rootShape.props.w === parent?.props.w &&
rootShape.props.h === parent?.props.h rootShape.props.h === parent?.props.h
) { ) {
@ -4249,7 +4273,7 @@ export class App extends EventEmitter<TLEventMap> {
const rootShapes: TLShape[] = [] const rootShapes: TLShape[] = []
const newShapes: TLShapePartial[] = shapes.map((shape): TLShape => { const newShapes: TLShape[] = shapes.map((shape): TLShape => {
let newShape: TLShape let newShape: TLShape
if (preserveIds) { if (preserveIds) {
@ -4280,7 +4304,7 @@ export class App extends EventEmitter<TLEventMap> {
index = getIndexAbove(index) index = getIndexAbove(index)
} }
if (TLArrowShapeDef.is(newShape)) { if (this.isShapeOfType(newShape, TLArrowUtil)) {
if (newShape.props.start.type === 'binding') { if (newShape.props.start.type === 'binding') {
const mappedId = idMap.get(newShape.props.start.boundShapeId) const mappedId = idMap.get(newShape.props.start.boundShapeId)
newShape.props.start = mappedId newShape.props.start = mappedId
@ -4374,7 +4398,7 @@ export class App extends EventEmitter<TLEventMap> {
} }
for (let i = 0; i < newShapes.length; i++) { for (let i = 0; i < newShapes.length; i++) {
const shape = newShapes[i] as TLShape const shape = newShapes[i]
const result = this.store.schema.migratePersistedRecord(shape, content.schema) const result = this.store.schema.migratePersistedRecord(shape, content.schema)
if (result.type === 'success') { if (result.type === 'success') {
newShapes[i] = result.value as TLShape newShapes[i] = result.value as TLShape
@ -4435,7 +4459,7 @@ export class App extends EventEmitter<TLEventMap> {
while ( while (
this.getShapesAtPoint(point).some( this.getShapesAtPoint(point).some(
(shape) => (shape) =>
TLFrameShapeDef.is(shape) && this.isShapeOfType(shape, TLFrameUtil) &&
shape.props.w === onlyRoot.props.w && shape.props.w === onlyRoot.props.w &&
shape.props.h === onlyRoot.props.h shape.props.h === onlyRoot.props.h
) )
@ -4589,7 +4613,7 @@ export class App extends EventEmitter<TLEventMap> {
const shapeRecordsToCreate: TLShape[] = [] const shapeRecordsToCreate: TLShape[] = []
for (const partial of partials) { for (const partial of partials) {
const util = this.getShapeUtil(partial as TLShape) const util = this.getShapeUtil(partial)
// If an index is not explicitly provided, then add the // If an index is not explicitly provided, then add the
// shapes to the top of their parents' children; using the // shapes to the top of their parents' children; using the
@ -4850,7 +4874,7 @@ export class App extends EventEmitter<TLEventMap> {
return newRecord ?? prev return newRecord ?? prev
}) })
) as TLShape[] )
const updates = Object.fromEntries(updated.map((shape) => [shape.id, shape])) const updates = Object.fromEntries(updated.map((shape) => [shape.id, shape]))
@ -5535,9 +5559,9 @@ export class App extends EventEmitter<TLEventMap> {
/* ------------------- SubCommands ------------------ */ /* ------------------- SubCommands ------------------ */
async getSvg( async getSvg(
ids: TLShapeId[] = (this.selectedIds.length ids: TLShapeId[] = this.selectedIds.length
? this.selectedIds ? this.selectedIds
: Object.keys(this.shapeIds)) as TLShapeId[], : (Object.keys(this.shapeIds) as TLShapeId[]),
opts = {} as Partial<{ opts = {} as Partial<{
scale: number scale: number
background: boolean background: boolean
@ -6152,15 +6176,17 @@ export class App extends EventEmitter<TLEventMap> {
if (!shapes.length) return this if (!shapes.length) return this
shapes = shapes shapes = compact(
.map((shape) => { shapes
if (shape.type === 'group') { .map((shape) => {
return this.getSortedChildIds(shape.id).map((id) => this.getShapeById(id)) if (shape.type === 'group') {
} return this.getSortedChildIds(shape.id).map((id) => this.getShapeById(id))
}
return shape return shape
}) })
.flat() as TLShape[] .flat()
)
const scaleOriginPage = Box2d.Common(compact(shapes.map((id) => this.getPageBounds(id)))).center const scaleOriginPage = Box2d.Common(compact(shapes.map((id) => this.getPageBounds(id)))).center
@ -6214,7 +6240,7 @@ export class App extends EventEmitter<TLEventMap> {
const shapes = compact(ids.map((id) => this.getShapeById(id))).filter((shape) => { const shapes = compact(ids.map((id) => this.getShapeById(id))).filter((shape) => {
if (!shape) return false if (!shape) return false
if (TLArrowShapeDef.is(shape)) { if (this.isShapeOfType(shape, TLArrowUtil)) {
if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') { if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') {
return false return false
} }
@ -6346,7 +6372,7 @@ export class App extends EventEmitter<TLEventMap> {
.filter((shape) => { .filter((shape) => {
if (!shape) return false if (!shape) return false
if (TLArrowShapeDef.is(shape)) { if (this.isShapeOfType(shape, TLArrowUtil)) {
if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') { if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') {
return false return false
} }
@ -6462,7 +6488,7 @@ export class App extends EventEmitter<TLEventMap> {
const translateStartChange = this.getShapeUtil(shape).onTranslateStart?.({ const translateStartChange = this.getShapeUtil(shape).onTranslateStart?.({
...shape, ...shape,
...change, ...change,
} as TLShape) })
if (translateStartChange) { if (translateStartChange) {
changes.push({ ...change, ...translateStartChange }) changes.push({ ...change, ...translateStartChange })
@ -7567,8 +7593,8 @@ export class App extends EventEmitter<TLEventMap> {
let newShape: TLShape = deepCopy(shape) let newShape: TLShape = deepCopy(shape)
if (TLArrowShapeDef.is(shape) && TLArrowShapeDef.is(newShape)) { if (this.isShapeOfType(shape, TLArrowUtil) && this.isShapeOfType(newShape, TLArrowUtil)) {
const info = this.getShapeUtilByDef(TLArrowShapeDef).getArrowInfo(shape) const info = this.getShapeUtil(TLArrowUtil).getArrowInfo(shape)
let newStartShapeId: TLShapeId | undefined = undefined let newStartShapeId: TLShapeId | undefined = undefined
let newEndShapeId: TLShapeId | undefined = undefined let newEndShapeId: TLShapeId | undefined = undefined
@ -7767,7 +7793,7 @@ export class App extends EventEmitter<TLEventMap> {
id: shape.id, id: shape.id,
type: shape.type, type: shape.type,
props, props,
} as TLShape }
}), }),
ephemeral ephemeral
) )
@ -7790,7 +7816,7 @@ export class App extends EventEmitter<TLEventMap> {
if (boundsA.width !== boundsB.width) { if (boundsA.width !== boundsB.width) {
didChange = true didChange = true
if (TLTextShapeDef.is(shape)) { if (this.isShapeOfType(shape, TLTextUtil)) {
switch (shape.props.align) { switch (shape.props.align) {
case 'middle': { case 'middle': {
change.x = currentShape.x + (boundsA.width - boundsB.width) / 2 change.x = currentShape.x + (boundsA.width - boundsB.width) / 2
@ -8848,7 +8874,7 @@ export class App extends EventEmitter<TLEventMap> {
const groups: TLGroupShape[] = [] const groups: TLGroupShape[] = []
shapes.forEach((shape) => { shapes.forEach((shape) => {
if (TLGroupShapeDef.is(shape)) { if (this.isShapeOfType(shape, TLGroupUtil)) {
groups.push(shape) groups.push(shape)
} else { } else {
idsToSelect.add(shape.id) idsToSelect.add(shape.id)
@ -8858,10 +8884,10 @@ export class App extends EventEmitter<TLEventMap> {
if (groups.length === 0) return this if (groups.length === 0) return this
this.batch(() => { this.batch(() => {
let group: TLShape let group: TLGroupShape
for (let i = 0, n = groups.length; i < n; i++) { for (let i = 0, n = groups.length; i < n; i++) {
group = groups[i] as TLGroupShape group = groups[i]
const childIds = this.getSortedChildIds(group.id) const childIds = this.getSortedChildIds(group.id)
for (let j = 0, n = childIds.length; j < n; j++) { for (let j = 0, n = childIds.length; j < n; j++) {

View file

@ -1,11 +1,15 @@
import { TLArrowShape, TLShape, TLShapeId, TLStore } from '@tldraw/tlschema' import { TLArrowShape, TLShape, TLShapeId, TLStore } from '@tldraw/tlschema'
import { Computed, RESET_VALUE, computed, isUninitialized } from 'signia' import { Computed, RESET_VALUE, computed, isUninitialized } from 'signia'
import { TLArrowShapeDef } from '../shapeutils/TLArrowUtil/TLArrowUtil'
export type TLArrowBindingsIndex = Record< export type TLArrowBindingsIndex = Record<
TLShapeId, TLShapeId,
undefined | { arrowId: TLShapeId; handleId: 'start' | 'end' }[] undefined | { arrowId: TLShapeId; handleId: 'start' | 'end' }[]
> >
function isArrowType(shape: any): shape is TLArrowShape {
return shape.type === 'arrow'
}
export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsIndex> => { export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsIndex> => {
const shapeHistory = store.query.filterHistory('shape') const shapeHistory = store.query.filterHistory('shape')
const arrowQuery = store.query.records('shape', () => ({ type: { eq: 'arrow' as const } })) const arrowQuery = store.query.records('shape', () => ({ type: { eq: 'arrow' as const } }))
@ -79,7 +83,7 @@ export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsInde
for (const changes of diff) { for (const changes of diff) {
for (const newShape of Object.values(changes.added)) { for (const newShape of Object.values(changes.added)) {
if (TLArrowShapeDef.is(newShape)) { if (isArrowType(newShape)) {
const { start, end } = newShape.props const { start, end } = newShape.props
if (start.type === 'binding') { if (start.type === 'binding') {
addBinding(start.boundShapeId, newShape.id, 'start') addBinding(start.boundShapeId, newShape.id, 'start')
@ -91,7 +95,7 @@ export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsInde
} }
for (const [prev, next] of Object.values(changes.updated) as [TLShape, TLShape][]) { for (const [prev, next] of Object.values(changes.updated) as [TLShape, TLShape][]) {
if (!TLArrowShapeDef.is(prev) || !TLArrowShapeDef.is(next)) continue if (!isArrowType(prev) || !isArrowType(next)) continue
for (const handle of ['start', 'end'] as const) { for (const handle of ['start', 'end'] as const) {
const prevTerminal = prev.props[handle] const prevTerminal = prev.props[handle]
@ -116,7 +120,7 @@ export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsInde
} }
for (const prev of Object.values(changes.removed)) { for (const prev of Object.values(changes.removed)) {
if (TLArrowShapeDef.is(prev)) { if (isArrowType(prev)) {
const { start, end } = prev.props const { start, end } = prev.props
if (start.type === 'binding') { if (start.type === 'binding') {
removingBinding(start.boundShapeId, prev.id, 'start') removingBinding(start.boundShapeId, prev.id, 'start')

View file

@ -17,7 +17,7 @@ import { compact, dedupe, deepCopy } from '@tldraw/utils'
import { atom, computed, EMPTY_ARRAY } from 'signia' import { atom, computed, EMPTY_ARRAY } from 'signia'
import { uniqueId } from '../../utils/data' import { uniqueId } from '../../utils/data'
import type { App } from '../App' import type { App } from '../App'
import { getSplineForLineShape, TLLineShapeDef } from '../shapeutils/TLLineUtil/TLLineUtil' import { getSplineForLineShape, TLLineUtil } from '../shapeutils/TLLineUtil/TLLineUtil'
export type PointsSnapLine = { export type PointsSnapLine = {
id: string id: string
@ -249,7 +249,7 @@ export class SnapManager {
const processParent = (parentId: TLParentId) => { const processParent = (parentId: TLParentId) => {
const children = this.app.getSortedChildIds(parentId) const children = this.app.getSortedChildIds(parentId)
for (const id of children) { for (const id of children) {
const shape = this.app.getShapeById(id) as TLShape const shape = this.app.getShapeById(id)
if (!shape) continue if (!shape) continue
if (shape.type === 'arrow') continue if (shape.type === 'arrow') continue
if (selectedIds.includes(id)) continue if (selectedIds.includes(id)) continue
@ -495,7 +495,7 @@ export class SnapManager {
// and then pass them to the snap function as 'additionalOutlines' // and then pass them to the snap function as 'additionalOutlines'
// First, let's find which handle we're dragging // First, let's find which handle we're dragging
const util = this.app.getShapeUtilByDef(TLLineShapeDef) const util = this.app.getShapeUtil(TLLineUtil)
const handles = util.handles(line).sort(sortByIndex) const handles = util.handles(line).sort(sortByIndex)
if (handles.length < 3) return { nudge: new Vec2d(0, 0) } if (handles.length < 3) return { nudge: new Vec2d(0, 0) }

View file

@ -2,7 +2,7 @@ import { TAU } from '@tldraw/primitives'
import { createCustomShapeId, TLArrowShape, TLArrowTerminal, TLShapeId } from '@tldraw/tlschema' import { createCustomShapeId, TLArrowShape, TLArrowTerminal, TLShapeId } from '@tldraw/tlschema'
import { assert } from '@tldraw/utils' import { assert } from '@tldraw/utils'
import { TestApp } from '../../../test/TestApp' import { TestApp } from '../../../test/TestApp'
import { TLArrowShapeDef } from './TLArrowUtil' import { TLArrowUtil } from './TLArrowUtil'
let app: TestApp let app: TestApp
@ -299,7 +299,7 @@ describe('Other cases when arrow are moved', () => {
app.setSelectedTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350) app.setSelectedTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350)
let arrow = app.shapesArray[app.shapesArray.length - 1] let arrow = app.shapesArray[app.shapesArray.length - 1]
assert(TLArrowShapeDef.is(arrow)) assert(app.isShapeOfType(arrow, TLArrowUtil))
assert(arrow.props.end.type === 'binding') assert(arrow.props.end.type === 'binding')
expect(arrow.props.end.boundShapeId).toBe(ids.box3) expect(arrow.props.end.boundShapeId).toBe(ids.box3)
@ -308,7 +308,7 @@ describe('Other cases when arrow are moved', () => {
// arrow should still be bound to box3 // arrow should still be bound to box3
arrow = app.getShapeById(arrow.id)! arrow = app.getShapeById(arrow.id)!
assert(TLArrowShapeDef.is(arrow)) assert(app.isShapeOfType(arrow, TLArrowUtil))
assert(arrow.props.end.type === 'binding') assert(arrow.props.end.type === 'binding')
expect(arrow.props.end.boundShapeId).toBe(ids.box3) expect(arrow.props.end.boundShapeId).toBe(ids.box3)
}) })

View file

@ -11,8 +11,6 @@ import {
VecLike, VecLike,
} from '@tldraw/primitives' } from '@tldraw/primitives'
import { import {
arrowShapeTypeMigrations,
arrowShapeTypeValidator,
TLArrowheadType, TLArrowheadType,
TLArrowShape, TLArrowShape,
TLColorType, TLColorType,
@ -27,7 +25,6 @@ import { deepCopy, last, minBy } from '@tldraw/utils'
import * as React from 'react' import * as React from 'react'
import { computed, EMPTY_ARRAY } from 'signia' import { computed, EMPTY_ARRAY } from 'signia'
import { SVGContainer } from '../../../components/SVGContainer' import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { ARROW_LABEL_FONT_SIZES, FONT_FAMILIES, TEXT_PROPS } from '../../../constants' import { ARROW_LABEL_FONT_SIZES, FONT_FAMILIES, TEXT_PROPS } from '../../../constants'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans' import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
import { getPerfectDashProps } from '../shared/getPerfectDashProps' import { getPerfectDashProps } from '../shared/getPerfectDashProps'
@ -60,7 +57,7 @@ let globalRenderIndex = 0
/** @public */ /** @public */
export class TLArrowUtil extends TLShapeUtil<TLArrowShape> { export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
static type = 'arrow' static override type = 'arrow'
override canEdit = () => true override canEdit = () => true
override canBind = () => false override canBind = () => false
@ -1135,11 +1132,3 @@ function getArrowheadSvgPath(
function isPrecise(normalizedAnchor: Vec2dModel) { function isPrecise(normalizedAnchor: Vec2dModel) {
return normalizedAnchor.x !== 0.5 || normalizedAnchor.y !== 0.5 return normalizedAnchor.x !== 0.5 || normalizedAnchor.y !== 0.5
} }
/** @public */
export const TLArrowShapeDef = defineShape<TLArrowShape, TLArrowUtil>({
type: 'arrow',
getShapeUtil: () => TLArrowUtil,
validator: arrowShapeTypeValidator,
migrations: arrowShapeTypeMigrations,
})

View file

@ -1,15 +1,7 @@
import { toDomPrecision } from '@tldraw/primitives' import { toDomPrecision } from '@tldraw/primitives'
import { import { TLAsset, TLAssetId, TLBookmarkAsset, TLBookmarkShape } from '@tldraw/tlschema'
bookmarkShapeTypeMigrations,
bookmarkShapeTypeValidator,
TLAsset,
TLAssetId,
TLBookmarkAsset,
TLBookmarkShape,
} from '@tldraw/tlschema'
import { debounce, getHashForString } from '@tldraw/utils' import { debounce, getHashForString } from '@tldraw/utils'
import { HTMLContainer } from '../../../components/HTMLContainer' import { HTMLContainer } from '../../../components/HTMLContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { import {
DEFAULT_BOOKMARK_HEIGHT, DEFAULT_BOOKMARK_HEIGHT,
DEFAULT_BOOKMARK_WIDTH, DEFAULT_BOOKMARK_WIDTH,
@ -20,13 +12,13 @@ import {
stopEventPropagation, stopEventPropagation,
truncateStringWithEllipsis, truncateStringWithEllipsis,
} from '../../../utils/dom' } from '../../../utils/dom'
import { HyperlinkButton } from '../shared/HyperlinkButton'
import { TLBoxUtil } from '../TLBoxUtil' import { TLBoxUtil } from '../TLBoxUtil'
import { OnBeforeCreateHandler, OnBeforeUpdateHandler } from '../TLShapeUtil' import { OnBeforeCreateHandler, OnBeforeUpdateHandler } from '../TLShapeUtil'
import { HyperlinkButton } from '../shared/HyperlinkButton'
/** @public */ /** @public */
export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> { export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
static type = 'bookmark' static override type = 'bookmark'
override canResize = () => false override canResize = () => false
@ -191,11 +183,3 @@ export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
} }
}, 500) }, 500)
} }
/** @public */
export const TLBookmarkShapeDef = defineShape<TLBookmarkShape, TLBookmarkUtil>({
type: 'bookmark',
getShapeUtil: () => TLBookmarkUtil,
validator: bookmarkShapeTypeValidator,
migrations: bookmarkShapeTypeMigrations,
})

View file

@ -9,15 +9,9 @@ import {
Vec2d, Vec2d,
VecLike, VecLike,
} from '@tldraw/primitives' } from '@tldraw/primitives'
import { import { TLDrawShape, TLDrawShapeSegment } from '@tldraw/tlschema'
drawShapeTypeMigrations,
drawShapeTypeValidator,
TLDrawShape,
TLDrawShapeSegment,
} from '@tldraw/tlschema'
import { last, rng } from '@tldraw/utils' import { last, rng } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer' import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { getSvgPathFromStroke, getSvgPathFromStrokePoints } from '../../../utils/svg' import { getSvgPathFromStroke, getSvgPathFromStrokePoints } from '../../../utils/svg'
import { getShapeFillSvg, ShapeFill } from '../shared/ShapeFill' import { getShapeFillSvg, ShapeFill } from '../shared/ShapeFill'
import { TLExportColors } from '../shared/TLExportColors' import { TLExportColors } from '../shared/TLExportColors'
@ -27,7 +21,7 @@ import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments
/** @public */ /** @public */
export class TLDrawUtil extends TLShapeUtil<TLDrawShape> { export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
static type = 'draw' static override type = 'draw'
hideResizeHandles = (shape: TLDrawShape) => this.getIsDot(shape) hideResizeHandles = (shape: TLDrawShape) => this.getIsDot(shape)
hideRotateHandle = (shape: TLDrawShape) => this.getIsDot(shape) hideRotateHandle = (shape: TLDrawShape) => this.getIsDot(shape)
@ -310,14 +304,6 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
} }
} }
/** @public */
export const TLDrawShapeDef = defineShape<TLDrawShape, TLDrawUtil>({
type: 'draw',
getShapeUtil: () => TLDrawUtil,
migrations: drawShapeTypeMigrations,
validator: drawShapeTypeValidator,
})
function getDot(point: VecLike, sw: number) { function getDot(point: VecLike, sw: number) {
const r = (sw + 1) * 0.5 const r = (sw + 1) * 0.5
return `M ${point.x} ${point.y} m -${r}, 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 -${ return `M ${point.x} ${point.y} m -${r}, 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 -${

View file

@ -1,8 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { toDomPrecision } from '@tldraw/primitives' import { toDomPrecision } from '@tldraw/primitives'
import { import {
embedShapeTypeMigrations,
embedShapeTypeValidator,
TLEmbedShape, TLEmbedShape,
tlEmbedShapePermissionDefaults, tlEmbedShapePermissionDefaults,
TLEmbedShapePermissions, TLEmbedShapePermissions,
@ -10,10 +8,9 @@ import {
import * as React from 'react' import * as React from 'react'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useValue } from 'signia-react' import { useValue } from 'signia-react'
import { DefaultSpinner } from '../../../components/DefaultSpinner'
import { HTMLContainer } from '../../../components/HTMLContainer' import { HTMLContainer } from '../../../components/HTMLContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { ROTATING_SHADOWS } from '../../../constants' import { ROTATING_SHADOWS } from '../../../constants'
import { useEditorComponents } from '../../../hooks/useEditorComponents'
import { useIsEditing } from '../../../hooks/useIsEditing' import { useIsEditing } from '../../../hooks/useIsEditing'
import { rotateBoxShadow } from '../../../utils/dom' import { rotateBoxShadow } from '../../../utils/dom'
import { getEmbedInfo, getEmbedInfoUnsafely } from '../../../utils/embeds' import { getEmbedInfo, getEmbedInfoUnsafely } from '../../../utils/embeds'
@ -30,7 +27,7 @@ const getSandboxPermissions = (permissions: TLEmbedShapePermissions) => {
/** @public */ /** @public */
export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> { export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
static type = 'embed' static override type = 'embed'
override canUnmount: TLShapeUtilFlag<TLEmbedShape> = () => false override canUnmount: TLShapeUtilFlag<TLEmbedShape> = () => false
override canResize = (shape: TLEmbedShape) => { override canResize = (shape: TLEmbedShape) => {
@ -84,8 +81,6 @@ export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
const isEditing = useIsEditing(shape.id) const isEditing = useIsEditing(shape.id)
const embedInfo = useMemo(() => getEmbedInfoUnsafely(url), [url]) const embedInfo = useMemo(() => getEmbedInfoUnsafely(url), [url])
const { Spinner } = useEditorComponents()
const isHoveringWhileEditingSameShape = useValue( const isHoveringWhileEditingSameShape = useValue(
'is hovering', 'is hovering',
() => { () => {
@ -150,11 +145,11 @@ export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
background: embedInfo?.definition.backgroundColor, background: embedInfo?.definition.backgroundColor,
}} }}
/> />
) : Spinner ? ( ) : (
<g transform={`translate(${(w - 38) / 2}, ${(h - 38) / 2})`}> <g transform={`translate(${(w - 38) / 2}, ${(h - 38) / 2})`}>
<Spinner /> <DefaultSpinner />
</g> </g>
) : null} )}
</HTMLContainer> </HTMLContainer>
) )
} }
@ -230,11 +225,3 @@ function Gist({
/> />
) )
} }
/** @public */
export const TLEmbedShapeDef = defineShape<TLEmbedShape, TLEmbedUtil>({
type: 'embed',
getShapeUtil: () => TLEmbedUtil,
validator: embedShapeTypeValidator,
migrations: embedShapeTypeMigrations,
})

View file

@ -1,14 +1,7 @@
import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives' import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives'
import { import { TLFrameShape, TLShape, TLShapeId, TLShapeType } from '@tldraw/tlschema'
frameShapeTypeValidator,
TLFrameShape,
TLShape,
TLShapeId,
TLShapeType,
} from '@tldraw/tlschema'
import { last } from '@tldraw/utils' import { last } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer' import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { defaultEmptyAs } from '../../../utils/string' import { defaultEmptyAs } from '../../../utils/string'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans' import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
import { TLExportColors } from '../shared/TLExportColors' import { TLExportColors } from '../shared/TLExportColors'
@ -18,7 +11,7 @@ import { FrameHeading } from './components/FrameHeading'
/** @public */ /** @public */
export class TLFrameUtil extends TLBoxUtil<TLFrameShape> { export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
static type = 'frame' static override type = 'frame'
override canBind = () => true override canBind = () => true
@ -211,10 +204,3 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
} }
} }
} }
/** @public */
export const TLFrameShapeDef = defineShape<TLFrameShape, TLFrameUtil>({
type: 'frame',
getShapeUtil: () => TLFrameUtil,
validator: frameShapeTypeValidator,
})

View file

@ -12,15 +12,8 @@ import {
Vec2d, Vec2d,
VecLike, VecLike,
} from '@tldraw/primitives' } from '@tldraw/primitives'
import { import { TLDashType, TLGeoShape, TLGeoShapeProps } from '@tldraw/tlschema'
geoShapeTypeMigrations,
geoShapeTypeValidator,
TLDashType,
TLGeoShape,
TLGeoShapeProps,
} from '@tldraw/tlschema'
import { SVGContainer } from '../../../components/SVGContainer' import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants' import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { App } from '../../App' import { App } from '../../App'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans' import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
@ -48,7 +41,7 @@ const MIN_SIZE_WITH_LABEL = 17 * 3
/** @public */ /** @public */
export class TLGeoUtil extends TLBoxUtil<TLGeoShape> { export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
static type = 'geo' static override type = 'geo'
canEdit = () => true canEdit = () => true
@ -1002,11 +995,3 @@ function getCheckBoxLines(w: number, h: number) {
[new Vec2d(ox + size * 0.45, oy + size * 0.82), new Vec2d(ox + size * 0.82, oy + size * 0.22)], [new Vec2d(ox + size * 0.45, oy + size * 0.82), new Vec2d(ox + size * 0.82, oy + size * 0.22)],
] ]
} }
/** @public */
export const TLGeoShapeDef = defineShape<TLGeoShape, TLGeoUtil>({
type: 'geo',
getShapeUtil: () => TLGeoUtil,
validator: geoShapeTypeValidator,
migrations: geoShapeTypeMigrations,
})

View file

@ -1,13 +1,12 @@
import { Box2d, Matrix2d } from '@tldraw/primitives' import { Box2d, Matrix2d } from '@tldraw/primitives'
import { TLGroupShape, Vec2dModel, groupShapeTypeValidator } from '@tldraw/tlschema' import { TLGroupShape, Vec2dModel } from '@tldraw/tlschema'
import { SVGContainer } from '../../../components/SVGContainer' import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { OnChildrenChangeHandler, TLShapeUtil } from '../TLShapeUtil' import { OnChildrenChangeHandler, TLShapeUtil } from '../TLShapeUtil'
import { DashedOutlineBox } from '../shared/DashedOutlineBox' import { DashedOutlineBox } from '../shared/DashedOutlineBox'
/** @public */ /** @public */
export class TLGroupUtil extends TLShapeUtil<TLGroupShape> { export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
static type = 'group' static override type = 'group'
hideSelectionBoundsBg = () => false hideSelectionBoundsBg = () => false
hideSelectionBoundsFg = () => true hideSelectionBoundsFg = () => true
@ -104,10 +103,3 @@ export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
} }
} }
} }
/** @public */
export const TLGroupShapeDef = defineShape<TLGroupShape, TLGroupUtil>({
type: 'group',
getShapeUtil: () => TLGroupUtil,
validator: groupShapeTypeValidator,
})

View file

@ -1,17 +1,11 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { Vec2d, toDomPrecision } from '@tldraw/primitives' import { Vec2d, toDomPrecision } from '@tldraw/primitives'
import { import { TLImageShape, TLShapePartial } from '@tldraw/tlschema'
TLImageShape,
TLShapePartial,
imageShapeTypeMigrations,
imageShapeTypeValidator,
} from '@tldraw/tlschema'
import { deepCopy } from '@tldraw/utils' import { deepCopy } from '@tldraw/utils'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useValue } from 'signia-react' import { useValue } from 'signia-react'
import { DefaultSpinner } from '../../../components/DefaultSpinner'
import { HTMLContainer } from '../../../components/HTMLContainer' import { HTMLContainer } from '../../../components/HTMLContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { useEditorComponents } from '../../../hooks/useEditorComponents'
import { useIsCropping } from '../../../hooks/useIsCropping' import { useIsCropping } from '../../../hooks/useIsCropping'
import { usePrefersReducedMotion } from '../../../utils/dom' import { usePrefersReducedMotion } from '../../../utils/dom'
import { TLBoxUtil } from '../TLBoxUtil' import { TLBoxUtil } from '../TLBoxUtil'
@ -55,7 +49,7 @@ async function getDataURIFromURL(url: string): Promise<string> {
/** @public */ /** @public */
export class TLImageUtil extends TLBoxUtil<TLImageShape> { export class TLImageUtil extends TLBoxUtil<TLImageShape> {
static type = 'image' static override type = 'image'
override isAspectRatioLocked = () => true override isAspectRatioLocked = () => true
override canCrop = () => true override canCrop = () => true
@ -77,7 +71,6 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
const isCropping = useIsCropping(shape.id) const isCropping = useIsCropping(shape.id)
const prefersReducedMotion = usePrefersReducedMotion() const prefersReducedMotion = usePrefersReducedMotion()
const [staticFrameSrc, setStaticFrameSrc] = useState('') const [staticFrameSrc, setStaticFrameSrc] = useState('')
const { Spinner } = useEditorComponents()
const { w, h } = shape.props const { w, h } = shape.props
const asset = shape.props.assetId ? this.app.getAssetById(shape.props.assetId) : undefined const asset = shape.props.assetId ? this.app.getAssetById(shape.props.assetId) : undefined
@ -148,11 +141,11 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
}} }}
draggable={false} draggable={false}
/> />
) : Spinner ? ( ) : (
<g transform={`translate(${(w - 38) / 2}, ${(h - 38) / 2})`}> <g transform={`translate(${(w - 38) / 2}, ${(h - 38) / 2})`}>
<Spinner /> <DefaultSpinner />
</g> </g>
) : null} )}
{asset?.props.isAnimated && !shape.props.playing && ( {asset?.props.isAnimated && !shape.props.playing && (
<div className="tl-image__tg">GIF</div> <div className="tl-image__tg">GIF</div>
)} )}
@ -270,14 +263,6 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
} }
} }
/** @public */
export const TLImageShapeDef = defineShape<TLImageShape, TLImageUtil>({
type: 'image',
getShapeUtil: () => TLImageUtil,
validator: imageShapeTypeValidator,
migrations: imageShapeTypeMigrations,
})
/** /**
* When an image is cropped we need to translate the image to show the portion withing the cropped * When an image is cropped we need to translate the image to show the portion withing the cropped
* area. We do this by translating the image by the negative of the top left corner of the crop * area. We do this by translating the image by the negative of the top left corner of the crop

View file

@ -169,7 +169,7 @@ describe('Misc', () => {
const boxID = createCustomShapeId('box1') const boxID = createCustomShapeId('box1')
app.createShapes([{ id: boxID, type: 'geo', x: 500, y: 150, props: { w: 100, h: 50 } }]) app.createShapes([{ id: boxID, type: 'geo', x: 500, y: 150, props: { w: 100, h: 50 } }])
const box = app.getShapeById(boxID)! as TLGeoShape const box = app.getShapeById<TLGeoShape>(boxID)!
const line = app.getShapeById<TLLineShape>(id)! const line = app.getShapeById<TLLineShape>(id)!
app.select(boxID, id) app.select(boxID, id)

View file

@ -9,10 +9,9 @@ import {
intersectLineSegmentPolyline, intersectLineSegmentPolyline,
pointNearToPolyline, pointNearToPolyline,
} from '@tldraw/primitives' } from '@tldraw/primitives'
import { TLHandle, TLLineShape, lineShapeTypeValidator } from '@tldraw/tlschema' import { TLHandle, TLLineShape } from '@tldraw/tlschema'
import { deepCopy } from '@tldraw/utils' import { deepCopy } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer' import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { WeakMapCache } from '../../../utils/WeakMapCache' import { WeakMapCache } from '../../../utils/WeakMapCache'
import { OnHandleChangeHandler, OnResizeHandler, TLShapeUtil } from '../TLShapeUtil' import { OnHandleChangeHandler, OnResizeHandler, TLShapeUtil } from '../TLShapeUtil'
import { ShapeFill } from '../shared/ShapeFill' import { ShapeFill } from '../shared/ShapeFill'
@ -27,7 +26,7 @@ const handlesCache = new WeakMapCache<TLLineShape['props'], TLHandle[]>()
/** @public */ /** @public */
export class TLLineUtil extends TLShapeUtil<TLLineShape> { export class TLLineUtil extends TLShapeUtil<TLLineShape> {
static type = 'line' static override type = 'line'
override hideResizeHandles = () => true override hideResizeHandles = () => true
override hideRotateHandle = () => true override hideRotateHandle = () => true
@ -335,13 +334,6 @@ export class TLLineUtil extends TLShapeUtil<TLLineShape> {
} }
} }
/** @public */
export const TLLineShapeDef = defineShape<TLLineShape, TLLineUtil>({
type: 'line',
getShapeUtil: () => TLLineUtil,
validator: lineShapeTypeValidator,
})
/** @public */ /** @public */
export function getSplineForLineShape(shape: TLLineShape) { export function getSplineForLineShape(shape: TLLineShape) {
return splinesCache.get(shape.props, () => { return splinesCache.get(shape.props, () => {

View file

@ -1,6 +1,5 @@
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives' import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
import { noteShapeTypeMigrations, noteShapeTypeValidator, TLNoteShape } from '@tldraw/tlschema' import { TLNoteShape } from '@tldraw/tlschema'
import { defineShape } from '../../../config/TLShapeDefinition'
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants' import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { App } from '../../App' import { App } from '../../App'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans' import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
@ -13,7 +12,7 @@ const NOTE_SIZE = 200
/** @public */ /** @public */
export class TLNoteUtil extends TLShapeUtil<TLNoteShape> { export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
static type = 'note' static override type = 'note'
canEdit = () => true canEdit = () => true
hideResizeHandles = () => true hideResizeHandles = () => true
@ -197,14 +196,6 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
} }
} }
/** @public */
export const TLNoteShapeDef = defineShape<TLNoteShape, TLNoteUtil>({
getShapeUtil: () => TLNoteUtil,
type: 'note',
validator: noteShapeTypeValidator,
migrations: noteShapeTypeMigrations,
})
function getGrowY(app: App, shape: TLNoteShape, prevGrowY = 0) { function getGrowY(app: App, shape: TLNoteShape, prevGrowY = 0) {
const PADDING = 17 const PADDING = 17

View file

@ -31,9 +31,11 @@ export interface TLShapeUtilConstructor<
export type TLShapeUtilFlag<T> = (shape: T) => boolean export type TLShapeUtilFlag<T> = (shape: T) => boolean
/** @public */ /** @public */
export abstract class TLShapeUtil<T extends TLUnknownShape> { export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
constructor(public app: App, public readonly type: T['type']) {} constructor(public app: App, public readonly type: T['type']) {}
static type: string
/** /**
* Check if a shape is of this type. * Check if a shape is of this type.
* *

View file

@ -1,8 +1,7 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives' import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
import { textShapeTypeMigrations, textShapeTypeValidator, TLTextShape } from '@tldraw/tlschema' import { TLTextShape } from '@tldraw/tlschema'
import { HTMLContainer } from '../../../components/HTMLContainer' import { HTMLContainer } from '../../../components/HTMLContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../../../constants' import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { stopEventPropagation } from '../../../utils/dom' import { stopEventPropagation } from '../../../utils/dom'
import { WeakMapCache } from '../../../utils/WeakMapCache' import { WeakMapCache } from '../../../utils/WeakMapCache'
@ -19,7 +18,7 @@ const sizeCache = new WeakMapCache<TLTextShape['props'], { height: number; width
/** @public */ /** @public */
export class TLTextUtil extends TLShapeUtil<TLTextShape> { export class TLTextUtil extends TLShapeUtil<TLTextShape> {
static type = 'text' static override type = 'text'
canEdit = () => true canEdit = () => true
@ -369,14 +368,6 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
} }
} }
/** @public */
export const TLTextShapeDef = defineShape<TLTextShape, TLTextUtil>({
type: 'text',
getShapeUtil: () => TLTextUtil,
validator: textShapeTypeValidator,
migrations: textShapeTypeMigrations,
})
function getTextSize(app: App, props: TLTextShape['props']) { function getTextSize(app: App, props: TLTextShape['props']) {
const { font, text, autoSize, size, w } = props const { font, text, autoSize, size, w } = props

View file

@ -1,10 +1,9 @@
import { toDomPrecision } from '@tldraw/primitives' import { toDomPrecision } from '@tldraw/primitives'
import { TLVideoShape, videoShapeTypeMigrations, videoShapeTypeValidator } from '@tldraw/tlschema' import { TLVideoShape } from '@tldraw/tlschema'
import * as React from 'react' import * as React from 'react'
import { track } from 'signia-react' import { track } from 'signia-react'
import { DefaultSpinner } from '../../../components/DefaultSpinner'
import { HTMLContainer } from '../../../components/HTMLContainer' import { HTMLContainer } from '../../../components/HTMLContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { useEditorComponents } from '../../../hooks/useEditorComponents'
import { useIsEditing } from '../../../hooks/useIsEditing' import { useIsEditing } from '../../../hooks/useIsEditing'
import { usePrefersReducedMotion } from '../../../utils/dom' import { usePrefersReducedMotion } from '../../../utils/dom'
import { TLBoxUtil } from '../TLBoxUtil' import { TLBoxUtil } from '../TLBoxUtil'
@ -12,7 +11,7 @@ import { HyperlinkButton } from '../shared/HyperlinkButton'
/** @public */ /** @public */
export class TLVideoUtil extends TLBoxUtil<TLVideoShape> { export class TLVideoUtil extends TLBoxUtil<TLVideoShape> {
static type = 'video' static override type = 'video'
override canEdit = () => true override canEdit = () => true
override isAspectRatioLocked = () => true override isAspectRatioLocked = () => true
@ -49,14 +48,6 @@ export class TLVideoUtil extends TLBoxUtil<TLVideoShape> {
} }
} }
/** @public */
export const TLVideoShapeDef = defineShape<TLVideoShape, TLVideoUtil>({
type: 'video',
getShapeUtil: () => TLVideoUtil,
validator: videoShapeTypeValidator,
migrations: videoShapeTypeMigrations,
})
// Function from v1, could be improved bu explicitly using this.model.time (?) // Function from v1, could be improved bu explicitly using this.model.time (?)
function serializeVideo(id: string): string { function serializeVideo(id: string): string {
const splitId = id.split(':')[1] const splitId = id.split(':')[1]
@ -74,7 +65,6 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
shape: TLVideoShape shape: TLVideoShape
videoUtil: TLVideoUtil videoUtil: TLVideoUtil
}) { }) {
const { Spinner } = useEditorComponents()
const { shape, videoUtil } = props const { shape, videoUtil } = props
const showControls = videoUtil.app.getBounds(shape).w * videoUtil.app.zoomLevel >= 110 const showControls = videoUtil.app.getBounds(shape).w * videoUtil.app.zoomLevel >= 110
const asset = shape.props.assetId ? videoUtil.app.getAssetById(shape.props.assetId) : null const asset = shape.props.assetId ? videoUtil.app.getAssetById(shape.props.assetId) : null
@ -202,11 +192,11 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
> >
<source src={asset.props.src} /> <source src={asset.props.src} />
</video> </video>
) : Spinner ? ( ) : (
<g transform={`translate(${(w - 38) / 2}, ${(h - 38) / 2})`}> <g transform={`translate(${(w - 38) / 2}, ${(h - 38) / 2})`}>
<Spinner /> <DefaultSpinner />
</g> </g>
) : null} )}
</div> </div>
</HTMLContainer> </HTMLContainer>
{'url' in shape.props && shape.props.url && ( {'url' in shape.props && shape.props.url && (

View file

@ -1,5 +1,5 @@
import { createShapeId, TLArrowShape, TLShapeType } from '@tldraw/tlschema' import { createShapeId, TLArrowShape, TLShapeType } from '@tldraw/tlschema'
import { TLArrowShapeDef } from '../../../shapeutils/TLArrowUtil/TLArrowUtil' import { TLArrowUtil } from '../../../shapeutils/TLArrowUtil/TLArrowUtil'
import { TLEventHandlers } from '../../../types/event-types' import { TLEventHandlers } from '../../../types/event-types'
import { StateNode } from '../../StateNode' import { StateNode } from '../../StateNode'
import { TLArrowTool } from '../TLArrowTool' import { TLArrowTool } from '../TLArrowTool'
@ -48,8 +48,8 @@ export class Pointing extends StateNode {
}, },
]) ])
const util = this.app.getShapeUtilByDef(TLArrowShapeDef) const util = this.app.getShapeUtil(TLArrowUtil)
const shape = this.app.getShapeById(id) as TLArrowShape const shape = this.app.getShapeById<TLArrowShape>(id)
if (!shape) return if (!shape) return
const handles = util.handles?.(shape) const handles = util.handles?.(shape)
@ -96,7 +96,7 @@ export class Pointing extends StateNode {
} }
if (!this.didTimeout) { if (!this.didTimeout) {
const util = this.app.getShapeUtilByDef(TLArrowShapeDef) const util = this.app.getShapeUtil(TLArrowUtil)
const shape = this.app.getShapeById<TLArrowShape>(this.shape.id) const shape = this.app.getShapeById<TLArrowShape>(this.shape.id)
if (!shape) return if (!shape) return

View file

@ -92,7 +92,7 @@ export class Pointing extends StateNode {
]) ])
const shape = this.app.getShapeById<TLBoxLike>(id)! const shape = this.app.getShapeById<TLBoxLike>(id)!
const { w, h } = this.app.getShapeUtil<TLBoxLike>(shape).defaultProps() const { w, h } = this.app.getShapeUtil(shape).defaultProps() as TLBoxLike['props']
const delta = this.app.getDeltaInParentSpace(shape, new Vec2d(w / 2, h / 2)) const delta = this.app.getDeltaInParentSpace(shape, new Vec2d(w / 2, h / 2))
this.app.updateShapes([ this.app.updateShapes([

View file

@ -9,7 +9,7 @@ import {
import { last, structuredClone } from '@tldraw/utils' import { last, structuredClone } from '@tldraw/utils'
import { DRAG_DISTANCE } from '../../../../constants' import { DRAG_DISTANCE } from '../../../../constants'
import { uniqueId } from '../../../../utils/data' import { uniqueId } from '../../../../utils/data'
import { TLDrawShapeDef } from '../../../shapeutils/TLDrawUtil/TLDrawUtil' import { TLDrawUtil } from '../../../shapeutils/TLDrawUtil/TLDrawUtil'
import { TLEventHandlers, TLPointerEventInfo } from '../../../types/event-types' import { TLEventHandlers, TLPointerEventInfo } from '../../../types/event-types'
import { StateNode } from '../../StateNode' import { StateNode } from '../../StateNode'
@ -21,7 +21,7 @@ export class Drawing extends StateNode {
initialShape?: TLDrawShape initialShape?: TLDrawShape
util = this.app.getShapeUtilByDef(TLDrawShapeDef) util = this.app.getShapeUtil(TLDrawUtil)
isPen = false isPen = false
@ -157,7 +157,7 @@ export class Drawing extends StateNode {
this.lastRecordedPoint = originPagePoint.clone() this.lastRecordedPoint = originPagePoint.clone()
if (this.initialShape) { if (this.initialShape) {
const shape = this.app.getShapeById(this.initialShape.id) as TLDrawShape const shape = this.app.getShapeById<TLDrawShape>(this.initialShape.id)
if (shape && this.segmentMode === 'straight') { if (shape && this.segmentMode === 'straight') {
// Connect dots // Connect dots

View file

@ -1,5 +1,5 @@
import { assert } from '@tldraw/utils' import { TLNoteShape } from '@tldraw/tlschema'
import { TLNoteShapeDef } from '../../../shapeutils/TLNoteUtil/TLNoteUtil' import { TLNoteUtil } from '../../../shapeutils/TLNoteUtil/TLNoteUtil'
import { TLEventHandlers, TLInterruptEvent, TLPointerEventInfo } from '../../../types/event-types' import { TLEventHandlers, TLInterruptEvent, TLPointerEventInfo } from '../../../types/event-types'
import { StateNode } from '../../StateNode' import { StateNode } from '../../StateNode'
@ -97,9 +97,8 @@ export class Pointing extends StateNode {
true true
) )
const util = this.app.getShapeUtilByDef(TLNoteShapeDef) const util = this.app.getShapeUtil(TLNoteUtil)
const shape = this.app.getShapeById(id)! const shape = this.app.getShapeById<TLNoteShape>(id)!
assert(TLNoteShapeDef.is(shape))
const bounds = util.bounds(shape) const bounds = util.bounds(shape)
// Center the text around the created point // Center the text around the created point

View file

@ -77,7 +77,7 @@ export class Cropping extends StateNode {
const { shape, cursorHandleOffset } = this.snapshot const { shape, cursorHandleOffset } = this.snapshot
if (!shape) return if (!shape) return
const util = this.app.getShapeUtil(shape) as TLImageUtil const util = this.app.getShapeUtil(TLImageUtil)
if (!util) return if (!util) return
const props = shape.props as TLImageShapeProps const props = shape.props as TLImageShapeProps

View file

@ -1,52 +0,0 @@
import { TLUnknownShape } from '@tldraw/tlschema'
import { Migrations, StoreValidator } from '@tldraw/tlstore'
import { App } from '../app/App'
import { TLShapeUtil, TLShapeUtilConstructor } from '../app/shapeutils/TLShapeUtil'
/** @public */
export interface TLShapeDef<
ShapeType extends TLUnknownShape,
ShapeUtil extends TLShapeUtil<ShapeType> = TLShapeUtil<ShapeType>
> {
readonly type: ShapeType['type']
readonly createShapeUtils: (app: App) => ShapeUtil
readonly is: (shape: TLUnknownShape) => shape is ShapeType
readonly validator?: StoreValidator<ShapeType>
readonly migrations: Migrations
}
/** @public */
export type TLUnknownShapeDef = TLShapeDef<TLUnknownShape, TLShapeUtil<TLUnknownShape>>
/** @public */
export function defineShape<
ShapeType extends TLUnknownShape,
ShapeUtil extends TLShapeUtil<ShapeType> = TLShapeUtil<ShapeType>
>({
type,
getShapeUtil,
validator,
migrations = { currentVersion: 0, firstVersion: 0, migrators: {} },
}: {
type: ShapeType['type']
getShapeUtil: () => TLShapeUtilConstructor<ShapeType, ShapeUtil>
validator?: StoreValidator<ShapeType>
migrations?: Migrations
}): TLShapeDef<ShapeType, ShapeUtil> {
if (!validator && process.env.NODE_ENV === 'development') {
console.warn(
`No validator provided for shape type ${type}! Validators are highly recommended for use in production.`
)
}
return {
type,
createShapeUtils: (app: App) => {
const ShapeUtil = getShapeUtil()
return new ShapeUtil(app, type)
},
is: (shape: TLUnknownShape): shape is ShapeType => shape.type === type,
validator,
migrations,
}
}

View file

@ -1,5 +1,6 @@
import { import {
CLIENT_FIXUP_SCRIPT, CLIENT_FIXUP_SCRIPT,
MigrationsForShapes,
TLDOCUMENT_ID, TLDOCUMENT_ID,
TLInstance, TLInstance,
TLInstanceId, TLInstanceId,
@ -8,65 +9,147 @@ import {
TLShape, TLShape,
TLStore, TLStore,
TLStoreProps, TLStoreProps,
TLUnknownShape,
TLUser, TLUser,
TLUserId, TLUserId,
ValidatorsForShapes,
arrowShapeTypeMigrations,
arrowShapeTypeValidator,
bookmarkShapeTypeMigrations,
bookmarkShapeTypeValidator,
createTLSchema, createTLSchema,
drawShapeTypeMigrations,
drawShapeTypeValidator,
embedShapeTypeMigrations,
embedShapeTypeValidator,
frameShapeTypeMigrations,
frameShapeTypeValidator,
geoShapeTypeMigrations,
geoShapeTypeValidator,
groupShapeTypeMigrations,
groupShapeTypeValidator,
imageShapeTypeMigrations,
imageShapeTypeValidator,
lineShapeTypeMigrations,
lineShapeTypeValidator,
noteShapeTypeMigrations,
noteShapeTypeValidator,
textShapeTypeMigrations,
textShapeTypeValidator,
videoShapeTypeMigrations,
videoShapeTypeValidator,
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { RecordType, Store, StoreSchema, StoreSnapshot } from '@tldraw/tlstore' import {
Migrations,
RecordType,
Store,
StoreSchema,
StoreSnapshot,
defineMigrations,
} from '@tldraw/tlstore'
import { T } from '@tldraw/tlvalidate'
import { Signal } from 'signia' import { Signal } from 'signia'
import { TLArrowShapeDef } from '../app/shapeutils/TLArrowUtil/TLArrowUtil' import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
import { TLBookmarkShapeDef } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil' import { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
import { TLDrawShapeDef } from '../app/shapeutils/TLDrawUtil/TLDrawUtil' import { TLDrawUtil } from '../app/shapeutils/TLDrawUtil/TLDrawUtil'
import { TLEmbedShapeDef } from '../app/shapeutils/TLEmbedUtil/TLEmbedUtil' import { TLEmbedUtil } from '../app/shapeutils/TLEmbedUtil/TLEmbedUtil'
import { TLFrameShapeDef } from '../app/shapeutils/TLFrameUtil/TLFrameUtil' import { TLFrameUtil } from '../app/shapeutils/TLFrameUtil/TLFrameUtil'
import { TLGeoShapeDef } from '../app/shapeutils/TLGeoUtil/TLGeoUtil' import { TLGeoUtil } from '../app/shapeutils/TLGeoUtil/TLGeoUtil'
import { TLGroupShapeDef } from '../app/shapeutils/TLGroupUtil/TLGroupUtil' import { TLGroupUtil } from '../app/shapeutils/TLGroupUtil/TLGroupUtil'
import { TLImageShapeDef } from '../app/shapeutils/TLImageUtil/TLImageUtil' import { TLImageUtil } from '../app/shapeutils/TLImageUtil/TLImageUtil'
import { TLLineShapeDef } from '../app/shapeutils/TLLineUtil/TLLineUtil' import { TLLineUtil } from '../app/shapeutils/TLLineUtil/TLLineUtil'
import { TLNoteShapeDef } from '../app/shapeutils/TLNoteUtil/TLNoteUtil' import { TLNoteUtil } from '../app/shapeutils/TLNoteUtil/TLNoteUtil'
import { TLTextShapeDef } from '../app/shapeutils/TLTextUtil/TLTextUtil' import { TLShapeUtilConstructor } from '../app/shapeutils/TLShapeUtil'
import { TLVideoShapeDef } from '../app/shapeutils/TLVideoUtil/TLVideoUtil' import { TLTextUtil } from '../app/shapeutils/TLTextUtil/TLTextUtil'
import { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil'
import { StateNodeConstructor } from '../app/statechart/StateNode' import { StateNodeConstructor } from '../app/statechart/StateNode'
import { TLShapeDef, TLUnknownShapeDef } from './TLShapeDefinition'
type CustomShapeInfo<T extends TLUnknownShape> = {
util: TLShapeUtilConstructor<any>
validator?: { validate: (record: T) => T }
migrations?: Migrations
}
type UtilsForShapes<T extends TLUnknownShape> = Record<T['type'], TLShapeUtilConstructor<any>>
type TldrawEditorConfigOptions<T extends TLUnknownShape = TLShape> = {
tools?: readonly StateNodeConstructor[]
shapes?: { [K in T['type']]: CustomShapeInfo<T> }
/** @internal */
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
}
/** @public */ /** @public */
export class TldrawEditorConfig { export class TldrawEditorConfig {
static readonly default = new TldrawEditorConfig({}) static readonly default = new TldrawEditorConfig({})
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps> readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>
readonly shapes: readonly TLUnknownShapeDef[]
readonly TLShape: RecordType<TLShape, 'type' | 'props' | 'index' | 'parentId'> readonly TLShape: RecordType<TLShape, 'type' | 'props' | 'index' | 'parentId'>
readonly tools: readonly StateNodeConstructor[] readonly tools: readonly StateNodeConstructor[]
constructor(args: { readonly shapeUtils: UtilsForShapes<TLShape>
shapes?: readonly TLShapeDef<any, any>[] readonly shapeValidators: ValidatorsForShapes<TLShape>
tools?: readonly StateNodeConstructor[] readonly shapeMigrations: MigrationsForShapes<TLShape>
allowUnknownShapes?: boolean
/** @internal */ constructor(opts: TldrawEditorConfigOptions) {
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null> const { shapes = [], tools = [], derivePresenceState } = opts
}) {
const { shapes = [], tools = [], allowUnknownShapes = false, derivePresenceState } = args
this.tools = tools this.tools = tools
this.shapes = [ this.shapeUtils = {
TLArrowShapeDef, arrow: TLArrowUtil,
TLBookmarkShapeDef, bookmark: TLBookmarkUtil,
TLDrawShapeDef, draw: TLDrawUtil,
TLEmbedShapeDef, embed: TLEmbedUtil,
TLFrameShapeDef, frame: TLFrameUtil,
TLGeoShapeDef, geo: TLGeoUtil,
TLGroupShapeDef, group: TLGroupUtil,
TLImageShapeDef, image: TLImageUtil,
TLLineShapeDef, line: TLLineUtil,
TLNoteShapeDef, note: TLNoteUtil,
TLTextShapeDef, text: TLTextUtil,
TLVideoShapeDef, video: TLVideoUtil,
...shapes, }
]
this.shapeMigrations = {
arrow: arrowShapeTypeMigrations,
bookmark: bookmarkShapeTypeMigrations,
draw: drawShapeTypeMigrations,
embed: embedShapeTypeMigrations,
frame: frameShapeTypeMigrations,
geo: geoShapeTypeMigrations,
group: groupShapeTypeMigrations,
image: imageShapeTypeMigrations,
line: lineShapeTypeMigrations,
note: noteShapeTypeMigrations,
text: textShapeTypeMigrations,
video: videoShapeTypeMigrations,
}
this.shapeValidators = {
arrow: arrowShapeTypeValidator,
bookmark: bookmarkShapeTypeValidator,
draw: drawShapeTypeValidator,
embed: embedShapeTypeValidator,
frame: frameShapeTypeValidator,
geo: geoShapeTypeValidator,
group: groupShapeTypeValidator,
image: imageShapeTypeValidator,
line: lineShapeTypeValidator,
note: noteShapeTypeValidator,
text: textShapeTypeValidator,
video: videoShapeTypeValidator,
}
for (const [type, shape] of Object.entries(shapes)) {
this.shapeUtils[type] = shape.util
this.shapeMigrations[type] = shape.migrations ?? defineMigrations({})
this.shapeValidators[type] = shape.validator ?? T.any
}
this.storeSchema = createTLSchema({ this.storeSchema = createTLSchema({
allowUnknownShapes, shapeMigrations: this.shapeMigrations,
customShapeDefs: shapes, shapeValidators: this.shapeValidators,
derivePresenceState, derivePresenceState,
}) })

View file

@ -406,7 +406,7 @@ describe('flipping rotated shapes', () => {
const getStartAndEndPoints = (id: TLShapeId) => { const getStartAndEndPoints = (id: TLShapeId) => {
const transform = app.getPageTransformById(id) const transform = app.getPageTransformById(id)
if (!transform) throw new Error('no transform') if (!transform) throw new Error('no transform')
const arrow = app.getShapeById(id) as TLArrowShape const arrow = app.getShapeById<TLArrowShape>(id)!
if (arrow.props.start.type !== 'point' || arrow.props.end.type !== 'point') if (arrow.props.start.type !== 'point' || arrow.props.end.type !== 'point')
throw new Error('not a point') throw new Error('not a point')
const start = Matrix2d.applyToPoint(transform, arrow.props.start) const start = Matrix2d.applyToPoint(transform, arrow.props.start)

View file

@ -1,5 +1,5 @@
import { createCustomShapeId } from '@tldraw/tlschema' import { createCustomShapeId } from '@tldraw/tlschema'
import { TLGeoShapeDef } from '../../app/shapeutils/TLGeoUtil/TLGeoUtil' import { TLGeoUtil } from '../../app/shapeutils/TLGeoUtil/TLGeoUtil'
import { TestApp } from '../TestApp' import { TestApp } from '../TestApp'
let app: TestApp let app: TestApp
@ -43,7 +43,7 @@ beforeEach(() => {
describe('app.rotateShapes', () => { describe('app.rotateShapes', () => {
it('Rotates shapes and fires events', () => { it('Rotates shapes and fires events', () => {
// Set start / change / end events on only the geo shape // Set start / change / end events on only the geo shape
const util = app.getShapeUtilByDef(TLGeoShapeDef) const util = app.getShapeUtil(TLGeoUtil)
// Bad! who did this (did I do this) // Bad! who did this (did I do this)
const fnStart = jest.fn() const fnStart = jest.fn()

View file

@ -22,7 +22,7 @@ beforeEach(() => {
}, },
}, },
]) ])
shape = app.getShapeById(id) as TLGeoShape shape = app.getShapeById<TLGeoShape>(id)!
}) })
describe('Resize box', () => { describe('Resize box', () => {

View file

@ -1,6 +1,6 @@
import { createCustomShapeId } from '@tldraw/tlschema' import { createCustomShapeId } from '@tldraw/tlschema'
import { TLFrameShapeDef } from '../app/shapeutils/TLFrameUtil/TLFrameUtil' import { TLFrameUtil } from '../app/shapeutils/TLFrameUtil/TLFrameUtil'
import { TLGeoShapeDef } from '../app/shapeutils/TLGeoUtil/TLGeoUtil' import { TLGeoUtil } from '../app/shapeutils/TLGeoUtil/TLGeoUtil'
import { TestApp } from './TestApp' import { TestApp } from './TestApp'
let app: TestApp let app: TestApp
@ -56,7 +56,7 @@ beforeEach(() => {
describe('When interacting with a shape...', () => { describe('When interacting with a shape...', () => {
it('fires rotate events', () => { it('fires rotate events', () => {
// Set start / change / end events on only the geo shape // Set start / change / end events on only the geo shape
const util = app.getShapeUtilByDef(TLFrameShapeDef) const util = app.getShapeUtil(TLFrameUtil)
const fnStart = jest.fn() const fnStart = jest.fn()
util.onRotateStart = fnStart util.onRotateStart = fnStart
@ -89,12 +89,12 @@ describe('When interacting with a shape...', () => {
}) })
it('cleans up events', () => { it('cleans up events', () => {
const util = app.getShapeUtilByDef(TLGeoShapeDef) const util = app.getShapeUtil(TLGeoUtil)
expect(util.onRotateStart).toBeUndefined() expect(util.onRotateStart).toBeUndefined()
}) })
it('fires double click handler event', () => { it('fires double click handler event', () => {
const util = app.getShapeUtilByDef(TLGeoShapeDef) const util = app.getShapeUtil(TLGeoUtil)
const fnStart = jest.fn() const fnStart = jest.fn()
util.onDoubleClick = fnStart util.onDoubleClick = fnStart
@ -105,7 +105,7 @@ describe('When interacting with a shape...', () => {
}) })
it('Fires resisizing events', () => { it('Fires resisizing events', () => {
const util = app.getShapeUtilByDef(TLFrameShapeDef) const util = app.getShapeUtil(TLFrameUtil)
const fnStart = jest.fn() const fnStart = jest.fn()
util.onResizeStart = fnStart util.onResizeStart = fnStart
@ -142,7 +142,7 @@ describe('When interacting with a shape...', () => {
}) })
it('Fires translating events', () => { it('Fires translating events', () => {
const util = app.getShapeUtilByDef(TLFrameShapeDef) const util = app.getShapeUtil(TLFrameUtil)
const fnStart = jest.fn() const fnStart = jest.fn()
util.onTranslateStart = fnStart util.onTranslateStart = fnStart
@ -170,7 +170,7 @@ describe('When interacting with a shape...', () => {
}) })
it('Uses the shape utils onClick handler', () => { it('Uses the shape utils onClick handler', () => {
const util = app.getShapeUtilByDef(TLFrameShapeDef) const util = app.getShapeUtil(TLFrameUtil)
const fnClick = jest.fn() const fnClick = jest.fn()
util.onClick = fnClick util.onClick = fnClick
@ -184,7 +184,7 @@ describe('When interacting with a shape...', () => {
}) })
it('Uses the shape utils onClick handler', () => { it('Uses the shape utils onClick handler', () => {
const util = app.getShapeUtilByDef(TLFrameShapeDef) const util = app.getShapeUtil(TLFrameUtil)
const fnClick = jest.fn((shape: any) => { const fnClick = jest.fn((shape: any) => {
return { return {

View file

@ -1,8 +1,5 @@
import { TLBookmarkShape } from '@tldraw/tlschema' import { TLBookmarkShape } from '@tldraw/tlschema'
import { import { TLBookmarkUtil } from '../../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
TLBookmarkShapeDef,
TLBookmarkUtil,
} from '../../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
import { TestApp } from '../TestApp' import { TestApp } from '../TestApp'
let app: TestApp let app: TestApp
@ -74,14 +71,14 @@ describe('The URL formatter', () => {
}, },
]) ])
const a = app.getShapeById(ids.a) as TLBookmarkShape const a = app.getShapeById<TLBookmarkShape>(ids.a)!
const b = app.getShapeById(ids.b) as TLBookmarkShape const b = app.getShapeById<TLBookmarkShape>(ids.b)!
const c = app.getShapeById(ids.c) as TLBookmarkShape const c = app.getShapeById<TLBookmarkShape>(ids.c)!
const d = app.getShapeById(ids.d) as TLBookmarkShape const d = app.getShapeById<TLBookmarkShape>(ids.d)!
const e = app.getShapeById(ids.e) as TLBookmarkShape const e = app.getShapeById<TLBookmarkShape>(ids.e)!
const f = app.getShapeById(ids.f) as TLBookmarkShape const f = app.getShapeById<TLBookmarkShape>(ids.f)!
const util = app.getShapeUtilByDef(TLBookmarkShapeDef) const util = app.getShapeUtil(TLBookmarkUtil)
expect(util.getHumanReadableAddress(a)).toBe('www.github.com') expect(util.getHumanReadableAddress(a)).toBe('www.github.com')
expect(util.getHumanReadableAddress(b)).toBe('www.github.com') expect(util.getHumanReadableAddress(b)).toBe('www.github.com')
expect(util.getHumanReadableAddress(c)).toBe('www.github.com/TodePond') expect(util.getHumanReadableAddress(c)).toBe('www.github.com/TodePond')

View file

@ -1,5 +1,5 @@
import { assert } from '@tldraw/utils' import { assert } from '@tldraw/utils'
import { TLLineShapeDef } from '../../app/shapeutils/TLLineUtil/TLLineUtil' import { TLLineUtil } from '../../app/shapeutils/TLLineUtil/TLLineUtil'
import { TestApp } from '../TestApp' import { TestApp } from '../TestApp'
let app: TestApp let app: TestApp
@ -124,7 +124,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
.pointerUp(20, 10) .pointerUp(20, 10)
const line = app.shapesArray[app.shapesArray.length - 1] const line = app.shapesArray[app.shapesArray.length - 1]
assert(TLLineShapeDef.is(line)) assert(app.isShapeOfType(line, TLLineUtil))
const handles = Object.values(line.props.handles) const handles = Object.values(line.props.handles)
expect(handles.length).toBe(3) expect(handles.length).toBe(3)
}) })
@ -141,7 +141,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
.pointerUp(30, 10) .pointerUp(30, 10)
const line = app.shapesArray[app.shapesArray.length - 1] const line = app.shapesArray[app.shapesArray.length - 1]
assert(TLLineShapeDef.is(line)) assert(app.isShapeOfType(line, TLLineUtil))
const handles = Object.values(line.props.handles) const handles = Object.values(line.props.handles)
expect(handles.length).toBe(3) expect(handles.length).toBe(3)
}) })
@ -159,7 +159,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
.pointerUp(30, 10) .pointerUp(30, 10)
const line = app.shapesArray[app.shapesArray.length - 1] const line = app.shapesArray[app.shapesArray.length - 1]
assert(TLLineShapeDef.is(line)) assert(app.isShapeOfType(line, TLLineUtil))
const handles = Object.values(line.props.handles) const handles = Object.values(line.props.handles)
expect(handles.length).toBe(3) expect(handles.length).toBe(3)
}) })
@ -179,7 +179,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
.pointerUp(30, 10) .pointerUp(30, 10)
const line = app.shapesArray[app.shapesArray.length - 1] const line = app.shapesArray[app.shapesArray.length - 1]
assert(TLLineShapeDef.is(line)) assert(app.isShapeOfType(line, TLLineUtil))
const handles = Object.values(line.props.handles) const handles = Object.values(line.props.handles)
expect(handles.length).toBe(3) expect(handles.length).toBe(3)
}) })
@ -201,7 +201,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
.pointerUp(40, 10) .pointerUp(40, 10)
const line = app.shapesArray[app.shapesArray.length - 1] const line = app.shapesArray[app.shapesArray.length - 1]
assert(TLLineShapeDef.is(line)) assert(app.isShapeOfType(line, TLLineUtil))
const handles = Object.values(line.props.handles) const handles = Object.values(line.props.handles)
expect(handles.length).toBe(3) expect(handles.length).toBe(3)
}) })

View file

@ -1,5 +1,5 @@
import { createCustomShapeId, TLArrowShape } from '@tldraw/tlschema' import { createCustomShapeId, TLArrowShape } from '@tldraw/tlschema'
import { TLFrameShapeDef } from '../../app/shapeutils/TLFrameUtil/TLFrameUtil' import { TLFrameUtil } from '../../app/shapeutils/TLFrameUtil/TLFrameUtil'
import { TestApp } from '../TestApp' import { TestApp } from '../TestApp'
let app: TestApp let app: TestApp
@ -33,7 +33,7 @@ describe('creating frames', () => {
app.setSelectedTool('frame') app.setSelectedTool('frame')
app.pointerDown(100, 100).pointerUp(100, 100) app.pointerDown(100, 100).pointerUp(100, 100)
expect(app.onlySelectedShape?.type).toBe('frame') expect(app.onlySelectedShape?.type).toBe('frame')
const { w, h } = app.getShapeUtilByDef(TLFrameShapeDef).defaultProps() const { w, h } = app.getShapeUtil(TLFrameUtil).defaultProps()
expect(app.getPageBounds(app.onlySelectedShape!)).toMatchObject({ expect(app.getPageBounds(app.onlySelectedShape!)).toMatchObject({
x: 100 - w / 2, x: 100 - w / 2,
y: 100 - h / 2, y: 100 - h / 2,

View file

@ -10,8 +10,8 @@ import {
TLShapePartial, TLShapePartial,
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { assert, compact } from '@tldraw/utils' import { assert, compact } from '@tldraw/utils'
import { TLArrowShapeDef } from '../../app/shapeutils/TLArrowUtil/TLArrowUtil' import { TLArrowUtil } from '../../app/shapeutils/TLArrowUtil/TLArrowUtil'
import { TLGroupShapeDef, TLGroupUtil } from '../../app/shapeutils/TLGroupUtil/TLGroupUtil' import { TLGroupUtil } from '../../app/shapeutils/TLGroupUtil/TLGroupUtil'
import { TLArrowTool } from '../../app/statechart/TLArrowTool/TLArrowTool' import { TLArrowTool } from '../../app/statechart/TLArrowTool/TLArrowTool'
import { TLDrawTool } from '../../app/statechart/TLDrawTool/TLDrawTool' import { TLDrawTool } from '../../app/statechart/TLDrawTool/TLDrawTool'
import { TLEraserTool } from '../../app/statechart/TLEraserTool/TLEraserTool' import { TLEraserTool } from '../../app/statechart/TLEraserTool/TLEraserTool'
@ -1671,7 +1671,7 @@ describe('moving handles within a group', () => {
target: 'handle', target: 'handle',
shape: arrow, shape: arrow,
handle: app handle: app
.getShapeUtilByDef(TLArrowShapeDef) .getShapeUtil(TLArrowUtil)
.handles(arrow) .handles(arrow)
.find((h) => h.id === 'end'), .find((h) => h.id === 'end'),
}) })
@ -1890,7 +1890,7 @@ describe('Group opacity', () => {
app.setProp('opacity', '0.5') app.setProp('opacity', '0.5')
app.groupShapes() app.groupShapes()
const group = app.getShapeById(onlySelectedId())! const group = app.getShapeById(onlySelectedId())!
assert(TLGroupShapeDef.is(group)) assert(app.isShapeOfType(group, TLGroupUtil))
expect(group.props.opacity).toBe('1') expect(group.props.opacity).toBe('1')
}) })
}) })

View file

@ -1,9 +1,7 @@
import { Box2d, Vec2d, VecLike } from '@tldraw/primitives' import { Box2d, Vec2d, VecLike } from '@tldraw/primitives'
import { TLShapeId, TLShapePartial, Vec2dModel, createCustomShapeId } from '@tldraw/tlschema' import { TLShapeId, TLShapePartial, Vec2dModel, createCustomShapeId } from '@tldraw/tlschema'
import { defineMigrations } from '@tldraw/tlstore'
import { GapsSnapLine, PointsSnapLine, SnapLine } from '../../app/managers/SnapManager' import { GapsSnapLine, PointsSnapLine, SnapLine } from '../../app/managers/SnapManager'
import { TLShapeUtil } from '../../app/shapeutils/TLShapeUtil' import { TLShapeUtil } from '../../app/shapeutils/TLShapeUtil'
import { defineShape } from '../../config/TLShapeDefinition'
import { TldrawEditorConfig } from '../../config/TldrawEditorConfig' import { TldrawEditorConfig } from '../../config/TldrawEditorConfig'
import { TestApp } from '../TestApp' import { TestApp } from '../TestApp'
@ -12,8 +10,8 @@ import { getSnapLines } from '../testutils/getSnapLines'
type __TopLeftSnapOnlyShape = any type __TopLeftSnapOnlyShape = any
class __TopLeftSnapOnlyShapeUtil extends TLShapeUtil<__TopLeftSnapOnlyShape> { class __TopLeftSnapOnlyShapeUtil extends TLShapeUtil<__TopLeftSnapOnlyShape> {
type = '__test_top_left_snap_only' as const static override type = '__test_top_left_snap_only' as const
static type = '__test_top_left_snap_only' as const
defaultProps(): __TopLeftSnapOnlyShape['props'] { defaultProps(): __TopLeftSnapOnlyShape['props'] {
return { width: 10, height: 10 } return { width: 10, height: 10 }
} }
@ -41,14 +39,14 @@ class __TopLeftSnapOnlyShapeUtil extends TLShapeUtil<__TopLeftSnapOnlyShape> {
return [Vec2d.From({ x: shape.x, y: shape.y })] return [Vec2d.From({ x: shape.x, y: shape.y })]
} }
} }
const __TopLeftSnapOnlyShapeDef = defineShape<__TopLeftSnapOnlyShape, __TopLeftSnapOnlyShapeUtil>({
type: '__test_top_left_snap_only',
getShapeUtil: () => __TopLeftSnapOnlyShapeUtil,
validator: { validate: (record) => record as __TopLeftSnapOnlyShape },
migrations: defineMigrations({}),
})
const configWithCustomShape = new TldrawEditorConfig({ shapes: [__TopLeftSnapOnlyShapeDef] }) const configWithCustomShape = new TldrawEditorConfig({
shapes: {
__test_top_left_snap_only: {
util: __TopLeftSnapOnlyShapeUtil,
},
},
})
let app: TestApp let app: TestApp

View file

@ -2,16 +2,14 @@ import { Box2d, Vec2d, VecLike } from '@tldraw/primitives'
import { import {
TLAsset, TLAsset,
TLAssetId, TLAssetId,
TLAssetShape,
TLBookmarkAsset, TLBookmarkAsset,
TLImageShape, TLImageShape,
TLShape,
TLShapePartial, TLShapePartial,
TLVideoShape, TLVideoShape,
Vec2dModel, Vec2dModel,
createShapeId, createShapeId,
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { compact, getHashForString, isNonNullish } from '@tldraw/utils' import { compact, getHashForString } from '@tldraw/utils'
import uniq from 'lodash.uniq' import uniq from 'lodash.uniq'
import { App } from '../app/App' import { App } from '../app/App'
import { MAX_ASSET_HEIGHT, MAX_ASSET_WIDTH } from '../constants' import { MAX_ASSET_HEIGHT, MAX_ASSET_WIDTH } from '../constants'
@ -328,7 +326,7 @@ export async function createShapesFromFiles(
const shapeUpdates = await Promise.all( const shapeUpdates = await Promise.all(
files.map(async (file, i) => { files.map(async (file, i) => {
const shape = results[i] as TLShapePartial<TLImageShape | TLVideoShape> const shape = results[i]
if (!shape) return if (!shape) return
const asset = newAssetsForFiles.get(file) const asset = newAssetsForFiles.get(file)
@ -344,7 +342,7 @@ export async function createShapesFromFiles(
shape.props.assetId = existing.id shape.props.assetId = existing.id
} }
return shape as TLShape return shape
} }
existing = app.getAssetBySrc(asset.props!.src!) existing = app.getAssetBySrc(asset.props!.src!)
@ -354,7 +352,7 @@ export async function createShapesFromFiles(
shape.props.assetId = existing.id shape.props.assetId = existing.id
} }
return shape as TLAssetShape return shape
} }
// Create a new model for the new source file // Create a new model for the new source file
@ -366,7 +364,7 @@ export async function createShapesFromFiles(
}) })
) )
const filteredUpdates = shapeUpdates.filter(isNonNullish) const filteredUpdates = compact(shapeUpdates)
app.createAssets(compact([...newAssetsForFiles.values()])) app.createAssets(compact([...newAssetsForFiles.values()]))
app.createShapes(filteredUpdates) app.createShapes(filteredUpdates)

View file

@ -24,6 +24,7 @@ import {
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { transact } from 'signia' import { transact } from 'signia'
import { App } from '../app/App' import { App } from '../app/App'
import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
import { MAX_SHAPES_PER_PAGE } from '../constants' import { MAX_SHAPES_PER_PAGE } from '../constants'
const TLDRAW_V1_VERSION = 15.5 const TLDRAW_V1_VERSION = 15.5
@ -515,8 +516,7 @@ export function buildFromV1Document(app: App, document: LegacyTldrawDocument) {
} }
const v2ShapeId = v1ShapeIdsToV2ShapeIds.get(v1Shape.id)! const v2ShapeId = v1ShapeIdsToV2ShapeIds.get(v1Shape.id)!
const v2ShapeStale = app.getShapeById<TLArrowShape>(v2ShapeId)! const util = app.getShapeUtil(TLArrowUtil)
const util = app.getShapeUtil(v2ShapeStale)
// dumb but necessary // dumb but necessary
app.inputs.ctrlKey = false app.inputs.ctrlKey = false

View file

@ -13,7 +13,6 @@ import { Store } from '@tldraw/tlstore';
import { StoreSchema } from '@tldraw/tlstore'; import { StoreSchema } from '@tldraw/tlstore';
import { StoreSchemaOptions } from '@tldraw/tlstore'; import { StoreSchemaOptions } from '@tldraw/tlstore';
import { StoreSnapshot } from '@tldraw/tlstore'; import { StoreSnapshot } from '@tldraw/tlstore';
import { StoreValidator } from '@tldraw/tlstore';
import { T } from '@tldraw/tlvalidate'; import { T } from '@tldraw/tlvalidate';
// @internal (undocumented) // @internal (undocumented)
@ -105,9 +104,9 @@ export function createShapeValidator<Type extends string, Props extends object>(
}>; }>;
// @public (undocumented) // @public (undocumented)
export function createTLSchema({ customShapeDefs, allowUnknownShapes, derivePresenceState, }: { export function createTLSchema({ shapeMigrations, shapeValidators, derivePresenceState, }: {
customShapeDefs?: readonly CustomShapeTypeInfo[]; shapeValidators: ValidatorsForShapes<TLShape>;
allowUnknownShapes?: boolean; shapeMigrations: MigrationsForShapes<TLShape>;
derivePresenceState?: (store: TLStore) => Signal<null | TLInstancePresence>; derivePresenceState?: (store: TLStore) => Signal<null | TLInstancePresence>;
}): StoreSchema<TLRecord, TLStoreProps>; }): StoreSchema<TLRecord, TLStoreProps>;
@ -117,13 +116,6 @@ export const cursorTypeValidator: T.Validator<string>;
// @public (undocumented) // @public (undocumented)
export const cursorValidator: T.Validator<TLCursor>; export const cursorValidator: T.Validator<TLCursor>;
// @public (undocumented)
export type CustomShapeTypeInfo = {
type: string;
migrations?: Migrations;
validator?: StoreValidator<TLShape>;
};
// @internal (undocumented) // @internal (undocumented)
export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">; export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">;
@ -344,6 +336,9 @@ export function fixupRecord(oldRecord: TLRecord): {
// @internal (undocumented) // @internal (undocumented)
export const fontValidator: T.Validator<"draw" | "mono" | "sans" | "serif">; export const fontValidator: T.Validator<"draw" | "mono" | "sans" | "serif">;
// @public (undocumented)
export const frameShapeTypeMigrations: Migrations;
// @public (undocumented) // @public (undocumented)
export const frameShapeTypeValidator: T.Validator<TLFrameShape>; export const frameShapeTypeValidator: T.Validator<TLFrameShape>;
@ -356,12 +351,18 @@ export const geoShapeTypeValidator: T.Validator<TLGeoShape>;
// @internal (undocumented) // @internal (undocumented)
export const geoValidator: T.Validator<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">; export const geoValidator: T.Validator<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
// @public (undocumented)
export const groupShapeTypeMigrations: Migrations;
// @public (undocumented) // @public (undocumented)
export const groupShapeTypeValidator: T.Validator<TLGroupShape>; export const groupShapeTypeValidator: T.Validator<TLGroupShape>;
// @public (undocumented) // @public (undocumented)
export const handleTypeValidator: T.Validator<TLHandle>; export const handleTypeValidator: T.Validator<TLHandle>;
// @public (undocumented)
export const iconShapeTypeMigrations: Migrations;
// @public (undocumented) // @public (undocumented)
export const iconShapeTypeValidator: T.Validator<TLIconShape>; export const iconShapeTypeValidator: T.Validator<TLIconShape>;
@ -404,9 +405,15 @@ export function isShape(record?: BaseRecord<string>): record is TLShape;
// @public (undocumented) // @public (undocumented)
export function isShapeId(id?: string): id is TLShapeId; export function isShapeId(id?: string): id is TLShapeId;
// @public (undocumented)
export const lineShapeTypeMigrations: Migrations;
// @public (undocumented) // @public (undocumented)
export const lineShapeTypeValidator: T.Validator<TLLineShape>; export const lineShapeTypeValidator: T.Validator<TLLineShape>;
// @public (undocumented)
export type MigrationsForShapes<T extends TLUnknownShape> = Record<T['type'], Migrations>;
// @public (undocumented) // @public (undocumented)
export const noteShapeTypeMigrations: Migrations; export const noteShapeTypeMigrations: Migrations;
@ -1371,6 +1378,11 @@ export const userPresenceTypeValidator: T.Validator<TLUserPresence>;
// @public (undocumented) // @public (undocumented)
export const userTypeValidator: T.Validator<TLUser>; export const userTypeValidator: T.Validator<TLUser>;
// @public (undocumented)
export type ValidatorsForShapes<T extends TLUnknownShape> = Record<T['type'], {
validate: (record: T) => T;
}>;
// @public (undocumented) // @public (undocumented)
export interface Vec2dModel { export interface Vec2dModel {
// (undocumented) // (undocumented)

View file

@ -1,10 +1,4 @@
import { import { Migrations, StoreSchema, createRecordType, defineMigrations } from '@tldraw/tlstore'
Migrations,
StoreSchema,
StoreValidator,
createRecordType,
defineMigrations,
} from '@tldraw/tlstore'
import { T } from '@tldraw/tlvalidate' import { T } from '@tldraw/tlvalidate'
import { Signal } from 'signia' import { Signal } from 'signia'
import { TLRecord } from './TLRecord' import { TLRecord } from './TLRecord'
@ -17,68 +11,32 @@ import { TLInstance } from './records/TLInstance'
import { TLInstancePageState } from './records/TLInstancePageState' import { TLInstancePageState } from './records/TLInstancePageState'
import { TLInstancePresence } from './records/TLInstancePresence' import { TLInstancePresence } from './records/TLInstancePresence'
import { TLPage } from './records/TLPage' import { TLPage } from './records/TLPage'
import { TLShape, rootShapeTypeMigrations } from './records/TLShape' import { TLShape, TLUnknownShape, rootShapeTypeMigrations } from './records/TLShape'
import { TLUser } from './records/TLUser' import { TLUser } from './records/TLUser'
import { TLUserDocument } from './records/TLUserDocument' import { TLUserDocument } from './records/TLUserDocument'
import { TLUserPresence } from './records/TLUserPresence' import { TLUserPresence } from './records/TLUserPresence'
import { storeMigrations } from './schema' import { storeMigrations } from './schema'
import { arrowShapeTypeMigrations, arrowShapeTypeValidator } from './shapes/TLArrowShape'
import { bookmarkShapeTypeMigrations, bookmarkShapeTypeValidator } from './shapes/TLBookmarkShape'
import { drawShapeTypeMigrations, drawShapeTypeValidator } from './shapes/TLDrawShape'
import { embedShapeTypeMigrations, embedShapeTypeValidator } from './shapes/TLEmbedShape'
import { frameShapeTypeMigrations, frameShapeTypeValidator } from './shapes/TLFrameShape'
import { geoShapeTypeMigrations, geoShapeTypeValidator } from './shapes/TLGeoShape'
import { groupShapeTypeMigrations, groupShapeTypeValidator } from './shapes/TLGroupShape'
import { imageShapeTypeMigrations, imageShapeTypeValidator } from './shapes/TLImageShape'
import { lineShapeTypeMigrations, lineShapeTypeValidator } from './shapes/TLLineShape'
import { noteShapeTypeMigrations, noteShapeTypeValidator } from './shapes/TLNoteShape'
import { textShapeTypeMigrations, textShapeTypeValidator } from './shapes/TLTextShape'
import { videoShapeTypeMigrations, videoShapeTypeValidator } from './shapes/TLVideoShape'
const CORE_SHAPE_DEFS: readonly CustomShapeTypeInfo[] = [
{ type: 'draw', migrations: drawShapeTypeMigrations, validator: drawShapeTypeValidator },
{ type: 'text', migrations: textShapeTypeMigrations, validator: textShapeTypeValidator },
{ type: 'line', migrations: lineShapeTypeMigrations, validator: lineShapeTypeValidator },
{ type: 'arrow', migrations: arrowShapeTypeMigrations, validator: arrowShapeTypeValidator },
{ type: 'image', migrations: imageShapeTypeMigrations, validator: imageShapeTypeValidator },
{ type: 'video', migrations: videoShapeTypeMigrations, validator: videoShapeTypeValidator },
{ type: 'geo', migrations: geoShapeTypeMigrations, validator: geoShapeTypeValidator },
{ type: 'note', migrations: noteShapeTypeMigrations, validator: noteShapeTypeValidator },
{ type: 'group', migrations: groupShapeTypeMigrations, validator: groupShapeTypeValidator },
{
type: 'bookmark',
migrations: bookmarkShapeTypeMigrations,
validator: bookmarkShapeTypeValidator,
},
{ type: 'frame', migrations: frameShapeTypeMigrations, validator: frameShapeTypeValidator },
{ type: 'embed', migrations: embedShapeTypeMigrations, validator: embedShapeTypeValidator },
]
/** @public */ /** @public */
export type CustomShapeTypeInfo = { export type ValidatorsForShapes<T extends TLUnknownShape> = Record<
type: string T['type'],
migrations?: Migrations { validate: (record: T) => T }
validator?: StoreValidator<TLShape> >
}
/** @public */
export type MigrationsForShapes<T extends TLUnknownShape> = Record<T['type'], Migrations>
/** @public */ /** @public */
export function createTLSchema({ export function createTLSchema({
customShapeDefs, shapeMigrations,
allowUnknownShapes, shapeValidators,
derivePresenceState, derivePresenceState,
}: { }: {
customShapeDefs?: readonly CustomShapeTypeInfo[] shapeValidators: ValidatorsForShapes<TLShape>
allowUnknownShapes?: boolean shapeMigrations: MigrationsForShapes<TLShape>
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null> derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
}) { }) {
const allShapeDefs = [...CORE_SHAPE_DEFS, ...(customShapeDefs ?? [])] // Removed check to see whether a shape type has already been defined
const typeSet = new Set<string>()
for (const shapeDef of allShapeDefs) {
if (typeSet.has(shapeDef.type)) {
throw new Error(`Shape type ${shapeDef.type} is already defined`)
}
typeSet.add(shapeDef.type)
}
const shapeTypeMigrations = defineMigrations({ const shapeTypeMigrations = defineMigrations({
currentVersion: rootShapeTypeMigrations.currentVersion, currentVersion: rootShapeTypeMigrations.currentVersion,
@ -86,20 +44,18 @@ export function createTLSchema({
migrators: rootShapeTypeMigrations.migrators, migrators: rootShapeTypeMigrations.migrators,
subTypeKey: 'type', subTypeKey: 'type',
subTypeMigrations: Object.fromEntries( subTypeMigrations: Object.fromEntries(
allShapeDefs.map((def) => [def.type, def.migrations ?? {}]) Object.entries(shapeMigrations) as [TLShape['type'], Migrations][]
) as Record<string, Migrations>, ),
}) })
let shapeValidator = T.union('type', { const shapeTypeValidator = T.union(
...Object.fromEntries(allShapeDefs.map((def) => [def.type, def.validator ?? (T.any as any)])), 'type',
}) as T.UnionValidator<'type', any, any> Object.fromEntries(Object.entries(shapeValidators) as [TLShape['type'], T.Validator<any>][])
if (allowUnknownShapes) { )
shapeValidator = shapeValidator.validateUnknownVariants((shape) => shape as any)
}
const shapeRecord = createRecordType<TLShape>('shape', { const shapeRecord = createRecordType<TLShape>('shape', {
migrations: shapeTypeMigrations, migrations: shapeTypeMigrations,
validator: T.model('shape', shapeValidator), validator: T.model('shape', shapeTypeValidator),
scope: 'document', scope: 'document',
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false })) }).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false }))

View file

@ -24,8 +24,11 @@ export {
type TLVideoAsset, type TLVideoAsset,
} from './assets/TLVideoAsset' } from './assets/TLVideoAsset'
export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation' export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation'
export { createTLSchema } from './createTLSchema' export {
export type { CustomShapeTypeInfo } from './createTLSchema' createTLSchema,
type MigrationsForShapes,
type ValidatorsForShapes,
} from './createTLSchema'
export { defaultDerivePresenceState } from './defaultDerivePresenceState' export { defaultDerivePresenceState } from './defaultDerivePresenceState'
export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup' export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup'
export { type Box2dModel, type Vec2dModel } from './geometry-types' export { type Box2dModel, type Vec2dModel } from './geometry-types'
@ -121,6 +124,7 @@ export {
type TLEmbedShapeProps, type TLEmbedShapeProps,
} from './shapes/TLEmbedShape' } from './shapes/TLEmbedShape'
export { export {
frameShapeTypeMigrations,
frameShapeTypeValidator, frameShapeTypeValidator,
type TLFrameShape, type TLFrameShape,
type TLFrameShapeProps, type TLFrameShapeProps,
@ -132,11 +136,13 @@ export {
type TLGeoShapeProps, type TLGeoShapeProps,
} from './shapes/TLGeoShape' } from './shapes/TLGeoShape'
export { export {
groupShapeTypeMigrations,
groupShapeTypeValidator, groupShapeTypeValidator,
type TLGroupShape, type TLGroupShape,
type TLGroupShapeProps, type TLGroupShapeProps,
} from './shapes/TLGroupShape' } from './shapes/TLGroupShape'
export { export {
iconShapeTypeMigrations,
iconShapeTypeValidator, iconShapeTypeValidator,
type TLIconShape, type TLIconShape,
type TLIconShapeProps, type TLIconShapeProps,
@ -149,6 +155,7 @@ export {
type TLImageShapeProps, type TLImageShapeProps,
} from './shapes/TLImageShape' } from './shapes/TLImageShape'
export { export {
lineShapeTypeMigrations,
lineShapeTypeValidator, lineShapeTypeValidator,
type TLLineShape, type TLLineShape,
type TLLineShapeProps, type TLLineShapeProps,

View file

@ -33,13 +33,13 @@ export type TLShape =
| TLFrameShape | TLFrameShape
| TLGeoShape | TLGeoShape
| TLGroupShape | TLGroupShape
| TLIconShape
| TLImageShape | TLImageShape
| TLLineShape | TLLineShape
| TLNoteShape | TLNoteShape
| TLTextShape | TLTextShape
| TLVideoShape | TLVideoShape
| TLUnknownShape | TLUnknownShape
| TLIconShape
/** @public */ /** @public */
export type TLShapeType = TLShape['type'] export type TLShapeType = TLShape['type']

View file

@ -42,7 +42,7 @@ export type ComputedCache<Data, R extends BaseRecord> = {
// @public // @public
export function createRecordType<R extends BaseRecord>(typeName: R['typeName'], config: { export function createRecordType<R extends BaseRecord>(typeName: R['typeName'], config: {
migrations?: Migrations; migrations?: Migrations;
validator: StoreValidator<R>; validator?: StoreValidator<R>;
scope: Scope; scope: Scope;
}): RecordType<R, keyof Omit<R, 'id' | 'typeName'>>; }): RecordType<R, keyof Omit<R, 'id' | 'typeName'>>;

View file

@ -218,8 +218,7 @@ export function createRecordType<R extends BaseRecord>(
typeName: R['typeName'], typeName: R['typeName'],
config: { config: {
migrations?: Migrations migrations?: Migrations
// todo: optional validations validator?: StoreValidator<R>
validator: StoreValidator<R>
scope: Scope scope: Scope
} }
): RecordType<R, keyof Omit<R, 'id' | 'typeName'>> { ): RecordType<R, keyof Omit<R, 'id' | 'typeName'>> {

View file

@ -152,7 +152,7 @@ export class StoreSchema<R extends BaseRecord, P = unknown> {
const persistedSubTypeVersion = const persistedSubTypeVersion =
'subTypeVersions' in persistedType 'subTypeVersions' in persistedType
? persistedType.subTypeVersions[record[ourType.migrations.subTypeKey as keyof R] as string] ? persistedType.subTypeVersions[record[ourType.migrations.subTypeKey as keyof R] as string]
: null : undefined
// if ourSubTypeMigrations is undefined then we don't have access to the migrations for this subtype // if ourSubTypeMigrations is undefined then we don't have access to the migrations for this subtype
// that is almost certainly because we are running on the server and this type was supplied by a 3rd party. // that is almost certainly because we are running on the server and this type was supplied by a 3rd party.
@ -165,7 +165,7 @@ export class StoreSchema<R extends BaseRecord, P = unknown> {
// if the persistedSubTypeVersion is undefined then the record was either created after the schema // if the persistedSubTypeVersion is undefined then the record was either created after the schema
// was persisted, or it was created in a different place to where the schema was persisted. // was persisted, or it was created in a different place to where the schema was persisted.
// either way we don't know what to do with it safely, so let's return failure. // either way we don't know what to do with it safely, so let's return failure.
if (persistedSubTypeVersion == null) { if (persistedSubTypeVersion === undefined) {
return { type: 'error', reason: MigrationFailureReason.IncompatibleSubtype } return { type: 'error', reason: MigrationFailureReason.IncompatibleSubtype }
} }

View file

@ -1,4 +1,4 @@
import { useApp } from '@tldraw/editor' import { TLBookmarkUtil, TLShape, useApp } from '@tldraw/editor'
import { useCallback, useRef, useState } from 'react' import { useCallback, useRef, useState } from 'react'
import { track } from 'signia-react' import { track } from 'signia-react'
import { DialogProps } from '../hooks/useDialogsProvider' import { DialogProps } from '../hooks/useDialogsProvider'
@ -19,12 +19,30 @@ function valiateUrl(url: string) {
return false return false
} }
export const EditLinkDialog = track(function EditLink({ onClose }: DialogProps) { export const EditLinkDialog = track(function EditLinkDialog({ onClose }: DialogProps) {
const app = useApp() const app = useApp()
const msg = useTranslation()
const selectedShape = app.onlySelectedShape const selectedShape = app.onlySelectedShape
if (!(selectedShape && 'url' in selectedShape.props)) {
return null
}
return (
<EditLinkDialogInner
onClose={onClose}
selectedShape={selectedShape as TLShape & { props: { url: string } }}
/>
)
})
export const EditLinkDialogInner = track(function EditLinkDialogInner({
onClose,
selectedShape,
}: DialogProps & { selectedShape: TLShape & { props: { url: string } } }) {
const app = useApp()
const msg = useTranslation()
const [validState, setValid] = useState(valiateUrl(selectedShape?.props.url)) const [validState, setValid] = useState(valiateUrl(selectedShape?.props.url))
const rInitialValue = useRef(selectedShape?.props.url) const rInitialValue = useRef(selectedShape?.props.url)
@ -64,13 +82,13 @@ export const EditLinkDialog = track(function EditLink({ onClose }: DialogProps)
const shape = app.selectedShapes[0] const shape = app.selectedShapes[0]
if (shape) { if (shape && 'url' in shape.props) {
const current = shape.props.url const current = shape.props.url
const next = validState const next = validState
? validState === 'needs protocol' ? validState === 'needs protocol'
? 'https://' + value ? 'https://' + value
: value : value
: shape.type === 'bookmark' : app.isShapeOfType(shape, TLBookmarkUtil)
? rInitialValue.current ? rInitialValue.current
: '' : ''

View file

@ -4,7 +4,7 @@ import {
FONT_SIZES, FONT_SIZES,
INDENT, INDENT,
TEXT_PROPS, TEXT_PROPS,
TLTextShapeDef, TLTextUtil,
createShapeId, createShapeId,
} from '@tldraw/editor' } from '@tldraw/editor'
import { VecLike } from '@tldraw/primitives' import { VecLike } from '@tldraw/primitives'
@ -64,7 +64,7 @@ function stripTrailingWhitespace(text: string): string {
*/ */
export async function pastePlainText(app: App, text: string, point?: VecLike) { export async function pastePlainText(app: App, text: string, point?: VecLike) {
const p = point ?? (app.inputs.shiftKey ? app.inputs.currentPagePoint : app.viewportPageCenter) const p = point ?? (app.inputs.shiftKey ? app.inputs.currentPagePoint : app.viewportPageCenter)
const defaultProps = app.getShapeUtilByDef(TLTextShapeDef).defaultProps() const defaultProps = app.getShapeUtil(TLTextUtil).defaultProps()
const textToPaste = stripTrailingWhitespace( const textToPaste = stripTrailingWhitespace(
stripCommonMinimumIndentation(replaceTabsWithSpaces(text)) stripCommonMinimumIndentation(replaceTabsWithSpaces(text))

View file

@ -1,4 +1,4 @@
import { App, TLArrowShapeDef, useApp } from '@tldraw/editor' import { App, TLArrowUtil, useApp } from '@tldraw/editor'
import { assert, exhaustiveSwitchError } from '@tldraw/utils' import { assert, exhaustiveSwitchError } from '@tldraw/utils'
import { useValue } from 'signia-react' import { useValue } from 'signia-react'
import { ActionItem } from './useActions' import { ActionItem } from './useActions'
@ -136,10 +136,10 @@ function shapesWithUnboundArrows(app: App) {
return selectedShapes.filter((shape) => { return selectedShapes.filter((shape) => {
if (!shape) return false if (!shape) return false
if (TLArrowShapeDef.is(shape) && shape.props.start.type === 'binding') { if (app.isShapeOfType(shape, TLArrowUtil) && shape.props.start.type === 'binding') {
return false return false
} }
if (TLArrowShapeDef.is(shape) && shape.props.end.type === 'binding') { if (app.isShapeOfType(shape, TLArrowUtil) && shape.props.end.type === 'binding') {
return false return false
} }
return true return true

View file

@ -5,11 +5,12 @@ import {
DEFAULT_BOOKMARK_WIDTH, DEFAULT_BOOKMARK_WIDTH,
getEmbedInfo, getEmbedInfo,
openWindow, openWindow,
TLBookmarkShapeDef, TLBookmarkUtil,
TLEmbedShapeDef, TLEmbedUtil,
TLShapeId, TLShapeId,
TLShapePartial, TLShapePartial,
TLTextShape, TLTextShape,
TLTextUtil,
useApp, useApp,
} from '@tldraw/editor' } from '@tldraw/editor'
import { approximately, Box2d, TAU, Vec2d } from '@tldraw/primitives' import { approximately, Box2d, TAU, Vec2d } from '@tldraw/primitives'
@ -210,19 +211,21 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
trackEvent('toggle-auto-size', { source }) trackEvent('toggle-auto-size', { source })
app.mark() app.mark()
app.updateShapes( app.updateShapes(
app.selectedShapes (
.filter((shape) => shape && shape.type === 'text' && shape.props.autoSize === false) app.selectedShapes.filter(
.map((shape: TLTextShape) => { (shape) => app.isShapeOfType(shape, TLTextUtil) && shape.props.autoSize === false
return { ) as TLTextShape[]
id: shape.id, ).map((shape) => {
type: shape.type, return {
props: { id: shape.id,
...shape.props, type: shape.type,
w: 8, props: {
autoSize: true, ...shape.props,
}, w: 8,
} as TLTextShape autoSize: true,
}) },
}
})
) )
}, },
}, },
@ -239,7 +242,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
return return
} }
const shape = app.getShapeById(ids[0]) const shape = app.getShapeById(ids[0])
if (!shape || !TLEmbedShapeDef.is(shape)) { if (!shape || !app.isShapeOfType(shape, TLEmbedUtil)) {
console.error(warnMsg) console.error(warnMsg)
return return
} }
@ -259,7 +262,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
const createList: TLShapePartial[] = [] const createList: TLShapePartial[] = []
const deleteList: TLShapeId[] = [] const deleteList: TLShapeId[] = []
for (const shape of shapes) { for (const shape of shapes) {
if (!shape || !TLEmbedShapeDef.is(shape) || !shape.props.url) continue if (!shape || !app.isShapeOfType(shape, TLEmbedUtil) || !shape.props.url) continue
const newPos = new Vec2d(shape.x, shape.y) const newPos = new Vec2d(shape.x, shape.y)
newPos.rot(-shape.rotation) newPos.rot(-shape.rotation)
@ -302,7 +305,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
const createList: TLShapePartial[] = [] const createList: TLShapePartial[] = []
const deleteList: TLShapeId[] = [] const deleteList: TLShapeId[] = []
for (const shape of shapes) { for (const shape of shapes) {
if (!TLBookmarkShapeDef.is(shape)) continue if (!app.isShapeOfType(shape, TLBookmarkUtil)) continue
const { url } = shape.props const { url } = shape.props

View file

@ -3,12 +3,12 @@ import {
getValidHttpURLList, getValidHttpURLList,
isSvgText, isSvgText,
isValidHttpURL, isValidHttpURL,
TLArrowShapeDef, TLArrowUtil,
TLBookmarkShapeDef, TLBookmarkUtil,
TLClipboardModel, TLClipboardModel,
TLEmbedShapeDef, TLEmbedUtil,
TLGeoShapeDef, TLGeoUtil,
TLTextShapeDef, TLTextUtil,
useApp, useApp,
} from '@tldraw/editor' } from '@tldraw/editor'
import { VecLike } from '@tldraw/primitives' import { VecLike } from '@tldraw/primitives'
@ -495,10 +495,14 @@ const handleNativeOrMenuCopy = (app: App) => {
// Extract the text from the clipboard // Extract the text from the clipboard
const textItems = content.shapes const textItems = content.shapes
.map((shape) => { .map((shape) => {
if (TLTextShapeDef.is(shape) || TLGeoShapeDef.is(shape) || TLArrowShapeDef.is(shape)) { if (
app.isShapeOfType(shape, TLTextUtil) ||
app.isShapeOfType(shape, TLGeoUtil) ||
app.isShapeOfType(shape, TLArrowUtil)
) {
return shape.props.text return shape.props.text
} }
if (TLBookmarkShapeDef.is(shape) || TLEmbedShapeDef.is(shape)) { if (app.isShapeOfType(shape, TLBookmarkUtil) || app.isShapeOfType(shape, TLEmbedUtil)) {
return shape.props.url return shape.props.url
} }
return null return null

View file

@ -1,12 +1,12 @@
import { App, getEmbedInfo, TLBookmarkShapeDef, TLEmbedShapeDef, useApp } from '@tldraw/editor' import { App, TLBookmarkUtil, TLEmbedUtil, getEmbedInfo, useApp } from '@tldraw/editor'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { track, useValue } from 'signia-react' import { track, useValue } from 'signia-react'
import { import {
MenuSchema,
compactMenuItems, compactMenuItems,
menuCustom, menuCustom,
menuGroup, menuGroup,
menuItem, menuItem,
MenuSchema,
menuSubmenu, menuSubmenu,
showMenuPaste, showMenuPaste,
useAllowGroup, useAllowGroup,
@ -63,7 +63,7 @@ export const ContextMenuSchemaProvider = track(function ContextMenuSchemaProvide
if (app.selectedIds.length !== 1) return false if (app.selectedIds.length !== 1) return false
return app.selectedIds.some((selectedId) => { return app.selectedIds.some((selectedId) => {
const shape = app.getShapeById(selectedId) const shape = app.getShapeById(selectedId)
return shape && TLEmbedShapeDef.is(shape) && shape.props.url return shape && app.isShapeOfType(shape, TLEmbedUtil) && shape.props.url
}) })
}, },
[] []
@ -74,7 +74,7 @@ export const ContextMenuSchemaProvider = track(function ContextMenuSchemaProvide
if (app.selectedIds.length !== 1) return false if (app.selectedIds.length !== 1) return false
return app.selectedIds.some((selectedId) => { return app.selectedIds.some((selectedId) => {
const shape = app.getShapeById(selectedId) const shape = app.getShapeById(selectedId)
return shape && TLBookmarkShapeDef.is(shape) && getEmbedInfo(shape.props.url) return shape && app.isShapeOfType(shape, TLBookmarkUtil) && getEmbedInfo(shape.props.url)
}) })
}, },
[] []

View file

@ -3,7 +3,7 @@ import {
getSvgAsDataUrl, getSvgAsDataUrl,
getSvgAsImage, getSvgAsImage,
TLExportType, TLExportType,
TLFrameShape, TLFrameUtil,
TLShapeId, TLShapeId,
useApp, useApp,
} from '@tldraw/editor' } from '@tldraw/editor'
@ -38,8 +38,8 @@ export function useExportAs() {
if (ids.length === 1) { if (ids.length === 1) {
const first = app.getShapeById(ids[0])! const first = app.getShapeById(ids[0])!
if (first.type === 'frame') { if (app.isShapeOfType(first, TLFrameUtil)) {
name = (first as TLFrameShape).props.name ?? 'frame' name = first.props.name ?? 'frame'
} else { } else {
name = first.id.replace(/:/, '_') name = first.id.replace(/:/, '_')
} }

View file

@ -1,4 +1,4 @@
import { useApp } from '@tldraw/editor' import { TLTextUtil, useApp } from '@tldraw/editor'
import { useValue } from 'signia-react' import { useValue } from 'signia-react'
export function useShowAutoSizeToggle() { export function useShowAutoSizeToggle() {
@ -9,7 +9,7 @@ export function useShowAutoSizeToggle() {
const { selectedShapes } = app const { selectedShapes } = app
return ( return (
selectedShapes.length === 1 && selectedShapes.length === 1 &&
selectedShapes[0].type === 'text' && app.isShapeOfType(selectedShapes[0], TLTextUtil) &&
selectedShapes[0].props.autoSize === false selectedShapes[0].props.autoSize === false
) )
}, },