Fixes database issues, fixes rendering issue

This commit is contained in:
Steve Ruiz 2021-08-18 00:11:00 +01:00
parent 07dcfb8df5
commit f19d3e6366
14 changed files with 185 additions and 59 deletions

View file

@ -19,7 +19,7 @@
"test": "jest",
"lerna": "lerna",
"start": "lerna run start --stream --parallel",
"start:www": "lerna run start --stream --parallel & cd packages/www && yarn build",
"start:www": "lerna run start --stream --parallel & cd packages/www && yarn dev",
"build": "yarn build:packages && cd packages/www && yarn build",
"build:packages": "cd packages/core && yarn build && cd ../tldraw && yarn build",
"publish:patch": "yarn build:packages && lerna publish patch"

View file

@ -43,8 +43,10 @@
"react-dom": "^17.0.2"
},
"dependencies": {
"@types/merge-deep": "^3.0.0",
"deepmerge": "^4.2.2",
"ismobilejs": "^1.1.1",
"merge-deep": "^3.0.3",
"react-error-boundary": "^3.1.3",
"react-use-gesture": "^9.1.3"
},

View file

@ -21,8 +21,32 @@ export class Utils {
return Object.fromEntries((Object.entries(obj) as Entry<T>[]).filter(fn)) as Partial<T>
}
static deepMerge<T>(a: T, b: DeepPartial<T>): T {
return deepmerge<T, DeepPartial<T>>(a, b, { arrayMerge: (_a, b) => b }) as T
static deepMerge<T>(target: T, source: any): T {
return deepmerge(target, source, { arrayMerge: (a, b) => b, clone: false })
// const result = {} as T
// for (const key of Object.keys(result)) {
// const tprop = target[key as keyof T]
// const sprop = source[key]
// if (tprop === sprop) {
// continue
// } else if (!(key in target) || target[key as keyof T] === undefined) {
// result[key as keyof T] = sprop
// } else if (!(key in source)) {
// continue
// } else if (source[key as keyof T] === undefined) {
// delete result[key as keyof T]
// } else {
// if (typeof tprop === 'object' && typeof sprop === 'object') {
// result[key as keyof T] = this.deepMerge(tprop, sprop)
// } else {
// result[key as keyof T] = sprop
// }
// }
// }
// return result
}
/**

View file

@ -7,6 +7,7 @@ import {
SizeStyle,
TLDrawShapeType,
TLDrawState,
TLDrawPatch,
} from '@tldraw/tldraw'
import { usePersistence } from '../hooks/usePersistence'
@ -88,7 +89,7 @@ export default function Editor(): JSX.Element {
const { value, setValue, status } = usePersistence('doc', initialDoc)
const handleChange = React.useCallback(
(tlstate: TLDrawState, reason: string) => {
(tlstate: TLDrawState, patch: TLDrawPatch, reason: string) => {
if (reason.startsWith('session')) {
return
}

View file

@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import * as React from 'react'
import { openDB, DBSchema, deleteDB } from 'idb'
import { openDB, DBSchema } from 'idb'
import type { TLDrawDocument } from '@tldraw/tldraw'
const VERSION = 1
const VERSION = 4
interface TLDatabase extends DBSchema {
documents: {
@ -57,11 +57,12 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// the state.
React.useEffect(() => {
async function handleLoad() {
await deleteDB('db')
const db = await openDB<TLDatabase>('db', VERSION, {
upgrade(db) {
db.createObjectStore('documents')
upgrade(db, oldVersion, newVersion) {
if (newVersion) {
db.deleteObjectStore('documents')
db.createObjectStore('documents')
}
},
})

View file

@ -760,6 +760,11 @@ function getCurvedArrowHeadPoints(
sweep: boolean
) {
const ints = Intersect.circle.circle(A, r1 * 0.618, C, r2).points
if (!ints) {
console.warn('Could not find an intersection for the arrow head.')
return { left: A, right: A }
}
const int = sweep ? ints[0] : ints[1]
const left = Vec.nudge(Vec.rotWith(int, A, Math.PI / 6), A, r1 * -0.382)
const right = Vec.nudge(Vec.rotWith(int, A, -Math.PI / 6), A, r1 * -0.382)
@ -768,6 +773,11 @@ function getCurvedArrowHeadPoints(
function getStraightArrowHeadPoints(A: number[], B: number[], r: number) {
const ints = Intersect.circle.lineSegment(A, r, A, B).points
if (!ints) {
console.warn('Could not find an intersection for the arrow head.')
return { left: A, right: A }
}
const int = ints[0]
const left = Vec.rotWith(int, A, Math.PI / 6)
const right = Vec.rotWith(int, A, -Math.PI / 6)

View file

@ -414,7 +414,6 @@ export class TLDR {
after: Record<string, Partial<T>>
data: Data
} {
const page = { ...this.getPage(data, pageId) }
const beforeShapes: Record<string, Partial<T>> = {}
const afterShapes: Record<string, Partial<T>> = {}
@ -425,7 +424,6 @@ export class TLDR {
Object.keys(change).map((key) => [key, shape[key as keyof T]])
) as Partial<T>
afterShapes[id] = change
page.shapes[id] = this.getShapeUtils(shape).mutate(shape as T, change as Partial<T>)
})
const dataWithChildrenChanges = ids.reduce<Data>((cData, id) => {

View file

@ -1,5 +1,6 @@
import { TLDrawState } from './tlstate'
import { mockDocument, TLStateUtils } from '~test'
import { Utils } from '@tldraw/core'
describe('TLDrawState', () => {
const tlstate = new TLDrawState()

View file

@ -116,7 +116,7 @@ export class TLDrawState implements TLCallbacks {
isCreating = false
_onChange?: (patch: DeepPartial<Data>, reason: string) => void
_onChange?: (tlstate: TLDrawState, patch: DeepPartial<Data>, reason: string) => void
// Low API
private getState = this.store.getState
@ -124,7 +124,7 @@ export class TLDrawState implements TLCallbacks {
private produce(patch: DeepPartial<Data>, reason: string) {
const next = Utils.deepMerge<Data>(this.data, patch)
this.setState(next)
this._onChange?.(patch, reason)
this._onChange?.(this, patch, reason)
return this
}
@ -160,38 +160,31 @@ export class TLDrawState implements TLCallbacks {
const prevPage = prev.document.pages[pageId]
const nextPage = {
...page,
shapes: { ...page.shapes },
bindings: { ...page.bindings },
}
if (!prevPage || page.shapes !== prevPage.shapes || page.bindings !== prevPage.bindings) {
page.shapes = { ...page.shapes }
page.bindings = { ...page.bindings }
if (
!prevPage ||
nextPage.shapes !== prevPage.shapes ||
nextPage.bindings !== prevPage.bindings
) {
Object.keys(nextPage.shapes).forEach((id) => {
if (!nextPage.shapes[id]) delete nextPage.shapes[id]
Object.keys(page.shapes).forEach((id) => {
if (!page.shapes[id]) delete page.shapes[id]
})
Object.keys(nextPage.bindings).forEach((id) => {
if (!nextPage.bindings[id]) delete nextPage.bindings[id]
Object.keys(page.bindings).forEach((id) => {
if (!page.bindings[id]) delete page.bindings[id]
})
const changedShapeIds = Object.values(nextPage.shapes)
.filter((shape) => this.data.document.pages[pageId].shapes[shape.id] !== shape)
const changedShapeIds = Object.values(page.shapes)
.filter((shape) => prevPage.shapes[shape.id] !== shape)
.map((shape) => shape.id)
this.data.document.pages[pageId] = nextPage
this.data.document.pages[pageId] = page
// Get bindings related to the changed shapes
const bindingsToUpdate = TLDR.getRelatedBindings(this.data, changedShapeIds, pageId)
// Update all of the bindings we've just collected
bindingsToUpdate.forEach((binding) => {
const toShape = nextPage.shapes[binding.toId]
const fromShape = nextPage.shapes[binding.fromId]
const toShape = page.shapes[binding.toId]
const fromShape = page.shapes[binding.fromId]
const toUtils = TLDR.getShapeUtils(toShape)
// We only need to update the binding's "from" shape
@ -211,7 +204,7 @@ export class TLDrawState implements TLCallbacks {
...fromDelta,
} as TLDrawShape
nextPage.shapes[fromShape.id] = nextShape
page.shapes[fromShape.id] = nextShape
}
})
}
@ -222,21 +215,20 @@ export class TLDrawState implements TLCallbacks {
...this.data.document.pageStates[pageId],
}
if (nextPageState.hoveredId && !nextPage.shapes[nextPageState.hoveredId]) {
if (nextPageState.hoveredId && !page.shapes[nextPageState.hoveredId]) {
delete nextPageState.hoveredId
}
if (nextPageState.bindingId && !nextPage.bindings[nextPageState.bindingId]) {
if (nextPageState.bindingId && !page.bindings[nextPageState.bindingId]) {
console.warn('Could not find the binding shape!', pageId)
delete nextPageState.bindingId
}
if (nextPageState.editingId && !nextPage.bindings[nextPageState.editingId]) {
if (nextPageState.editingId && !page.bindings[nextPageState.editingId]) {
console.warn('Could not find the editing shape!')
delete nextPageState.editingId
}
this.data.document.pages[pageId] = nextPage
this.data.document.pageStates[pageId] = nextPageState
})
}
@ -375,7 +367,7 @@ export class TLDrawState implements TLCallbacks {
}
this.setState(emptyData)
this._onChange?.(this.data, `reset`)
this._onChange?.(this, this.data, `reset`)
return this
}

View file

@ -0,0 +1,19 @@
import { Utils } from '@tldraw/core'
describe('deep merge', () => {
it('merges an object', () => {
const a = { a: 1, b: 2 }
const b = { ...a, a: 2 }
const c = Utils.deepMerge(a, b)
expect(c.a).toBe(2)
expect(c.b).toBe(a.b)
})
it('merges a complex object', () => {
const a = { a: 1, b: { name: 'steve', age: 93 } }
const b = { a: 2 }
const c = Utils.deepMerge<typeof a>(a, b)
expect(c.a).toBe(2)
expect(c.b === a.b).toBeTruthy()
})
})

View file

@ -40,6 +40,9 @@ export interface Data {
status: { current: TLDrawStatus; previous: TLDrawStatus }
}
}
export type TLDrawPatch = DeepPartial<Data>
export type PagePartial = {
shapes: DeepPartial<TLDrawPage['shapes']>
bindings: DeepPartial<TLDrawPage['bindings']>
@ -55,8 +58,8 @@ export type DeepPartial<T> = T extends Function
export interface Command {
id: string
before: DeepPartial<Data>
after: DeepPartial<Data>
before: TLDrawPatch
after: TLDrawPatch
}
export interface History {
@ -72,10 +75,10 @@ export interface SelectHistory {
export interface Session {
id: string
status: TLDrawStatus
start: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data> | void
update: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data>
complete: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data> | Command | undefined
cancel: (data: Readonly<Data>, ...args: any[]) => DeepPartial<Data>
start: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | void
update: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch
complete: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | Command | undefined
cancel: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch
}
export enum TLDrawStatus {

View file

@ -1,6 +1,6 @@
import * as React from 'react'
import { ColorStyle, DashStyle, SizeStyle, TLDrawShapeType, TLDrawState } from '@tldraw/tldraw'
import { TLDraw, TLDrawDocument } from '@tldraw/tldraw'
import { TLDraw, TLDrawDocument, TLDrawPatch } from '@tldraw/tldraw'
import { usePersistence } from '../hooks/usePersistence'
const initialDoc: TLDrawDocument = {
@ -81,7 +81,7 @@ export default function Editor(): JSX.Element {
const { value, setValue, status } = usePersistence('doc', initialDoc)
const handleChange = React.useCallback(
(tlstate: TLDrawState, reason: string) => {
(tlstate: TLDrawState, patch: TLDrawPatch, reason: string) => {
if (reason.startsWith('session')) {
return
}

View file

@ -9,7 +9,7 @@ interface TLDatabase extends DBSchema {
}
}
const VERSION = 2
const VERSION = 4
/**
* Persist a value in indexdb. This hook is designed to be used primarily through
@ -35,7 +35,7 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
_setValue(null)
setStatus('loading')
openDB<TLDatabase>('db', VERSION).then((db) =>
openDB<TLDatabase>('db1', VERSION).then((db) =>
db.get('documents', id).then((v) => {
if (!v) throw Error(`Could not find document with id: ${id}`)
_setValue(v)
@ -48,7 +48,7 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// value in the database.
const setValue = React.useCallback(
(doc: TLDrawDocument) => {
openDB<TLDatabase>('db', VERSION).then((db) => db.put('documents', doc, id))
openDB<TLDatabase>('db1', VERSION).then((db) => db.put('documents', doc, id))
},
[id]
)
@ -57,16 +57,19 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
// the state.
React.useEffect(() => {
async function handleLoad() {
const db = await openDB<TLDatabase>('db', VERSION, {
upgrade(db) {
db.createObjectStore('documents')
const db1 = await openDB<TLDatabase>('db1', VERSION, {
upgrade(db, oldVersion, newVersion) {
if (newVersion) {
db.deleteObjectStore('documents')
db.createObjectStore('documents')
}
},
})
let savedDoc: TLDrawDocument
try {
const restoredDoc = await db.get('documents', id)
const restoredDoc = await db1.get('documents', id)
if (!restoredDoc) throw Error('No document')
savedDoc = restoredDoc
restoredDoc.pageStates = Object.fromEntries(
@ -80,7 +83,7 @@ export function usePersistence(id: string, doc: TLDrawDocument) {
])
)
} catch (e) {
await db.put('documents', doc, id)
await db1.put('documents', doc, id)
savedDoc = doc
}

View file

@ -2246,6 +2246,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
"@types/merge-deep@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/merge-deep/-/merge-deep-3.0.0.tgz#3815fce13b2b0e9fadf5d71d13db5937a35f27fc"
integrity sha512-t5B5UfacpaP8opUvFGUwT0uQetFrD+qm1/I2ksxokJFLT0Tb4B2NI2G2LYz3ugMDKOE7adkNBZ6coK7RW6MAqA==
"@types/minimatch@*":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
@ -3402,6 +3407,17 @@ cliui@^7.0.2:
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
clone-deep@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6"
integrity sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=
dependencies:
for-own "^0.1.3"
is-plain-object "^2.0.1"
kind-of "^3.0.2"
lazy-cache "^1.0.3"
shallow-clone "^0.1.2"
clone-deep@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
@ -4851,11 +4867,23 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
for-in@^1.0.2:
for-in@^0.1.3:
version "0.1.8"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=
for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
for-own@^0.1.3:
version "0.1.5"
resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=
dependencies:
for-in "^1.0.1"
foreach@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
@ -5667,7 +5695,7 @@ is-boolean-object@^1.1.0:
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
is-buffer@^1.1.5:
is-buffer@^1.0.2, is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
@ -5854,7 +5882,7 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
is-plain-object@^2.0.3, is-plain-object@^2.0.4:
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
@ -6593,6 +6621,13 @@ jsprim@^1.2.2:
array-includes "^3.1.2"
object.assign "^4.1.2"
kind-of@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5"
integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=
dependencies:
is-buffer "^1.0.2"
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@ -6634,6 +6669,16 @@ language-tags@^1.0.5:
dependencies:
language-subtag-registry "~0.3.2"
lazy-cache@^0.2.3:
version "0.2.7"
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
integrity sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=
lazy-cache@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4=
lerna@^3.15.0:
version "3.22.1"
resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62"
@ -6997,6 +7042,15 @@ meow@^8.0.0:
type-fest "^0.18.0"
yargs-parser "^20.2.3"
merge-deep@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.3.tgz#1a2b2ae926da8b2ae93a0ac15d90cd1922766003"
integrity sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==
dependencies:
arr-union "^3.1.0"
clone-deep "^0.2.4"
kind-of "^3.0.2"
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -7147,6 +7201,14 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
mixin-object@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=
dependencies:
for-in "^0.1.3"
is-extendable "^0.1.1"
mkdirp-promise@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz#e9b8f68e552c68a9c1713b84883f7a1dd039b8a1"
@ -8878,6 +8940,16 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shallow-clone@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060"
integrity sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=
dependencies:
is-extendable "^0.1.1"
kind-of "^2.0.1"
lazy-cache "^0.2.3"
mixin-object "^2.0.1"
shallow-clone@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"