Merge pull request #48 from fsih/addUndo

Add undo/redo
This commit is contained in:
DD Liu 2017-10-12 11:25:31 -04:00 committed by GitHub
commit f7a5111950
25 changed files with 578 additions and 228 deletions

View file

@ -58,11 +58,13 @@ class PaintEditorComponent extends React.Component {
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<button <button
className={styles.button} className={styles.button}
onClick={this.props.onUndo}
> >
Undo Undo
</button> </button>
<button <button
className={styles.button} className={styles.button}
onClick={this.props.onRedo}
> >
Redo Redo
</button> </button>
@ -101,11 +103,17 @@ class PaintEditorComponent extends React.Component {
{/* Second Row */} {/* Second Row */}
<div className={styles.row}> <div className={styles.row}>
{/* fill */} {/* fill */}
<FillColorIndicatorComponent /> <FillColorIndicatorComponent
onUpdateSvg={this.props.onUpdateSvg}
/>
{/* stroke */} {/* stroke */}
<StrokeColorIndicatorComponent /> <StrokeColorIndicatorComponent
onUpdateSvg={this.props.onUpdateSvg}
/>
{/* stroke width */} {/* stroke width */}
<StrokeWidthIndicatorComponent /> <StrokeWidthIndicatorComponent
onUpdateSvg={this.props.onUpdateSvg}
/>
<div className={styles.inputGroup}> <div className={styles.inputGroup}>
Mode tools Mode tools
@ -154,6 +162,8 @@ class PaintEditorComponent extends React.Component {
PaintEditorComponent.propTypes = { PaintEditorComponent.propTypes = {
intl: intlShape, intl: intlShape,
onRedo: PropTypes.func.isRequired,
onUndo: PropTypes.func.isRequired,
onUpdateSvg: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired,
rotationCenterX: PropTypes.number, rotationCenterX: PropTypes.number,
rotationCenterY: PropTypes.number, rotationCenterY: PropTypes.number,

View file

@ -4,6 +4,7 @@ import BroadBrushHelper from './broad-brush-helper';
import SegmentBrushHelper from './segment-brush-helper'; import SegmentBrushHelper from './segment-brush-helper';
import {MIXED, styleCursorPreview} from '../../helper/style-path'; import {MIXED, styleCursorPreview} from '../../helper/style-path';
import {clearSelection} from '../../helper/selection'; import {clearSelection} from '../../helper/selection';
import {getGuideLayer} from '../../helper/layer';
/** /**
* Shared code for the brush and eraser mode. Adds functions on the paper tool object * Shared code for the brush and eraser mode. Adds functions on the paper tool object
@ -26,13 +27,13 @@ class Blobbiness {
} }
/** /**
* @param {function} updateCallback call when the drawing has changed to let listeners know * @param {function} onUpdateSvg call when the drawing has changed to let listeners know
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state * @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
*/ */
constructor (updateCallback, clearSelectedItems) { constructor (onUpdateSvg, clearSelectedItems) {
this.broadBrushHelper = new BroadBrushHelper(); this.broadBrushHelper = new BroadBrushHelper();
this.segmentBrushHelper = new SegmentBrushHelper(); this.segmentBrushHelper = new SegmentBrushHelper();
this.updateCallback = updateCallback; this.onUpdateSvg = onUpdateSvg;
this.clearSelectedItems = clearSelectedItems; this.clearSelectedItems = clearSelectedItems;
// The following are stored to check whether these have changed and the cursor preview needs to be redrawn. // The following are stored to check whether these have changed and the cursor preview needs to be redrawn.
@ -143,7 +144,7 @@ class Blobbiness {
} }
blob.cursorPreview.visible = false; blob.cursorPreview.visible = false;
blob.updateCallback(); blob.onUpdateSvg();
blob.cursorPreview.visible = true; blob.cursorPreview.visible = true;
blob.cursorPreview.bringToFront(); blob.cursorPreview.bringToFront();
blob.cursorPreview.position = event.point; blob.cursorPreview.position = event.point;
@ -166,7 +167,7 @@ class Blobbiness {
this.cursorPreviewLastPoint = point; this.cursorPreviewLastPoint = point;
} }
if (this.cursorPreview && if (this.cursorPreview && this.cursorPreview.parent &&
this.brushSize === this.options.brushSize && this.brushSize === this.options.brushSize &&
this.fillColor === this.options.fillColor && this.fillColor === this.options.fillColor &&
this.strokeColor === this.options.strokeColor) { this.strokeColor === this.options.strokeColor) {
@ -176,6 +177,8 @@ class Blobbiness {
center: point, center: point,
radius: this.options.brushSize / 2 radius: this.options.brushSize / 2
}); });
newPreview.parent = getGuideLayer();
newPreview.data.isHelperItem = true;
if (this.cursorPreview) { if (this.cursorPreview) {
this.cursorPreview.remove(); this.cursorPreview.remove();
} }
@ -234,8 +237,6 @@ class Blobbiness {
paths.splice(i, 1); paths.splice(i, 1);
} }
} }
// TODO: Add back undo
// pg.undo.snapshot('broadbrush');
} }
mergeEraser (lastPath) { mergeEraser (lastPath) {
@ -284,8 +285,6 @@ class Blobbiness {
} }
} }
lastPath.remove(); lastPath.remove();
// TODO add back undo
// pg.undo.snapshot('eraser');
continue; continue;
} }
// Erase // Erase
@ -358,8 +357,6 @@ class Blobbiness {
items[i].remove(); items[i].remove();
} }
lastPath.remove(); lastPath.remove();
// TODO: Add back undo handling
// pg.undo.snapshot('eraser');
} }
colorMatch (existingPath, addedPath) { colorMatch (existingPath, addedPath) {

View file

@ -4,10 +4,12 @@ import {connect} from 'react-redux';
import bindAll from 'lodash.bindall'; import bindAll from 'lodash.bindall';
import Modes from '../modes/modes'; import Modes from '../modes/modes';
import Blobbiness from './blob/blob'; import Blobbiness from './blob/blob';
import {changeBrushSize} from '../reducers/brush-mode'; import {changeBrushSize} from '../reducers/brush-mode';
import {changeMode} from '../reducers/modes'; import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items'; import {clearSelectedItems} from '../reducers/selected-items';
import {clearSelection} from '../helper/selection'; import {clearSelection} from '../helper/selection';
import BrushModeComponent from '../components/brush-mode.jsx'; import BrushModeComponent from '../components/brush-mode.jsx';
class BrushMode extends React.Component { class BrushMode extends React.Component {
@ -18,7 +20,8 @@ class BrushMode extends React.Component {
'deactivateTool', 'deactivateTool',
'onScroll' 'onScroll'
]); ]);
this.blob = new Blobbiness(this.props.onUpdateSvg, this.props.clearSelectedItems); this.blob = new Blobbiness(
this.props.onUpdateSvg, this.props.clearSelectedItems);
} }
componentDidMount () { componentDidMount () {
if (this.props.isBrushModeActive) { if (this.props.isBrushModeActive) {

View file

@ -17,7 +17,8 @@ class EraserMode extends React.Component {
'deactivateTool', 'deactivateTool',
'onScroll' 'onScroll'
]); ]);
this.blob = new Blobbiness(this.props.onUpdateSvg, this.props.clearSelectedItems); this.blob = new Blobbiness(
this.props.onUpdateSvg, this.props.clearSelectedItems);
} }
componentDidMount () { componentDidMount () {
if (this.props.isEraserModeActive) { if (this.props.isEraserModeActive) {

View file

@ -1,19 +1,48 @@
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import {changeFillColor} from '../reducers/fill-color'; import {changeFillColor} from '../reducers/fill-color';
import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
import {applyFillColorToSelection} from '../helper/style-path'; import {applyFillColorToSelection} from '../helper/style-path';
class FillColorIndicator extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleChangeFillColor'
]);
}
handleChangeFillColor (newColor) {
applyFillColorToSelection(newColor, this.props.onUpdateSvg);
this.props.onChangeFillColor(newColor);
}
render () {
return (
<FillColorIndicatorComponent
fillColor={this.props.fillColor}
onChangeFillColor={this.handleChangeFillColor}
/>
);
}
}
const mapStateToProps = state => ({ const mapStateToProps = state => ({
fillColor: state.scratchPaint.color.fillColor fillColor: state.scratchPaint.color.fillColor
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onChangeFillColor: fillColor => { onChangeFillColor: fillColor => {
applyFillColorToSelection(fillColor);
dispatch(changeFillColor(fillColor)); dispatch(changeFillColor(fillColor));
} }
}); });
FillColorIndicator.propTypes = {
fillColor: PropTypes.string,
onChangeFillColor: PropTypes.func.isRequired,
onUpdateSvg: PropTypes.func.isRequired
};
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(FillColorIndicatorComponent); )(FillColorIndicator);

