diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index da5fcd3f..46f9cc72 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -58,11 +58,13 @@ class PaintEditorComponent extends React.Component {
@@ -154,6 +156,8 @@ class PaintEditorComponent extends React.Component { PaintEditorComponent.propTypes = { intl: intlShape, + onRedo: PropTypes.func.isRequired, + onUndo: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index 3025be8d..3a24597f 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -4,6 +4,7 @@ import BroadBrushHelper from './broad-brush-helper'; import SegmentBrushHelper from './segment-brush-helper'; import {MIXED, styleCursorPreview} from '../../helper/style-path'; import {clearSelection} from '../../helper/selection'; +import {performSnapshot} from '../../helper/undo'; /** * Shared code for the brush and eraser mode. Adds functions on the paper tool object @@ -29,11 +30,12 @@ class Blobbiness { * @param {function} updateCallback 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 */ - constructor (updateCallback, clearSelectedItems) { + constructor (updateCallback, clearSelectedItems, undoSnapshot) { this.broadBrushHelper = new BroadBrushHelper(); this.segmentBrushHelper = new SegmentBrushHelper(); this.updateCallback = updateCallback; this.clearSelectedItems = clearSelectedItems; + this.undoSnapshot = undoSnapshot; // The following are stored to check whether these have changed and the cursor preview needs to be redrawn. this.strokeColor = null; @@ -144,6 +146,7 @@ class Blobbiness { blob.cursorPreview.visible = false; blob.updateCallback(); + performSnapshot(blob.undoSnapshot); blob.cursorPreview.visible = true; blob.cursorPreview.bringToFront(); blob.cursorPreview.position = event.point; @@ -234,8 +237,6 @@ class Blobbiness { paths.splice(i, 1); } } - // TODO: Add back undo - // pg.undo.snapshot('broadbrush'); } mergeEraser (lastPath) { @@ -284,8 +285,6 @@ class Blobbiness { } } lastPath.remove(); - // TODO add back undo - // pg.undo.snapshot('eraser'); continue; } // Erase @@ -358,8 +357,6 @@ class Blobbiness { items[i].remove(); } lastPath.remove(); - // TODO: Add back undo handling - // pg.undo.snapshot('eraser'); } colorMatch (existingPath, addedPath) { diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index b7027c14..6c4a0b7c 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -4,10 +4,13 @@ import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; import Blobbiness from './blob/blob'; + import {changeBrushSize} from '../reducers/brush-mode'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; +import {undoSnapshot} from '../reducers/undo'; import {clearSelection} from '../helper/selection'; + import BrushModeComponent from '../components/brush-mode.jsx'; class BrushMode extends React.Component { @@ -18,7 +21,8 @@ class BrushMode extends React.Component { 'deactivateTool', 'onScroll' ]); - this.blob = new Blobbiness(this.props.onUpdateSvg, this.props.clearSelectedItems); + this.blob = new Blobbiness( + this.props.onUpdateSvg, this.props.clearSelectedItems, this.props.undoSnapshot); } componentDidMount () { if (this.props.isBrushModeActive) { @@ -87,7 +91,8 @@ BrushMode.propTypes = { }).isRequired, handleMouseDown: PropTypes.func.isRequired, isBrushModeActive: PropTypes.bool.isRequired, - onUpdateSvg: PropTypes.func.isRequired + onUpdateSvg: PropTypes.func.isRequired, + undoSnapshot: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -104,6 +109,9 @@ const mapDispatchToProps = dispatch => ({ }, handleMouseDown: () => { dispatch(changeMode(Modes.BRUSH)); + }, + undoSnapshot: snapshot => { + dispatch(undoSnapshot(snapshot)); } }); diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx index 423058c2..5d5f6baa 100644 --- a/src/containers/eraser-mode.jsx +++ b/src/containers/eraser-mode.jsx @@ -6,6 +6,7 @@ import Modes from '../modes/modes'; import Blobbiness from './blob/blob'; import {changeBrushSize} from '../reducers/eraser-mode'; import {clearSelectedItems} from '../reducers/selected-items'; +import {undoSnapshot} from '../reducers/undo'; import EraserModeComponent from '../components/eraser-mode.jsx'; import {changeMode} from '../reducers/modes'; @@ -17,7 +18,8 @@ class EraserMode extends React.Component { 'deactivateTool', 'onScroll' ]); - this.blob = new Blobbiness(this.props.onUpdateSvg, this.props.clearSelectedItems); + this.blob = new Blobbiness( + this.props.onUpdateSvg, this.props.clearSelectedItems, this.props.undoSnapshot); } componentDidMount () { if (this.props.isEraserModeActive) { @@ -72,7 +74,8 @@ EraserMode.propTypes = { }), handleMouseDown: PropTypes.func.isRequired, isEraserModeActive: PropTypes.bool.isRequired, - onUpdateSvg: PropTypes.func.isRequired + onUpdateSvg: PropTypes.func.isRequired, + undoSnapshot: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -88,6 +91,9 @@ const mapDispatchToProps = dispatch => ({ }, handleMouseDown: () => { dispatch(changeMode(Modes.ERASER)); + }, + undoSnapshot: snapshot => { + dispatch(undoSnapshot(snapshot)); } }); diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 35611f45..8c3cb5aa 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -2,13 +2,16 @@ import {connect} from 'react-redux'; import {changeFillColor} from '../reducers/fill-color'; import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; import {applyFillColorToSelection} from '../helper/style-path'; +import {performSnapshot} from '../helper/undo'; +import {undoSnapshot} from '../reducers/undo'; const mapStateToProps = state => ({ fillColor: state.scratchPaint.color.fillColor }); const mapDispatchToProps = dispatch => ({ onChangeFillColor: fillColor => { - applyFillColorToSelection(fillColor); + applyFillColorToSelection(fillColor, undoSnapshot); + performSnapshot(snapshot => dispatch(undoSnapshot(snapshot))); dispatch(changeFillColor(fillColor)); } }); diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 69356d62..0e11e3d4 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -1,3 +1,4 @@ +import paper from 'paper'; import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; @@ -6,10 +7,13 @@ import Modes from '../modes/modes'; import {changeStrokeWidth} from '../reducers/stroke-width'; import {clearSelection, getSelectedLeafItems} from '../helper/selection'; 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 paper from 'paper'; +import {changeStrokeWidth} from '../reducers/stroke-width'; +import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; +import {performSnapshot} from '../helper/undo'; +import {undoSnapshot} from '../reducers/undo'; + +import LineModeComponent from '../components/line-mode.jsx'; class LineMode extends React.Component { static get SNAP_TOLERANCE () { @@ -209,10 +213,9 @@ class LineMode extends React.Component { this.props.onUpdateSvg(); this.props.setSelectedItems(); - // TODO add back undo - // if (this.path) { - // pg.undo.snapshot('line'); - // } + if (this.path) { + performSnapshot(this.props.undoSnapshot); + } } toleranceSquared () { return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2); @@ -284,7 +287,8 @@ LineMode.propTypes = { handleMouseDown: PropTypes.func.isRequired, isLineModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, - setSelectedItems: PropTypes.func.isRequired + setSelectedItems: PropTypes.func.isRequired, + undoSnapshot: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -303,6 +307,9 @@ const mapDispatchToProps = dispatch => ({ }, handleMouseDown: () => { dispatch(changeMode(Modes.LINE)); + }, + undoSnapshot: snapshot => { + dispatch(undoSnapshot(snapshot)); } }); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 9d48fa2d..232a2b43 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -1,8 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; import PaintEditorComponent from '../components/paint-editor.jsx'; + import {changeMode} from '../reducers/modes'; +import {undo, redo} from '../reducers/undo'; + import {getGuideLayer} from '../helper/layer'; +import {performUndo, performRedo} from '../helper/undo'; + import Modes from '../modes/modes'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; @@ -12,7 +17,9 @@ class PaintEditor extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleUpdateSvg' + 'handleUpdateSvg', + 'handleUndo', + 'handleRedo' ]); } componentDidMount () { @@ -34,12 +41,20 @@ class PaintEditor extends React.Component { paper.project.view.center.y - bounds.y); getGuideLayer().visible = true; } + handleUndo () { + performUndo(this.props.undoState, this.props.onUndo); + } + handleRedo () { + performRedo(this.props.undoState, this.props.onRedo); + } render () { return ( ); @@ -48,12 +63,21 @@ class PaintEditor extends React.Component { PaintEditor.propTypes = { onKeyPress: PropTypes.func.isRequired, + onRedo: PropTypes.func.isRequired, + onUndo: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, - svg: PropTypes.string + svg: PropTypes.string, + undoState: PropTypes.shape({ + stack: PropTypes.arrayOf(PropTypes.object).isRequired, + pointer: PropTypes.number.isRequired + }) }; +const mapStateToProps = state => ({ + undoState: state.scratchPaint.undo +}); const mapDispatchToProps = dispatch => ({ onKeyPress: event => { if (event.key === 'e') { @@ -65,10 +89,16 @@ const mapDispatchToProps = dispatch => ({ } else if (event.key === 's') { dispatch(changeMode(Modes.SELECT)); } + }, + onUndo: () => { + dispatch(undo()); + }, + onRedo: () => { + dispatch(redo()); } }); export default connect( - null, + mapStateToProps, mapDispatchToProps )(PaintEditor); diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index ae4eda24..030eaec4 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -1,8 +1,12 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; +import {connect} from 'react-redux'; import paper from 'paper'; +import {performSnapshot} from '../helper/undo'; +import {undoSnapshot} from '../reducers/undo'; + import styles from './paper-canvas.css'; class PaperCanvas extends React.Component { @@ -20,6 +24,7 @@ class PaperCanvas extends React.Component { if (this.props.svg) { this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY); } + performSnapshot(this.props.undoSnapshot); } componentWillReceiveProps (newProps) { paper.project.activeLayer.removeChildren(); @@ -85,7 +90,16 @@ PaperCanvas.propTypes = { canvasRef: PropTypes.func, rotationCenterX: 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); diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx index 1683874c..f192b454 100644 --- a/src/containers/reshape-mode.jsx +++ b/src/containers/reshape-mode.jsx @@ -8,6 +8,7 @@ import {changeMode} from '../reducers/modes'; import {clearHoveredItem, setHoveredItem} from '../reducers/hover'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {getSelectedLeafItems} from '../helper/selection'; +import {undoSnapshot} from '../reducers/undo'; import ReshapeTool from '../helper/selection-tools/reshape-tool'; import ReshapeModeComponent from '../components/reshape-mode.jsx'; @@ -45,7 +46,9 @@ class ReshapeMode extends React.Component { this.props.clearHoveredItem, this.props.setSelectedItems, this.props.clearSelectedItems, - this.props.onUpdateSvg); + this.props.onUpdateSvg, + this.props.undoSnapshot + ); this.tool.setPrevHoveredItemId(this.props.hoveredItemId); this.tool.activate(); } @@ -70,7 +73,8 @@ ReshapeMode.propTypes = { isReshapeModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, setHoveredItem: PropTypes.func.isRequired, - setSelectedItems: PropTypes.func.isRequired + setSelectedItems: PropTypes.func.isRequired, + undoSnapshot: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -92,6 +96,9 @@ const mapDispatchToProps = dispatch => ({ }, handleMouseDown: () => { dispatch(changeMode(Modes.RESHAPE)); + }, + undoSnapshot: snapshot => { + dispatch(undoSnapshot(snapshot)); } }); diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 505bf412..0d946f60 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -7,6 +7,7 @@ import Modes from '../modes/modes'; import {changeMode} from '../reducers/modes'; import {clearHoveredItem, setHoveredItem} from '../reducers/hover'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; +import {undoSnapshot} from '../reducers/undo'; import {getSelectedLeafItems} from '../helper/selection'; import SelectTool from '../helper/selection-tools/select-tool'; @@ -45,7 +46,9 @@ class SelectMode extends React.Component { this.props.clearHoveredItem, this.props.setSelectedItems, this.props.clearSelectedItems, - this.props.onUpdateSvg); + this.props.onUpdateSvg, + this.props.undoSnapshot + ); this.tool.activate(); } deactivateTool () { @@ -68,7 +71,8 @@ SelectMode.propTypes = { isSelectModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, setHoveredItem: PropTypes.func.isRequired, - setSelectedItems: PropTypes.func.isRequired + setSelectedItems: PropTypes.func.isRequired, + undoSnapshot: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -90,6 +94,9 @@ const mapDispatchToProps = dispatch => ({ }, handleMouseDown: () => { dispatch(changeMode(Modes.SELECT)); + }, + undoSnapshot: snapshot => { + dispatch(undoSnapshot(snapshot)); } }); diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index f7ffcbab..20bd9fee 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -2,13 +2,16 @@ import {connect} from 'react-redux'; import {changeStrokeColor} from '../reducers/stroke-color'; import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx'; import {applyStrokeColorToSelection} from '../helper/style-path'; +import {performSnapshot} from '../helper/undo'; +import {undoSnapshot} from '../reducers/undo'; const mapStateToProps = state => ({ strokeColor: state.scratchPaint.color.strokeColor }); const mapDispatchToProps = dispatch => ({ onChangeStrokeColor: strokeColor => { - applyStrokeColorToSelection(strokeColor); + applyStrokeColorToSelection(strokeColor, undoSnapshot); + performSnapshot(snapshot => dispatch(undoSnapshot(snapshot))); dispatch(changeStrokeColor(strokeColor)); } }); diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx index d1b0def3..105263c9 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -2,13 +2,16 @@ import {connect} from 'react-redux'; import {changeStrokeWidth} from '../reducers/stroke-width'; import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx'; import {applyStrokeWidthToSelection} from '../helper/style-path'; +import {performSnapshot} from '../helper/undo'; +import {undoSnapshot} from '../reducers/undo'; const mapStateToProps = state => ({ strokeWidth: state.scratchPaint.color.strokeWidth }); const mapDispatchToProps = dispatch => ({ onChangeStrokeWidth: strokeWidth => { - applyStrokeWidthToSelection(strokeWidth); + applyStrokeWidthToSelection(strokeWidth, undoSnapshot); + performSnapshot(snapshot => dispatch(undoSnapshot(snapshot))); dispatch(changeStrokeWidth(strokeWidth)); } }); diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index 1826f365..46d94aa7 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -36,16 +36,16 @@ class BoundingBoxTool { * @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ - constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot) { this.onUpdateSvg = onUpdateSvg; this.mode = null; this.boundsPath = null; this.boundsScaleHandles = []; this.boundsRotHandles = []; this._modeMap = {}; - this._modeMap[Modes.SCALE] = new ScaleTool(onUpdateSvg); - this._modeMap[Modes.ROTATE] = new RotateTool(onUpdateSvg); - this._modeMap[Modes.MOVE] = new MoveTool(setSelectedItems, clearSelectedItems, onUpdateSvg); + this._modeMap[Modes.SCALE] = new ScaleTool(onUpdateSvg, undoSnapshot); + this._modeMap[Modes.ROTATE] = new RotateTool(onUpdateSvg, undoSnapshot); + this._modeMap[Modes.MOVE] = new MoveTool(setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot); } /** diff --git a/src/helper/selection-tools/handle-tool.js b/src/helper/selection-tools/handle-tool.js index c3d26a1d..fd55b0f7 100644 --- a/src/helper/selection-tools/handle-tool.js +++ b/src/helper/selection-tools/handle-tool.js @@ -1,4 +1,5 @@ import {clearSelection, getSelectedLeafItems} from '../selection'; +import {performSnapshot} from '../undo'; /** Sub tool of the Reshape tool for moving handles, which adjust bezier curves. */ class HandleTool { @@ -7,11 +8,13 @@ class HandleTool { * @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ - constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot) { this.hitType = null; this.setSelectedItems = setSelectedItems; this.clearSelectedItems = clearSelectedItems; this.onUpdateSvg = onUpdateSvg; + this.undoSnapshot = undoSnapshot; + this.selectedItems = []; } /** * @param {!object} hitProperties Describes the mouse event @@ -28,9 +31,9 @@ class HandleTool { this.hitType = hitProperties.hitResult.type; } 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) { // add the point of the segment before the drag started // for later use in the snap calculation @@ -66,8 +69,24 @@ class HandleTool { } } onMouseUp () { - // @todo add back undo - this.onUpdateSvg(); + // resetting the items and segments origin points for the next usage + 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) { + performSnapshot(this.undoSnapshot); + this.onUpdateSvg(); + } + this.selectedItems = []; } } diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js index 0a16277b..c55cf6db 100644 --- a/src/helper/selection-tools/move-tool.js +++ b/src/helper/selection-tools/move-tool.js @@ -2,6 +2,7 @@ import {isGroup} from '../group'; import {isCompoundPathItem, getRootItem} from '../item'; import {snapDeltaToAngle} from '../math'; import {clearSelection, cloneSelection, getSelectedLeafItems, setItemSelection} from '../selection'; +import {performSnapshot} from '../undo'; /** * Tool to handle dragging an item to reposition it in a selection mode. @@ -12,11 +13,12 @@ class MoveTool { * @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ - constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot) { this.setSelectedItems = setSelectedItems; this.clearSelectedItems = clearSelectedItems; this.selectedItems = null; this.onUpdateSvg = onUpdateSvg; + this.undoSnapshot = undoSnapshot; } /** @@ -51,7 +53,7 @@ class MoveTool { } this._select(item, true, hitProperties.subselect); } - if (hitProperties.clone) cloneSelection(hitProperties.subselect); + if (hitProperties.clone) cloneSelection(hitProperties.subselect, this.undoSnapshot); this.selectedItems = getSelectedLeafItems(); } /** @@ -94,15 +96,20 @@ class MoveTool { } } onMouseUp () { + let moved = false; // resetting the items origin point for the next usage for (const item of this.selectedItems) { + if (item.data.origPos && !item.position.equals(item.data.origPos)) { + moved = true; + } item.data.origPos = null; } this.selectedItems = null; - // @todo add back undo - // pg.undo.snapshot('moveSelection'); - this.onUpdateSvg(); + if (moved) { + performSnapshot(this.undoSnapshot); + this.onUpdateSvg(); + } } } diff --git a/src/helper/selection-tools/point-tool.js b/src/helper/selection-tools/point-tool.js index 9b54cb7e..73bcbd27 100644 --- a/src/helper/selection-tools/point-tool.js +++ b/src/helper/selection-tools/point-tool.js @@ -1,6 +1,7 @@ import paper from 'paper'; import {snapDeltaToAngle} from '../math'; import {clearSelection, getSelectedLeafItems} from '../selection'; +import {performSnapshot} from '../undo'; /** Subtool of ReshapeTool for moving control points. */ class PointTool { @@ -9,7 +10,7 @@ class PointTool { * @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ - constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot) { /** * Deselection often does not happen until mouse up. If the mouse is dragged before * mouse up, deselection is cancelled. This variable keeps track of which paper.Item to deselect. @@ -29,6 +30,7 @@ class PointTool { this.setSelectedItems = setSelectedItems; this.clearSelectedItems = clearSelectedItems; this.onUpdateSvg = onUpdateSvg; + this.undoSnapshot = undoSnapshot; } /** @@ -166,11 +168,15 @@ class PointTool { } onMouseUp () { // resetting the items and segments origin points for the next usage + 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; } } @@ -193,8 +199,10 @@ class PointTool { } this.selectedItems = null; this.setSelectedItems(); - // @todo add back undo - this.onUpdateSvg(); + if (moved) { + performSnapshot(this.undoSnapshot); + this.onUpdateSvg(); + } } } diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js index efa75158..b7e428a7 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -45,18 +45,19 @@ class ReshapeTool extends paper.Tool { * @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ - constructor (setHoveredItem, clearHoveredItem, setSelectedItems, clearSelectedItems, onUpdateSvg) { + constructor (setHoveredItem, clearHoveredItem, setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot) { super(); this.setHoveredItem = setHoveredItem; this.clearHoveredItem = clearHoveredItem; this.onUpdateSvg = onUpdateSvg; + this.undoSnapshot = undoSnapshot; this.prevHoveredItemId = null; this.lastEvent = null; this.mode = ReshapeModes.SELECTION_BOX; this._modeMap = {}; - this._modeMap[ReshapeModes.FILL] = new MoveTool(setSelectedItems, clearSelectedItems, onUpdateSvg); - this._modeMap[ReshapeModes.POINT] = new PointTool(setSelectedItems, clearSelectedItems, onUpdateSvg); - this._modeMap[ReshapeModes.HANDLE] = new HandleTool(setSelectedItems, clearSelectedItems, onUpdateSvg); + this._modeMap[ReshapeModes.FILL] = new MoveTool(setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot); + this._modeMap[ReshapeModes.POINT] = new PointTool(setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot); + this._modeMap[ReshapeModes.HANDLE] = new HandleTool(setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot); this._modeMap[ReshapeModes.SELECTION_BOX] = new SelectionBoxTool(Modes.RESHAPE, setSelectedItems, clearSelectedItems); @@ -221,7 +222,7 @@ class ReshapeTool extends paper.Tool { handleKeyUp (event) { // Backspace, delete if (event.key === 'delete' || event.key === 'backspace') { - deleteSelection(Modes.RESHAPE); + deleteSelection(Modes.RESHAPE, this.undoSnapshot); this.onUpdateSvg(); } } diff --git a/src/helper/selection-tools/rotate-tool.js b/src/helper/selection-tools/rotate-tool.js index 2006cebf..0b197c7a 100644 --- a/src/helper/selection-tools/rotate-tool.js +++ b/src/helper/selection-tools/rotate-tool.js @@ -1,4 +1,5 @@ import paper from 'paper'; +import {performSnapshot} from '../undo'; /** * Tool to handle rotation when dragging the rotation handle in the bounding box tool. @@ -7,11 +8,12 @@ class RotateTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ - constructor (onUpdateSvg) { + constructor (onUpdateSvg, undoSnapshot) { this.rotItems = []; this.rotGroupPivot = null; this.prevRot = []; this.onUpdateSvg = onUpdateSvg; + this.undoSnapshot = undoSnapshot; } /** @@ -63,7 +65,7 @@ class RotateTool { this.rotGroupPivot = null; this.prevRot = []; - // @todo add back undo + performSnapshot(this.undoSnapshot); this.onUpdateSvg(); } } diff --git a/src/helper/selection-tools/scale-tool.js b/src/helper/selection-tools/scale-tool.js index 8744ab72..7518ca9b 100644 --- a/src/helper/selection-tools/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -1,4 +1,5 @@ import paper from 'paper'; +import {performSnapshot} from '../undo'; /** * Tool to handle scaling items by pulling on the handles around the edges of the bounding @@ -8,7 +9,7 @@ class ScaleTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ - constructor (onUpdateSvg) { + constructor (onUpdateSvg, undoSnapshot) { this.pivot = null; this.origPivot = null; this.corner = null; @@ -22,6 +23,7 @@ class ScaleTool { this.boundsScaleHandles = []; this.boundsRotHandles = []; this.onUpdateSvg = onUpdateSvg; + this.undoSnapshot = undoSnapshot; } /** @@ -157,7 +159,7 @@ class ScaleTool { } this.itemGroup.remove(); - // @todo add back undo + performSnapshot(this.undoSnapshot); this.onUpdateSvg(); } _getRectCornerNameByIndex (index) { diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index 93fc31cf..f0592cd1 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -25,12 +25,12 @@ class SelectTool extends paper.Tool { * @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ - constructor (setHoveredItem, clearHoveredItem, setSelectedItems, clearSelectedItems, onUpdateSvg) { + constructor (setHoveredItem, clearHoveredItem, setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot) { super(); this.setHoveredItem = setHoveredItem; this.clearHoveredItem = clearHoveredItem; this.onUpdateSvg = onUpdateSvg; - this.boundingBoxTool = new BoundingBoxTool(setSelectedItems, clearSelectedItems, onUpdateSvg); + this.boundingBoxTool = new BoundingBoxTool(setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot); this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems); this.selectionBoxMode = false; this.prevHoveredItemId = null; @@ -126,7 +126,7 @@ class SelectTool extends paper.Tool { handleKeyUp (event) { // Backspace, delete if (event.key === 'delete' || event.key === 'backspace') { - deleteSelection(Modes.SELECT); + deleteSelection(Modes.SELECT, this.undoSnapshot); this.clearHoveredItem(); this.boundingBoxTool.removeBoundsPath(); this.onUpdateSvg(); diff --git a/src/helper/selection.js b/src/helper/selection.js index 2c12b161..b2d4f7dc 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -4,6 +4,7 @@ import Modes from '../modes/modes'; import {getItemsGroup, isGroup} from './group'; import {getRootItem, isCompoundPathItem, isBoundsItem, isPathItem, isPGTextItem} from './item'; import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compound-path'; +import {performSnapshot} from './undo'; /** * @param {boolean} includeGuides True if guide layer items like the bounding box should @@ -165,20 +166,18 @@ const getSelectedLeafItems = function () { return items; }; -const deleteItemSelection = function (items) { +const deleteItemSelection = function (items, undoSnapshot) { for (let i = 0; i < items.length; i++) { items[i].remove(); } // @todo: Update toolbar state on change paper.project.view.update(); - // @todo add back undo - // pg.undo.snapshot('deleteItemSelection'); + performSnapshot(undoSnapshot); }; -const removeSelectedSegments = function (items) { - // @todo add back undo - // pg.undo.snapshot('removeSelectedSegments'); +const removeSelectedSegments = function (items, undoSnapshot) { + performSnapshot(undoSnapshot); const segmentsToRemove = []; @@ -201,16 +200,16 @@ const removeSelectedSegments = function (items) { return removedSegments; }; -const deleteSelection = function (mode) { +const deleteSelection = function (mode, undoSnapshot) { if (mode === Modes.RESHAPE) { const selectedItems = getSelectedLeafItems(); // If there are points selected remove them. If not delete the item selected. - if (!removeSelectedSegments(selectedItems)) { - deleteItemSelection(selectedItems); + if (!removeSelectedSegments(selectedItems, undoSnapshot)) { + deleteItemSelection(selectedItems, undoSnapshot); } } else { const selectedItems = getSelectedRootItems(); - deleteItemSelection(selectedItems); + deleteItemSelection(selectedItems, undoSnapshot); } }; @@ -281,11 +280,11 @@ const splitPathAtSelectedSegments = function () { } }; -const deleteSegments = function (item) { +const deleteSegments = function (item, undoSnapshot) { if (item.children) { for (let i = 0; i < item.children.length; i++) { const child = item.children[i]; - deleteSegments(child); + deleteSegments(child, undoSnapshot); } } else { const segments = item.segments; @@ -299,7 +298,7 @@ const deleteSegments = function (item) { !segment.previous.selected)) { splitPathRetainSelection(item, j); - deleteSelection(); + deleteSelection(Modes.RESHAPE, undoSnapshot); return; } else if (!item.closed) { @@ -316,26 +315,24 @@ const deleteSegments = function (item) { } }; -const deleteSegmentSelection = function (items) { +const deleteSegmentSelection = function (items, undoSnapshot) { for (let i = 0; i < items.length; i++) { - deleteSegments(items[i]); + deleteSegments(items[i], undoSnapshot); } // @todo: Update toolbar state on change paper.project.view.update(); - // @todo add back undo - // pg.undo.snapshot('deleteSegmentSelection'); + performSnapshot(undoSnapshot); }; -const cloneSelection = function (recursive) { +const cloneSelection = function (recursive, undoSnapshot) { const selectedItems = recursive ? getSelectedLeafItems() : getSelectedRootItems(); for (let i = 0; i < selectedItems.length; i++) { const item = selectedItems[i]; item.clone(); item.selected = false; } - // @todo add back undo - // pg.undo.snapshot('cloneSelection'); + performSnapshot(undoSnapshot); }; // Only returns paths, no compound paths, groups or any other stuff diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 6e85b9f6..92c79d67 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -1,6 +1,7 @@ import {getSelectedLeafItems} from './selection'; import {isPGTextItem, isPointTextItem} from './item'; import {isGroup} from './group'; +import {performSnapshot} from './undo'; const MIXED = 'scratch-paint/style-path/mixed'; @@ -8,7 +9,7 @@ const MIXED = 'scratch-paint/style-path/mixed'; * Called when setting fill color * @param {string} colorString New color, css format */ -const applyFillColorToSelection = function (colorString) { +const applyFillColorToSelection = function (colorString, undoSnapshot) { const items = getSelectedLeafItems(); for (const item of items) { if (isPGTextItem(item)) { @@ -30,14 +31,14 @@ const applyFillColorToSelection = function (colorString) { item.fillColor = colorString; } } - // @todo add back undo + performSnapshot(undoSnapshot); }; /** * Called when setting stroke color * @param {string} colorString New color, css format */ -const applyStrokeColorToSelection = function (colorString) { +const applyStrokeColorToSelection = function (colorString, undoSnapshot) { const items = getSelectedLeafItems(); for (const item of items) { @@ -61,14 +62,14 @@ const applyStrokeColorToSelection = function (colorString) { item.strokeColor = colorString; } } - // @todo add back undo + performSnapshot(undoSnapshot); }; /** * Called when setting stroke width * @param {number} value New stroke width */ -const applyStrokeWidthToSelection = function (value) { +const applyStrokeWidthToSelection = function (value, undoSnapshot) { const items = getSelectedLeafItems(); for (const item of items) { if (isGroup(item)) { @@ -77,7 +78,7 @@ const applyStrokeWidthToSelection = function (value) { item.strokeWidth = value; } } - // @todo add back undo + performSnapshot(undoSnapshot); }; /** @@ -168,7 +169,12 @@ const stylePath = function (path, options) { if (options.isEraser) { path.fillColor = 'white'; } else { - path.fillColor = options.fillColor; + if (options.fillColor) { + path.fillColor = options.fillColor; + } else { + // Make sure something visible is drawn + path.fillColor = 'black'; + } } }; @@ -178,7 +184,12 @@ const styleCursorPreview = function (path, options) { path.strokeColor = 'cornflowerblue'; path.strokeWidth = 1; } else { - path.fillColor = options.fillColor; + if (options.fillColor) { + path.fillColor = options.fillColor; + } else { + // Make sure something visible is drawn + path.fillColor = 'black'; + } } }; diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 2ac4a2ee..12fb6d7f 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -5,6 +5,7 @@ import eraserModeReducer from './eraser-mode'; import colorReducer from './color'; import hoverReducer from './hover'; import selectedItemReducer from './selected-items'; +import undoReducer from './undo'; export default combineReducers({ mode: modeReducer, @@ -12,5 +13,6 @@ export default combineReducers({ eraserMode: eraserModeReducer, color: colorReducer, hoveredItemId: hoverReducer, - selectedItems: selectedItemReducer + selectedItems: selectedItemReducer, + undo: undoReducer }); diff --git a/src/reducers/undo.js b/src/reducers/undo.js index e950df8b..2a669383 100644 --- a/src/reducers/undo.js +++ b/src/reducers/undo.js @@ -13,7 +13,7 @@ const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { case UNDO: - if (state.pointer === -1) { + if (state.pointer <= 0) { log.warn(`Can't undo, undo stack is empty`); return state; } @@ -22,7 +22,7 @@ const reducer = function (state, action) { pointer: state.pointer - 1 }; case REDO: - if (state.pointer === state.stack.length - 1) { + if (state.pointer <= -1 || state.pointer === state.stack.length - 1) { log.warn(`Can't redo, redo stack is empty`); return state; } diff --git a/test/unit/undo-reducer.test.js b/test/unit/undo-reducer.test.js index 4f20299c..6201d9e3 100644 --- a/test/unit/undo-reducer.test.js +++ b/test/unit/undo-reducer.test.js @@ -52,12 +52,20 @@ test('clearUndoState', () => { test('cantUndo', () => { let defaultState; + const state1 = {state: 1}; // Undo when there's no undo stack - const reduxState = undoReducer(defaultState /* state */, undo() /* action */); + 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', () => { @@ -111,23 +119,23 @@ test('undoSnapshotCantRedo', () => { let defaultState; const state1 = {state: 1}; const state2 = {state: 2}; + const state3 = {state: 3}; - // Push 2 states then undo twice + // 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 */); - reduxState = undoReducer(reduxState /* state */, undo() /* action */); - expect(reduxState.pointer).toEqual(-1); + expect(reduxState.pointer).toEqual(0); expect(reduxState.stack).toHaveLength(2); // Snapshot - reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */); + 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[0]).toEqual(state2); + expect(newReduxState.stack[1]).toEqual(state3); });