@tldraw/tlschema
Version:
tldraw infinite canvas SDK (schema).
182 lines (176 loc) • 5.09 kB
text/typescript
import { IndexKey, JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { idValidator } from '../misc/id-validator'
import { TLOpacityType, opacityValidator } from '../misc/TLOpacity'
import { TLParentId, TLShapeId } from '../records/TLShape'
/**
* Base interface for all shapes in tldraw.
*
* This interface defines the common properties that all shapes share, regardless of their
* specific type. Every default shape extends this base with additional type-specific properties.
*
* Custom shapes should be defined by augmenting the TLGlobalShapePropsMap type and getting the shape type from the TLShape type.
*
* @example
* ```ts
* // Define a default shape type
* interface TLArrowShape extends TLBaseShape<'arrow', {
* kind: TLArrowShapeKind
* labelColor: TLDefaultColorStyle
* color: TLDefaultColorStyle
* fill: TLDefaultFillStyle
* dash: TLDefaultDashStyle
* size: TLDefaultSizeStyle
* arrowheadStart: TLArrowShapeArrowheadStyle
* arrowheadEnd: TLArrowShapeArrowheadStyle
* font: TLDefaultFontStyle
* start: VecModel
* end: VecModel
* bend: number
* richText: TLRichText
* labelPosition: number
* scale: number
* elbowMidPoint: number
* }> {}
*
* // Create a shape instance
* const arrowShape: TLArrowShape = {
* id: 'shape:abc123',
* typeName: 'shape',
* type: 'arrow',
* x: 100,
* y: 200,
* rotation: 0,
* index: 'a1',
* parentId: 'page:main',
* isLocked: false,
* opacity: 1,
* props: {
* kind: 'arc',
* start: { x: 0, y: 0 },
* end: { x: 100, y: 100 },
* // ... other props
* },
* meta: {}
* }
* ```
*
* @public
*/
export interface TLBaseShape<Type extends string, Props extends object> {
// using real `extends BaseRecord<'shape', TLShapeId>` introduces a circularity in the types
// and for that reason those "base members" have to be declared manually here
readonly id: TLShapeId
readonly typeName: 'shape'
type: Type
x: number
y: number
rotation: number
index: IndexKey
parentId: TLParentId
isLocked: boolean
opacity: TLOpacityType
props: Props
meta: JsonObject
}
/**
* Validator for parent IDs, ensuring they follow the correct format.
*
* Parent IDs must start with either "page:" (for shapes directly on a page)
* or "shape:" (for shapes inside other shapes like frames or groups).
*
* @example
* ```ts
* // Valid parent IDs
* const pageParent = parentIdValidator.validate('page:main') // ✓
* const shapeParent = parentIdValidator.validate('shape:frame1') // ✓
*
* // Invalid parent ID (throws error)
* const invalid = parentIdValidator.validate('invalid:123') // ✗
* ```
*
* @public
*/
export const parentIdValidator = T.string.refine((id) => {
if (!id.startsWith('page:') && !id.startsWith('shape:')) {
throw new Error('Parent ID must start with "page:" or "shape:"')
}
return id as TLParentId
})
/**
* Validator for shape IDs, ensuring they follow the "shape:" format.
*
* @example
* ```ts
* const validId = shapeIdValidator.validate('shape:abc123') // ✓
* const invalidId = shapeIdValidator.validate('page:abc123') // ✗ throws error
* ```
*
* @public
*/
export const shapeIdValidator = idValidator<TLShapeId>('shape')
/**
* Creates a validator for a specific shape type.
*
* This function generates a complete validator that can validate shape records
* of the specified type, including both the base shape properties and any
* custom properties and metadata specific to that shape type.
*
* @param type - The string literal type for this shape (e.g., 'geo', 'arrow')
* @param props - Optional validator configuration for shape-specific properties
* @param meta - Optional validator configuration for shape-specific metadata
* @returns A validator that can validate complete shape records of the specified type
*
* @example
* ```ts
* // Create a validator for a custom shape type
* const customShapeValidator = createShapeValidator('custom', {
* width: T.number,
* height: T.number,
* color: T.string
* })
*
* // Use the validator to validate shape data
* const shapeData = {
* id: 'shape:abc123',
* typeName: 'shape',
* type: 'custom',
* x: 100,
* y: 200,
* // ... other base properties
* props: {
* width: 150,
* height: 100,
* color: 'red'
* }
* }
*
* const validatedShape = customShapeValidator.validate(shapeData)
* ```
*
* @public
*/
export function createShapeValidator<
Type extends string,
Props extends JsonObject,
Meta extends JsonObject,
>(
type: Type,
props?: { [K in keyof Props]: T.Validatable<Props[K]> },
meta?: { [K in keyof Meta]: T.Validatable<Meta[K]> }
) {
return T.object<TLBaseShape<Type, Props>>({
id: shapeIdValidator,
typeName: T.literal('shape'),
x: T.number,
y: T.number,
rotation: T.number,
index: T.indexKey,
parentId: parentIdValidator,
type: T.literal(type),
isLocked: T.boolean,
opacity: opacityValidator,
props: props ? T.object(props) : (T.jsonValue as any),
meta: meta ? T.object(meta) : (T.jsonValue as any),
})
}