View file

@ -1,15 +1,16 @@
import paper from 'paper';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import bindAll from 'lodash.bindall'; import bindAll from 'lodash.bindall';
import Modes from '../modes/modes'; import Modes from '../modes/modes';
import {changeStrokeWidth} from '../reducers/stroke-width';
import {clearSelection, getSelectedLeafItems} from '../helper/selection'; import {clearSelection, getSelectedLeafItems} from '../helper/selection';
import {MIXED} from '../helper/style-path'; import {MIXED} from '../helper/style-path';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import LineModeComponent from '../components/line-mode.jsx';
import {changeMode} from '../reducers/modes'; import {changeMode} from '../reducers/modes';
import paper from 'paper'; import {changeStrokeWidth} from '../reducers/stroke-width';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import LineModeComponent from '../components/line-mode.jsx';
class LineMode extends React.Component { class LineMode extends React.Component {
static get SNAP_TOLERANCE () { static get SNAP_TOLERANCE () {
@ -206,13 +207,11 @@ class LineMode extends React.Component {
} }
this.hitResult = null; this.hitResult = null;
} }
this.props.onUpdateSvg();
this.props.setSelectedItems();
// TODO add back undo this.props.setSelectedItems();
// if (this.path) { if (this.path) {
// pg.undo.snapshot('line'); this.props.onUpdateSvg();
// } }
} }
toleranceSquared () { toleranceSquared () {
return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2); return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2);

View file

