[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 {
defineShape,
HTMLContainer,
MenuGroup,
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
// ----------
// 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
// for this shape? What should we render for it, or for its indicator?
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
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
const customTldrawConfig = new TldrawEditorConfig({
tools: [CardTool],
shapes: [CardShape],
allowUnknownShapes: true,
shapes: {
card: {
util: CardUtil,
},
},
})
// ... and we can make our custom shape example!

View file

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

View file

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

View file

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

View file

@ -76,7 +76,6 @@ import {
import { EventEmitter } from 'eventemitter3'
import { nanoid } from 'nanoid'
import { EMPTY_ARRAY, atom, computed, transact } from 'signia'
import { TLShapeDef } from '../config/TLShapeDefinition'
import { TldrawEditorConfig } from '../config/TldrawEditorConfig'
import {
ANIMATION_MEDIUM_MS,
@ -118,17 +117,17 @@ import { HistoryManager } from './managers/HistoryManager'
import { SnapManager } from './managers/SnapManager'
import { TextManager } from './managers/TextManager'
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 {
getArrowTerminalsInArrowSpace,
getIsArrowStraight,
} from './shapeutils/TLArrowUtil/arrow/shared'
import { getStraightArrowInfo } from './shapeutils/TLArrowUtil/arrow/straight-arrow'
import { TLFrameShapeDef } from './shapeutils/TLFrameUtil/TLFrameUtil'
import { TLGroupShapeDef } from './shapeutils/TLGroupUtil/TLGroupUtil'
import { TLFrameUtil } from './shapeutils/TLFrameUtil/TLFrameUtil'
import { TLGroupUtil } from './shapeutils/TLGroupUtil/TLGroupUtil'
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 { RootState } from './statechart/RootState'
import { StateNode } from './statechart/StateNode'
@ -192,10 +191,7 @@ export class App extends EventEmitter<TLEventMap> {
// Set the shape utils
this.shapeUtils = Object.fromEntries(
config.shapes.map((def) => [
def.type,
def.createShapeUtils(this) as TLShapeUtil<TLUnknownShape>,
])
Object.entries(config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)])
)
if (typeof window !== 'undefined' && 'navigator' in window) {
@ -243,7 +239,7 @@ export class App extends EventEmitter<TLEventMap> {
this._updateDepth--
}
this.store.onAfterCreate = (record) => {
if (record.typeName === 'shape' && TLArrowShapeDef.is(record)) {
if (record.typeName === 'shape' && this.isShapeOfType(record, TLArrowUtil)) {
this._arrowDidUpdate(record)
}
}
@ -934,38 +930,42 @@ export class App extends EventEmitter<TLEventMap> {
*/
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.
*
* @example
*
* ```ts
* app.getShapeUtilByDef(TLDrawShapeDef)
* app.getShapeUtil(TLArrowUtil)
* ```
*
* @param def - The shape definition.
* @param util - The shape util.
* @public
*/
getShapeUtilByDef<Def extends TLShapeDef<any, any>>(
def: Def
): ReturnType<Def['createShapeUtils']> {
return this.shapeUtils[def.type] as ReturnType<Def['createShapeUtils']>
getShapeUtil<C extends { new (...args: any[]): TLShapeUtil<any>; type: string }>(
util: C
): InstanceType<C>
/**
* 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) {
this._invalidParents.delete(parentId)
const parent = this.getShapeById(parentId)
@ -1213,7 +1214,7 @@ export class App extends EventEmitter<TLEventMap> {
/** @internal */
private _reparentArrow(arrowId: TLShapeId) {
const arrow = this.getShapeById(arrowId) as TLArrowShape | undefined
const arrow = this.getShapeById<TLArrowShape>(arrowId)
if (!arrow) return
const { start, end } = arrow.props
const startShape = start.type === 'binding' ? this.getShapeById(start.boundShapeId) : undefined
@ -1237,7 +1238,8 @@ export class App extends EventEmitter<TLEventMap> {
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 endSibling = this.getNearestSiblingShape(reparentedArrow, endShape)
@ -1303,6 +1305,7 @@ export class App extends EventEmitter<TLEventMap> {
// const update = this.getShapeUtil(next).onUpdate?.(prev, next)
// return update ?? next
// }
@computed
private get _allPageStates() {
return this.store.query.records('instance_page_state')
@ -1400,7 +1403,7 @@ export class App extends EventEmitter<TLEventMap> {
/** @internal */
private _shapeDidChange(prev: TLShape, next: TLShape) {
if (TLArrowShapeDef.is(next)) {
if (this.isShapeOfType(next, TLArrowUtil)) {
this._arrowDidUpdate(next)
}
@ -3019,8 +3022,8 @@ export class App extends EventEmitter<TLEventMap> {
* @readonly
* @public
*/
@computed get shapesArray() {
return Array.from(this.shapeIds).map((id) => this.store.get(id)! as TLShape)
@computed get shapesArray(): TLShape[] {
return Array.from(this.shapeIds).map((id) => this.store.get(id)!)
}
/**
@ -3074,7 +3077,7 @@ export class App extends EventEmitter<TLEventMap> {
* @public
* @readonly
*/
@computed get selectedShapes() {
@computed get selectedShapes(): TLShape[] {
const { selectedIds } = this.pageState
return compact(selectedIds.map((id) => this.store.get(id)))
}
@ -3093,11 +3096,32 @@ export class App extends EventEmitter<TLEventMap> {
* @public
* @readonly
*/
@computed get onlySelectedShape() {
@computed get onlySelectedShape(): TLShape | null {
const { selectedShapes } = this
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.
*
@ -4024,12 +4048,12 @@ export class App extends EventEmitter<TLEventMap> {
let shapes = dedupe(
ids
.map((id) => this.getShapeById(id) as TLShape)
.map((id) => this.getShapeById(id)!)
.sort(sortByIndex)
.flatMap((shape) => {
const allShapes = [shape]
this.visitDescendants(shape.id, (descendant) => {
allShapes.push(this.getShapeById(descendant) as TLShape)
allShapes.push(this.getShapeById(descendant)!)
})
return allShapes
})
@ -4040,14 +4064,14 @@ export class App extends EventEmitter<TLEventMap> {
shape = structuredClone(shape) as typeof shape
if (TLArrowShapeDef.is(shape)) {
if (this.isShapeOfType(shape, TLArrowUtil)) {
const startBindingId =
shape.props.start.type === 'binding' ? shape.props.start.boundShapeId : undefined
const endBindingId =
shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : undefined
const info = this.getShapeUtilByDef(TLArrowShapeDef).getArrowInfo(shape)
const info = this.getShapeUtil(TLArrowUtil).getArrowInfo(shape)
if (shape.props.start.type === 'binding') {
if (!shapes.some((s) => s.id === startBindingId)) {
@ -4223,8 +4247,8 @@ export class App extends EventEmitter<TLEventMap> {
if (rootShapeIds.length === 1) {
const rootShape = shapes.find((s) => s.id === rootShapeIds[0])!
if (
TLFrameShapeDef.is(parent) &&
TLFrameShapeDef.is(rootShape) &&
this.isShapeOfType(parent, TLFrameUtil) &&
this.isShapeOfType(rootShape, TLFrameUtil) &&
rootShape.props.w === parent?.props.w &&
rootShape.props.h === parent?.props.h
) {
@ -4249,7 +4273,7 @@ export class App extends EventEmitter<TLEventMap> {
const rootShapes: TLShape[] = []
const newShapes: TLShapePartial[] = shapes.map((shape): TLShape => {
const newShapes: TLShape[] = shapes.map((shape): TLShape => {
let newShape: TLShape
if (preserveIds) {
@ -4280,7 +4304,7 @@ export class App extends EventEmitter<TLEventMap> {
index = getIndexAbove(index)
}
if (TLArrowShapeDef.is(newShape)) {
if (this.isShapeOfType(newShape, TLArrowUtil)) {
if (newShape.props.start.type === 'binding') {
const mappedId = idMap.get(newShape.props.start.boundShapeId)
newShape.props.start = mappedId
@ -4374,7 +4398,7 @@ export class App extends EventEmitter<TLEventMap> {
}
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)
if (result.type === 'success') {
newShapes[i] = result.value as TLShape
@ -4435,7 +4459,7 @@ export class App extends EventEmitter<TLEventMap> {
while (
this.getShapesAtPoint(point).some(
(shape) =>
TLFrameShapeDef.is(shape) &&
this.isShapeOfType(shape, TLFrameUtil) &&
shape.props.w === onlyRoot.props.w &&
shape.props.h === onlyRoot.props.h
)
@ -4589,7 +4613,7 @@ export class App extends EventEmitter<TLEventMap> {
const shapeRecordsToCreate: TLShape[] = []
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
// shapes to the top of their parents' children; using the
@ -4850,7 +4874,7 @@ export class App extends EventEmitter<TLEventMap> {
return newRecord ?? prev
})
) as TLShape[]
)
const updates = Object.fromEntries(updated.map((shape) => [shape.id, shape]))
@ -5535,9 +5559,9 @@ export class App extends EventEmitter<TLEventMap> {
/* ------------------- SubCommands ------------------ */
async getSvg(
ids: TLShapeId[] = (this.selectedIds.length
ids: TLShapeId[] = this.selectedIds.length
? this.selectedIds
: Object.keys(this.shapeIds)) as TLShapeId[],
: (Object.keys(this.shapeIds) as TLShapeId[]),
opts = {} as Partial<{
scale: number
background: boolean
@ -6152,15 +6176,17 @@ export class App extends EventEmitter<TLEventMap> {
if (!shapes.length) return this
shapes = shapes
.map((shape) => {
if (shape.type === 'group') {
return this.getSortedChildIds(shape.id).map((id) => this.getShapeById(id))
}
shapes = compact(
shapes
.map((shape) => {
if (shape.type === 'group') {
return this.getSortedChildIds(shape.id).map((id) => this.getShapeById(id))
}
return shape
})
.flat() as TLShape[]
return shape
})
.flat()
)
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) => {
if (!shape) return false
if (TLArrowShapeDef.is(shape)) {
if (this.isShapeOfType(shape, TLArrowUtil)) {
if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') {
return false
}
@ -6346,7 +6372,7 @@ export class App extends EventEmitter<TLEventMap> {
.filter((shape) => {
if (!shape) return false
if (TLArrowShapeDef.is(shape)) {
if (this.isShapeOfType(shape, TLArrowUtil)) {
if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') {
return false
}
@ -6462,7 +6488,7 @@ export class App extends EventEmitter<TLEventMap> {
const translateStartChange = this.getShapeUtil(shape).onTranslateStart?.({
...shape,
...change,
} as TLShape)
})
if (translateStartChange) {
changes.push({ ...change, ...translateStartChange })
@ -7567,8 +7593,8 @@ export class App extends EventEmitter<TLEventMap> {
let newShape: TLShape = deepCopy(shape)
if (TLArrowShapeDef.is(shape) && TLArrowShapeDef.is(newShape)) {
const info = this.getShapeUtilByDef(TLArrowShapeDef).getArrowInfo(shape)
if (this.isShapeOfType(shape, TLArrowUtil) && this.isShapeOfType(newShape, TLArrowUtil)) {
const info = this.getShapeUtil(TLArrowUtil).getArrowInfo(shape)
let newStartShapeId: TLShapeId | undefined = undefined
let newEndShapeId: TLShapeId | undefined = undefined
@ -7767,7 +7793,7 @@ export class App extends EventEmitter<TLEventMap> {
id: shape.id,
type: shape.type,
props,
} as TLShape
}
}),
ephemeral
)
@ -7790,7 +7816,7 @@ export class App extends EventEmitter<TLEventMap> {
if (boundsA.width !== boundsB.width) {
didChange = true
if (TLTextShapeDef.is(shape)) {
if (this.isShapeOfType(shape, TLTextUtil)) {
switch (shape.props.align) {
case 'middle': {
change.x = currentShape.x + (boundsA.width - boundsB.width) / 2
@ -8848,7 +8874,7 @@ export class App extends EventEmitter<TLEventMap> {
const groups: TLGroupShape[] = []
shapes.forEach((shape) => {
if (TLGroupShapeDef.is(shape)) {
if (this.isShapeOfType(shape, TLGroupUtil)) {
groups.push(shape)
} else {
idsToSelect.add(shape.id)
@ -8858,10 +8884,10 @@ export class App extends EventEmitter<TLEventMap> {
if (groups.length === 0) return this
this.batch(() => {
let group: TLShape
let group: TLGroupShape
for (let i = 0, n = groups.length; i < n; i++) {
group = groups[i] as TLGroupShape
group = groups[i]
const childIds = this.getSortedChildIds(group.id)
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 { Computed, RESET_VALUE, computed, isUninitialized } from 'signia'
import { TLArrowShapeDef } from '../shapeutils/TLArrowUtil/TLArrowUtil'
export type TLArrowBindingsIndex = Record<
TLShapeId,
undefined | { arrowId: TLShapeId; handleId: 'start' | 'end' }[]
>
function isArrowType(shape: any): shape is TLArrowShape {
return shape.type === 'arrow'
}
export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsIndex> => {
const shapeHistory = store.query.filterHistory('shape')
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 newShape of Object.values(changes.added)) {
if (TLArrowShapeDef.is(newShape)) {
if (isArrowType(newShape)) {
const { start, end } = newShape.props
if (start.type === 'binding') {
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][]) {
if (!TLArrowShapeDef.is(prev) || !TLArrowShapeDef.is(next)) continue
if (!isArrowType(prev) || !isArrowType(next)) continue
for (const handle of ['start', 'end'] as const) {
const prevTerminal = prev.props[handle]
@ -116,7 +120,7 @@ export const arrowBindingsIndex = (store: TLStore): Computed<TLArrowBindingsInde
}
for (const prev of Object.values(changes.removed)) {
if (TLArrowShapeDef.is(prev)) {
if (isArrowType(prev)) {
const { start, end } = prev.props
if (start.type === 'binding') {
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 { uniqueId } from '../../utils/data'
import type { App } from '../App'
import { getSplineForLineShape, TLLineShapeDef } from '../shapeutils/TLLineUtil/TLLineUtil'
import { getSplineForLineShape, TLLineUtil } from '../shapeutils/TLLineUtil/TLLineUtil'
export type PointsSnapLine = {
id: string
@ -249,7 +249,7 @@ export class SnapManager {
const processParent = (parentId: TLParentId) => {
const children = this.app.getSortedChildIds(parentId)
for (const id of children) {
const shape = this.app.getShapeById(id) as TLShape
const shape = this.app.getShapeById(id)
if (!shape) continue
if (shape.type === 'arrow') continue
if (selectedIds.includes(id)) continue
@ -495,7 +495,7 @@ export class SnapManager {
// and then pass them to the snap function as 'additionalOutlines'
// 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)
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 { assert } from '@tldraw/utils'
import { TestApp } from '../../../test/TestApp'
import { TLArrowShapeDef } from './TLArrowUtil'
import { TLArrowUtil } from './TLArrowUtil'
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)
let arrow = app.shapesArray[app.shapesArray.length - 1]
assert(TLArrowShapeDef.is(arrow))
assert(app.isShapeOfType(arrow, TLArrowUtil))
assert(arrow.props.end.type === 'binding')
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 = app.getShapeById(arrow.id)!
assert(TLArrowShapeDef.is(arrow))
assert(app.isShapeOfType(arrow, TLArrowUtil))
assert(arrow.props.end.type === 'binding')
expect(arrow.props.end.boundShapeId).toBe(ids.box3)
})

View file

@ -11,8 +11,6 @@ import {
VecLike,
} from '@tldraw/primitives'
import {
arrowShapeTypeMigrations,
arrowShapeTypeValidator,
TLArrowheadType,
TLArrowShape,
TLColorType,
@ -27,7 +25,6 @@ import { deepCopy, last, minBy } from '@tldraw/utils'
import * as React from 'react'
import { computed, EMPTY_ARRAY } from 'signia'
import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { ARROW_LABEL_FONT_SIZES, FONT_FAMILIES, TEXT_PROPS } from '../../../constants'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
@ -60,7 +57,7 @@ let globalRenderIndex = 0
/** @public */
export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
static type = 'arrow'
static override type = 'arrow'
override canEdit = () => true
override canBind = () => false
@ -1135,11 +1132,3 @@ function getArrowheadSvgPath(
function isPrecise(normalizedAnchor: Vec2dModel) {
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 {
bookmarkShapeTypeMigrations,
bookmarkShapeTypeValidator,
TLAsset,
TLAssetId,
TLBookmarkAsset,
TLBookmarkShape,
} from '@tldraw/tlschema'
import { TLAsset, TLAssetId, TLBookmarkAsset, TLBookmarkShape } from '@tldraw/tlschema'
import { debounce, getHashForString } from '@tldraw/utils'
import { HTMLContainer } from '../../../components/HTMLContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import {
DEFAULT_BOOKMARK_HEIGHT,
DEFAULT_BOOKMARK_WIDTH,
@ -20,13 +12,13 @@ import {
stopEventPropagation,
truncateStringWithEllipsis,
} from '../../../utils/dom'
import { HyperlinkButton } from '../shared/HyperlinkButton'
import { TLBoxUtil } from '../TLBoxUtil'
import { OnBeforeCreateHandler, OnBeforeUpdateHandler } from '../TLShapeUtil'
import { HyperlinkButton } from '../shared/HyperlinkButton'
/** @public */
export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
static type = 'bookmark'
static override type = 'bookmark'
override canResize = () => false
@ -191,11 +183,3 @@ export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
}
}, 500)
}
/** @public */
export const TLBookmarkShapeDef = defineShape<TLBookmarkShape, TLBookmarkUtil>({
type: 'bookmark',
getShapeUtil: () => TLBookmarkUtil,
validator: bookmarkShapeTypeValidator,
migrations: bookmarkShapeTypeMigrations,
})

View file

@ -9,15 +9,9 @@ import {
Vec2d,
VecLike,
} from '@tldraw/primitives'
import {
drawShapeTypeMigrations,
drawShapeTypeValidator,
TLDrawShape,
TLDrawShapeSegment,
} from '@tldraw/tlschema'
import { TLDrawShape, TLDrawShapeSegment } from '@tldraw/tlschema'
import { last, rng } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { getSvgPathFromStroke, getSvgPathFromStrokePoints } from '../../../utils/svg'
import { getShapeFillSvg, ShapeFill } from '../shared/ShapeFill'
import { TLExportColors } from '../shared/TLExportColors'
@ -27,7 +21,7 @@ import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments
/** @public */
export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
static type = 'draw'
static override type = 'draw'
hideResizeHandles = (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) {
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 -${

View file

@ -1,8 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { toDomPrecision } from '@tldraw/primitives'
import {
embedShapeTypeMigrations,
embedShapeTypeValidator,
TLEmbedShape,
tlEmbedShapePermissionDefaults,
TLEmbedShapePermissions,
@ -10,10 +8,9 @@ import {
import * as React from 'react'
import { useMemo } from 'react'
import { useValue } from 'signia-react'
import { DefaultSpinner } from '../../../components/DefaultSpinner'
import { HTMLContainer } from '../../../components/HTMLContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { ROTATING_SHADOWS } from '../../../constants'
import { useEditorComponents } from '../../../hooks/useEditorComponents'
import { useIsEditing } from '../../../hooks/useIsEditing'
import { rotateBoxShadow } from '../../../utils/dom'
import { getEmbedInfo, getEmbedInfoUnsafely } from '../../../utils/embeds'
@ -30,7 +27,7 @@ const getSandboxPermissions = (permissions: TLEmbedShapePermissions) => {
/** @public */
export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
static type = 'embed'
static override type = 'embed'
override canUnmount: TLShapeUtilFlag<TLEmbedShape> = () => false
override canResize = (shape: TLEmbedShape) => {
@ -84,8 +81,6 @@ export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
const isEditing = useIsEditing(shape.id)
const embedInfo = useMemo(() => getEmbedInfoUnsafely(url), [url])
const { Spinner } = useEditorComponents()
const isHoveringWhileEditingSameShape = useValue(
'is hovering',
() => {
@ -150,11 +145,11 @@ export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
background: embedInfo?.definition.backgroundColor,
}}
/>
) : Spinner ? (
) : (
<g transform={`translate(${(w - 38) / 2}, ${(h - 38) / 2})`}>
<Spinner />
<DefaultSpinner />
</g>
) : null}
)}
</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 {
frameShapeTypeValidator,
TLFrameShape,
TLShape,
TLShapeId,
TLShapeType,
} from '@tldraw/tlschema'
import { TLFrameShape, TLShape, TLShapeId, TLShapeType } from '@tldraw/tlschema'
import { last } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { defaultEmptyAs } from '../../../utils/string'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
import { TLExportColors } from '../shared/TLExportColors'
@ -18,7 +11,7 @@ import { FrameHeading } from './components/FrameHeading'
/** @public */
export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
static type = 'frame'
static override type = 'frame'
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,
VecLike,
} from '@tldraw/primitives'
import {
geoShapeTypeMigrations,
geoShapeTypeValidator,
TLDashType,
TLGeoShape,
TLGeoShapeProps,
} from '@tldraw/tlschema'
import { TLDashType, TLGeoShape, TLGeoShapeProps } from '@tldraw/tlschema'
import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { App } from '../../App'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
@ -48,7 +41,7 @@ const MIN_SIZE_WITH_LABEL = 17 * 3
/** @public */
export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
static type = 'geo'
static override type = 'geo'
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)],
]
}
/** @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 { TLGroupShape, Vec2dModel, groupShapeTypeValidator } from '@tldraw/tlschema'
import { TLGroupShape, Vec2dModel } from '@tldraw/tlschema'
import { SVGContainer } from '../../../components/SVGContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { OnChildrenChangeHandler, TLShapeUtil } from '../TLShapeUtil'
import { DashedOutlineBox } from '../shared/DashedOutlineBox'
/** @public */
export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
static type = 'group'
static override type = 'group'
hideSelectionBoundsBg = () => false
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 */
import { Vec2d, toDomPrecision } from '@tldraw/primitives'
import {
TLImageShape,
TLShapePartial,
imageShapeTypeMigrations,
imageShapeTypeValidator,
} from '@tldraw/tlschema'
import { TLImageShape, TLShapePartial } from '@tldraw/tlschema'
import { deepCopy } from '@tldraw/utils'
import { useEffect, useState } from 'react'
import { useValue } from 'signia-react'
import { DefaultSpinner } from '../../../components/DefaultSpinner'
import { HTMLContainer } from '../../../components/HTMLContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { useEditorComponents } from '../../../hooks/useEditorComponents'
import { useIsCropping } from '../../../hooks/useIsCropping'
import { usePrefersReducedMotion } from '../../../utils/dom'
import { TLBoxUtil } from '../TLBoxUtil'
@ -55,7 +49,7 @@ async function getDataURIFromURL(url: string): Promise<string> {
/** @public */
export class TLImageUtil extends TLBoxUtil<TLImageShape> {
static type = 'image'
static override type = 'image'
override isAspectRatioLocked = () => true
override canCrop = () => true
@ -77,7 +71,6 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
const isCropping = useIsCropping(shape.id)
const prefersReducedMotion = usePrefersReducedMotion()
const [staticFrameSrc, setStaticFrameSrc] = useState('')
const { Spinner } = useEditorComponents()
const { w, h } = shape.props
const asset = shape.props.assetId ? this.app.getAssetById(shape.props.assetId) : undefined
@ -148,11 +141,11 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
}}
draggable={false}
/>
) : Spinner ? (
) : (
<g transform={`translate(${(w - 38) / 2}, ${(h - 38) / 2})`}>
<Spinner />
<DefaultSpinner />
</g>
) : null}
)}
{asset?.props.isAnimated && !shape.props.playing && (
<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
* 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')
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)!
app.select(boxID, id)

View file

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

View file

@ -1,6 +1,5 @@
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
import { noteShapeTypeMigrations, noteShapeTypeValidator, TLNoteShape } from '@tldraw/tlschema'
import { defineShape } from '../../../config/TLShapeDefinition'
import { TLNoteShape } from '@tldraw/tlschema'
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { App } from '../../App'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
@ -13,7 +12,7 @@ const NOTE_SIZE = 200
/** @public */
export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
static type = 'note'
static override type = 'note'
canEdit = () => 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) {
const PADDING = 17

View file

@ -31,9 +31,11 @@ export interface TLShapeUtilConstructor<
export type TLShapeUtilFlag<T> = (shape: T) => boolean
/** @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']) {}
static type: string
/**
* Check if a shape is of this type.
*

View file

@ -1,8 +1,7 @@
/* eslint-disable react-hooks/rules-of-hooks */
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 { defineShape } from '../../../config/TLShapeDefinition'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { stopEventPropagation } from '../../../utils/dom'
import { WeakMapCache } from '../../../utils/WeakMapCache'
@ -19,7 +18,7 @@ const sizeCache = new WeakMapCache<TLTextShape['props'], { height: number; width
/** @public */
export class TLTextUtil extends TLShapeUtil<TLTextShape> {
static type = 'text'
static override type = 'text'
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']) {
const { font, text, autoSize, size, w } = props

View file

@ -1,10 +1,9 @@
import { toDomPrecision } from '@tldraw/primitives'
import { TLVideoShape, videoShapeTypeMigrations, videoShapeTypeValidator } from '@tldraw/tlschema'
import { TLVideoShape } from '@tldraw/tlschema'
import * as React from 'react'
import { track } from 'signia-react'
import { DefaultSpinner } from '../../../components/DefaultSpinner'
import { HTMLContainer } from '../../../components/HTMLContainer'
import { defineShape } from '../../../config/TLShapeDefinition'
import { useEditorComponents } from '../../../hooks/useEditorComponents'
import { useIsEditing } from '../../../hooks/useIsEditing'
import { usePrefersReducedMotion } from '../../../utils/dom'
import { TLBoxUtil } from '../TLBoxUtil'
@ -12,7 +11,7 @@ import { HyperlinkButton } from '../shared/HyperlinkButton'
/** @public */
export class TLVideoUtil extends TLBoxUtil<TLVideoShape> {
static type = 'video'
static override type = 'video'
override canEdit = () => 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 serializeVideo(id: string): string {
const splitId = id.split(':')[1]
@ -74,7 +65,6 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
shape: TLVideoShape
videoUtil: TLVideoUtil
}) {
const { Spinner } = useEditorComponents()
const { shape, videoUtil } = props
const showControls = videoUtil.app.getBounds(shape).w * videoUtil.app.zoomLevel >= 110
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} />
</video>
) : Spinner ? (
) : (
<g transform={`translate(${(w - 38) / 2}, ${(h - 38) / 2})`}>
<Spinner />
<DefaultSpinner />
</g>
) : null}
)}
</div>
</HTMLContainer>
{'url' in shape.props && shape.props.url && (

View file

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

View file

@ -92,7 +92,7 @@ export class Pointing extends StateNode {
])
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))
this.app.updateShapes([

View file

@ -9,7 +9,7 @@ import {
import { last, structuredClone } from '@tldraw/utils'
import { DRAG_DISTANCE } from '../../../../constants'
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 { StateNode } from '../../StateNode'
@ -21,7 +21,7 @@ export class Drawing extends StateNode {
initialShape?: TLDrawShape
util = this.app.getShapeUtilByDef(TLDrawShapeDef)
util = this.app.getShapeUtil(TLDrawUtil)
isPen = false
@ -157,7 +157,7 @@ export class Drawing extends StateNode {
this.lastRecordedPoint = originPagePoint.clone()
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') {
// Connect dots

View file

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

View file

@ -77,7 +77,7 @@ export class Cropping extends StateNode {
const { shape, cursorHandleOffset } = this.snapshot
if (!shape) return
const util = this.app.getShapeUtil(shape) as TLImageUtil
const util = this.app.getShapeUtil(TLImageUtil)
if (!util) return
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 {
CLIENT_FIXUP_SCRIPT,
MigrationsForShapes,
TLDOCUMENT_ID,
TLInstance,
TLInstanceId,
@ -8,65 +9,147 @@ import {
TLShape,
TLStore,
TLStoreProps,
TLUnknownShape,
TLUser,
TLUserId,
ValidatorsForShapes,
arrowShapeTypeMigrations,
arrowShapeTypeValidator,
bookmarkShapeTypeMigrations,
bookmarkShapeTypeValidator,
createTLSchema,
drawShapeTypeMigrations,
drawShapeTypeValidator,
embedShapeTypeMigrations,
embedShapeTypeValidator,
frameShapeTypeMigrations,
frameShapeTypeValidator,
geoShapeTypeMigrations,
geoShapeTypeValidator,
groupShapeTypeMigrations,
groupShapeTypeValidator,
imageShapeTypeMigrations,
imageShapeTypeValidator,
lineShapeTypeMigrations,
lineShapeTypeValidator,
noteShapeTypeMigrations,
noteShapeTypeValidator,
textShapeTypeMigrations,
textShapeTypeValidator,
videoShapeTypeMigrations,
videoShapeTypeValidator,
} 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 { TLArrowShapeDef } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
import { TLBookmarkShapeDef } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
import { TLDrawShapeDef } from '../app/shapeutils/TLDrawUtil/TLDrawUtil'
import { TLEmbedShapeDef } from '../app/shapeutils/TLEmbedUtil/TLEmbedUtil'
import { TLFrameShapeDef } from '../app/shapeutils/TLFrameUtil/TLFrameUtil'
import { TLGeoShapeDef } from '../app/shapeutils/TLGeoUtil/TLGeoUtil'
import { TLGroupShapeDef } from '../app/shapeutils/TLGroupUtil/TLGroupUtil'
import { TLImageShapeDef } from '../app/shapeutils/TLImageUtil/TLImageUtil'
import { TLLineShapeDef } from '../app/shapeutils/TLLineUtil/TLLineUtil'
import { TLNoteShapeDef } from '../app/shapeutils/TLNoteUtil/TLNoteUtil'
import { TLTextShapeDef } from '../app/shapeutils/TLTextUtil/TLTextUtil'
import { TLVideoShapeDef } from '../app/shapeutils/TLVideoUtil/TLVideoUtil'
import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
import { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
import { TLDrawUtil } from '../app/shapeutils/TLDrawUtil/TLDrawUtil'
import { TLEmbedUtil } from '../app/shapeutils/TLEmbedUtil/TLEmbedUtil'
import { TLFrameUtil } from '../app/shapeutils/TLFrameUtil/TLFrameUtil'
import { TLGeoUtil } from '../app/shapeutils/TLGeoUtil/TLGeoUtil'
import { TLGroupUtil } from '../app/shapeutils/TLGroupUtil/TLGroupUtil'
import { TLImageUtil } from '../app/shapeutils/TLImageUtil/TLImageUtil'
import { TLLineUtil } from '../app/shapeutils/TLLineUtil/TLLineUtil'
import { TLNoteUtil } from '../app/shapeutils/TLNoteUtil/TLNoteUtil'
import { TLShapeUtilConstructor } from '../app/shapeutils/TLShapeUtil'
import { TLTextUtil } from '../app/shapeutils/TLTextUtil/TLTextUtil'
import { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil'
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 */
export class TldrawEditorConfig {
static readonly default = new TldrawEditorConfig({})
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>
readonly shapes: readonly TLUnknownShapeDef[]
readonly TLShape: RecordType<TLShape, 'type' | 'props' | 'index' | 'parentId'>
readonly tools: readonly StateNodeConstructor[]
constructor(args: {
shapes?: readonly TLShapeDef<any, any>[]
tools?: readonly StateNodeConstructor[]
allowUnknownShapes?: boolean
/** @internal */
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
}) {
const { shapes = [], tools = [], allowUnknownShapes = false, derivePresenceState } = args
readonly shapeUtils: UtilsForShapes<TLShape>
readonly shapeValidators: ValidatorsForShapes<TLShape>
readonly shapeMigrations: MigrationsForShapes<TLShape>
constructor(opts: TldrawEditorConfigOptions) {
const { shapes = [], tools = [], derivePresenceState } = opts
this.tools = tools
this.shapes = [
TLArrowShapeDef,
TLBookmarkShapeDef,
TLDrawShapeDef,
TLEmbedShapeDef,
TLFrameShapeDef,
TLGeoShapeDef,
TLGroupShapeDef,
TLImageShapeDef,
TLLineShapeDef,
TLNoteShapeDef,
TLTextShapeDef,
TLVideoShapeDef,
...shapes,
]
this.shapeUtils = {
arrow: TLArrowUtil,
bookmark: TLBookmarkUtil,
draw: TLDrawUtil,
embed: TLEmbedUtil,
frame: TLFrameUtil,
geo: TLGeoUtil,
group: TLGroupUtil,
image: TLImageUtil,
line: TLLineUtil,
note: TLNoteUtil,
text: TLTextUtil,
video: TLVideoUtil,
}
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({
allowUnknownShapes,
customShapeDefs: shapes,
shapeMigrations: this.shapeMigrations,
shapeValidators: this.shapeValidators,
derivePresenceState,
})

View file

@ -406,7 +406,7 @@ describe('flipping rotated shapes', () => {
const getStartAndEndPoints = (id: TLShapeId) => {
const transform = app.getPageTransformById(id)
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')
throw new Error('not a point')
const start = Matrix2d.applyToPoint(transform, arrow.props.start)

View file

@ -1,5 +1,5 @@
import { createCustomShapeId } from '@tldraw/tlschema'
import { TLGeoShapeDef } from '../../app/shapeutils/TLGeoUtil/TLGeoUtil'
import { TLGeoUtil } from '../../app/shapeutils/TLGeoUtil/TLGeoUtil'
import { TestApp } from '../TestApp'
let app: TestApp
@ -43,7 +43,7 @@ beforeEach(() => {
describe('app.rotateShapes', () => {
it('Rotates shapes and fires events', () => {
// 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)
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', () => {

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { createCustomShapeId, TLArrowShape } from '@tldraw/tlschema'
import { TLFrameShapeDef } from '../../app/shapeutils/TLFrameUtil/TLFrameUtil'
import { TLFrameUtil } from '../../app/shapeutils/TLFrameUtil/TLFrameUtil'
import { TestApp } from '../TestApp'
let app: TestApp
@ -33,7 +33,7 @@ describe('creating frames', () => {
app.setSelectedTool('frame')
app.pointerDown(100, 100).pointerUp(100, 100)
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({
x: 100 - w / 2,
y: 100 - h / 2,

View file

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

View file

@ -1,9 +1,7 @@
import { Box2d, Vec2d, VecLike } from '@tldraw/primitives'
import { TLShapeId, TLShapePartial, Vec2dModel, createCustomShapeId } from '@tldraw/tlschema'
import { defineMigrations } from '@tldraw/tlstore'
import { GapsSnapLine, PointsSnapLine, SnapLine } from '../../app/managers/SnapManager'
import { TLShapeUtil } from '../../app/shapeutils/TLShapeUtil'
import { defineShape } from '../../config/TLShapeDefinition'
import { TldrawEditorConfig } from '../../config/TldrawEditorConfig'
import { TestApp } from '../TestApp'
@ -12,8 +10,8 @@ import { getSnapLines } from '../testutils/getSnapLines'
type __TopLeftSnapOnlyShape = any
class __TopLeftSnapOnlyShapeUtil extends TLShapeUtil<__TopLeftSnapOnlyShape> {
type = '__test_top_left_snap_only' as const
static type = '__test_top_left_snap_only' as const
static override type = '__test_top_left_snap_only' as const
defaultProps(): __TopLeftSnapOnlyShape['props'] {
return { width: 10, height: 10 }
}
@ -41,14 +39,14 @@ class __TopLeftSnapOnlyShapeUtil extends TLShapeUtil<__TopLeftSnapOnlyShape> {
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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,4 @@
import {
Migrations,
StoreSchema,
StoreValidator,
createRecordType,
defineMigrations,
} from '@tldraw/tlstore'
import { Migrations, StoreSchema, createRecordType, defineMigrations } from '@tldraw/tlstore'
import { T } from '@tldraw/tlvalidate'
import { Signal } from 'signia'
import { TLRecord } from './TLRecord'
@ -17,68 +11,32 @@ import { TLInstance } from './records/TLInstance'
import { TLInstancePageState } from './records/TLInstancePageState'
import { TLInstancePresence } from './records/TLInstancePresence'
import { TLPage } from './records/TLPage'
import { TLShape, rootShapeTypeMigrations } from './records/TLShape'
import { TLShape, TLUnknownShape, rootShapeTypeMigrations } from './records/TLShape'
import { TLUser } from './records/TLUser'
import { TLUserDocument } from './records/TLUserDocument'
import { TLUserPresence } from './records/TLUserPresence'
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 */
export type CustomShapeTypeInfo = {
type: string
migrations?: Migrations
validator?: StoreValidator<TLShape>
}
export type ValidatorsForShapes<T extends TLUnknownShape> = Record<
T['type'],
{ validate: (record: T) => T }
>
/** @public */
export type MigrationsForShapes<T extends TLUnknownShape> = Record<T['type'], Migrations>
/** @public */
export function createTLSchema({
customShapeDefs,
allowUnknownShapes,
shapeMigrations,
shapeValidators,
derivePresenceState,
}: {
customShapeDefs?: readonly CustomShapeTypeInfo[]
allowUnknownShapes?: boolean
shapeValidators: ValidatorsForShapes<TLShape>
shapeMigrations: MigrationsForShapes<TLShape>
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
}) {
const allShapeDefs = [...CORE_SHAPE_DEFS, ...(customShapeDefs ?? [])]
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)
}
// Removed check to see whether a shape type has already been defined
const shapeTypeMigrations = defineMigrations({
currentVersion: rootShapeTypeMigrations.currentVersion,
@ -86,20 +44,18 @@ export function createTLSchema({
migrators: rootShapeTypeMigrations.migrators,
subTypeKey: 'type',
subTypeMigrations: Object.fromEntries(
allShapeDefs.map((def) => [def.type, def.migrations ?? {}])
) as Record<string, Migrations>,
Object.entries(shapeMigrations) as [TLShape['type'], Migrations][]
),
})
let shapeValidator = T.union('type', {
...Object.fromEntries(allShapeDefs.map((def) => [def.type, def.validator ?? (T.any as any)])),
}) as T.UnionValidator<'type', any, any>
if (allowUnknownShapes) {
shapeValidator = shapeValidator.validateUnknownVariants((shape) => shape as any)
}
const shapeTypeValidator = T.union(
'type',
Object.fromEntries(Object.entries(shapeValidators) as [TLShape['type'], T.Validator<any>][])
)
const shapeRecord = createRecordType<TLShape>('shape', {
migrations: shapeTypeMigrations,
validator: T.model('shape', shapeValidator),
validator: T.model('shape', shapeTypeValidator),
scope: 'document',
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false }))

View file

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

View file

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

View file

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

View file

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

View file

@ -152,7 +152,7 @@ export class StoreSchema<R extends BaseRecord, P = unknown> {
const persistedSubTypeVersion =
'subTypeVersions' in persistedType
? 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
// 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
// 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.
if (persistedSubTypeVersion == null) {
if (persistedSubTypeVersion === undefined) {
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 { track } from 'signia-react'
import { DialogProps } from '../hooks/useDialogsProvider'
@ -19,12 +19,30 @@ function valiateUrl(url: string) {
return false
}
export const EditLinkDialog = track(function EditLink({ onClose }: DialogProps) {
export const EditLinkDialog = track(function EditLinkDialog({ onClose }: DialogProps) {
const app = useApp()
const msg = useTranslation()
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 rInitialValue = useRef(selectedShape?.props.url)
@ -64,13 +82,13 @@ export const EditLinkDialog = track(function EditLink({ onClose }: DialogProps)
const shape = app.selectedShapes[0]
if (shape) {
if (shape && 'url' in shape.props) {
const current = shape.props.url
const next = validState
? validState === 'needs protocol'
? 'https://' + value
: value
: shape.type === 'bookmark'
: app.isShapeOfType(shape, TLBookmarkUtil)
? rInitialValue.current
: ''

View file

@ -4,7 +4,7 @@ import {
FONT_SIZES,
INDENT,
TEXT_PROPS,
TLTextShapeDef,
TLTextUtil,
createShapeId,
} from '@tldraw/editor'
import { VecLike } from '@tldraw/primitives'
@ -64,7 +64,7 @@ function stripTrailingWhitespace(text: string): string {
*/
export async function pastePlainText(app: App, text: string, point?: VecLike) {
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(
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 { useValue } from 'signia-react'
import { ActionItem } from './useActions'
@ -136,10 +136,10 @@ function shapesWithUnboundArrows(app: App) {
return selectedShapes.filter((shape) => {
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
}
if (TLArrowShapeDef.is(shape) && shape.props.end.type === 'binding') {
if (app.isShapeOfType(shape, TLArrowUtil) && shape.props.end.type === 'binding') {
return false
}
return true

View file

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

View file

@ -3,12 +3,12 @@ import {
getValidHttpURLList,
isSvgText,
isValidHttpURL,
TLArrowShapeDef,
TLBookmarkShapeDef,
TLArrowUtil,
TLBookmarkUtil,
TLClipboardModel,
TLEmbedShapeDef,
TLGeoShapeDef,
TLTextShapeDef,
TLEmbedUtil,
TLGeoUtil,
TLTextUtil,
useApp,
} from '@tldraw/editor'
import { VecLike } from '@tldraw/primitives'
@ -495,10 +495,14 @@ const handleNativeOrMenuCopy = (app: App) => {
// Extract the text from the clipboard
const textItems = content.shapes
.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
}
if (TLBookmarkShapeDef.is(shape) || TLEmbedShapeDef.is(shape)) {
if (app.isShapeOfType(shape, TLBookmarkUtil) || app.isShapeOfType(shape, TLEmbedUtil)) {
return shape.props.url
}
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 { track, useValue } from 'signia-react'
import {
MenuSchema,
compactMenuItems,
menuCustom,
menuGroup,
menuItem,
MenuSchema,
menuSubmenu,
showMenuPaste,
useAllowGroup,
@ -63,7 +63,7 @@ export const ContextMenuSchemaProvider = track(function ContextMenuSchemaProvide
if (app.selectedIds.length !== 1) return false
return app.selectedIds.some((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
return app.selectedIds.some((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,
getSvgAsImage,
TLExportType,
TLFrameShape,
TLFrameUtil,
TLShapeId,
useApp,
} from '@tldraw/editor'
@ -38,8 +38,8 @@ export function useExportAs() {
if (ids.length === 1) {
const first = app.getShapeById(ids[0])!
if (first.type === 'frame') {
name = (first as TLFrameShape).props.name ?? 'frame'
if (app.isShapeOfType(first, TLFrameUtil)) {
name = first.props.name ?? 'frame'
} else {
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'
export function useShowAutoSizeToggle() {
@ -9,7 +9,7 @@ export function useShowAutoSizeToggle() {
const { selectedShapes } = app
return (
selectedShapes.length === 1 &&
selectedShapes[0].type === 'text' &&
app.isShapeOfType(selectedShapes[0], TLTextUtil) &&
selectedShapes[0].props.autoSize === false
)
},