diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 987ae1701..9036bbb6b 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -34,7 +34,7 @@ export interface TLHandle { index: number point: number[] canBind?: boolean - bindingId?: string + bindingId: string | null } export interface TLShape { diff --git a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx index f59850296..24e646622 100644 --- a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx +++ b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx @@ -30,7 +30,8 @@ export class Arrow extends TLDrawShapeUtil { pathCache = new WeakMap() defaultProps = { - id: 'id', + id: 'arrow_id', + nonce: 1, type: TLDrawShapeType.Arrow as const, name: 'Arrow', parentId: 'page', diff --git a/packages/tldraw/src/shape/shapes/draw/draw.tsx b/packages/tldraw/src/shape/shapes/draw/draw.tsx index 553ceb3ca..9f96597b6 100644 --- a/packages/tldraw/src/shape/shapes/draw/draw.tsx +++ b/packages/tldraw/src/shape/shapes/draw/draw.tsx @@ -22,7 +22,8 @@ export class Draw extends TLDrawShapeUtil { polygonCache = new WeakMap([]) defaultProps: DrawShape = { - id: 'id', + id: 'draw_id', + nonce: 1, type: TLDrawShapeType.Draw as const, name: 'Draw', parentId: 'page', diff --git a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx index 9328d2dac..a5d7c50cb 100644 --- a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx +++ b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx @@ -22,7 +22,8 @@ export class Ellipse extends TLDrawShapeUtil { canBind = true defaultProps = { - id: 'id', + id: 'ellipse_id', + nonce: 1, type: TLDrawShapeType.Ellipse as const, name: 'Ellipse', parentId: 'page', diff --git a/packages/tldraw/src/shape/shapes/group/group.tsx b/packages/tldraw/src/shape/shapes/group/group.tsx index 562808fbe..f87e18b20 100644 --- a/packages/tldraw/src/shape/shapes/group/group.tsx +++ b/packages/tldraw/src/shape/shapes/group/group.tsx @@ -23,7 +23,8 @@ export class Group extends TLDrawShapeUtil { pathCache = new WeakMap([]) defaultProps: GroupShape = { - id: 'id', + id: 'group_id', + nonce: 1, type: TLDrawShapeType.Group as const, name: 'Group', parentId: 'page', diff --git a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx index 2d350efe0..5b0937a7b 100644 --- a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx +++ b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx @@ -23,7 +23,8 @@ export class Rectangle extends TLDrawShapeUtil { pathCache = new WeakMap([]) defaultProps: RectangleShape = { - id: 'id', + id: 'rectangle_id', + nonce: 1, type: TLDrawShapeType.Rectangle as const, name: 'Rectangle', parentId: 'page', diff --git a/packages/tldraw/src/shape/shapes/text/text.tsx b/packages/tldraw/src/shape/shapes/text/text.tsx index 7fdbb6329..352f3e5dd 100644 --- a/packages/tldraw/src/shape/shapes/text/text.tsx +++ b/packages/tldraw/src/shape/shapes/text/text.tsx @@ -66,7 +66,8 @@ export class Text extends TLDrawShapeUtil { pathCache = new WeakMap([]) defaultProps = { - id: 'id', + id: 'text_id', + nonce: 1, type: TLDrawShapeType.Text as const, name: 'Text', parentId: 'page', diff --git a/packages/tldraw/src/state/__snapshots__/tlstate.spec.ts.snap b/packages/tldraw/src/state/__snapshots__/tlstate.spec.ts.snap index 3ec68c8d1..f03cc9f91 100644 --- a/packages/tldraw/src/state/__snapshots__/tlstate.spec.ts.snap +++ b/packages/tldraw/src/state/__snapshots__/tlstate.spec.ts.snap @@ -23,6 +23,7 @@ Array [ "childIndex": 1, "id": "rect1", "name": "Rectangle", + "nonce": 1487076708000, "parentId": "page1", "point": Array [ 0, @@ -81,6 +82,7 @@ Array [ "childIndex": 1, "id": "rect2", "name": "Rectangle", + "nonce": 1487076708000, "parentId": "page1", "point": Array [ 0, diff --git a/packages/tldraw/src/state/command/create/create.command.ts b/packages/tldraw/src/state/command/create/create.command.ts index e4749ecf9..b5d3efd25 100644 --- a/packages/tldraw/src/state/command/create/create.command.ts +++ b/packages/tldraw/src/state/command/create/create.command.ts @@ -5,11 +5,11 @@ import type { TLDrawShape, Data, TLDrawCommand } from '~types' export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand { const { currentPageId } = data.appState - const beforeShapes: Record | undefined> = {} - const afterShapes: Record | undefined> = {} + const beforeShapes: Record | null> = {} + const afterShapes: Record | null> = {} shapes.forEach((shape) => { - beforeShapes[shape.id] = undefined + beforeShapes[shape.id] = null afterShapes[shape.id] = shape }) diff --git a/packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.ts b/packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.ts index 935a77228..abdcd8012 100644 --- a/packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.ts +++ b/packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.ts @@ -31,10 +31,10 @@ export function duplicatePage(data: Data, pageId: string): TLDrawCommand { }, document: { pages: { - [newId]: undefined, + [newId]: null, }, pageStates: { - [newId]: undefined, + [newId]: null, }, }, }, @@ -53,10 +53,10 @@ export function duplicatePage(data: Data, pageId: string): TLDrawCommand { selectedIds: [], camera: { point: [-window.innerWidth / 2, -window.innerHeight / 2], zoom: 1 }, currentParentId: newId, - editingId: undefined, - bindingId: undefined, - hoveredId: undefined, - pointedId: undefined, + editingId: null, + bindingId: null, + hoveredId: null, + pointedId: null, }, }, }, diff --git a/packages/tldraw/src/state/command/duplicate/duplicate.command.ts b/packages/tldraw/src/state/command/duplicate/duplicate.command.ts index db45ec07e..eec22c60a 100644 --- a/packages/tldraw/src/state/command/duplicate/duplicate.command.ts +++ b/packages/tldraw/src/state/command/duplicate/duplicate.command.ts @@ -31,7 +31,7 @@ export function duplicate(data: Data, ids: string[]): TLDrawCommand { .filter((shape) => !ids.includes(shape.parentId)) .forEach((shape) => { const duplicatedId = Utils.uniqueId() - before.shapes[duplicatedId] = undefined + before.shapes[duplicatedId] = null after.shapes[duplicatedId] = { ...Utils.deepClone(shape), @@ -68,7 +68,7 @@ export function duplicate(data: Data, ids: string[]): TLDrawCommand { const child = TLDR.getShape(data, childId, currentPageId) const duplicatedId = Utils.uniqueId() const duplicatedParentId = duplicateMap[shape.id] - before.shapes[duplicatedId] = undefined + before.shapes[duplicatedId] = null after.shapes[duplicatedId] = { ...Utils.deepClone(child), id: duplicatedId, @@ -102,7 +102,7 @@ export function duplicate(data: Data, ids: string[]): TLDrawCommand { toId: duplicateMap[binding.toId], } - before.bindings[duplicatedBindingId] = undefined + before.bindings[duplicatedBindingId] = null after.bindings[duplicatedBindingId] = duplicatedBinding // Change the duplicated shape's handle so that it reference @@ -119,7 +119,7 @@ export function duplicate(data: Data, ids: string[]): TLDrawCommand { const boundShape = after.shapes[duplicateMap[binding.fromId]] Object.values(boundShape!.handles!).forEach((handle) => { if (handle!.bindingId === binding.id) { - handle!.bindingId = undefined + handle!.bindingId = null } }) } diff --git a/packages/tldraw/src/state/command/group/group.command.ts b/packages/tldraw/src/state/command/group/group.command.ts index 74197a30a..578e9d512 100644 --- a/packages/tldraw/src/state/command/group/group.command.ts +++ b/packages/tldraw/src/state/command/group/group.command.ts @@ -10,11 +10,11 @@ export function group( groupId: string, pageId: string ): TLDrawCommand | undefined { - const beforeShapes: Record> = {} - const afterShapes: Record> = {} + const beforeShapes: Record> = {} + const afterShapes: Record> = {} - const beforeBindings: Record> = {} - const afterBindings: Record> = {} + const beforeBindings: Record> = {} + const afterBindings: Record> = {} const idsToGroup = [...ids] const shapesToGroup: TLDrawShape[] = [] @@ -24,7 +24,7 @@ export function group( // Collect all of the shapes to group (and their ids) for (const id of ids) { const shape = TLDR.getShape(data, id, pageId) - if (shape.children === undefined) { + if (!shape.children) { shapesToGroup.push(shape) } else { otherEffectedGroups.push(shape) @@ -74,7 +74,7 @@ export function group( const groupBounds = Utils.getCommonBounds(shapesToGroup.map((shape) => TLDR.getBounds(shape))) // Create the group - beforeShapes[groupId] = undefined + beforeShapes[groupId] = null afterShapes[groupId] = TLDR.getShapeUtils({ type: TLDrawShapeType.Group } as TLDrawShape).create({ id: groupId, @@ -119,7 +119,7 @@ export function group( // If the parent has no children, remove it if (nextChildren.length === 0) { beforeShapes[shape.id] = shape - afterShapes[shape.id] = undefined + afterShapes[shape.id] = null // And if that parent is part of a different group, mark it for cleanup // (This is necessary only when we implement nested groups.) @@ -148,10 +148,10 @@ export function group( Object.values(page.bindings).forEach((binding) => { for (const id of [binding.toId, binding.fromId]) { // If the binding references a deleted shape... - if (afterShapes[id] === undefined) { + if (!afterShapes[id]) { // Delete this binding beforeBindings[binding.id] = binding - afterBindings[binding.id] = undefined + afterBindings[binding.id] = null // Let's also look each the bound shape... const shape = TLDR.getShape(data, id, pageId) @@ -177,7 +177,7 @@ export function group( ...afterShapes[id], handles: { ...afterShapes[id]?.handles, - [handle.id]: { bindingId: undefined }, + [handle.id]: { bindingId: null }, }, } } diff --git a/packages/tldraw/src/state/command/move-to-page/move-to-page.command.ts b/packages/tldraw/src/state/command/move-to-page/move-to-page.command.ts index a0cbbe97e..0a8595164 100644 --- a/packages/tldraw/src/state/command/move-to-page/move-to-page.command.ts +++ b/packages/tldraw/src/state/command/move-to-page/move-to-page.command.ts @@ -44,7 +44,7 @@ export function moveToPage( .forEach((shape) => { movingShapeIds.add(shape.id) shapesToMove.add(shape) - if (shape.children !== undefined) { + if (shape.children) { shape.children.forEach((childId) => { movingShapeIds.add(childId) shapesToMove.add(TLDR.getShape(data, childId, fromPageId)) @@ -61,10 +61,10 @@ export function moveToPage( movingShapes.forEach((shape, i) => { // Remove the shape from the fromPage fromPage.before.shapes[shape.id] = shape - fromPage.after.shapes[shape.id] = undefined + fromPage.after.shapes[shape.id] = null // But the moved shape on the "to" page - toPage.before.shapes[shape.id] = undefined + toPage.before.shapes[shape.id] = null toPage.after.shapes[shape.id] = shape // If the shape's parent isn't moving too, reparent the shape to @@ -98,7 +98,7 @@ export function moveToPage( // Always delete the binding from the from page fromPage.before.bindings[binding.id] = binding - fromPage.after.bindings[binding.id] = undefined + fromPage.after.bindings[binding.id] = null // Delete the reference from the binding's fromShape @@ -110,7 +110,7 @@ export function moveToPage( if (shouldCopy) { // Just move the binding to the new page - toPage.before.bindings[binding.id] = undefined + toPage.before.bindings[binding.id] = null toPage.after.bindings[binding.id] = binding } else { if (movingShapeIds.has(binding.fromId)) { diff --git a/packages/tldraw/src/state/command/toggle-decoration/toggle-decoration.command.spec.ts b/packages/tldraw/src/state/command/toggle-decoration/toggle-decoration.command.spec.ts index 693b568e7..b5775b6c0 100644 --- a/packages/tldraw/src/state/command/toggle-decoration/toggle-decoration.command.spec.ts +++ b/packages/tldraw/src/state/command/toggle-decoration/toggle-decoration.command.spec.ts @@ -36,6 +36,7 @@ describe('Toggle decoration command', () => { TLDR.getShapeUtils({ type: 'arrow' } as TLDrawShape).create({ id: 'arrow1', parentId: 'page1', + nonce: Date.now(), }) ) .select('arrow1') diff --git a/packages/tldraw/src/state/command/translate/translate.command.ts b/packages/tldraw/src/state/command/translate/translate.command.ts index d0e8a1b67..a64e727ca 100644 --- a/packages/tldraw/src/state/command/translate/translate.command.ts +++ b/packages/tldraw/src/state/command/translate/translate.command.ts @@ -34,7 +34,7 @@ export function translate(data: Data, ids: string[], delta: number[]): TLDrawCom bindingsToDelete.forEach((binding) => { before.bindings[binding.id] = binding - after.bindings[binding.id] = undefined + after.bindings[binding.id] = null for (const id of [binding.toId, binding.fromId]) { // Let's also look at the bound shape... diff --git a/packages/tldraw/src/state/command/ungroup/ungroup.command.ts b/packages/tldraw/src/state/command/ungroup/ungroup.command.ts index f96fe0471..a1f4d45b3 100644 --- a/packages/tldraw/src/state/command/ungroup/ungroup.command.ts +++ b/packages/tldraw/src/state/command/ungroup/ungroup.command.ts @@ -1,14 +1,13 @@ import type { GroupShape, TLDrawBinding, TLDrawShape } from '~types' -import type { Data, TLDrawCommand } from '~types' +import type { Data, TLDrawCommand, Patch } from '~types' import { TLDR } from '~state/tldr' -import type { Patch } from 'rko' export function ungroup(data: Data, groupId: string, pageId: string): TLDrawCommand | undefined { - const beforeShapes: Record> = {} - const afterShapes: Record> = {} + const beforeShapes: Record | null> = {} + const afterShapes: Record | null> = {} - const beforeBindings: Record> = {} - const afterBindings: Record> = {} + const beforeBindings: Record | null> = {} + const afterBindings: Record | null> = {} // The group shape const groupShape = TLDR.getShape(data, groupId, pageId) @@ -36,7 +35,7 @@ export function ungroup(data: Data, groupId: string, pageId: string): TLDrawComm // Remove the group shape beforeShapes[groupId] = groupShape - afterShapes[groupId] = undefined + afterShapes[groupId] = null // Reparent shapes to the page sortedShapes.forEach((shape, index) => { @@ -59,10 +58,10 @@ export function ungroup(data: Data, groupId: string, pageId: string): TLDrawComm .forEach((binding) => { for (const id of [binding.toId, binding.fromId]) { // If the binding references the deleted group... - if (afterShapes[id] === undefined) { + if (!afterShapes[id]) { // Delete the binding beforeBindings[binding.id] = binding - afterBindings[binding.id] = undefined + afterBindings[binding.id] = null // Let's also look each the bound shape... const shape = TLDR.getShape(data, id, pageId) @@ -88,7 +87,7 @@ export function ungroup(data: Data, groupId: string, pageId: string): TLDrawComm ...afterShapes[id], handles: { ...afterShapes[id]?.handles, - [handle.id]: { bindingId: undefined }, + [handle.id]: { bindingId: null }, }, } } diff --git a/packages/tldraw/src/state/command/utils/removeShapesFromPage.ts b/packages/tldraw/src/state/command/utils/removeShapesFromPage.ts index 9cd049eea..7208077ab 100644 --- a/packages/tldraw/src/state/command/utils/removeShapesFromPage.ts +++ b/packages/tldraw/src/state/command/utils/removeShapesFromPage.ts @@ -22,16 +22,16 @@ export function removeShapesFromPage(data: Data, ids: string[], pageId: string) deletedIds.add(id) const shape = TLDR.getShape(data, id, pageId) before.shapes[id] = shape - after.shapes[id] = undefined + after.shapes[id] = null // Also delete the shape's children - if (shape.children !== undefined) { + if (shape.children) { shape.children.forEach((childId) => { deletedIds.add(childId) const child = TLDR.getShape(data, childId, pageId) before.shapes[childId] = child - after.shapes[childId] = undefined + after.shapes[childId] = null }) } @@ -57,10 +57,10 @@ export function removeShapesFromPage(data: Data, ids: string[], pageId: string) .forEach((binding) => { for (const id of [binding.toId, binding.fromId]) { // If the binding references a deleted shape... - if (after.shapes[id] === undefined) { + if (!after.shapes[id]) { // Delete this binding before.bindings[binding.id] = binding - after.bindings[binding.id] = undefined + after.bindings[binding.id] = null // Let's also look each the bound shape... const shape = TLDR.getShape(data, id, pageId) diff --git a/packages/tldraw/src/state/session/sessions/arrow/arrow.session.ts b/packages/tldraw/src/state/session/sessions/arrow/arrow.session.ts index 278f4321e..9bf103320 100644 --- a/packages/tldraw/src/state/session/sessions/arrow/arrow.session.ts +++ b/packages/tldraw/src/state/session/sessions/arrow/arrow.session.ts @@ -7,7 +7,7 @@ import { Session, TLDrawStatus, } from '~types' -import { Vec, Utils, TLHandle } from '@tldraw/core' +import { Vec, Utils } from '@tldraw/core' import { TLDR } from '~state/tldr' export class ArrowSession implements Session { @@ -40,7 +40,7 @@ export class ArrowSession implements Session { this.initialBinding = page.bindings[initialBindingId] } else { // Explicitly set this handle to undefined, so that it gets deleted on undo - this.initialShape.handles[this.handleId].bindingId = undefined + this.initialShape.handles[this.handleId].bindingId = null } } @@ -82,7 +82,7 @@ export class ArrowSession implements Session { // If nothing changes, we want this to be the same object reference as // before. If it does change, we'll redefine this later on. - let nextBindings: Record = page.bindings + let nextBindings: Record = page.bindings // If the handle can bind, then we need to search bindable shapes for // a binding. If the handle already has a binding, then we will either @@ -99,8 +99,8 @@ export class ArrowSession implements Session { // metaKey // ) - let binding: ArrowBinding | undefined = undefined - let target: TLDrawShape | undefined = undefined + let binding: ArrowBinding | null = null + let target: TLDrawShape | null = null // Alt key skips binding if (!altKey) { @@ -153,21 +153,21 @@ export class ArrowSession implements Session { } // If we didn't find a target... - if (binding === undefined) { + if (!binding) { this.didBind = false if (handle.bindingId) { nextBindings = { ...nextBindings } - nextBindings[handle.bindingId] = undefined + nextBindings[handle.bindingId] = null } - nextShape.handles[handleId].bindingId = undefined + nextShape.handles[handleId].bindingId = null } else if (target) { this.didBind = true nextBindings = { ...nextBindings } if (handle.bindingId && handle.bindingId !== this.newBindingId) { - nextBindings[handle.bindingId] = undefined - nextShape.handles[handleId].bindingId = undefined + nextBindings[handle.bindingId] = null + nextShape.handles[handleId].bindingId = null } // If we found a new binding, add its id to the shape's handle... @@ -254,8 +254,8 @@ export class ArrowSession implements Session { const { initialShape, initialBinding, handleId } = this const page = TLDR.getPage(data, data.appState.currentPageId) - const beforeBindings: Partial> = {} - const afterBindings: Partial> = {} + const beforeBindings: Partial> = {} + const afterBindings: Partial> = {} const currentShape = TLDR.getShape( data, @@ -266,11 +266,11 @@ export class ArrowSession implements Session { if (initialBinding) { beforeBindings[initialBinding.id] = initialBinding - afterBindings[initialBinding.id] = undefined + afterBindings[initialBinding.id] = null } if (currentBindingId) { - beforeBindings[currentBindingId] = undefined + beforeBindings[currentBindingId] = null afterBindings[currentBindingId] = page.bindings[currentBindingId] } diff --git a/packages/tldraw/src/state/session/sessions/brush/brush.session.ts b/packages/tldraw/src/state/session/sessions/brush/brush.session.ts index 2b93b8279..83a5e82e9 100644 --- a/packages/tldraw/src/state/session/sessions/brush/brush.session.ts +++ b/packages/tldraw/src/state/session/sessions/brush/brush.session.ts @@ -115,7 +115,7 @@ export function getBrushSnapshot(data: Data) { (shape) => !( shape.isHidden || - shape.children !== undefined || + shape.children || selectedIds.includes(shape.id) || selectedIds.includes(shape.parentId) ) diff --git a/packages/tldraw/src/state/session/sessions/draw/draw.session.spec.ts b/packages/tldraw/src/state/session/sessions/draw/draw.session.spec.ts index 8c9bcc786..1ecd0a39e 100644 --- a/packages/tldraw/src/state/session/sessions/draw/draw.session.spec.ts +++ b/packages/tldraw/src/state/session/sessions/draw/draw.session.spec.ts @@ -11,19 +11,11 @@ describe('Draw session', () => { expect(tlstate.getShape('draw1')).toBe(undefined) tlstate - .create({ + .createShapes({ id: 'draw1', - parentId: 'page1', - name: 'Draw', - childIndex: 5, type: TLDrawShapeType.Draw, point: [32, 32], points: [[0, 0]], - style: { - dash: DashStyle.Draw, - size: SizeStyle.Medium, - color: ColorStyle.Blue, - }, }) .select('draw1') .startDrawSession('draw1', [0, 0]) diff --git a/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts b/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts index a883f85d4..7e90b9be5 100644 --- a/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts +++ b/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts @@ -153,7 +153,7 @@ export function getRotateSnapshot(data: Data) { boundsRotation: pageState.boundsRotation || 0, commonBoundsCenter, initialShapes: initialShapes - .filter((shape) => shape.children === undefined) + .filter((shape) => !shape.children) .map((shape) => { const bounds = TLDR.getBounds(shape) const center = Utils.getBoundsCenter(bounds) diff --git a/packages/tldraw/src/state/session/sessions/translate/translate.session.ts b/packages/tldraw/src/state/session/sessions/translate/translate.session.ts index dbafaf07e..b5318cff3 100644 --- a/packages/tldraw/src/state/session/sessions/translate/translate.session.ts +++ b/packages/tldraw/src/state/session/sessions/translate/translate.session.ts @@ -34,7 +34,7 @@ export class TranslateSession implements Session { const nextBindings: Patch> = {} - bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined)) + bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = null)) return { document: { @@ -134,11 +134,11 @@ export class TranslateSession implements Session { // Delete the bindings - bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined)) + bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = null)) // Delete the clones clones.forEach((clone) => { - nextShapes[clone.id] = undefined + nextShapes[clone.id] = null if (clone.parentId !== currentPageId) { nextShapes[clone.parentId] = { ...nextShapes[clone.parentId], @@ -156,7 +156,7 @@ export class TranslateSession implements Session { // Delete the cloned bindings for (const binding of this.snapshot.clonedBindings) { - nextBindings[binding.id] = undefined + nextBindings[binding.id] = null } // Set selected ids @@ -195,8 +195,8 @@ export class TranslateSession implements Session { cancel = (data: Data) => { const { initialShapes, clones, clonedBindings, bindingsToDelete } = this.snapshot - const nextBindings: Record | undefined> = {} - const nextShapes: Record | undefined> = {} + const nextBindings: Record | null> = {} + const nextShapes: Record | null> = {} const nextPageState: Partial = {} // Put back any deleted bindings @@ -206,10 +206,10 @@ export class TranslateSession implements Session { initialShapes.forEach(({ id, point }) => (nextShapes[id] = { ...nextShapes[id], point })) // Delete clones - clones.forEach((clone) => (nextShapes[clone.id] = undefined)) + clones.forEach((clone) => (nextShapes[clone.id] = null)) // Delete cloned bindings - clonedBindings.forEach((binding) => (nextBindings[binding.id] = undefined)) + clonedBindings.forEach((binding) => (nextBindings[binding.id] = null)) nextPageState.selectedIds = this.snapshot.selectedIds @@ -243,7 +243,7 @@ export class TranslateSession implements Session { if (this.isCloning) { // Update the clones clones.forEach((clone) => { - beforeShapes[clone.id] = undefined + beforeShapes[clone.id] = null afterShapes[clone.id] = TLDR.getShape(data, clone.id, pageId) @@ -262,7 +262,7 @@ export class TranslateSession implements Session { // Update the cloned bindings clonedBindings.forEach((binding) => { - beforeBindings[binding.id] = undefined + beforeBindings[binding.id] = null afterBindings[binding.id] = TLDR.getBinding(data, binding.id, pageId) }) } else { @@ -399,7 +399,7 @@ export function getTranslateSnapshot(data: Data) { }) clones.forEach((clone) => { - if (clone.children !== undefined) { + if (clone.children) { clone.children = clone.children.map((childId) => cloneMap[childId]) } }) @@ -445,7 +445,7 @@ export function getTranslateSnapshot(data: Data) { if (clone.handles) { for (const id in clone.handles) { const handle = clone.handles[id as keyof ArrowShape['handles']] - handle.bindingId = handle.bindingId ? clonedBindingsMap[handle.bindingId] : undefined + handle.bindingId = handle.bindingId ? clonedBindingsMap[handle.bindingId] : null } } } diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 86741bbc0..a06224fed 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -39,6 +39,8 @@ import { TLDrawBinding, GroupShape, TLDrawCommand, + TLDrawPatch, + PagePartial, } from '~types' import { TLDR } from './tldr' import { defaultStyle } from '~shape' @@ -72,7 +74,7 @@ const initialData: Data = { settings: { isPenMode: false, isDarkMode: false, - isZoomSnap: true, + isZoomSnap: false, isDebugMode: process.env.NODE_ENV === 'development', isReadonlyMode: false, nudgeDistanceLarge: 10, @@ -162,12 +164,16 @@ export class TLDrawState extends StateManager { protected cleanup = (state: Data, prev: Data, patch: Patch, reason?: string): Data => { const data = { ...state } - this._onPatch?.(this, reason || 'patch', patch) + const isMergingExternalPatch = reason !== 'patch:merge' + + if (reason !== 'patch:merge') { + this._onPatch?.(this, reason || 'patch', patch) + } // Remove deleted shapes and bindings (in Commands, these will be set to undefined) if (data.document !== prev.document) { Object.entries(data.document.pages).forEach(([pageId, page]) => { - if (page === undefined) { + if (!page) { // If page is undefined, delete the page and pagestate delete data.document.pages[pageId] delete data.document.pageStates[pageId] @@ -177,50 +183,55 @@ export class TLDrawState extends StateManager { const prevPage = prev.document.pages[pageId] if (!prevPage || page.shapes !== prevPage.shapes || page.bindings !== prevPage.bindings) { + // Work through the shapes and bindings page.shapes = { ...page.shapes } page.bindings = { ...page.bindings } + // And collect groups that need to be updated or removed const groupsToUpdate = new Set() - // If shape is undefined, delete the shape - Object.keys(page.shapes).forEach((id) => { + // Find which shapes have changed + const changedShapeIds = Object.keys(page.shapes).filter( + (id) => prevPage?.shapes[id] !== page.shapes[id] + ) + + changedShapeIds.forEach((id) => { const shape = page.shapes[id] - let parentId: string + const parentId = shape?.parentId || prevPage.shapes[id]?.parentId if (!shape) { - parentId = prevPage.shapes[id]?.parentId + // If shape is undefined, delete the shape delete page.shapes[id] } else { - parentId = shape.parentId + if (!isMergingExternalPatch) { + // Update the shape's nonce + shape.nonce = Date.now() + } } // If the shape is the child of a group, then update the group // (unless the group is being deleted too) - if (parentId && parentId !== pageId) { - const group = page.shapes[parentId] - if (group !== undefined) { - groupsToUpdate.add(page.shapes[parentId] as GroupShape) - } + if (parentId !== pageId) { + const group = page.shapes[parentId] as GroupShape | null + if (group) groupsToUpdate.add(group) } }) - // If binding is undefined, delete the binding + // Scan the page's bindings, looking for bindings to delete Object.keys(page.bindings).forEach((id) => { if (!page.bindings[id]) delete page.bindings[id] }) - // Find which shapes have changed - const changedShapeIds = Object.values(page.shapes) - .filter((shape) => prevPage?.shapes[shape.id] !== shape) - .map((shape) => shape.id) - - data.document.pages[pageId] = page - // Get bindings related to the changed shapes const bindingsToUpdate = TLDR.getRelatedBindings(data, changedShapeIds, pageId) // Update all of the bindings we've just collected bindingsToUpdate.forEach((binding) => { + // Update the nonce + if (!isMergingExternalPatch) { + binding.nonce = Date.now() + } + const toShape = page.shapes[binding.toId] const fromShape = page.shapes[binding.fromId] const toUtils = TLDR.getShapeUtils(toShape) @@ -248,7 +259,7 @@ export class TLDrawState extends StateManager { groupsToUpdate.forEach((group) => { if (!group) throw Error('no group!') - const children = group.children.filter((id) => page.shapes[id] !== undefined) + const children = group.children.filter((id) => !!page.shapes[id]) const commonBounds = Utils.getCommonBounds( children @@ -264,6 +275,8 @@ export class TLDrawState extends StateManager { children, } }) + + data.document.pages[pageId] = page } // Clean up page state, preventing hovers on deleted shapes @@ -286,6 +299,10 @@ export class TLDrawState extends StateManager { delete nextPageState.editingId } + // Delete any new selected ids + + nextPageState.selectedIds = nextPageState.selectedIds.filter((id) => page.shapes[id]) + data.document.pageStates[pageId] = nextPageState }) } @@ -322,7 +339,9 @@ export class TLDrawState extends StateManager { this.clearSelectHistory() } - this._onChange?.(this, id) + if (id !== 'patch:merge') { + this._onChange?.(this, id) + } } /** @@ -490,9 +509,103 @@ export class TLDrawState extends StateManager { * @param document */ mergeDocument = (document: TLDrawDocument): this => { - const next = { ...this.state } - next.document.pages[next.appState.currentPageId] = document.pages[next.appState.currentPageId] - return this.replaceState(next, 'merge') + const currentPageId = this.state.appState.currentPageId + + const mergingPage = document.pages[currentPageId] + + const currentPage = this.state.document.pages[currentPageId] + + const nextPage: TLDrawPage = { + id: currentPageId, + shapes: {}, + bindings: {}, + } + + let didChange = false + + Object.entries(mergingPage.shapes).forEach(([id, shape]) => { + if (currentPage.shapes[id] !== shape) { + didChange = true + nextPage.shapes[id] = shape + } + }) + + Object.entries(mergingPage.bindings).forEach(([id, binding]) => { + if (currentPage.bindings[id] !== binding) { + didChange = true + nextPage.bindings[id] = binding + } + }) + + if (!didChange) return this + + return this.replaceState( + { + ...this.state, + document: { + ...this.document, + pages: { + ...this.document.pages, + [currentPageId]: nextPage, + }, + }, + }, + 'merge' + ) + } + + mergePatch = (patch: TLDrawPatch): this => { + const currentPageId = this.state.appState.currentPageId + + const mergingPage = patch.document?.pages?.[currentPageId] + + const currentPage = this.state.document.pages[currentPageId] + + const patchPage: PagePartial = { + shapes: {}, + bindings: {}, + } + + let didChange = false + + if (mergingPage) { + Object.entries(mergingPage?.shapes || {}).forEach(([id, shape]) => { + if (currentPage.shapes[id] !== shape) { + didChange = true + patchPage.shapes[id] = shape + } + }) + + Object.entries(mergingPage?.bindings || {}).forEach(([id, binding]) => { + if (currentPage.bindings[id] !== binding) { + didChange = true + patchPage.bindings[id] = binding + } + }) + } + + if (!didChange) return this + + return this.patchState( + { + document: { + pages: { + [currentPageId]: patchPage, + }, + }, + }, + 'merge' + ) + } + + // merge a set of patches + mergePatches = (patches: { time: number; patch: TLDrawPatch }[]): this => { + for (const patch of patches) { + setTimeout(() => { + this.mergePatch(patch.patch) + }, patch.time) + } + return this } /** @@ -848,14 +961,7 @@ export class TLDrawState extends StateManager { this.getPagePoint([window.innerWidth / 2, window.innerHeight / 2]) ) - this.create( - TLDR.getShapeUtils(TLDrawShapeType.Text).create({ - id: Utils.uniqueId(), - parentId: this.appState.currentPageId, - childIndex, - point: [boundsCenter.minX, boundsCenter.minY], - }) - ) + this.create({ ...shape, point: [boundsCenter.minX, boundsCenter.minY] }) } return this @@ -1647,6 +1753,7 @@ export class TLDrawState extends StateManager { ...shapes.map((shape) => { return TLDR.getShapeUtils(shape as TLDrawShape).create({ ...shape, + nonce: shape.nonce || Date.now(), parentId: shape.parentId || this.currentPageId, }) }) @@ -2056,6 +2163,7 @@ export class TLDrawState extends StateManager { shapes: { [id]: utils.create({ id, + nonce: Date.now(), parentId: this.currentPageId, childIndex, point: pagePoint, diff --git a/packages/tldraw/src/test/mock-document.tsx b/packages/tldraw/src/test/mock-document.tsx index 84c03f974..d2014b1d7 100644 --- a/packages/tldraw/src/test/mock-document.tsx +++ b/packages/tldraw/src/test/mock-document.tsx @@ -8,6 +8,7 @@ export const mockDocument: TLDrawDocument = { shapes: { rect1: { id: 'rect1', + nonce: 0, parentId: 'page1', name: 'Rectangle', childIndex: 1, @@ -22,6 +23,7 @@ export const mockDocument: TLDrawDocument = { }, rect2: { id: 'rect2', + nonce: 0, parentId: 'page1', name: 'Rectangle', childIndex: 2, @@ -36,6 +38,7 @@ export const mockDocument: TLDrawDocument = { }, rect3: { id: 'rect3', + nonce: 0, parentId: 'page1', name: 'Rectangle', childIndex: 3, diff --git a/packages/tldraw/src/types.ts b/packages/tldraw/src/types.ts index ad416827d..290c8cf8a 100644 --- a/packages/tldraw/src/types.ts +++ b/packages/tldraw/src/types.ts @@ -4,7 +4,6 @@ import type { TLBinding, TLRenderInfo } from '@tldraw/core' import { TLShape, TLShapeUtil, TLHandle } from '@tldraw/core' import type { TLPage, TLPageState } from '@tldraw/core' import type { StoreApi } from 'zustand' -import type { Command, Patch } from 'rko' export type TLStore = StoreApi @@ -52,6 +51,18 @@ export interface Data { } } +export type Patch = Partial< + { + [P in keyof T]: Patch | null + } +> + +export interface Command { + id?: string + before: Patch + after: Patch +} + export type TLDrawPatch = Patch export type TLDrawCommand = Command @@ -147,6 +158,7 @@ export enum Decoration { } export interface TLDrawBaseShape extends TLShape { + nonce?: number style: ShapeStyles type: TLDrawShapeType } @@ -205,7 +217,11 @@ export abstract class TLDrawShapeUtil extends TLShapeUtil export type TLDrawShapeUtils = Record> -export interface ArrowBinding extends TLBinding { +export interface TLDrawBaseBinding extends TLBinding { + nonce?: number +} + +export interface ArrowBinding extends TLDrawBaseBinding { type: 'arrow' handleId: keyof ArrowShape['handles'] distance: number diff --git a/packages/www/pages/r/[id].tsx b/packages/www/pages/r/[id].tsx index ef9ce67fa..a743a071f 100644 --- a/packages/www/pages/r/[id].tsx +++ b/packages/www/pages/r/[id].tsx @@ -4,7 +4,7 @@ import Head from 'next/head' import dynamic from 'next/dynamic' import { supabase } from '-supabase/client' import type { TLDrawProject } from '-types' -import type { TLDrawState } from '@tldraw/tldraw' +import type { TLDrawState, TLDrawPatch } from '@tldraw/tldraw' import { Utils } from '@tldraw/core' const Editor = dynamic(() => import('components/editor'), { ssr: false }) @@ -15,40 +15,81 @@ interface RoomProps { export default function Room({ id }: RoomProps): JSX.Element { const rState = React.useRef(null) + const [ready, setReady] = React.useState(false) + + const rTimer = React.useRef(0) + const rStartTime = React.useRef(0) + const rPendingPatches = React.useRef<{ time: number; patch: TLDrawPatch }[]>([]) + const rUnsub = React.useRef() const userId = React.useRef(Utils.uniqueId()) React.useEffect(() => { - const sub = supabase - .from('projects') - .on('*', (payload) => { - if (payload.new.nonce !== userId.current) { - rState.current.mergeDocument(payload.new.document) - } - }) - .subscribe() return () => { - sub.unsubscribe() + clearTimeout(rTimer.current) + if (rUnsub.current) { + rUnsub.current?.() + } } - }, [id]) + }, [id, ready]) const handleMount = React.useCallback((tlstate: TLDrawState) => { rState.current = tlstate }, []) - const handleChange = React.useCallback( - (tlstate: TLDrawState, reason: string) => { - if ( - !(reason.startsWith('command') || reason.startsWith('undo') || reason.startsWith('redo')) - ) { - return + React.useEffect(() => { + supabase + .from('projects') + .select('*') + .eq('id', id) + .then(({ data }) => { + const project = data[0] + + if (project && rState.current) { + rState.current.loadDocument(project.document) + setReady(true) + } + }) + + const sub = supabase + .from('projects') + .on('*', (payload) => { + if (payload.new.nonce !== userId.current) { + rState.current.mergePatches(payload.new.patch) + } + }) + .subscribe() + + rUnsub.current = () => sub.unsubscribe() + + return () => { + rUnsub.current?.() + } + }) + + const handlePatch = React.useCallback( + (tlstate: TLDrawState, reason: string, patch: TLDrawPatch) => { + if (rPendingPatches.current.length === 0) { + rStartTime.current = Date.now() + const handle = setTimeout(() => { + const patches = rPendingPatches.current + rPendingPatches.current = [] + rStartTime.current = 0 + supabase + .from('projects') + .update({ + document: tlstate.document, + patch: patches, + nonce: userId.current, + }) + .eq('id', id) + .then(() => void null) + }, 100) + + rTimer.current = handle as any } - supabase - .from('projects') - .update({ document: tlstate.document, nonce: userId.current }) - .eq('id', id) - .then(() => void null) + rPendingPatches.current.push({ time: Date.now() - rStartTime.current, patch }) }, [id] ) @@ -58,7 +99,7 @@ export default function Room({ id }: RoomProps): JSX.Element { tldraw - + ) } diff --git a/packages/www/types.ts b/packages/www/types.ts index 59ed98334..752b6a56a 100644 --- a/packages/www/types.ts +++ b/packages/www/types.ts @@ -1,8 +1,9 @@ -import { TLDrawDocument } from '@tldraw/tldraw' +import { TLDrawDocument, TLDrawPatch } from '@tldraw/tldraw' export interface TLDrawProject { uuid: string id: string nonce: string + patch: { time: number; patch: TLDrawPatch }[] document: TLDrawDocument } diff --git a/setupTests.ts b/setupTests.ts index 496300bd6..9ad202b66 100644 --- a/setupTests.ts +++ b/setupTests.ts @@ -1,2 +1,3 @@ import '@testing-library/jest-dom/extend-expect' -import "fake-indexeddb/auto" \ No newline at end of file +import 'fake-indexeddb/auto' +Date.now = jest.fn(() => 1487076708000) //14.02.2017