@ -1,8 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import PaintEditorComponent from '../components/paint-editor.jsx'; import PaintEditorComponent from '../components/paint-editor.jsx';
import {changeMode} from '../reducers/modes'; import {changeMode} from '../reducers/modes';
import {undo, redo, undoSnapshot} from '../reducers/undo';
import {getGuideLayer} from '../helper/layer'; import {getGuideLayer} from '../helper/layer';
import {performUndo, performRedo, performSnapshot} from '../helper/undo';
import Modes from '../modes/modes'; import Modes from '../modes/modes';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import bindAll from 'lodash.bindall'; import bindAll from 'lodash.bindall';
@ -12,7 +17,9 @@ class PaintEditor extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'handleUpdateSvg' 'handleUpdateSvg',
'handleUndo',
'handleRedo'
]); ]);
} }
componentDidMount () { componentDidMount () {
@ -21,7 +28,7 @@ class PaintEditor extends React.Component {
componentWillUnmount () { componentWillUnmount () {
document.removeEventListener('keydown', this.props.onKeyPress); document.removeEventListener('keydown', this.props.onKeyPress);
} }
handleUpdateSvg () { handleUpdateSvg (skipSnapshot) {
// Hide bounding box // Hide bounding box
getGuideLayer().visible = false; getGuideLayer().visible = false;
const bounds = paper.project.activeLayer.bounds; const bounds = paper.project.activeLayer.bounds;
@ -32,14 +39,25 @@ class PaintEditor extends React.Component {
}), }),
paper.project.view.center.x - bounds.x, paper.project.view.center.x - bounds.x,
paper.project.view.center.y - bounds.y); paper.project.view.center.y - bounds.y);
if (!skipSnapshot) {
performSnapshot(this.props.undoSnapshot);
}
getGuideLayer().visible = true; getGuideLayer().visible = true;
} }
handleUndo () {
performUndo(this.props.undoState, this.props.onUndo, this.handleUpdateSvg);
}
handleRedo () {
performRedo(this.props.undoState, this.props.onRedo, this.handleUpdateSvg);
}
render () { render () {
return ( return (
<PaintEditorComponent <PaintEditorComponent
rotationCenterX={this.props.rotationCenterX} rotationCenterX={this.props.rotationCenterX}
rotationCenterY={this.props.rotationCenterY} rotationCenterY={this.props.rotationCenterY}
svg={this.props.svg} svg={this.props.svg}
onRedo={this.handleRedo}
onUndo={this.handleUndo}
onUpdateSvg={this.handleUpdateSvg} onUpdateSvg={this.handleUpdateSvg}
/> />
); );
@ -48,12 +66,22 @@ class PaintEditor extends React.Component {
PaintEditor.propTypes = { PaintEditor.propTypes = {
onKeyPress: PropTypes.func.isRequired, onKeyPress: PropTypes.func.isRequired,
onRedo: PropTypes.func.isRequired,
onUndo: PropTypes.func.isRequired,
onUpdateSvg: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired,
rotationCenterX: PropTypes.number, rotationCenterX: PropTypes.number,
rotationCenterY: PropTypes.number, rotationCenterY: PropTypes.number,
svg: PropTypes.string svg: PropTypes.string,
undoSnapshot: PropTypes.func.isRequired,
undoState: PropTypes.shape({
stack: PropTypes.arrayOf(PropTypes.object).isRequired,
pointer: PropTypes.number.isRequired
})
}; };
const mapStateToProps = state => ({
undoState: state.scratchPaint.undo
});
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onKeyPress: event => { onKeyPress: event => {
if (event.key === 'e') { if (event.key === 'e') {
@ -65,10 +93,19 @@ const mapDispatchToProps = dispatch => ({
} else if (event.key === 's') { } else if (event.key === 's') {
dispatch(changeMode(Modes.SELECT)); dispatch(changeMode(Modes.SELECT));
} }
},
onUndo: () => {
dispatch(undo());
},
onRedo: () => {
dispatch(redo());
},
undoSnapshot: snapshot => {
dispatch(undoSnapshot(snapshot));
} }
}); });
export default connect( export default connect(
null, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(PaintEditor); )(PaintEditor);

View file

@ -1,8 +1,12 @@
import bindAll from 'lodash.bindall'; import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import {connect} from 'react-redux';
import paper from 'paper'; import paper from 'paper';
import {performSnapshot} from '../helper/undo';
import {undoSnapshot} from '../reducers/undo';
import styles from './paper-canvas.css'; import styles from './paper-canvas.css';
class PaperCanvas extends React.Component { class PaperCanvas extends React.Component {
@ -20,6 +24,7 @@ class PaperCanvas extends React.Component {
if (this.props.svg) { if (this.props.svg) {
this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY); this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY);
} }
performSnapshot(this.props.undoSnapshot);
} }
componentWillReceiveProps (newProps) { componentWillReceiveProps (newProps) {
paper.project.activeLayer.removeChildren(); paper.project.activeLayer.removeChildren();
@ -85,7 +90,16 @@ PaperCanvas.propTypes = {
canvasRef: PropTypes.func, canvasRef: PropTypes.func,
rotationCenterX: PropTypes.number, rotationCenterX: PropTypes.number,
rotationCenterY: PropTypes.number, rotationCenterY: PropTypes.number,
svg: PropTypes.string svg: PropTypes.string,
undoSnapshot: PropTypes.func.isRequired
}; };
const mapDispatchToProps = dispatch => ({
undoSnapshot: snapshot => {
dispatch(undoSnapshot(snapshot));
}
});
export default PaperCanvas; export default connect(
null,
mapDispatchToProps
)(PaperCanvas);

View file

@ -45,7 +45,8 @@ class ReshapeMode extends React.Component {
this.props.clearHoveredItem, this.props.clearHoveredItem,
this.props.setSelectedItems, this.props.setSelectedItems,
this.props.clearSelectedItems, this.props.clearSelectedItems,
this.props.onUpdateSvg); this.props.onUpdateSvg
);
this.tool.setPrevHoveredItemId(this.props.hoveredItemId); this.tool.setPrevHoveredItemId(this.props.hoveredItemId);
this.tool.activate(); this.tool.activate();
} }

View file

@ -45,7 +45,8 @@ class SelectMode extends React.Component {
this.props.clearHoveredItem, this.props.clearHoveredItem,
this.props.setSelectedItems, this.props.setSelectedItems,
this.props.clearSelectedItems, this.props.clearSelectedItems,
this.props.onUpdateSvg); this.props.onUpdateSvg
);
this.tool.activate(); this.tool.activate();
} }
deactivateTool () { deactivateTool () {

View file

@ -1,19 +1,48 @@
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import {changeStrokeColor} from '../reducers/stroke-color'; import {changeStrokeColor} from '../reducers/stroke-color';
import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx'; import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
import {applyStrokeColorToSelection} from '../helper/style-path'; import {applyStrokeColorToSelection} from '../helper/style-path';
class StrokeColorIndicator extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleChangeStrokeColor'
]);
}
handleChangeStrokeColor (newColor) {
applyStrokeColorToSelection(newColor, this.props.onUpdateSvg);
this.props.onChangeStrokeColor(newColor);
}
render () {
return (
<StrokeColorIndicatorComponent
strokeColor={this.props.strokeColor}
onChangeStrokeColor={this.handleChangeStrokeColor}
/>
);
}
}
const mapStateToProps = state => ({ const mapStateToProps = state => ({
strokeColor: state.scratchPaint.color.strokeColor strokeColor: state.scratchPaint.color.strokeColor
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onChangeStrokeColor: strokeColor => { onChangeStrokeColor: strokeColor => {
applyStrokeColorToSelection(strokeColor);
dispatch(changeStrokeColor(strokeColor)); dispatch(changeStrokeColor(strokeColor));
} }
}); });
StrokeColorIndicator.propTypes = {
onChangeStrokeColor: PropTypes.func.isRequired,
onUpdateSvg: PropTypes.func.isRequired,
strokeColor: PropTypes.string
};
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(StrokeColorIndicatorComponent); )(StrokeColorIndicator);

