From 78c6cf08600f60c8c1a73e60ba146abad68a0c88 Mon Sep 17 00:00:00 2001 From: Alexander Litty Date: Wed, 16 Jun 2021 23:42:06 -0700 Subject: [PATCH 1/5] Added basic event dispatching utility. --- public/eventable.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 public/eventable.js diff --git a/public/eventable.js b/public/eventable.js new file mode 100644 index 0000000..2c53d59 --- /dev/null +++ b/public/eventable.js @@ -0,0 +1,26 @@ +export class Eventable +{ + constructor() + { + this._events = { }; + } + + on(eventName, handler) + { + if (!this._events[eventName]) + { + this._events[eventName] = [ ]; + } + + this._events[eventName].push(handler); + } + + emit(eventName, eventData) + { + let handlers = this._events[eventName] || [ ]; + for (let i = 0; i < handlers.length; i++) + { + handlers[i](eventData); + } + } +} From a526a73f2a9ef82c1bdcab23e78d4d715aaa0a60 Mon Sep 17 00:00:00 2001 From: Alexander Litty Date: Wed, 16 Jun 2021 23:44:46 -0700 Subject: [PATCH 2/5] Added serialize & deserialize methods to Model. --- public/model.js | 34 +++++++++++++++++++++++++++++++++- public/utils.js | 16 ++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/public/model.js b/public/model.js index 0494a5c..723b78b 100644 --- a/public/model.js +++ b/public/model.js @@ -59,7 +59,7 @@ undo, however. It could be more of a direct state update, or it's a special undoable action. */ -import { assert, treeCopy, treeEq } from './utils.js'; +import { assert, treeCopy, treeEq, isString, isObject } from './utils.js'; /** * High-level description/scheme for each type of node @@ -693,6 +693,38 @@ export class Model this.broadcast(this.state, null); } + // Serializes the model into a string representation + serialize() + { + return JSON.stringify({ + state: this.state + }); + } + + // Tries to deserialize a string representation of a model + // + // Returns true if successfully deserialized and loaded, false otherwise + deserialize(data) + { + if (!isString(data)) { + return false; + } + + let json; + try { + json = JSON.parse(data); + } catch (e) { + return false; + } + + if (!isObject(json) || !isObject(json.state)) { + return false; + } + + this.load(json.state); + return true; + } + /** Check if the graph contains a specific type of node */ hasNode(nodeType) { diff --git a/public/utils.js b/public/utils.js index 9901f6d..cee53d2 100644 --- a/public/utils.js +++ b/public/utils.js @@ -168,6 +168,22 @@ export function treeEq(a, b) return a === b; } +/** +Test that a value is an object +*/ +export function isObject(val) +{ + return (typeof val === 'object') && (val !== null); +} + +/** +Test that a value is a string +*/ +export function isString(val) +{ + return (typeof val === 'string') || (val instanceof String); +} + /** Test that a value is integer */ From 2f87af70e878ca9b116da6cb13d7f520f0c84b87 Mon Sep 17 00:00:00 2001 From: Alexander Litty Date: Wed, 16 Jun 2021 23:46:07 -0700 Subject: [PATCH 3/5] Preparing to load Model through a 'loading' event. --- public/main.js | 9 +++++++++ public/model.js | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/public/main.js b/public/main.js index 080e166..007dd6c 100644 --- a/public/main.js +++ b/public/main.js @@ -13,6 +13,15 @@ let playing = false; // Project model/state let model = new Model(); +model.on('loading', () => { + // Stop playback to avoid glitching + stopPlayback(); + + // Show the Edit tab before loading the graph, + // so it can resize itself correctly + /* showTab('edit'); */ +}); + // Graph editor view let editor = new Editor(model); diff --git a/public/model.js b/public/model.js index 723b78b..e871df9 100644 --- a/public/model.js +++ b/public/model.js @@ -60,6 +60,7 @@ or it's a special undoable action. */ import { assert, treeCopy, treeEq, isString, isObject } from './utils.js'; +import { Eventable } from './eventable.js'; /** * High-level description/scheme for each type of node @@ -638,10 +639,12 @@ export class Disconnect extends Action } /** Graph of nodes model, operates on internal state data */ -export class Model +export class Model extends Eventable { constructor() { + super(); + // List of views subscribed to model updates this.views = []; @@ -673,6 +676,8 @@ export class Model // Load the JSON state into the model load(state) { + this.emit('loading'); + // Current playback position this.playPos = 0; From 22ddf46c198ab6b58d467af0ea33448fef41480d Mon Sep 17 00:00:00 2001 From: Alexander Litty Date: Wed, 16 Jun 2021 23:47:41 -0700 Subject: [PATCH 4/5] Implemented autosaving / autoloading. --- public/main.js | 54 ++++++++++---------------------------------------- 1 file changed, 10 insertions(+), 44 deletions(-) diff --git a/public/main.js b/public/main.js index 007dd6c..19a4e9e 100644 --- a/public/main.js +++ b/public/main.js @@ -31,61 +31,27 @@ let audioView = new AudioView(model); // Create a new project model.new(); -/* -export function importData(jsonData) -{ - //console.log('json data:', jsonData); - - // Stop playback to avoid glitching - stopPlayback(); - - // Show the Edit tab before loading the graph, - // so it can resize itself correctly - showTab('edit'); - - let graph = JSON.parse(jsonData); - editor.load(graph); -} - -export function exportData() -{ - return JSON.stringify(editor.graph); -} - document.body.onload = function () { - let graphData = localStorage.getItem('graph'); + if (window.location.hash) + return; - if (graphData && !window.location.hash) - { - console.log('loading saved graph'); + let modelData = localStorage.getItem('model'); + if (!modelData) + return; - try - { - importData(graphData); - } - catch (exc) - { - //alert('Graph failed to load'); - console.log(exc); - localStorage.removeItem('graph'); - editor = new GraphEditor(); - editor.newGraph(); - //location.reload(false); - } - } - else - { - editor.newGraph(); + if (model.deserialize(modelData)) { + console.log('model restored from previous session'); + } else { + console.warn('could not restore model from previous session'); } } window.onunload = function () { // Save the graph when unloading the page - localStorage.setItem('graph', exportData()); + localStorage.setItem('model', model.serialize()); } -*/ window.onkeydown = function (event) { From ef93697c9f15c8e1cfaea858a7042475523fb00a Mon Sep 17 00:00:00 2001 From: Alexander Litty Date: Thu, 17 Jun 2021 18:30:41 -0700 Subject: [PATCH 5/5] Moving away from callback-based model loading. --- public/eventable.js | 26 -------------------------- public/main.js | 33 ++++++++++++++++----------------- public/model.js | 7 +------ 3 files changed, 17 insertions(+), 49 deletions(-) delete mode 100644 public/eventable.js diff --git a/public/eventable.js b/public/eventable.js deleted file mode 100644 index 2c53d59..0000000 --- a/public/eventable.js +++ /dev/null @@ -1,26 +0,0 @@ -export class Eventable -{ - constructor() - { - this._events = { }; - } - - on(eventName, handler) - { - if (!this._events[eventName]) - { - this._events[eventName] = [ ]; - } - - this._events[eventName].push(handler); - } - - emit(eventName, eventData) - { - let handlers = this._events[eventName] || [ ]; - for (let i = 0; i < handlers.length; i++) - { - handlers[i](eventData); - } - } -} diff --git a/public/main.js b/public/main.js index 19a4e9e..3b9f5df 100644 --- a/public/main.js +++ b/public/main.js @@ -13,15 +13,6 @@ let playing = false; // Project model/state let model = new Model(); -model.on('loading', () => { - // Stop playback to avoid glitching - stopPlayback(); - - // Show the Edit tab before loading the graph, - // so it can resize itself correctly - /* showTab('edit'); */ -}); - // Graph editor view let editor = new Editor(model); @@ -36,21 +27,17 @@ document.body.onload = function () if (window.location.hash) return; - let modelData = localStorage.getItem('model'); - if (!modelData) + let serializedModelData = localStorage.getItem('latestModelData'); + if (!serializedModelData) return; - if (model.deserialize(modelData)) { - console.log('model restored from previous session'); - } else { - console.warn('could not restore model from previous session'); - } + importModel(serializedModelData); } window.onunload = function () { // Save the graph when unloading the page - localStorage.setItem('model', model.serialize()); + localStorage.setItem('latestModelData', model.serialize()); } window.onkeydown = function (event) @@ -107,6 +94,18 @@ window.onkeydown = function (event) } } +export function importModel(serializedModelData) +{ + // Stop playback to avoid glitching + stopPlayback(); + + if (model.deserialize(serializedModelData)) { + console.log('model restored from previous session'); + } else { + console.warn('could not restore model from previous session'); + } +} + export function startPlayback() { if (playing) diff --git a/public/model.js b/public/model.js index e871df9..723b78b 100644 --- a/public/model.js +++ b/public/model.js @@ -60,7 +60,6 @@ or it's a special undoable action. */ import { assert, treeCopy, treeEq, isString, isObject } from './utils.js'; -import { Eventable } from './eventable.js'; /** * High-level description/scheme for each type of node @@ -639,12 +638,10 @@ export class Disconnect extends Action } /** Graph of nodes model, operates on internal state data */ -export class Model extends Eventable +export class Model { constructor() { - super(); - // List of views subscribed to model updates this.views = []; @@ -676,8 +673,6 @@ export class Model extends Eventable // Load the JSON state into the model load(state) { - this.emit('loading'); - // Current playback position this.playPos = 0;