UNPKG

@tldraw/sync-core

Version:

tldraw infinite canvas SDK (multiplayer sync).

406 lines (378 loc) • 8.31 kB
import { Editor, PageRecordType, TLArrowBinding, TLPage, TLPageId, TLShape, TLShapeId, TLStore, VecModel, createShapeId, defaultAddFontsFromNode, defaultBindingUtils, defaultShapeUtils, defaultTools, tipTapDefaultExtensions, } from 'tldraw' import { RandomSource } from './RandomSource' export type Op = | { type: 'create-box' parentId?: TLShapeId x: number y: number width: number height: number } | { type: 'create-frame' x: number y: number width: number height: number } | { type: 'group-selection' } | { type: 'ungroup-selection' } | { type: 'create-arrow' start: TLArrowBinding | VecModel end: TLArrowBinding | VecModel } | { type: 'delete-shape' id: TLShapeId } | { type: 'create-page' id: TLPageId } | { type: 'delete-page' id: TLPageId } | { type: 'undo' } | { type: 'redo' } | { type: 'switch-page' id: TLPageId } | { type: 'select-shape' id: TLShapeId } | { type: 'deselect-shape' id: TLShapeId } | { type: 'move-selection' dx: number dy: number } | { type: 'delete-selection' } | { type: 'move-selected-shapes-to-page' pageId: TLPageId } | { type: 'mark-stopping-point' } export class FuzzEditor extends RandomSource { editor: Editor constructor( public readonly id: string, _seed: number, public readonly store: TLStore ) { super(_seed) this.editor = new Editor({ shapeUtils: defaultShapeUtils, bindingUtils: defaultBindingUtils, tools: defaultTools, initialState: 'select', store, getContainer: () => document.createElement('div'), textOptions: { addFontsFromNode: defaultAddFontsFromNode, tipTapConfig: { extensions: tipTapDefaultExtensions, }, }, }) } ops: Op[] = [] getRandomShapeId({ selected }: { selected?: boolean } = {}): TLShapeId | undefined { return this.randomElement<TLShape>( selected ? this.editor.getSelectedShapes() : this.editor.getCurrentPageShapes() )?.id } getRandomOp(): Op { const op = this.randomAction<Op>( [ () => { const x = this.randomInt(1000) const y = this.randomInt(1000) const width = this.randomInt(1, 1000) const height = this.randomInt(1, 1000) let parentId: TLShapeId | undefined if (this.randomInt(2) === 0) { parentId = this.randomElement<TLShape>( this.editor.getCurrentPageShapes().filter((s) => s.type === 'frame') )?.id } return { type: 'create-box', x, y, width, height, parentId } }, () => { const x = this.randomInt(1000) const y = this.randomInt(1000) const width = this.randomInt(1, 1000) const height = this.randomInt(1, 1000) return { type: 'create-frame', x, y, width, height } }, // Need to disable arrows for the time being, the cleanup logic leads to state inconsistency. // We need a better way to handle state updates. // () => { // let start: TLArrowTerminal = { // type: 'point', // x: this.randomInt(1000), // y: this.randomInt(1000), // } // let end: TLArrowTerminal = { // type: 'point', // x: this.randomInt(1000), // y: this.randomInt(1000), // } // if (this.randomInt(2) === 0) { // const boundShapeId = this.getRandomShapeId() // if (boundShapeId) { // start = { // type: 'binding', // boundShapeId: boundShapeId, // isExact: true, // normalizedAnchor: { x: 0.5, y: 0.5 }, // } // } // } // if (this.randomInt(2) === 0) { // const boundShapeId = this.getRandomShapeId() // if (boundShapeId) { // end = { // type: 'binding', // boundShapeId: boundShapeId, // isExact: true, // normalizedAnchor: { x: 0.5, y: 0.5 }, // } // } // } // return { type: 'create-arrow', start, end } // }, () => { const id = this.getRandomShapeId() if (id) { return { type: 'delete-shape', id } } return this.getRandomOp() }, () => { return { type: 'create-page', id: PageRecordType.createId() } }, () => { const id = this.randomElement<TLPage>(this.editor.getPages())?.id if (id) { return { type: 'delete-page', id } } return this.getRandomOp() }, () => { return { type: 'undo' } }, () => { return { type: 'redo' } }, () => { return { type: 'mark-stopping-point' } }, () => { if (this.editor.getSelectedShapes().length > 1) { return { type: 'group-selection' } } return this.getRandomOp() }, () => { if (this.editor.getSelectedShapes().some((s) => s.type === 'group')) { return { type: 'ungroup-selection' } } return this.getRandomOp() }, () => { const id = this.randomElement<TLPage>(this.editor.getPages())?.id if (id) { return { type: 'switch-page', id } } return this.getRandomOp() }, () => { const id = this.getRandomShapeId() if (id) { return { type: 'select-shape', id } } return this.getRandomOp() }, () => { const id = this.getRandomShapeId({ selected: true }) if (id) { return { type: 'deselect-shape', id } } return this.getRandomOp() }, () => { if (this.editor.getSelectedShapes().length) { const dx = this.randomInt(1000) const dy = this.randomInt(1000) return { type: 'move-selection', dx, dy } } return this.getRandomOp() }, () => { if (this.editor.getSelectedShapes().length) { return { type: 'delete-selection' } } return this.getRandomOp() }, () => { if (this.editor.getSelectedShapes().length) { const pageId = this.randomElement<TLPage>( this.editor.getPages().filter((p) => p.id !== this.editor.getCurrentPageId()) )?.id if (pageId) { return { type: 'move-selected-shapes-to-page', pageId } } } return this.getRandomOp() }, ], true ) this.ops.push(op) return op } applyOp(op: Op) { switch (op.type) { case 'create-box': { this.editor.createShape({ type: 'geo', id: createShapeId(), x: op.x, y: op.y, parentId: op.parentId, props: { w: op.width, h: op.height, }, }) break } case 'create-frame': { this.editor.createShape({ type: 'frame', id: createShapeId(), x: op.x, y: op.y, props: { w: op.width, h: op.height, }, }) break } case 'create-arrow': { this.editor.createShape({ type: 'arrow', id: createShapeId(), x: 0, y: 0, props: { start: op.start, end: op.end, }, }) break } case 'delete-shape': { this.editor.deleteShape(op.id) break } case 'create-page': { this.editor.createPage({ id: op.id, name: op.id }) break } case 'delete-page': { this.editor.deletePage(op.id) break } case 'undo': { this.editor.undo() break } case 'redo': { this.editor.redo() break } case 'group-selection': { this.editor.groupShapes(this.editor.getSelectedShapeIds()) break } case 'ungroup-selection': { this.editor.ungroupShapes(this.editor.getSelectedShapeIds()) break } case 'mark-stopping-point': { this.editor.markHistoryStoppingPoint() break } case 'switch-page': { this.editor.setCurrentPage(op.id) break } case 'select-shape': { this.editor.select(op.id) break } case 'deselect-shape': { this.editor.deselect(op.id) break } case 'move-selection': { this.editor.updateShapes( this.editor.getSelectedShapes().map((s) => ({ ...s, x: s.x + op.dx, y: s.y + op.dy, })) ) break } case 'delete-selection': { this.editor.deleteShapes(this.editor.getSelectedShapeIds()) break } case 'move-selected-shapes-to-page': { this.editor.moveShapesToPage(this.editor.getSelectedShapeIds(), op.pageId) break } default: throw new Error(`Unknown op type: ${JSON.stringify((op as any).type)}`) } } }