View file

@ -1,19 +1,48 @@
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import {changeStrokeWidth} from '../reducers/stroke-width'; import {changeStrokeWidth} from '../reducers/stroke-width';
import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx'; import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx';
import {applyStrokeWidthToSelection} from '../helper/style-path'; import {applyStrokeWidthToSelection} from '../helper/style-path';
class StrokeWidthIndicator extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleChangeStrokeWidth'
]);
}
handleChangeStrokeWidth (newWidth) {
applyStrokeWidthToSelection(newWidth, this.props.onUpdateSvg);
this.props.onChangeStrokeWidth(newWidth);
}
render () {
return (
<StrokeWidthIndicatorComponent
strokeWidth={this.props.strokeWidth}
onChangeStrokeWidth={this.handleChangeStrokeWidth}
/>
);
}
}
const mapStateToProps = state => ({ const mapStateToProps = state => ({
strokeWidth: state.scratchPaint.color.strokeWidth strokeWidth: state.scratchPaint.color.strokeWidth
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onChangeStrokeWidth: strokeWidth => { onChangeStrokeWidth: strokeWidth => {
applyStrokeWidthToSelection(strokeWidth);
dispatch(changeStrokeWidth(strokeWidth)); dispatch(changeStrokeWidth(strokeWidth));
} }
}); });
StrokeWidthIndicator.propTypes = {
onChangeStrokeWidth: PropTypes.func.isRequired,
onUpdateSvg: PropTypes.func.isRequired,
strokeWidth: PropTypes.number
};
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(StrokeWidthIndicatorComponent); )(StrokeWidthIndicator);

View file

