Fixes deleting pages, adds local file saving / loading

This commit is contained in:
Steve Ruiz 2021-06-11 11:11:42 +01:00
parent e2b53b381b
commit 61f56b984e
18 changed files with 280 additions and 117 deletions

View file

@ -25,8 +25,6 @@ export default function Handles() {
const center = getShapeUtils(shape).getCenter(shape)
console.log(shape)
return (
<g transform={`rotate(${shape.rotation * (180 / Math.PI)},${center})`}>
{Object.values(shape.handles).map((handle) => (

View file

@ -134,7 +134,11 @@ export default function useKeyboardEvents() {
}
case 's': {
if (metaKey(e)) {
state.send('SAVED', getKeyboardEventInfo(e))
if (e.shiftKey) {
state.send('SAVED_AS_TO_FILESYSTEM', getKeyboardEventInfo(e))
} else {
state.send('SAVED', getKeyboardEventInfo(e))
}
}
break
}
@ -187,6 +191,9 @@ export default function useKeyboardEvents() {
}
case 'l': {
if (metaKey(e)) {
if (e.shiftKey) {
state.send('LOADED_FROM_FILE_STSTEM', getKeyboardEventInfo(e))
}
} else {
state.send('SELECTED_LINE_TOOL', getKeyboardEventInfo(e))
}

View file

@ -18,6 +18,7 @@
"@radix-ui/react-tooltip": "^0.0.18",
"@state-designer/react": "^1.7.3",
"@stitches/react": "^0.1.9",
"browser-fs-access": "^0.17.3",
"framer-motion": "^4.1.16",
"ismobilejs": "^1.1.1",
"next": "10.2.0",

View file

@ -13,9 +13,9 @@ export default function changePage(data: Data, pageId: string) {
category: 'canvas',
manualSelection: true,
do(data) {
storage.savePage(data, data.currentPageId)
storage.savePage(data, data.document.id, prevPageId)
data.currentPageId = pageId
storage.loadPage(data, data.currentPageId)
storage.loadPage(data)
},
undo(data) {
data.currentPageId = prevPageId

View file

@ -19,7 +19,7 @@ export default function createPage(data: Data) {
data.document.pages[page.id] = page
data.pageStates[page.id] = pageState
data.currentPageId = page.id
storage.savePage(data, page.id)
storage.savePage(data, data.document.id, page.id)
},
undo(data) {
const { page, currentPageId } = snapshot

View file

@ -5,6 +5,7 @@ import { current } from 'immer'
import { getPage, getSelectedShapes } from 'utils/utils'
import { getShapeUtils } from 'lib/shape-utils'
import * as vec from 'utils/vec'
import storage from 'state/storage'
export default function changePage(data: Data, pageId: string) {
const snapshot = getSnapshot(data, pageId)
@ -18,11 +19,13 @@ export default function changePage(data: Data, pageId: string) {
data.currentPageId = snapshot.nextPageId
delete data.document.pages[pageId]
delete data.pageStates[pageId]
storage.loadPage(data, snapshot.nextPageId)
},
undo(data) {
data.currentPageId = snapshot.currentPageId
data.document.pages[pageId] = snapshot.page
data.pageStates[pageId] = snapshot.pageState
storage.loadPage(data, snapshot.currentPageId)
},
})
)
@ -37,16 +40,14 @@ function getSnapshot(data: Data, pageId: string) {
const isCurrent = currentPageId === pageId
const nextIndex = isCurrent
? page.childIndex === 0
? 1
: page.childIndex - 1
: document.pages[currentPageId].childIndex
// const nextIndex = isCurrent
// ? page.childIndex === 0
// ? 1
// : page.childIndex - 1
// : document.pages[currentPageId].childIndex
const nextPageId = isCurrent
? Object.values(document.pages).find(
(page) => page.childIndex === nextIndex
)!.id
? Object.values(document.pages).filter((page) => page.id !== pageId)[0]?.id // TODO: should be at nextIndex
: cData.currentPageId
return {

View file

@ -10,7 +10,7 @@ export default function drawCommand(data: Data, id: string) {
history.execute(
data,
new Command({
name: 'set_points',
name: 'create_draw_shape',
category: 'canvas',
manualSelection: true,
do(data, initial) {

View file

@ -71,7 +71,7 @@ export default function nudgeCommand(data: Data, newPageId: string) {
getPageState(data, fromPageId).selectedIds.clear()
// Save the "from" page
storage.savePage(data, fromPageId)
storage.savePage(data, data.document.id, fromPageId)
// Load the "to" page
storage.loadPage(data, toPageId)
@ -124,7 +124,7 @@ export default function nudgeCommand(data: Data, newPageId: string) {
getPageState(data, fromPageId).selectedIds.clear()
storage.savePage(data, fromPageId)
storage.savePage(data, data.document.id, fromPageId)
storage.loadPage(data, toPageId)

View file

@ -2,6 +2,8 @@ import { Data, ShapeType } from 'types'
import shapeUtils from 'lib/shape-utils'
export const defaultDocument: Data['document'] = {
id: '0001',
name: 'My Document',
pages: {
page1: {
id: 'page1',

View file

@ -23,7 +23,7 @@ class History<T extends Data> {
this.pointer = this.maxLength - 1
}
storage.save(data)
storage.saveToLocalStorage(data)
}
undo = (data: T) => {
@ -32,7 +32,7 @@ class History<T extends Data> {
command.undo(data)
if (this.disabled) return
this.pointer--
storage.save(data)
storage.saveToLocalStorage(data)
}
redo = (data: T) => {
@ -41,7 +41,7 @@ class History<T extends Data> {
command.redo(data, false)
if (this.disabled) return
this.pointer++
storage.save(data)
storage.saveToLocalStorage(data)
}
disable = () => {

View file

@ -1,6 +1,6 @@
import React from 'react'
import { PointerInfo } from 'types'
import { isDarwin } from 'utils/utils'
import { isDarwin, getPoint } from 'utils/utils'
const DOUBLE_CLICK_DURATION = 300
@ -17,8 +17,8 @@ class Inputs {
const info = {
target,
pointerId: touch.identifier,
origin: [touch.clientX, touch.clientY],
point: [touch.clientX, touch.clientY],
origin: getPoint(touch),
point: getPoint(touch),
pressure: 0.5,
shiftKey,
ctrlKey,
@ -42,7 +42,7 @@ class Inputs {
const info = {
...prev,
pointerId: touch.identifier,
point: [touch.clientX, touch.clientY],
point: getPoint(touch),
pressure: 0.5,
shiftKey,
ctrlKey,
@ -63,8 +63,8 @@ class Inputs {
const info = {
target,
pointerId: e.pointerId,
origin: [e.clientX, e.clientY],
point: [e.clientX, e.clientY],
origin: getPoint(e),
point: getPoint(e),
pressure: e.pressure || 0.5,
shiftKey,
ctrlKey,
@ -84,8 +84,8 @@ class Inputs {
const info = {
target,
pointerId: e.pointerId,
origin: [e.clientX, e.clientY],
point: [e.clientX, e.clientY],
origin: getPoint(e),
point: getPoint(e),
pressure: e.pressure || 0.5,
shiftKey,
ctrlKey,
@ -104,7 +104,7 @@ class Inputs {
const info = {
...prev,
pointerId: e.pointerId,
point: [e.clientX, e.clientY],
point: getPoint(e),
pressure: e.pressure || 0.5,
shiftKey,
ctrlKey,
@ -126,8 +126,8 @@ class Inputs {
const info = {
...prev,
origin: prev?.origin || [e.clientX, e.clientY],
point: [e.clientX, e.clientY],
origin: prev?.origin || getPoint(e),
point: getPoint(e),
pressure: e.pressure || 0.5,
shiftKey,
ctrlKey,
@ -144,7 +144,7 @@ class Inputs {
wheel(e: WheelEvent) {
const { shiftKey, ctrlKey, metaKey, altKey } = e
return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
return { point: getPoint(e), shiftKey, ctrlKey, metaKey, altKey }
}
canAccept(pointerId: PointerEvent['pointerId']) {

View file

@ -72,7 +72,7 @@ export default class BrushSession extends BaseSession {
point = vec.med(this.previous, point)
const next = [...vec.sub(point, this.origin), pressure]
const next = vec.round([...vec.sub(point, this.origin), pressure])
// Don't add duplicate points
if (vec.isEqual(this.last, next)) return

View file

@ -172,15 +172,19 @@ const state = createState({
CHANGED_PAGE: 'changePage',
CREATED_PAGE: ['clearSelectedIds', 'createPage'],
DELETED_PAGE: { unless: 'hasOnlyOnePage', do: 'deletePage' },
LOADED_FROM_FILE: 'loadDocumentFromJson',
},
initial: 'selecting',
states: {
selecting: {
onEnter: 'setActiveToolSelect',
on: {
SAVED: 'forceSave',
UNDO: 'undo',
REDO: 'redo',
SAVED: 'forceSave',
LOADED_FROM_FILE_STSTEM: 'loadFromFileSystem',
SAVED_TO_FILESYSTEM: 'saveToFileSystem',
SAVED_AS_TO_FILESYSTEM: 'saveAsToFileSystem',
SAVED_CODE: 'saveCode',
DELETED: 'deleteSelection',
INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
@ -433,8 +437,12 @@ const state = createState({
do: 'createShape',
to: 'draw.editing',
},
UNDO: { do: 'undo' },
REDO: { do: 'redo' },
UNDO: 'undo',
REDO: 'redo',
SAVED: 'forceSave',
LOADED_FROM_FILE_STSTEM: 'loadFromFileSystem',
SAVED_TO_FILESYSTEM: 'saveToFileSystem',
SAVED_AS_TO_FILESYSTEM: 'saveAsToFileSystem',
},
},
editing: {
@ -1474,25 +1482,41 @@ const state = createState({
/* ---------------------- Data ---------------------- */
saveToFileSystem(data) {
storage.saveToFileSystem(data)
},
saveAsToFileSystem(data) {
storage.saveAsToFileSystem(data)
},
loadFromFileSystem() {
storage.loadDocumentFromFilesystem()
},
loadDocumentFromJson(data, payload: { restoredData: any }) {
storage.load(data, payload.restoredData)
},
forceSave(data) {
storage.save(data)
storage.saveToLocalStorage(data)
},
savePage(data) {
storage.savePage(data, data.currentPageId)
storage.savePage(data)
},
loadPage(data) {
storage.loadPage(data, data.currentPageId)
storage.loadPage(data)
},
saveCode(data, payload: { code: string }) {
data.document.code[data.currentCodeFileId].code = payload.code
storage.save(data)
storage.saveToLocalStorage(data)
},
restoreSavedData(data) {
storage.load(data)
storage.loadDocumentFromLocalStorage(data)
},
clearBoundsRotation(data) {

View file

@ -1,67 +1,190 @@
import { Data, Page, PageState } from 'types'
import * as fa from 'browser-fs-access'
import { Data, Page, PageState, TLDocument } from 'types'
import { setToArray } from 'utils/utils'
import state from './state'
import { v4 as uuid } from 'uuid'
const CURRENT_VERSION = 'code_slate_0.0.4'
const CURRENT_VERSION = 'code_slate_0.0.5'
const DOCUMENT_ID = '0001'
function storageId(label: string, id: string) {
return `${CURRENT_VERSION}_doc_${DOCUMENT_ID}_${label}_${id}`
function storageId(label: string, fileId: string, id: string) {
return `${CURRENT_VERSION}_doc_${fileId}_${label}_${id}`
}
class Storage {
// Saving
load(data: Data, id = CURRENT_VERSION) {
load(data: Data, restoredData: any) {
// Empty shapes in state for each page
for (let key in restoredData.document.pages) {
restoredData.document.pages[key].shapes = {}
}
// Empty page states for each page
for (let key in restoredData.pageStates) {
restoredData.document.pages[key].shapes = {}
}
data.document = {} as TLDocument
data.pageStates = {}
// Merge restored data into state
Object.assign(data, restoredData)
// Minor migrtation: add id and name to document
data.document = {
id: 'document0',
name: 'My Document',
...restoredData.document,
}
// Load current page
this.loadPage(data, data.currentPageId)
}
async loadDocumentFromFilesystem() {
const blob = await fa.fileOpen({
description: 'tldraw files',
})
const text = await getTextFromBlob(blob)
const restoredData = JSON.parse(text)
if (restoredData === null) {
console.warn('Could not load that data.')
return
}
state.send('LOADED_FROM_FILE', { restoredData })
}
loadDocumentFromLocalStorage(data: Data, fileId = DOCUMENT_ID) {
if (typeof window === 'undefined') return
if (typeof localStorage === 'undefined') return
// Load data from local storage
const savedData = localStorage.getItem(id)
const savedData = localStorage.getItem(fileId)
if (savedData !== null) {
const restoredData = JSON.parse(savedData)
if (savedData === null) return false
// Empty shapes in state for each page
for (let key in restoredData.document.pages) {
restoredData.document.pages[key].shapes = {}
}
const restoredData = JSON.parse(savedData)
// Empty page states for each page
for (let key in restoredData.pageStates) {
restoredData.document.pages[key].shapes = {}
}
// Merge restored data into state
Object.assign(data, restoredData)
// Load current page
this.loadPage(data, data.currentPageId)
}
this.load(data, restoredData)
}
save = (data: Data, id = CURRENT_VERSION) => {
getDataToSave = (data: Data) => {
const dataToSave: any = { ...data }
for (let pageId in data.document) {
const savedPage = localStorage.getItem(
storageId(data.document.id, 'page', pageId)
)
if (savedPage !== null) {
const restored: Page = JSON.parse(savedPage)
dataToSave.document.pages[pageId] = restored
}
}
dataToSave.pageStates = {}
return JSON.stringify(dataToSave, null, 2)
}
saveToLocalStorage = (data: Data, id = data.document.id) => {
if (typeof window === 'undefined') return
if (typeof localStorage === 'undefined') return
const dataToSave: any = { ...data }
// Don't save pageStates
dataToSave.pageStates = {}
// Save current data to local storage
localStorage.setItem(id, JSON.stringify(dataToSave))
localStorage.setItem(id, this.getDataToSave(data))
// Save current page
this.savePage(data, data.currentPageId)
// Save current page too
this.savePage(data, id, data.currentPageId)
state.send('SAVED_FILE_TO_LOCAL_STORAGE')
}
savePage(data: Data, pageId: string) {
saveAsToFileSystem = (data: Data) => {
// Create a new document id when saving to the file system
this.saveToFileSystem(data, uuid())
}
saveToFileSystem = (data: Data, id = data.document.id) => {
// Save to local storage first
this.saveToLocalStorage(data, id)
const json = this.getDataToSave(data)
const blob = new Blob([json], {
type: 'application/vnd.tldraw+json',
})
fa.fileSave(blob, {
fileName: `${data.document.name}.tldr`,
description: 'tldraw file',
extensions: ['.tldr'],
})
.then(() => {
state.send('SAVED_FILE_TO_FILE_SYSTEM')
})
.catch((e) => {
state.send('CANCELLED_SAVE', { reason: e.message })
})
}
loadPageFromLocalStorage(fileId: string, pageId: string) {
let restored: Page
const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId))
if (savedPage !== null) {
restored = JSON.parse(savedPage)
} else {
restored = {
id: pageId,
type: 'page',
childIndex: 0,
name: 'Page',
shapes: {},
}
}
return restored
}
loadPageStateFromLocalStorage(fileId: string, pageId: string) {
let restored: PageState
const savedPageState = localStorage.getItem(
storageId(fileId, 'pageState', pageId)
)
if (savedPageState !== null) {
restored = JSON.parse(savedPageState)
restored.selectedIds = new Set(restored.selectedIds)
} else {
restored = {
camera: {
point: [0, 0],
zoom: 1,
},
selectedIds: new Set([]),
}
}
return restored
}
savePage(data: Data, fileId = data.document.id, pageId = data.currentPageId) {
if (typeof window === 'undefined') return
if (typeof localStorage === 'undefined') return
// Save page
const page = data.document.pages[pageId]
localStorage.setItem(storageId('page', pageId), JSON.stringify(page))
localStorage.setItem(
storageId(fileId, 'page', pageId),
JSON.stringify(page)
)
// Save page state
@ -80,39 +203,20 @@ class Storage {
}
localStorage.setItem(
storageId('pageState', pageId),
storageId(fileId, 'pageState', pageId),
JSON.stringify(pageState)
)
}
loadPage(data: Data, pageId: string) {
loadPage(data: Data, pageId = data.currentPageId) {
if (typeof window === 'undefined') return
if (typeof localStorage === 'undefined') return
// Load page and merge into state
const savedPage = localStorage.getItem(storageId('page', pageId))
const fileId = data.document.id
if (savedPage !== null) {
const restored: Page = JSON.parse(savedPage)
data.document.pages[pageId] = restored
}
data.document.pages[pageId] = this.loadPageFromLocalStorage(fileId, pageId)
// Load page state and merge into state
const savedPageState = localStorage.getItem(storageId('pageState', pageId))
if (savedPageState !== null) {
const restored: PageState = JSON.parse(savedPageState)
restored.selectedIds = new Set(restored.selectedIds)
data.pageStates[pageId] = restored
} else {
data.pageStates[pageId] = {
camera: {
point: [0, 0],
zoom: 1,
},
selectedIds: new Set([]),
}
}
data.pageStates[pageId] = this.loadPageStateFromLocalStorage(fileId, pageId)
// Empty shapes in state for other pages
for (let key in data.document.pages) {
@ -120,13 +224,7 @@ class Storage {
data.document.pages[key].shapes = {}
}
// Empty page states for other pages
for (let key in data.pageStates) {
if (key === pageId) continue
data.document.pages[key].shapes = {}
}
// Update camera
// Update camera for the new page state
document.documentElement.style.setProperty(
'--camera-zoom',
data.pageStates[data.currentPageId].camera.zoom.toString()
@ -137,3 +235,21 @@ class Storage {
const storage = new Storage()
export default storage
async function getTextFromBlob(blob: Blob): Promise<string> {
// Return blob as text if a text file.
if ('text' in Blob) return blob.text()
// Return blob as text if a text file.
return new Promise((resolve) => {
const reader = new FileReader()
reader.onloadend = () => {
if (reader.readyState === FileReader.DONE) {
resolve(reader.result as string)
}
}
reader.readAsText(blob, 'utf8')
})
}

View file

@ -28,10 +28,7 @@ export interface Data {
currentParentId: string
currentCodeFileId: string
codeControls: Record<string, CodeControl>
document: {
pages: Record<string, Page>
code: Record<string, CodeFile>
}
document: TLDocument
pageStates: Record<string, PageState>
}
@ -39,6 +36,13 @@ export interface Data {
/* Document */
/* -------------------------------------------------- */
export interface TLDocument {
id: string
name: string
pages: Record<string, Page>
code: Record<string, CodeFile>
}
export interface Page {
id: string
type: 'page'

View file

@ -1758,3 +1758,13 @@ export function getTopParentId(data: Data, id: string): string {
export function uniqueArray<T extends string | number | Symbol>(...items: T[]) {
return Array.from(new Set(items).values())
}
export function getPoint(
e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent
) {
return [
Number(e.clientX.toPrecision(4)),
Number(e.clientY.toPrecision(4)),
'pressure' in e ? e.pressure || 0.5 : 0.5,
]
}

View file

@ -339,13 +339,8 @@ export function clockwise(p1: number[], pc: number[], p2: number[]) {
return isLeft(p1, pc, p2) > 0
}
const rounds = [1, 10, 100, 1000]
export function round(a: number[], d = 2) {
return [
Math.round(a[0] * rounds[d]) / rounds[d],
Math.round(a[1] * rounds[d]) / rounds[d],
]
return a.map((v) => +v.toFixed(d))
}
/**

View file

@ -2567,6 +2567,11 @@ brorand@^1.0.1, brorand@^1.1.0:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
browser-fs-access@^0.17.3:
version "0.17.3"
resolved "https://registry.yarnpkg.com/browser-fs-access/-/browser-fs-access-0.17.3.tgz#f91447b0b74bb8d224a8b01a7d18bd090468ef1d"
integrity sha512-zrEWlsaQf3RKAeLjA6veRzTtf8ge3dFfun50RI74kSWKQKnJaioPc4I8oPzO6rDNiR0CJUI+oORuPOFMUkr0+g==
browser-process-hrtime@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"