Tldraw/packages/tlschema/src/records/TLInstance.ts

442 wiersze
10 KiB
TypeScript

import {
BaseRecord,
createMigrationIds,
createRecordMigrationSequence,
createRecordType,
RecordId,
} from '@tldraw/store'
import { JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { BoxModel, boxModelValidator } from '../misc/geometry-types'
import { idValidator } from '../misc/id-validator'
import { cursorValidator, TLCursor } from '../misc/TLCursor'
import { opacityValidator, TLOpacityType } from '../misc/TLOpacity'
import { scribbleValidator, TLScribble } from '../misc/TLScribble'
import { StyleProp } from '../styles/StyleProp'
import { pageIdValidator, TLPageId } from './TLPage'
import { TLShapeId } from './TLShape'
/**
* TLInstance
*
* State that is particular to a single browser tab
*
* @public
*/
export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
currentPageId: TLPageId
opacityForNextShape: TLOpacityType
stylesForNextShape: Record<string, unknown>
// ephemeral
followingUserId: string | null
highlightedUserIds: string[]
brush: BoxModel | null
cursor: TLCursor
scribbles: TLScribble[]
isFocusMode: boolean
isDebugMode: boolean
isToolLocked: boolean
exportBackground: boolean
screenBounds: BoxModel
insets: boolean[]
zoomBrush: BoxModel | null
chatMessage: string
isChatting: boolean
isPenMode: boolean
isGridMode: boolean
canMoveCamera: boolean
isFocused: boolean
devicePixelRatio: number
/**
* This is whether the primary input mechanism includes a pointing device of limited accuracy,
* such as a finger on a touchscreen.
* See: https://developer.mozilla.org/en-US/docs/Web/CSS/\@media/pointer
*/
isCoarsePointer: boolean
/**
* Will be null if the pointer doesn't support hovering (e.g. touch), but true or false
* otherwise
*/
isHoveringCanvas: boolean | null
openMenus: string[]
isChangingStyle: boolean
isReadonly: boolean
meta: JsonObject
duplicateProps: {
shapeIds: TLShapeId[]
offset: {
x: number
y: number
}
} | null
}
/** @public */
export type TLInstanceId = RecordId<TLInstance>
/** @internal */
export const instanceIdValidator = idValidator<TLInstanceId>('instance')
export function createInstanceRecordType(stylesById: Map<string, StyleProp<unknown>>) {
const stylesForNextShapeValidators = {} as Record<string, T.Validator<unknown>>
for (const [id, style] of stylesById) {
stylesForNextShapeValidators[id] = T.optional(style)
}
const instanceTypeValidator: T.Validator<TLInstance> = T.model(
'instance',
T.object({
typeName: T.literal('instance'),
id: idValidator<TLInstanceId>('instance'),
currentPageId: pageIdValidator,
followingUserId: T.string.nullable(),
brush: boxModelValidator.nullable(),
opacityForNextShape: opacityValidator,
stylesForNextShape: T.object(stylesForNextShapeValidators),
cursor: cursorValidator,
scribbles: T.arrayOf(scribbleValidator),
isFocusMode: T.boolean,
isDebugMode: T.boolean,
isToolLocked: T.boolean,
exportBackground: T.boolean,
screenBounds: boxModelValidator,
insets: T.arrayOf(T.boolean),
zoomBrush: boxModelValidator.nullable(),
isPenMode: T.boolean,
isGridMode: T.boolean,
chatMessage: T.string,
isChatting: T.boolean,
highlightedUserIds: T.arrayOf(T.string),
canMoveCamera: T.boolean,
isFocused: T.boolean,
devicePixelRatio: T.number,
isCoarsePointer: T.boolean,
isHoveringCanvas: T.boolean.nullable(),
openMenus: T.arrayOf(T.string),
isChangingStyle: T.boolean,
isReadonly: T.boolean,
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
duplicateProps: T.object({
shapeIds: T.arrayOf(idValidator<TLShapeId>('shape')),
offset: T.object({
x: T.number,
y: T.number,
}),
}).nullable(),
})
)
return createRecordType<TLInstance>('instance', {
validator: instanceTypeValidator,
scope: 'session',
}).withDefaultProperties(
(): Omit<TLInstance, 'typeName' | 'id' | 'currentPageId'> => ({
followingUserId: null,
opacityForNextShape: 1,
stylesForNextShape: {},
brush: null,
scribbles: [],
cursor: {
type: 'default',
rotation: 0,
},
isFocusMode: false,
exportBackground: false,
isDebugMode: process.env.NODE_ENV === 'development',
isToolLocked: false,
screenBounds: { x: 0, y: 0, w: 1080, h: 720 },
insets: [false, false, false, false],
zoomBrush: null,
isGridMode: false,
isPenMode: false,
chatMessage: '',
isChatting: false,
highlightedUserIds: [],
canMoveCamera: true,
isFocused: false,
devicePixelRatio: typeof window === 'undefined' ? 1 : window.devicePixelRatio,
isCoarsePointer: false,
isHoveringCanvas: null,
openMenus: [] as string[],
isChangingStyle: false,
isReadonly: false,
meta: {},
duplicateProps: null,
})
)
}
/** @internal */
export const instanceVersions = createMigrationIds('com.tldraw.instance', {
AddTransparentExportBgs: 1,
RemoveDialog: 2,
AddToolLockMode: 3,
RemoveExtraPropsForNextShape: 4,
AddLabelColor: 5,
AddFollowingUserId: 6,
RemoveAlignJustify: 7,
AddZoom: 8,
AddVerticalAlign: 9,
AddScribbleDelay: 10,
RemoveUserId: 11,
AddIsPenModeAndIsGridMode: 12,
HoistOpacity: 13,
AddChat: 14,
AddHighlightedUserIds: 15,
ReplacePropsForNextShapeWithStylesForNextShape: 16,
AddMeta: 17,
RemoveCursorColor: 18,
AddLonelyProperties: 19,
ReadOnlyReadonly: 20,
AddHoveringCanvas: 21,
AddScribbles: 22,
AddInset: 23,
AddDuplicateProps: 24,
} as const)
// TODO: rewrite these to use mutation
/** @public */
export const instanceMigrations = createRecordMigrationSequence({
sequenceId: 'com.tldraw.instance',
recordType: 'instance',
sequence: [
{
id: instanceVersions.AddTransparentExportBgs,
up: (instance) => {
return { ...instance, exportBackground: true }
},
},
{
id: instanceVersions.RemoveDialog,
up: ({ dialog: _, ...instance }: any) => {
return instance
},
},
{
id: instanceVersions.AddToolLockMode,
up: (instance) => {
return { ...instance, isToolLocked: false }
},
},
{
id: instanceVersions.RemoveExtraPropsForNextShape,
up: ({ propsForNextShape, ...instance }: any) => {
return {
...instance,
propsForNextShape: Object.fromEntries(
Object.entries(propsForNextShape).filter(([key]) =>
[
'color',
'labelColor',
'dash',
'fill',
'size',
'font',
'align',
'verticalAlign',
'icon',
'geo',
'arrowheadStart',
'arrowheadEnd',
'spline',
].includes(key)
)
),
}
},
},
{
id: instanceVersions.AddLabelColor,
up: ({ propsForNextShape, ...instance }: any) => {
return {
...instance,
propsForNextShape: {
...propsForNextShape,
labelColor: 'black',
},
}
},
},
{
id: instanceVersions.AddFollowingUserId,
up: (instance) => {
return { ...instance, followingUserId: null }
},
},
{
id: instanceVersions.RemoveAlignJustify,
up: (instance: any) => {
let newAlign = instance.propsForNextShape.align
if (newAlign === 'justify') {
newAlign = 'start'
}
return {
...instance,
propsForNextShape: {
...instance.propsForNextShape,
align: newAlign,
},
}
},
},
{
id: instanceVersions.AddZoom,
up: (instance) => {
return { ...instance, zoomBrush: null }
},
},
{
id: instanceVersions.AddVerticalAlign,
up: (instance: any) => {
return {
...instance,
propsForNextShape: {
...instance.propsForNextShape,
verticalAlign: 'middle',
},
}
},
},
{
id: instanceVersions.AddScribbleDelay,
up: (instance: any) => {
if (instance.scribble !== null) {
return { ...instance, scribble: { ...instance.scribble, delay: 0 } }
}
return { ...instance }
},
},
{
id: instanceVersions.RemoveUserId,
up: ({ userId: _, ...instance }: any) => {
return instance
},
},
{
id: instanceVersions.AddIsPenModeAndIsGridMode,
up: (instance) => {
return { ...instance, isPenMode: false, isGridMode: false }
},
},
{
id: instanceVersions.HoistOpacity,
up: ({ propsForNextShape: { opacity, ...propsForNextShape }, ...instance }: any) => {
return { ...instance, opacityForNextShape: Number(opacity ?? '1'), propsForNextShape }
},
},
{
id: instanceVersions.AddChat,
up: (instance) => {
return { ...instance, chatMessage: '', isChatting: false }
},
},
{
id: instanceVersions.AddHighlightedUserIds,
up: (instance) => {
return { ...instance, highlightedUserIds: [] }
},
},
{
id: instanceVersions.ReplacePropsForNextShapeWithStylesForNextShape,
up: ({ propsForNextShape: _, ...instance }: any) => {
return { ...instance, stylesForNextShape: {} }
},
},
{
id: instanceVersions.AddMeta,
up: (record) => {
return {
...record,
meta: {},
}
},
},
{
id: instanceVersions.RemoveCursorColor,
up: (record: any) => {
const { color: _, ...cursor } = record.cursor
return {
...record,
cursor,
}
},
},
{
id: instanceVersions.AddLonelyProperties,
up: (record) => {
return {
...record,
canMoveCamera: true,
isFocused: false,
devicePixelRatio: 1,
isCoarsePointer: false,
openMenus: [],
isChangingStyle: false,
isReadOnly: false,
}
},
},
{
id: instanceVersions.ReadOnlyReadonly,
up: ({ isReadOnly: _isReadOnly, ...record }: any) => {
return {
...record,
isReadonly: _isReadOnly,
}
},
},
{
id: instanceVersions.AddHoveringCanvas,
up: (record) => {
return {
...record,
isHoveringCanvas: null,
}
},
},
{
id: instanceVersions.AddScribbles,
up: ({ scribble: _, ...record }: any) => {
return {
...record,
scribbles: [],
}
},
},
{
id: instanceVersions.AddInset,
up: (record) => {
return {
...record,
insets: [false, false, false, false],
}
},
down: ({ insets: _, ...record }: any) => {
return {
...record,
}
},
},
{
id: instanceVersions.AddDuplicateProps,
up: (record) => {
return {
...record,
duplicateProps: null,
}
},
down: ({ duplicateProps: _, ...record }: any) => {
return {
...record,
}
},
},
],
})
/** @public */
export const TLINSTANCE_ID = 'instance:instance' as TLInstanceId