@ -12,6 +12,7 @@ class HandleTool {
this.setSelectedItems = setSelectedItems; this.setSelectedItems = setSelectedItems;
this.clearSelectedItems = clearSelectedItems; this.clearSelectedItems = clearSelectedItems;
this.onUpdateSvg = onUpdateSvg; this.onUpdateSvg = onUpdateSvg;
this.selectedItems = [];
} }
/** /**
* @param {!object} hitProperties Describes the mouse event * @param {!object} hitProperties Describes the mouse event
@ -28,9 +29,9 @@ class HandleTool {
this.hitType = hitProperties.hitResult.type; this.hitType = hitProperties.hitResult.type;
} }
onMouseDrag (event) { onMouseDrag (event) {
const selectedItems = getSelectedLeafItems(); this.selectedItems = getSelectedLeafItems();
for (const item of selectedItems) { for (const item of this.selectedItems) {
for (const seg of item.segments) { for (const seg of item.segments) {
// add the point of the segment before the drag started // add the point of the segment before the drag started
// for later use in the snap calculation // for later use in the snap calculation
@ -66,8 +67,23 @@ class HandleTool {
} }
} }
onMouseUp () { onMouseUp () {
// @todo add back undo // resetting the items and segments origin points for the next usage
this.onUpdateSvg(); let moved = false;
for (const item of this.selectedItems) {
if (!item.segments) {
return;
}
for (const seg of item.segments) {
if (seg.origPoint && !seg.equals(seg.origPoint)) {
moved = true;
}
seg.origPoint = null;
}
}
if (moved) {
this.onUpdateSvg();
}
this.selectedItems = [];
} }
} }

View file

@ -51,7 +51,7 @@ class MoveTool {
} }
this._select(item, true, hitProperties.subselect); this._select(item, true, hitProperties.subselect);
} }
if (hitProperties.clone) cloneSelection(hitProperties.subselect); if (hitProperties.clone) cloneSelection(hitProperties.subselect, this.onUpdateSvg);
this.selectedItems = getSelectedLeafItems(); this.selectedItems = getSelectedLeafItems();
} }
/** /**
@ -94,15 +94,19 @@ class MoveTool {
} }
} }
onMouseUp () { onMouseUp () {
let moved = false;
// resetting the items origin point for the next usage // resetting the items origin point for the next usage
for (const item of this.selectedItems) { for (const item of this.selectedItems) {
if (item.data.origPos && !item.position.equals(item.data.origPos)) {
moved = true;
}
item.data.origPos = null; item.data.origPos = null;
} }
this.selectedItems = null; this.selectedItems = null;
// @todo add back undo if (moved) {
// pg.undo.snapshot('moveSelection'); this.onUpdateSvg();
this.onUpdateSvg(); }
} }
} }

View file

@ -166,11 +166,15 @@ class PointTool {
} }
onMouseUp () { onMouseUp () {
// resetting the items and segments origin points for the next usage // resetting the items and segments origin points for the next usage
let moved = false;
for (const item of this.selectedItems) { for (const item of this.selectedItems) {
if (!item.segments) { if (!item.segments) {
return; return;
} }
for (const seg of item.segments) { for (const seg of item.segments) {
if (seg.origPoint && !seg.equals(seg.origPoint)) {
moved = true;
}
seg.origPoint = null; seg.origPoint = null;
} }
} }
@ -193,8 +197,9 @@ class PointTool {
} }
this.selectedItems = null; this.selectedItems = null;
this.setSelectedItems(); this.setSelectedItems();
// @todo add back undo if (moved) {
this.onUpdateSvg(); this.onUpdateSvg();
}
} }
} }

View file

@ -221,8 +221,7 @@ class ReshapeTool extends paper.Tool {
handleKeyUp (event) { handleKeyUp (event) {
// Backspace, delete // Backspace, delete
if (event.key === 'delete' || event.key === 'backspace') { if (event.key === 'delete' || event.key === 'backspace') {
deleteSelection(Modes.RESHAPE); deleteSelection(Modes.RESHAPE, this.onUpdateSvg);
this.onUpdateSvg();
} }
} }
deactivateTool () { deactivateTool () {

View file

@ -63,7 +63,6 @@ class RotateTool {
this.rotGroupPivot = null; this.rotGroupPivot = null;
this.prevRot = []; this.prevRot = [];
// @todo add back undo
this.onUpdateSvg(); this.onUpdateSvg();
} }
} }

View file

@ -157,7 +157,6 @@ class ScaleTool {
} }
this.itemGroup.remove(); this.itemGroup.remove();
// @todo add back undo
this.onUpdateSvg(); this.onUpdateSvg();
} }
_getRectCornerNameByIndex (index) { _getRectCornerNameByIndex (index) {

View file

@ -126,10 +126,9 @@ class SelectTool extends paper.Tool {
handleKeyUp (event) { handleKeyUp (event) {
// Backspace, delete // Backspace, delete
if (event.key === 'delete' || event.key === 'backspace') { if (event.key === 'delete' || event.key === 'backspace') {
deleteSelection(Modes.SELECT); deleteSelection(Modes.SELECT, this.onUpdateSvg);
this.clearHoveredItem(); this.clearHoveredItem();
this.boundingBoxTool.removeBoundsPath(); this.boundingBoxTool.removeBoundsPath();
this.onUpdateSvg();
} }
} }
deactivateTool () { deactivateTool () {

View file

@ -57,7 +57,7 @@ const selectItemSegments = function (item, state) {
} }
}; };
const setGroupSelection = function (root, selected, fullySelected) { const _setGroupSelection = function (root, selected, fullySelected) {
root.fullySelected = fullySelected; root.fullySelected = fullySelected;
root.selected = selected; root.selected = selected;
// select children of compound-path or group // select children of compound-path or group
@ -66,7 +66,7 @@ const setGroupSelection = function (root, selected, fullySelected) {
if (children) { if (children) {
for (const child of children) { for (const child of children) {
if (isGroup(child)) { if (isGroup(child)) {
setGroupSelection(child, selected, fullySelected); _setGroupSelection(child, selected, fullySelected);
} else { } else {
child.fullySelected = fullySelected; child.fullySelected = fullySelected;
child.selected = selected; child.selected = selected;
@ -85,12 +85,12 @@ const setItemSelection = function (item, state, fullySelected) {
// do it recursive // do it recursive
setItemSelection(parentGroup, state, fullySelected); setItemSelection(parentGroup, state, fullySelected);
} else if (itemsCompoundPath) { } else if (itemsCompoundPath) {
setGroupSelection(itemsCompoundPath, state, fullySelected); _setGroupSelection(itemsCompoundPath, state, fullySelected);
} else { } else {
if (item.data && item.data.noSelect) { if (item.data && item.data.noSelect) {
return; return;
} }
setGroupSelection(item, state, fullySelected); _setGroupSelection(item, state, fullySelected);
} }
// @todo: Update toolbar state on change // @todo: Update toolbar state on change
@ -165,21 +165,19 @@ const getSelectedLeafItems = function () {
return items; return items;
}; };
const deleteItemSelection = function (items) { const _deleteItemSelection = function (items, onUpdateSvg) {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
items[i].remove(); items[i].remove();
} }
// @todo: Update toolbar state on change // @todo: Update toolbar state on change
paper.project.view.update(); if (items.length > 0) {
// @todo add back undo paper.project.view.update();
// pg.undo.snapshot('deleteItemSelection'); onUpdateSvg();
}
}; };
const removeSelectedSegments = function (items) { const _removeSelectedSegments = function (items, onUpdateSvg) {
// @todo add back undo
// pg.undo.snapshot('removeSelectedSegments');
const segmentsToRemove = []; const segmentsToRemove = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
@ -198,161 +196,37 @@ const removeSelectedSegments = function (items) {
seg.remove(); seg.remove();
removedSegments = true; removedSegments = true;
} }
if (removedSegments) {
paper.project.view.update();
onUpdateSvg();
}
return removedSegments; return removedSegments;
}; };
const deleteSelection = function (mode) { const deleteSelection = function (mode, onUpdateSvg) {
if (mode === Modes.RESHAPE) { if (mode === Modes.RESHAPE) {
const selectedItems = getSelectedLeafItems(); const selectedItems = getSelectedLeafItems();
// If there are points selected remove them. If not delete the item selected. // If there are points selected remove them. If not delete the item selected.
if (!removeSelectedSegments(selectedItems)) { if (!_removeSelectedSegments(selectedItems, onUpdateSvg)) {
deleteItemSelection(selectedItems); _deleteItemSelection(selectedItems, onUpdateSvg);
} }
} else { } else {
const selectedItems = getSelectedRootItems(); const selectedItems = getSelectedRootItems();
deleteItemSelection(selectedItems); _deleteItemSelection(selectedItems, onUpdateSvg);
} }
}; };
const splitPathRetainSelection = function (path, index, deselectSplitSegments) { const cloneSelection = function (recursive, onUpdateSvg) {
const selectedPoints = [];
// collect points of selected segments, so we can reselect them
// once the path is split.
for (let i = 0; i < path.segments.length; i++) {
const seg = path.segments[i];
if (seg.selected) {
if (deselectSplitSegments && i === index) {
continue;
}
selectedPoints.push(seg.point);
}
}
const newPath = path.split(index, 0);
if (!newPath) return;
// reselect all of the newPaths segments that are in the exact same location
// as the ones that are stored in selectedPoints
for (let i = 0; i < newPath.segments.length; i++) {
const seg = newPath.segments[i];
for (let j = 0; j < selectedPoints.length; j++) {
const point = selectedPoints[j];
if (point.x === seg.point.x && point.y === seg.point.y) {
seg.selected = true;
}
}
}
// only do this if path and newPath are different
// (split at more than one point)
if (path !== newPath) {
for (let i = 0; i < path.segments.length; i++) {
const seg = path.segments[i];
for (let j = 0; j < selectedPoints.length; j++) {
const point = selectedPoints[j];
if (point.x === seg.point.x && point.y === seg.point.y) {
seg.selected = true;
}
}
}
}
};
const splitPathAtSelectedSegments = function () {
const items = getSelectedRootItems();
for (let i = 0; i < items.length; i++) {
const item = items[i];
const segments = item.segments;
for (let j = 0; j < segments.length; j++) {
const segment = segments[j];
if (segment.selected) {
if (item.closed ||
(segment.next &&
!segment.next.selected &&
segment.previous &&
!segment.previous.selected)) {
splitPathRetainSelection(item, j, true);
splitPathAtSelectedSegments();
return;
}
}
}
}
};
const deleteSegments = function (item) {
if (item.children) {
for (let i = 0; i < item.children.length; i++) {
const child = item.children[i];
deleteSegments(child);
}
} else {
const segments = item.segments;
for (let j = 0; j < segments.length; j++) {
const segment = segments[j];
if (segment.selected) {
if (item.closed ||
(segment.next &&
!segment.next.selected &&
segment.previous &&
!segment.previous.selected)) {
splitPathRetainSelection(item, j);
deleteSelection();
return;
} else if (!item.closed) {
segment.remove();
j--; // decrease counter if we removed one from the loop
}
}
}
}
// remove items with no segments left
if (item.segments.length <= 0) {
item.remove();
}
};
const deleteSegmentSelection = function (items) {
for (let i = 0; i < items.length; i++) {
deleteSegments(items[i]);
}
// @todo: Update toolbar state on change
paper.project.view.update();
// @todo add back undo
// pg.undo.snapshot('deleteSegmentSelection');
};
const cloneSelection = function (recursive) {
const selectedItems = recursive ? getSelectedLeafItems() : getSelectedRootItems(); const selectedItems = recursive ? getSelectedLeafItems() : getSelectedRootItems();
for (let i = 0; i < selectedItems.length; i++) { for (let i = 0; i < selectedItems.length; i++) {
const item = selectedItems[i]; const item = selectedItems[i];
item.clone(); item.clone();
item.selected = false; item.selected = false;
} }
// @todo add back undo onUpdateSvg();
// pg.undo.snapshot('cloneSelection');
}; };
// Only returns paths, no compound paths, groups or any other stuff const _checkBoundsItem = function (selectionRect, item, event) {
const getSelectedPaths = function () {
const allPaths = getSelectedRootItems();
const paths = [];
for (let i = 0; i < allPaths.length; i++) {
const path = allPaths[i];
if (path.className === 'Path') {
paths.push(path);
}
}
return paths;
};
const checkBoundsItem = function (selectionRect, item, event) {
const itemBounds = new paper.Path([ const itemBounds = new paper.Path([
item.localToGlobal(item.internalBounds.topLeft), item.localToGlobal(item.internalBounds.topLeft),
item.localToGlobal(item.internalBounds.topRight), item.localToGlobal(item.internalBounds.topRight),
@ -441,7 +315,7 @@ const _handleRectangularSelectionItems = function (item, event, rect, mode, root
// @todo: Update toolbar state on change // @todo: Update toolbar state on change
} else if (isBoundsItem(item)) { } else if (isBoundsItem(item)) {
if (checkBoundsItem(rect, item, event)) { if (_checkBoundsItem(rect, item, event)) {
return false; return false;
} }
} }
@ -524,16 +398,10 @@ export {
selectAllSegments, selectAllSegments,
clearSelection, clearSelection,
deleteSelection, deleteSelection,
deleteItemSelection,
deleteSegmentSelection,
splitPathAtSelectedSegments,
cloneSelection, cloneSelection,
setItemSelection, setItemSelection,
setGroupSelection,
getSelectedLeafItems, getSelectedLeafItems,
getSelectedPaths,
getSelectedRootItems, getSelectedRootItems,
removeSelectedSegments,
processRectangularSelection, processRectangularSelection,
selectRootItem, selectRootItem,
shouldShowIfSelection, shouldShowIfSelection,

View file

@ -1,3 +1,4 @@
import paper from 'paper';
import {getSelectedLeafItems} from './selection'; import {getSelectedLeafItems} from './selection';
import {isPGTextItem, isPointTextItem} from './item'; import {isPGTextItem, isPointTextItem} from './item';
import {isGroup} from './group'; import {isGroup} from './group';
@ -7,39 +8,56 @@ const MIXED = 'scratch-paint/style-path/mixed';
/** /**
* Called when setting fill color * Called when setting fill color
* @param {string} colorString New color, css format * @param {string} colorString New color, css format
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
*/ */
const applyFillColorToSelection = function (colorString) { const applyFillColorToSelection = function (colorString, onUpdateSvg) {
const items = getSelectedLeafItems(); const items = getSelectedLeafItems();
let changed = false;
for (const item of items) { for (const item of items) {
if (isPGTextItem(item)) { if (isPGTextItem(item)) {
for (const child of item.children) { for (const child of item.children) {
if (child.children) { if (child.children) {
for (const path of child.children) { for (const path of child.children) {
if (!path.data.isPGGlyphRect) { if (!path.data.isPGGlyphRect) {
path.fillColor = colorString; if ((path.fillColor === null && colorString) ||
path.fillColor.toCSS() !== new paper.Color(colorString).toCSS()) {
changed = true;
path.fillColor = colorString;
}
} }
} }
} else if (!child.data.isPGGlyphRect) { } else if (!child.data.isPGGlyphRect) {
child.fillColor = colorString; if ((child.fillColor === null && colorString) ||
child.fillColor.toCSS() !== new paper.Color(colorString).toCSS()) {
changed = true;
child.fillColor = colorString;
}
} }
} }
} else { } else {
if (isPointTextItem(item) && !colorString) { if (isPointTextItem(item) && !colorString) {
colorString = 'rgba(0,0,0,0)'; colorString = 'rgba(0,0,0,0)';
} }
item.fillColor = colorString; if ((item.fillColor === null && colorString) ||
item.fillColor.toCSS() !== new paper.Color(colorString).toCSS()) {
changed = true;
item.fillColor = colorString;
}
} }
} }
// @todo add back undo if (changed) {
onUpdateSvg();
}
}; };
/** /**
* Called when setting stroke color * Called when setting stroke color
* @param {string} colorString New color, css format * @param {string} colorString New color, css format
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
*/ */
const applyStrokeColorToSelection = function (colorString) { const applyStrokeColorToSelection = function (colorString, onUpdateSvg) {
const items = getSelectedLeafItems(); const items = getSelectedLeafItems();
let changed = false;
for (const item of items) { for (const item of items) {
if (isPGTextItem(item)) { if (isPGTextItem(item)) {
if (item.children) { if (item.children) {
@ -47,37 +65,53 @@ const applyStrokeColorToSelection = function (colorString) {
if (child.children) { if (child.children) {
for (const path of child.children) { for (const path of child.children) {
if (!path.data.isPGGlyphRect) { if (!path.data.isPGGlyphRect) {
path.strokeColor = colorString; if ((path.strokeColor === null && colorString) ||
path.strokeColor.toCSS() !== new paper.Color(colorString).toCSS()) {
changed = true;
path.strokeColor = colorString;
}
} }
} }
} else if (!child.data.isPGGlyphRect) { } else if (!child.data.isPGGlyphRect) {
child.strokeColor = colorString; if (child.strokeColor !== colorString) {
changed = true;
child.strokeColor = colorString;
}
} }
} }
} else if (!item.data.isPGGlyphRect) { } else if (!item.data.isPGGlyphRect) {
item.strokeColor = colorString; if ((item.strokeColor === null && colorString) ||
item.strokeColor.toCSS() !== new paper.Color(colorString).toCSS()) {
changed = true;
item.strokeColor = colorString;
}
} }
} else { } else if ((item.strokeColor === null && colorString) ||
item.strokeColor.toCSS() !== new paper.Color(colorString).toCSS()) {
changed = true;
item.strokeColor = colorString; item.strokeColor = colorString;
} }
} }
// @todo add back undo if (changed) {
onUpdateSvg();
}
}; };
/** /**
* Called when setting stroke width * Called when setting stroke width
* @param {number} value New stroke width * @param {number} value New stroke width
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
*/ */
const applyStrokeWidthToSelection = function (value) { const applyStrokeWidthToSelection = function (value, onUpdateSvg) {
const items = getSelectedLeafItems(); const items = getSelectedLeafItems();
for (const item of items) { for (const item of items) {
if (isGroup(item)) { if (isGroup(item)) {
continue; continue;
} else { } else if (item.strokeWidth !== value) {
item.strokeWidth = value; item.strokeWidth = value;
onUpdateSvg();
} }
} }
// @todo add back undo
}; };
/** /**
@ -167,8 +201,11 @@ const getColorsFromSelection = function (selectedItems) {
const stylePath = function (path, options) { const stylePath = function (path, options) {
if (options.isEraser) { if (options.isEraser) {
path.fillColor = 'white'; path.fillColor = 'white';
} else { } else if (options.fillColor) {
path.fillColor = options.fillColor; path.fillColor = options.fillColor;
} else {
// Make sure something visible is drawn
path.fillColor = 'black';
} }
}; };
@ -177,8 +214,11 @@ const styleCursorPreview = function (path, options) {
path.fillColor = 'white'; path.fillColor = 'white';
path.strokeColor = 'cornflowerblue'; path.strokeColor = 'cornflowerblue';
path.strokeWidth = 1; path.strokeWidth = 1;
} else { } else if (options.fillColor) {
path.fillColor = options.fillColor; path.fillColor = options.fillColor;
} else {
// Make sure something visible is drawn
path.fillColor = 'black';
} }
}; };

49
src/helper/undo.js Normal file
View file

@ -0,0 +1,49 @@
// undo functionality
// modifed from https://github.com/memononen/stylii
import paper from 'paper';
const performSnapshot = function (dispatchPerformSnapshot) {
dispatchPerformSnapshot({
json: paper.project.exportJSON({asString: false})
});
// @todo enable/disable buttons
// updateButtonVisibility();
};
const _restore = function (entry, onUpdateSvg) {
for (const layer of paper.project.layers) {
layer.removeChildren();
}
paper.project.clear();
paper.project.importJSON(entry.json);
paper.view.update();
onUpdateSvg(true /* skipSnapshot */);
};
const performUndo = function (undoState, dispatchPerformUndo, onUpdateSvg) {
if (undoState.pointer > 0) {
_restore(undoState.stack[undoState.pointer - 1], onUpdateSvg);
dispatchPerformUndo();
// @todo enable/disable buttons
// updateButtonVisibility();
}
};
const performRedo = function (undoState, dispatchPerformRedo, onUpdateSvg) {
if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) {
_restore(undoState.stack[undoState.pointer + 1], onUpdateSvg);
dispatchPerformRedo();
// @todo enable/disable buttons
// updateButtonVisibility();
}
};
export {
performSnapshot,
performUndo,
performRedo
};

