Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Undo/redo support and local storage #24

Merged
merged 5 commits into from
Oct 14, 2018
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add undo/redo
davepagurek committed Oct 14, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
commit f1014511921ea1d42c4a267398c50acc532e54f4
8 changes: 5 additions & 3 deletions src/frontend/costFn.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { List } from 'immutable';
// tslint:disable-next-line:import-name
import Bezier = require('bezier-js');

import { onChange, setState, state } from './state';
import { commit, onChange, setState, state } from './state';
import { addModel } from './model';
import { renderer } from './renderer';

@@ -55,7 +55,7 @@ export const addCostFn = () => {
};

export const addCostFunctionViz = () => {
const { costFn, costFnParams, guidingCurves: oldGuidingCurves } = state;
const { costFn, costFnParams, selectedCurve } = state;
if (!costFn || !costFnParams) {
return;
}
@@ -64,7 +64,7 @@ export const addCostFunctionViz = () => {
const guidingCurves = List(costFn.generateGuidingCurve().map((path: [number, number, number][], index: number) => {
return {
path,
selected: oldGuidingCurves && oldGuidingCurves[index] && oldGuidingCurves[index].selected,
selected: index === selectedCurve,
bezier: costFnParams.get(index).bezier
};
}));
@@ -169,6 +169,7 @@ const updateCostFnParams = throttle(() => {
setState({ costFnParams });
addCostFn();
addModel();
commit();
}, 100, { trailing: true });

const deleteCurve = () => {
@@ -189,6 +190,7 @@ const deleteCurve = () => {
addCostFn();
addCostFunctionViz();
addModel();
commit();
}

costControls.addEventListener('input', updateCostFnParams);
5 changes: 4 additions & 1 deletion src/frontend/interactions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Camera, vec3ToVector } from 'calder-gl';
import { renderer } from './renderer';
import { setState, state } from './state'
import { commit, setState, state } from './state'
import { addCostFn, addCostFunctionViz, addNewCurve } from './costFn';
import { addModel } from './model';
import { mat4, quat, vec3, vec4 } from 'gl-matrix';
@@ -101,12 +101,14 @@ function handleMouseUp(event: MouseEvent) {
});

setState({ selectedCurve: selectedIndex < guidingCurves.size ? selectedIndex : null });
commit();
}

if (controlState.mode === ControlMode.DRAG_CURVE) {
addCostFn();
addCostFunctionViz();
addModel();
commit();
} else if (controlState.mode === ControlMode.DRAW_CURVE) {
const { pencilLine } = state;
if (pencilLine) {
@@ -120,6 +122,7 @@ function handleMouseUp(event: MouseEvent) {
if (guidingCurves) {
const selectedCurve = guidingCurves.size - 1;
setState({ selectedCurve, guidingCurves });
commit();
}
}
}
35 changes: 27 additions & 8 deletions src/frontend/model.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import * as calder from 'calder-gl';

import { setState, state } from './state';
import { merge, onUndoRedo, setState, state } from './state';

let generatorTask: calder.GeneratorTask | null = null;

export const addModel = () => {
if (state.generatorTask) {
if (generatorTask) {
// If we were in the middle of generating something else, cancel that task
state.generatorTask.cancel();
generatorTask.cancel();
}

if (state.generator && state.costFn) {
const generatorTask = state.generator.generateSOSMC(
setState({ generating: true });

generatorTask = state.generator.generateSOSMC(
{
start: 'START',
sosmcDepth: 100,
@@ -25,9 +29,24 @@ export const addModel = () => {
},
1 / 30
)
.then((model: calder.Model) =>
setState({ model, generatorTask: undefined }));

setState({ generatorTask });
.then((model: calder.Model) => {
generatorTask = null;
setState({ model, generating: false })
merge();
});
}
};

onUndoRedo(() => {
// We want to stop whatever we were generating before
if (generatorTask) {
generatorTask.cancel();
generatorTask = null;
}

// If the undo/redo state we jumped to was waiting on a generation
// task when it was committed, restart the task
if (state.generating) {
addModel();
}
});
56 changes: 55 additions & 1 deletion src/frontend/state.ts
Original file line number Diff line number Diff line change
@@ -10,13 +10,17 @@ export type State = {
vectorField?: Float32Array;
guidingCurves?: List<calder.GuidingCurveInfo>;
selectedCurve?: number | null;
generatorTask?: calder.GeneratorTask;
generating?: boolean;
pencilLine?: {x: number; y: number}[];
};

export const state: State = {};

const undoStack: State[] = [];
const redoStack: State[] = [];

const listeners: {[key in keyof State]: (() => void)[]} = {};
const undoRedoListeners: (() => void)[] = [];

export const setState = (newState: Partial<State>) => {
for (const key in newState) {
@@ -27,10 +31,60 @@ export const setState = (newState: Partial<State>) => {
}
};

export const commit = () => {
undoStack.push({ ...state });
redoStack.length = 0;
}

export const merge = () => {
if (undoStack.length === 0) {
undoStack.push({ ...state });
} else {
undoStack[undoStack.length - 1] = { ...state };
}
}

const undo = () => {
if (undoStack.length === 0) {
return;
}

redoStack.push(<State>undoStack.pop());
this.setState(undoStack[undoStack.length - 1]);
undoRedoListeners.forEach((callback) => callback());
};

const redo = () => {
if (redoStack.length === 0) {
return;
}

undoStack.push(<State>redoStack.pop());
this.setState(undoStack[undoStack.length - 1]);
undoRedoListeners.forEach((callback) => callback());
}

export const onUndoRedo = (callback: () => void) => {
undoRedoListeners.push(callback);
};

export const onChange = (key: keyof State, callback: () => void) => {
if (!listeners[key]) {
listeners[key] = [];
}

(<(() => void)[]>listeners[key]).push(callback);
};

window.addEventListener('keydown', (event: KeyboardEvent) => {
// cmd/ctrl-z for undo, cmd/ctrl-y or shift-cmd/ctrl-z for redo
if (event.code === 'KeyZ' && !event.shiftKey && (event.ctrlKey || event.metaKey)) {
undo();
event.stopPropagation();
event.preventDefault();
} else if ((event.code === 'KeyY' || (event.code === 'KeyZ' && event.shiftKey)) && (event.ctrlKey || event.metaKey)) {
redo();
event.stopPropagation();
event.preventDefault();
}
});