Browse Source

fix: optimize redo/undo

Peng Xiao 3 years ago
parent
commit
0196b792e2

+ 4 - 9
tldraw/apps/tldraw-logseq/src/components/AppUI.tsx

@@ -1,23 +1,18 @@
-import * as React from 'react'
 import { observer } from 'mobx-react-lite'
-import { ToolBar } from './Toolbar'
-import { StatusBar } from './StatusBar'
-import { PrimaryTools } from './PrimaryTools'
-import { DevTools } from './Devtools'
-import { Minimap } from './Minimap'
 import { ActionBar } from './ActionBar'
+import { DevTools } from './Devtools'
+import { PrimaryTools } from './PrimaryTools'
+import { StatusBar } from './StatusBar'
 
 const isDev = process.env.NODE_ENV === 'development'
 
 export const AppUI = observer(function AppUI() {
   return (
     <>
-      {/* <ToolBar /> */}
-      {/* <Minimap /> */}
       {isDev && <StatusBar />}
       {isDev && <DevTools />}
       <PrimaryTools />
-      <ActionBar></ActionBar>
+      <ActionBar />
     </>
   )
 })

+ 1 - 1
tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx

@@ -304,7 +304,7 @@ const SwatchAction = observer(() => {
       shapes.forEach(s => {
         s.update({ fill: latestValue, stroke: latestValue })
       })
-      app.persist(true)
+      app.persist()
     }
     return debounce(handler, 100, e => {
       latestValue = e.target.value

+ 22 - 0
tldraw/apps/tldraw-logseq/src/components/StatusBar/StatusBar.tsx

@@ -5,6 +5,27 @@ import { observer } from 'mobx-react-lite'
 import { useApp } from '@tldraw/react'
 import type { Shape } from '../../lib'
 
+const HistoryStack = observer(function HistoryStack() {
+  const app = useApp<Shape>()
+
+  return (
+    <div className="fixed left-4 top-4 flex gap-4">
+      {app.history.stack.map((item, i) => (
+        <div
+          style={{
+            background: app.history.pointer === i ? 'pink' : 'grey',
+          }}
+          key={i}
+          onClick={() => app.history.setPointer(i)}
+          className="flex items-center rounded-lg p-4"
+        >
+          {item.pages[0].nonce}
+        </div>
+      ))}
+    </div>
+  )
+})
+
 export const StatusBar = observer(function StatusBar() {
   const app = useApp<Shape>()
   React.useEffect(() => {
@@ -30,6 +51,7 @@ export const StatusBar = observer(function StatusBar() {
   })
   return (
     <div className="tl-statusbar">
+      <HistoryStack />
       {app.selectedTool.id} | {app.selectedTool.currentState.id}
       <div style={{ flex: 1 }} />
       <div id="tl-statusbar-anchor" style={{ display: 'flex' }} />

+ 3 - 3
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -31,7 +31,7 @@ import { TLViewport } from '../TLViewport'
 import { TLSelectTool, TLMoveTool } from '../tools'
 
 export interface TLDocumentModel<S extends TLShape = TLShape, A extends TLAsset = TLAsset> {
-  currentPageId: string
+  // currentPageId: string
   selectedIds: string[]
   pages: TLPageModel<S>[]
   assets?: A[]
@@ -247,7 +247,7 @@ export class TLApp<
 
   loadDocumentModel(model: TLDocumentModel<S>): this {
     this.history.deserialize(model)
-    if (model.assets) this.addAssets(model.assets)
+    if (model.assets && model.assets.length > 0) this.addAssets(model.assets)
 
     return this
   }
@@ -272,7 +272,7 @@ export class TLApp<
 
   @computed get serialized(): TLDocumentModel<S> {
     return {
-      currentPageId: this.currentPageId,
+      // currentPageId: this.currentPageId,
       selectedIds: Array.from(this.selectedIds.values()),
       pages: Array.from(this.pages.values()).map(page => page.serialized),
       assets: this.getCleanUpAssets(),

+ 31 - 25
tldraw/packages/core/src/lib/TLHistory.ts

@@ -1,10 +1,16 @@
-import { computed, makeObservable, transaction } from 'mobx'
+import { action, computed, makeObservable, observable, transaction } from 'mobx'
 import type { TLEventMap } from '../types'
-import { deepEqual } from '../utils'
+import { deepCopy, deepEqual, omit } from '../utils'
 import type { TLShape } from './shapes'
 import type { TLApp, TLDocumentModel } from './TLApp'
 import { TLPage } from './TLPage'
 
+const shouldPersist = (a: TLDocumentModel, b: TLDocumentModel) => {
+  const page0 = omit(a.pages[0], 'nonce')
+  const page1 = omit(b.pages[0], 'nonce')
+  return !deepEqual(page0, page1)
+}
+
 export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEventMap> {
   constructor(app: TLApp<S, K>) {
     this.app = app
@@ -12,8 +18,8 @@ export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEve
   }
 
   app: TLApp<S, K>
-  stack: TLDocumentModel[] = []
-  pointer = 0
+  @observable stack: TLDocumentModel[] = []
+  @observable pointer = 0
   isPaused = true
 
   get creating() {
@@ -40,7 +46,7 @@ export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEve
     this.isPaused = false
   }
 
-  reset = () => {
+  @action reset = () => {
     this.stack = [this.app.serialized]
     this.pointer = 0
     this.resume()
@@ -48,13 +54,15 @@ export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEve
     this.app.notify('persist', null)
   }
 
-  persist = (replace = false) => {
+  @action persist = (replace = false) => {
     if (this.isPaused || this.creating) return
 
     const { serialized } = this.app
 
     // Do not persist if the serialized state is the same as the last one
-    if (deepEqual(this.stack[this.pointer], serialized)) return
+    if (!shouldPersist(this.stack[this.pointer], serialized)) {
+      return
+    }
 
     if (replace) {
       this.stack[this.pointer] = serialized
@@ -62,37 +70,38 @@ export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEve
       if (this.pointer < this.stack.length) {
         this.stack = this.stack.slice(0, this.pointer + 1)
       }
-
       this.stack.push(serialized)
       this.pointer = this.stack.length - 1
     }
 
+    this.app.pages.forEach(page => page.bump()) // Is it ok here?
     this.app.notify('persist', null)
   }
 
-  undo = () => {
+  @action setPointer = (pointer: number) => {
+    this.pointer = pointer
+    const snapshot = this.stack[this.pointer]
+    this.deserialize(snapshot)
+    this.app.notify('persist', null)
+  }
+
+  @action undo = () => {
     if (this.isPaused) return
     if (this.app.selectedTool.currentState.id !== 'idle') return
     if (this.canUndo) {
-      this.pointer--
-      const snapshot = this.stack[this.pointer]
-      this.deserialize(snapshot)
-      this.app.notify('persist', null)
+      this.setPointer(this.pointer - 1)
     }
   }
 
-  redo = () => {
+  @action redo = () => {
     if (this.isPaused) return
     if (this.app.selectedTool.currentState.id !== 'idle') return
     if (this.canRedo) {
-      this.pointer++
-      const snapshot = this.stack[this.pointer]
-      this.deserialize(snapshot)
-      this.app.notify('persist', null)
+      this.setPointer(this.pointer + 1)
     }
   }
 
-  deserialize = (snapshot: TLDocumentModel) => {
+  @action deserialize = (snapshot: TLDocumentModel) => {
     transaction(() => {
       const { pages } = snapshot
       const wasPaused = this.isPaused
@@ -127,11 +136,6 @@ export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEve
               }
             }
 
-            // Do not remove any currently selected shapes
-            newSelectedIds.forEach(id => {
-              shapesMap.delete(id)
-            })
-
             // Do not remove shapes if currently state is creating or editing
             // Any shapes remaining in the shapes map need to be removed
             if (shapesMap.size > 0 && !this.app.selectedTool.isInAny('creating', 'editingShape')) {
@@ -142,13 +146,15 @@ export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEve
             // Remove the page from the map
             pagesMap.delete(serializedPage.id)
             page.updateBindings(serializedPage.bindings)
+            page.nonce = serializedPage.nonce ?? 0
           } else {
             // Create the page
-            const { id, name, shapes, bindings } = serializedPage
+            const { id, name, shapes, bindings, nonce } = serializedPage
             pagesToAdd.push(
               new TLPage(this.app, {
                 id,
                 name,
+                nonce,
                 bindings,
                 shapes: shapes.map(serializedShape => {
                   const ShapeClass = this.app.getShapeClass(serializedShape.type)

+ 5 - 3
tldraw/packages/core/src/lib/TLPage/TLPage.ts

@@ -21,15 +21,17 @@ export interface TLPageProps<S> {
   name: string
   shapes: S[]
   bindings: Record<string, TLBinding>
+  nonce?: number
 }
 
 export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventMap> {
   constructor(app: TLApp<S, E>, props = {} as TLPageProps<S>) {
-    const { id, name, shapes = [], bindings = {} } = props
+    const { id, name, shapes = [], bindings = {}, nonce } = props
     this.id = id
     this.name = name
     this.bindings = Object.assign({}, bindings) // make sure it is type of object
     this.app = app
+    this.nonce = nonce || 0
     this.addShapes(...shapes)
     makeObservable(this)
 
@@ -73,9 +75,9 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
     }
   }
 
-  nonce = 0
+  @observable nonce = 0
 
-  private bump = () => {
+  @action bump = () => {
     this.nonce++
   }
 

+ 5 - 0
tldraw/packages/core/src/utils/index.ts

@@ -62,6 +62,11 @@ export function dedupe<T>(arr: T[]) {
   return [...new Set(arr)]
 }
 
+export function omit<T, K extends keyof T>(obj: T, key: K): Omit<T, K> {
+  const { [key]: _, ...rest } = obj
+  return rest
+}
+
 /** Linear interpolate between two values. */
 export function lerp(a: number, b: number, t: number) {
   return a + (b - a) * t