View file

@ -5,6 +5,7 @@ import eraserModeReducer from './eraser-mode';
import colorReducer from './color'; import colorReducer from './color';
import hoverReducer from './hover'; import hoverReducer from './hover';
import selectedItemReducer from './selected-items'; import selectedItemReducer from './selected-items';
import undoReducer from './undo';
export default combineReducers({ export default combineReducers({
mode: modeReducer, mode: modeReducer,
@ -12,5 +13,6 @@ export default combineReducers({
eraserMode: eraserModeReducer, eraserMode: eraserModeReducer,
color: colorReducer, color: colorReducer,
hoveredItemId: hoverReducer, hoveredItemId: hoverReducer,
selectedItems: selectedItemReducer selectedItems: selectedItemReducer,
undo: undoReducer
}); });

79
src/reducers/undo.js Normal file
View file

@ -0,0 +1,79 @@
import log from '../log/log';
const UNDO = 'scratch-paint/undo/UNDO';
const REDO = 'scratch-paint/undo/REDO';
const SNAPSHOT = 'scratch-paint/undo/SNAPSHOT';
const CLEAR = 'scratch-paint/undo/CLEAR';
const initialState = {
stack: [],
pointer: -1
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case UNDO:
if (state.pointer <= 0) {
log.warn(`Can't undo, undo stack is empty`);
return state;
}
return {
stack: state.stack,
pointer: state.pointer - 1
};
case REDO:
if (state.pointer <= -1 || state.pointer === state.stack.length - 1) {
log.warn(`Can't redo, redo stack is empty`);
return state;
}
return {
stack: state.stack,
pointer: state.pointer + 1
};
case SNAPSHOT:
if (!action.snapshot) {
log.warn(`Couldn't create undo snapshot, no data provided`);
return state;
}
return {
// Performing an action clears the redo stack
stack: state.stack.slice(0, state.pointer + 1).concat(action.snapshot),
pointer: state.pointer + 1
};
case CLEAR:
return initialState;
default:
return state;
}
};
// Action creators ==================================
const undoSnapshot = function (snapshot) {
return {
type: SNAPSHOT,
snapshot: snapshot
};
};
const undo = function () {
return {
type: UNDO
};
};
const redo = function () {
return {
type: REDO
};
};
const clearUndoState = function () {
return {
type: CLEAR
};
};
export {
reducer as default,
undo,
redo,
undoSnapshot,
clearUndoState
};

View file

@ -0,0 +1,141 @@
/* eslint-env jest */
import undoReducer from '../../src/reducers/undo';
import {undoSnapshot, undo, redo, clearUndoState} from '../../src/reducers/undo';
test('initialState', () => {
let defaultState;
expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */).pointer).toEqual(-1);
expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */).stack).toHaveLength(0);
});
test('snapshot', () => {
let defaultState;
const state1 = {state: 1};
const state2 = {state: 2};
let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
expect(reduxState.pointer).toEqual(0);
expect(reduxState.stack).toHaveLength(1);
expect(reduxState.stack[0]).toEqual(state1);
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
expect(reduxState.pointer).toEqual(1);
expect(reduxState.stack).toHaveLength(2);
expect(reduxState.stack[0]).toEqual(state1);
expect(reduxState.stack[1]).toEqual(state2);
});
test('invalidSnapshot', () => {
let defaultState;
const state1 = {state: 1};
const reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
const newReduxState = undoReducer(reduxState /* state */, undoSnapshot() /* action */); // No snapshot provided
expect(reduxState).toEqual(newReduxState);
});
test('clearUndoState', () => {
let defaultState;
const state1 = {state: 1};
const state2 = {state: 2};
// Push 2 states then clear
const reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
const newReduxState = undoReducer(reduxState /* state */, clearUndoState() /* action */);
expect(newReduxState.pointer).toEqual(-1);
expect(newReduxState.stack).toHaveLength(0);
});
test('cantUndo', () => {
let defaultState;
const state1 = {state: 1};
// Undo when there's no undo stack
let reduxState = undoReducer(defaultState /* state */, undo() /* action */);
expect(reduxState.pointer).toEqual(-1);
expect(reduxState.stack).toHaveLength(0);
// Undo when there's only one state
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state1]) /* action */);
reduxState = undoReducer(reduxState /* state */, undo() /* action */);
expect(reduxState.pointer).toEqual(0);
expect(reduxState.stack).toHaveLength(1);
});
test('cantRedo', () => {
let defaultState;
const state1 = {state: 1};
let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
// Redo when there's no redo stack
reduxState = undoReducer(reduxState /* state */, redo() /* action */);
expect(reduxState.pointer).toEqual(0);
expect(reduxState.stack).toHaveLength(1);
});
test('undo', () => {
let defaultState;
const state1 = {state: 1};
const state2 = {state: 2};
// Push 2 states then undo one
let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
reduxState = undoReducer(reduxState /* state */, undo() /* action */);
expect(reduxState.pointer).toEqual(0);
expect(reduxState.stack).toHaveLength(2);
expect(reduxState.stack[0]).toEqual(state1);
expect(reduxState.stack[1]).toEqual(state2);
});
test('redo', () => {
let defaultState;
const state1 = {state: 1};
const state2 = {state: 2};
// Push 2 states then undo one
let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
let newReduxState = undoReducer(reduxState /* state */, undo() /* action */);
// Now redo and check equality with previous state
newReduxState = undoReducer(newReduxState /* state */, redo() /* action */);
expect(newReduxState.pointer).toEqual(reduxState.pointer);
expect(newReduxState.stack).toHaveLength(reduxState.stack.length);
expect(newReduxState.stack[0]).toEqual(reduxState.stack[0]);
expect(reduxState.stack[1]).toEqual(reduxState.stack[1]);
});
test('undoSnapshotCantRedo', () => {
let defaultState;
const state1 = {state: 1};
const state2 = {state: 2};
const state3 = {state: 3};
// Push 2 states then undo
let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
reduxState = undoReducer(reduxState /* state */, undo() /* action */);
expect(reduxState.pointer).toEqual(0);
expect(reduxState.stack).toHaveLength(2);
// Snapshot
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state3]) /* action */);
// Redo should do nothing
const newReduxState = undoReducer(reduxState /* state */, redo() /* action */);
expect(newReduxState.pointer).toEqual(reduxState.pointer);
expect(newReduxState.stack).toHaveLength(reduxState.stack.length);
expect(newReduxState.stack[0]).toEqual(reduxState.stack[0]);
expect(newReduxState.stack[1]).toEqual(state3);
});