Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
182 lines
5.0 KiB
TypeScript
182 lines
5.0 KiB
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),
|
|
})
|
|
}
|