diff --git a/libs/drawing-engine/src/engine/CanvasHistory.ts b/libs/drawing-engine/src/engine/CanvasHistory.ts index 0f9264d..42e17f5 100644 --- a/libs/drawing-engine/src/engine/CanvasHistory.ts +++ b/libs/drawing-engine/src/engine/CanvasHistory.ts @@ -56,32 +56,22 @@ export class CanvasHistory { protected history: Array = [] protected redoStack: Array = [] protected hasTruncated = false - protected db: HistoryDatabase | null = null - constructor( + private constructor( protected readonly engine: DrawingEngine, protected options: HistoryOptions, - ) { - if (!options.maxHistory || options.maxHistory < 1) { - options.maxHistory = 10 - } - - if (!options.actionsPerHistory || options.actionsPerHistory < 1) { - options.actionsPerHistory = 10 - } - - HistoryDatabase.create().then(async (db) => { - this.db = db - await this.restoreHistoryFromDb() - }) + protected db: HistoryDatabase, + ) {} + + static async create(engine: DrawingEngine, options: Partial = {}) { + const db = await HistoryDatabase.create() + const history = new CanvasHistory(engine, { maxHistory: 10, actionsPerHistory: 10, ...options }, db) + await history.restoreHistoryFromDb() + return history } protected async restoreHistoryFromDb() { - const db = this.db - if (!db) { - throw new Error("Database not initialized") - } - const historyKeys = await db.history.getAllKeys() + const historyKeys = await this.db.history.getAllKeys() this.history = historyKeys.reverse() const state = await this.getCurrentEntry() if (state) { @@ -101,15 +91,14 @@ export class CanvasHistory { return this.options } + public add(toolInfo: ToolInfo) { + this.redoStack = [] + this.save(toolInfo) + } + private async save(toolInfo: ToolInfo) { - const canvas = this.engine.gl.canvas - if (!(canvas instanceof HTMLCanvasElement)) { - throw new Error("Canvas is not an HTMLCanvasElement") - } + const canvas = this.engine.htmlCanvas const tool = this.engine.tools[toolInfo.tool] - if (!tool) { - throw new Error(`Tool ${toolInfo.tool} not found`) - } const current = await this.getIncompleteEntry() current.entry.actions.push(toolInfo) @@ -126,9 +115,6 @@ export class CanvasHistory { ) } - if (!this.db) { - throw new Error("Database not initialized") - } await this.db.history.put(current.key, current.entry) return current } @@ -149,9 +135,6 @@ export class CanvasHistory { if (!key) { return null } - if (!this.db) { - throw new Error("Database not initialized") - } const entry = await this.db.history.get(key) return { key, entry } } @@ -161,9 +144,6 @@ export class CanvasHistory { imageData: null, actions: [], } - if (!this.db) { - throw new Error("Database not initialized") - } const key = await this.db.history.add(state) this.appendHistoryKey(key) return { key, entry: state } @@ -174,9 +154,6 @@ export class CanvasHistory { this.engine.callListeners(EventType.undo, { toolInfo: null, canUndo: false }) return } - if (!this.db) { - throw new Error("Database not initialized") - } const key = this.history[0] if (!key) { return @@ -204,16 +181,10 @@ export class CanvasHistory { } public async redo() { - if (!this.canRedo()) { - this.engine.callListeners(EventType.redo, { toolInfo: null, canRedo: false }) - return - } - if (!this.db) { - throw new Error("Database not initialized") - } const toolInfo = this.redoStack.pop() if (!toolInfo) { - throw new Error("Could not get tool info from redo stack") + this.engine.callListeners(EventType.redo, { toolInfo: null, canRedo: false }) + return } const current = await this.save(toolInfo) await this.drawHistoryEntry(current.entry) @@ -221,10 +192,6 @@ export class CanvasHistory { } protected async appendHistoryKey(key: IDBValidKey) { - if (!this.db) { - throw new Error("Database not initialized") - } - const db = this.db this.history.push(key) if (this.history.length >= this.options.maxHistory) { @@ -257,7 +224,7 @@ export class CanvasHistory { if (!tool) { throw new Error(`Tool ${action.tool} not found`) } - await tool.drawFromHistory(action.path, action.options) + tool.drawFromHistory(action.path, action.options) } } diff --git a/libs/drawing-engine/src/engine/Database.ts b/libs/drawing-engine/src/engine/Database.ts index 475f67e..0a9ff16 100644 --- a/libs/drawing-engine/src/engine/Database.ts +++ b/libs/drawing-engine/src/engine/Database.ts @@ -1,6 +1,57 @@ export class Database> { protected constructor(protected db: IDBDatabase) {} + static async create>( + dbname: string, + createSchemaCallback: (db: IDBDatabase) => void, + version?: number, + ) { + const db = await this.createDb(dbname, createSchemaCallback, version) + return new Database(db) + } + + protected static createDb( + dbname: string, + createSchemaCallback: (db: IDBDatabase, resolve: () => void, reject: (error: Error) => void) => void, + version?: number, + ) { + return new Promise((resolve, reject) => { + const idbRequest = indexedDB.open(dbname, version) + + idbRequest.onupgradeneeded = () => { + const db = idbRequest.result + createSchemaCallback(db, () => resolve(db), reject) + } + + idbRequest.onblocked = () => { + reject(new Error("Database is blocked")) + } + + idbRequest.onsuccess = () => { + const db = idbRequest.result + if (!version || db.version === version) { + resolve(db) + } + + db.onversionchange = () => { + console.debug("Database version changed") + } + + db.onclose = () => { + console.debug("Database closed") + } + + db.onabort = () => { + reject(new Error("Database request was aborted")) + } + } + + idbRequest.onerror = () => { + reject(new Error("Could not open database")) + } + }) + } + public getStore(storeName: StoreName) { return { add: (state: Schema[StoreName]) => this.add(storeName, state), @@ -16,122 +67,36 @@ export class Database(storeName: StoreName) { - const transaction = this.db.transaction(storeName, "readwrite") - const objectStore = transaction.objectStore(storeName) - return objectStore + public get(storeName: StoreName, key: IDBValidKey) { + return this.promisifyRequest(this.getObjectStore(storeName, "readonly").get(key)) } - public add(storeName: StoreName, state: Schema[StoreName]) { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(storeName, "readwrite") - const objectStore = transaction.objectStore(storeName) - const request = objectStore.add(state) - request.addEventListener("success", () => { - resolve(request.result) - }) - request.addEventListener("error", () => { - reject(request.error) - }) - }) + public getAll(storeName: StoreName) { + return this.promisifyRequest(this.getObjectStore(storeName, "readonly").getAll()) } - public put(storeName: StoreName, key: IDBValidKey, state: Schema[StoreName]) { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(storeName, "readwrite") - const objectStore = transaction.objectStore(storeName) - const request = objectStore.put(state, key) - request.addEventListener("success", () => { - resolve(request.result) - }) - request.addEventListener("error", () => { - reject(request.error) - }) - }) + public getAllKeys(storeName: StoreName) { + return this.promisifyRequest(this.getObjectStore(storeName, "readonly").getAllKeys()) } public count(storeName: StoreName) { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(storeName, "readonly") - const objectStore = transaction.objectStore(storeName) - const request = objectStore.count() - request.addEventListener("success", () => { - resolve(request.result) - }) - request.addEventListener("error", () => { - reject(request.error) - }) - }) + return this.promisifyRequest(this.getObjectStore(storeName, "readonly").count()) } - public get(storeName: StoreName, key: IDBValidKey) { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(storeName, "readwrite") - const objectStore = transaction.objectStore(storeName) - const request = objectStore.get(key) - request.addEventListener("success", () => { - resolve(request.result) - }) - request.addEventListener("error", () => { - reject(request.error) - }) - }) + public add(storeName: StoreName, state: Schema[StoreName]) { + return this.promisifyRequest(this.getObjectStore(storeName, "readwrite").add(state)) } - public getAll(storeName: StoreName) { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(storeName, "readwrite") - const objectStore = transaction.objectStore(storeName) - const request = objectStore.getAll() - request.addEventListener("success", () => { - resolve(request.result) - }) - request.addEventListener("error", () => { - reject(request.error) - }) - }) + public put(storeName: StoreName, key: IDBValidKey, state: Schema[StoreName]) { + return this.promisifyRequest(this.getObjectStore(storeName, "readwrite").put(state, key)) } public delete(storeName: StoreName, key: IDBValidKey) { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(storeName, "readwrite") - const objectStore = transaction.objectStore(storeName) - const request = objectStore.delete(key) - request.addEventListener("success", () => { - resolve(request.result) - }) - request.addEventListener("error", () => { - reject(request.error) - }) - }) + return this.promisifyRequest(this.getObjectStore(storeName, "readwrite").delete(key)) } public clear(storeName: StoreName) { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(storeName, "readwrite") - const objectStore = transaction.objectStore(storeName) - const request = objectStore.clear() - request.addEventListener("success", () => { - resolve(request.result) - }) - request.addEventListener("error", () => { - reject(request.error) - }) - }) - } - - public getAllKeys(storeName: StoreName) { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(storeName, "readwrite") - const objectStore = transaction.objectStore(storeName) - const request = objectStore.getAllKeys() - request.addEventListener("success", () => { - resolve(request.result) - }) - request.addEventListener("error", () => { - reject(request.error) - }) - }) + return this.promisifyRequest(this.getObjectStore(storeName, "readwrite").clear()) } public async getLastKey(storeName: StoreName) { @@ -144,55 +109,19 @@ export class Database>( - dbname: string, - createSchemaCallback: (db: IDBDatabase) => void, - version?: number, - ) { - const db = await this.createDb(dbname, createSchemaCallback, version) - return new Database(db) + private getObjectStore(storeName: StoreName, access: IDBTransactionMode) { + const transaction = this.db.transaction(storeName, access) + const objectStore = transaction.objectStore(storeName) + return objectStore } - protected static createDb( - dbname: string, - createSchemaCallback: (db: IDBDatabase, resolve: () => void, reject: (error: Error) => void) => void, - version?: number, - ) { - console.log("Creating db", dbname, version) - return new Promise((resolve, reject) => { - const idbRequest = indexedDB.open(dbname, version) - - idbRequest.onupgradeneeded = () => { - const db = idbRequest.result - createSchemaCallback(db, () => resolve(db), reject) - } - - idbRequest.onblocked = () => { - reject(new Error("Database is blocked")) - } - - idbRequest.addEventListener("success", () => { - const db = idbRequest.result - if (!version || db.version === version) { - resolve(db) - } - - db.onversionchange = () => { - console.log("Database version changed") - } - - db.onclose = () => { - console.log("Database closed") - } - - db.onabort = () => { - reject(new Error("Database request was aborted")) - } + private promisifyRequest(request: IDBRequest) { + return new Promise((resolve, reject) => { + request.addEventListener("success", () => { + resolve(request.result) }) - - idbRequest.addEventListener("error", () => { - console.log("error") - reject(new Error("Could not open database")) + request.addEventListener("error", () => { + reject(request.error) }) }) } diff --git a/libs/drawing-engine/src/engine/DrawingEngine.ts b/libs/drawing-engine/src/engine/DrawingEngine.ts index 95207a1..7cb2a02 100644 --- a/libs/drawing-engine/src/engine/DrawingEngine.ts +++ b/libs/drawing-engine/src/engine/DrawingEngine.ts @@ -30,6 +30,7 @@ export interface DrawingEngineOptions { type ToolInfo = LineDrawInfo export enum EventType { + engineLoaded = "engineLoaded", draw = "draw", undo = "undo", redo = "redo", @@ -55,6 +56,7 @@ export interface DrawingEngineEventMap { [EventType.move]: { positions: ReadonlyArray; isPressed: boolean } [EventType.release]: { position: Readonly } [EventType.cancel]: undefined + [EventType.engineLoaded]: undefined } export type DrawingEngineEvent = { eventName: T @@ -89,7 +91,7 @@ export class DrawingEngine { } private listeners: Partial = {} - protected history: CanvasHistory + protected history: CanvasHistory | null = null constructor( public gl: WebGLRenderingContext, @@ -105,9 +107,12 @@ export class DrawingEngine { prevTool: defaultTool, } - this.history = new CanvasHistory(this, { + CanvasHistory.create(this, { maxHistory: 10, actionsPerHistory: 10, + }).then((history) => { + this.history = history + this.checkLoaded() }) this.savedDrawingLayer = this.makeLayer() @@ -125,6 +130,23 @@ export class DrawingEngine { this.callListeners(EventType.changeTool, { tool: this.state.tool }) } + get htmlCanvas(): HTMLCanvasElement { + if (!(this.gl.canvas instanceof HTMLCanvasElement)) { + throw new Error("Canvas is not an HTMLCanvasElement") + } + return this.gl.canvas + } + + public isLoaded() { + return this.history !== null + } + + private checkLoaded() { + if (this.isLoaded()) { + this.callListeners(EventType.engineLoaded, undefined) + } + } + private makeLayer(options?: Partial) { return new Layer(this.gl, options) } @@ -295,14 +317,14 @@ export class DrawingEngine { } public addHistory(toolInfo: ToolInfo) { - this.history.save(toolInfo) + this.history?.add(toolInfo) } public undo() { - return this.history.undo() + return this.history?.undo() } public redo() { - return this.history.redo() + return this.history?.redo() } }