import { JsonObject } from '@tldraw/utils' import { T } from '@tldraw/validate' import { idValidator } from '../misc/id-validator' import { TLBindingId } from '../records/TLBinding' import { TLShapeId } from '../records/TLShape' import { shapeIdValidator } from '../shapes/TLBaseShape' /** * Base interface for all binding types in tldraw. Bindings represent relationships * between shapes, such as arrows connecting to other shapes or organizational connections. * * All default bindings extend this base interface with specific type and property definitions. * The binding system enables shapes to maintain relationships that persist through * transformations, movements, and other operations. * * Custom bindings should be defined by augmenting the TLGlobalBindingPropsMap type and getting the binding type from the TLBinding type. * * @param Type - String literal type identifying the specific binding type (e.g., 'arrow') * @param Props - Object containing binding-specific properties and configuration * * @example * ```ts * // Define a default binding type * interface TLArrowBinding extends TLBaseBinding<'arrow', TLArrowBindingProps> {} * * interface TLArrowBindingProps { * terminal: 'start' | 'end' * normalizedAnchor: VecModel * isExact: boolean * isPrecise: boolean * snap: ElbowArrowSnap * } * * // Create a binding instance * const arrowBinding: TLArrowBinding = { * id: 'binding:abc123', * typeName: 'binding', * type: 'arrow', * fromId: 'shape:source1', * toId: 'shape:target1', * props: { * terminal: 'end', * normalizedAnchor: { x: 0.5, y: 0.5 }, * isExact: false, * isPrecise: true, * snap: 'edge' * }, * meta: {} * } * ``` * * @public */ export interface TLBaseBinding { // using real `extends BaseRecord<'binding', TLBindingId>` introduces a circularity in the types // and for that reason those "base members" have to be declared manually here readonly id: TLBindingId readonly typeName: 'binding' /** The specific type of this binding (e.g., 'arrow', 'custom') */ type: Type /** ID of the source shape in this binding relationship */ fromId: TLShapeId /** ID of the target shape in this binding relationship */ toId: TLShapeId /** Binding-specific properties that define behavior and appearance */ props: Props /** User-defined metadata for extending binding functionality */ meta: JsonObject } /** * Validator for binding IDs. Ensures that binding identifiers follow the correct * format and type constraints required by the tldraw schema system. * * Used internally by the schema validation system to verify binding IDs when * records are created or modified. All binding IDs must be prefixed with 'binding:'. * * @example * ```ts * import { bindingIdValidator } from '@tldraw/tlschema' * * // Validate a binding ID * const isValid = bindingIdValidator.isValid('binding:abc123') // true * const isInvalid = bindingIdValidator.isValid('shape:abc123') // false * * // Use in custom validation schema * const customBindingValidator = T.object({ * id: bindingIdValidator, * // ... other properties * }) * ``` * * @public */ export const bindingIdValidator = idValidator('binding') /** * Creates a runtime validator for a specific binding type. This factory function * generates a complete validation schema for custom bindings that extends TLBaseBinding. * * The validator ensures all binding records conform to the expected structure with * proper type safety and runtime validation. It validates the base binding properties * (id, type, fromId, toId) along with custom props and meta fields. * * @param type - The string literal type identifier for this binding (e.g., 'arrow', 'custom') * @param props - Optional validation schema for binding-specific properties * @param meta - Optional validation schema for metadata fields * * @returns A validator object that can validate complete binding records * * @example * ```ts * import { createBindingValidator } from '@tldraw/tlschema' * import { T } from '@tldraw/validate' * * // Create validator for a custom binding type * const myBindingValidator = createBindingValidator( * 'myBinding', * { * strength: T.number, * color: T.string, * enabled: T.boolean * }, * { * createdAt: T.number, * author: T.string * } * ) * * // Validate a binding instance * const bindingData = { * id: 'binding:123', * typeName: 'binding', * type: 'myBinding', * fromId: 'shape:abc', * toId: 'shape:def', * props: { * strength: 0.8, * color: 'red', * enabled: true * }, * meta: { * createdAt: Date.now(), * author: 'user123' * } * } * * const isValid = myBindingValidator.isValid(bindingData) // true * ``` * * @example * ```ts * // Simple binding without custom props or meta * const simpleBindingValidator = createBindingValidator('simple') * * // This will use JsonValue validation for props and meta * const binding = { * id: 'binding:456', * typeName: 'binding', * type: 'simple', * fromId: 'shape:start', * toId: 'shape:end', * props: {}, // Any JSON value allowed * meta: {} // Any JSON value allowed * } * ``` * * @public */ export function createBindingValidator< Type extends string, Props extends JsonObject, Meta extends JsonObject, >( type: Type, props?: { [K in keyof Props]: T.Validatable }, meta?: { [K in keyof Meta]: T.Validatable } ) { return T.object>({ id: bindingIdValidator, typeName: T.literal('binding'), type: T.literal(type), fromId: shapeIdValidator, toId: shapeIdValidator, props: props ? T.object(props) : (T.jsonValue as any), meta: meta ? T.object(meta) : (T.jsonValue as any), }) }