From 8baf731328029d887d5e8f39d9aba6dcb1fb20cd Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 4 Oct 2017 17:18:00 -0400 Subject: [PATCH 01/13] add undo reducer --- src/reducers/undo.js | 79 ++++++++++++++++++++ test/unit/undo-reducer.test.js | 133 +++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 src/reducers/undo.js create mode 100644 test/unit/undo-reducer.test.js diff --git a/src/reducers/undo.js b/src/reducers/undo.js new file mode 100644 index 00000000..e950df8b --- /dev/null +++ b/src/reducers/undo.js @@ -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 === -1) { + log.warn(`Can't undo, undo stack is empty`); + return state; + } + return { + stack: state.stack, + pointer: state.pointer - 1 + }; + case REDO: + if (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 +}; diff --git a/test/unit/undo-reducer.test.js b/test/unit/undo-reducer.test.js new file mode 100644 index 00000000..4f20299c --- /dev/null +++ b/test/unit/undo-reducer.test.js @@ -0,0 +1,133 @@ +/* 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; + + // Undo when there's no undo stack + const reduxState = undoReducer(defaultState /* state */, undo() /* action */); + + expect(reduxState.pointer).toEqual(-1); + expect(reduxState.stack).toHaveLength(0); +}); + +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}; + + // Push 2 states then undo twice + 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.stack).toHaveLength(2); + + // Snapshot + reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* 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); +}); From 28464b237b968c8939c1555d2ee658bb90331d2a Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Oct 2017 15:41:22 -0400 Subject: [PATCH 02/13] call undo --- src/components/paint-editor.jsx | 4 ++ src/containers/blob/blob.js | 11 ++---- src/containers/brush-mode.jsx | 12 +++++- src/containers/eraser-mode.jsx | 10 ++++- src/containers/fill-color-indicator.jsx | 5 ++- src/containers/line-mode.jsx | 23 ++++++++---- src/containers/paint-editor.jsx | 36 ++++++++++++++++-- src/containers/paper-canvas.jsx | 18 ++++++++- src/containers/reshape-mode.jsx | 11 +++++- src/containers/select-mode.jsx | 11 +++++- src/containers/stroke-color-indicator.jsx | 5 ++- src/containers/stroke-width-indicator.jsx | 5 ++- .../selection-tools/bounding-box-tool.js | 8 ++-- src/helper/selection-tools/handle-tool.js | 29 ++++++++++++--- src/helper/selection-tools/move-tool.js | 17 ++++++--- src/helper/selection-tools/point-tool.js | 14 +++++-- src/helper/selection-tools/reshape-tool.js | 11 +++--- src/helper/selection-tools/rotate-tool.js | 6 ++- src/helper/selection-tools/scale-tool.js | 6 ++- src/helper/selection-tools/select-tool.js | 6 +-- src/helper/selection.js | 37 +++++++++---------- src/helper/style-path.js | 27 ++++++++++---- src/reducers/scratch-paint-reducer.js | 4 +- src/reducers/undo.js | 4 +- test/unit/undo-reducer.test.js | 20 +++++++--- 25 files changed, 243 insertions(+), 97 deletions(-) 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); }); From 4300924671f713511707d5a4aba9a581b87551a7 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Oct 2017 16:27:47 -0400 Subject: [PATCH 03/13] clean up unused things in selection so I dont have to add undo to them --- src/helper/selection.js | 165 +++++----------------------------------- 1 file changed, 19 insertions(+), 146 deletions(-) diff --git a/src/helper/selection.js b/src/helper/selection.js index b2d4f7dc..69d19177 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -58,7 +58,7 @@ const selectItemSegments = function (item, state) { } }; -const setGroupSelection = function (root, selected, fullySelected) { +const _setGroupSelection = function (root, selected, fullySelected) { root.fullySelected = fullySelected; root.selected = selected; // select children of compound-path or group @@ -67,7 +67,7 @@ const setGroupSelection = function (root, selected, fullySelected) { if (children) { for (const child of children) { if (isGroup(child)) { - setGroupSelection(child, selected, fullySelected); + _setGroupSelection(child, selected, fullySelected); } else { child.fullySelected = fullySelected; child.selected = selected; @@ -86,12 +86,12 @@ const setItemSelection = function (item, state, fullySelected) { // do it recursive setItemSelection(parentGroup, state, fullySelected); } else if (itemsCompoundPath) { - setGroupSelection(itemsCompoundPath, state, fullySelected); + _setGroupSelection(itemsCompoundPath, state, fullySelected); } else { if (item.data && item.data.noSelect) { return; } - setGroupSelection(item, state, fullySelected); + _setGroupSelection(item, state, fullySelected); } // @todo: Update toolbar state on change @@ -166,19 +166,20 @@ const getSelectedLeafItems = function () { return items; }; -const deleteItemSelection = function (items, undoSnapshot) { +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(); - performSnapshot(undoSnapshot); + if (items.lenth > 0) { + paper.project.view.update(); + performSnapshot(undoSnapshot); + } }; -const removeSelectedSegments = function (items, undoSnapshot) { +const _removeSelectedSegments = function (items, undoSnapshot) { performSnapshot(undoSnapshot); - const segmentsToRemove = []; for (let i = 0; i < items.length; i++) { @@ -197,6 +198,10 @@ const removeSelectedSegments = function (items, undoSnapshot) { seg.remove(); removedSegments = true; } + if (removedSegments) { + paper.project.view.update(); + performSnapshot(undoSnapshot); + } return removedSegments; }; @@ -204,127 +209,15 @@ 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, undoSnapshot)) { - deleteItemSelection(selectedItems, undoSnapshot); + if (!_removeSelectedSegments(selectedItems, undoSnapshot)) { + _deleteItemSelection(selectedItems, undoSnapshot); } } else { const selectedItems = getSelectedRootItems(); - deleteItemSelection(selectedItems, undoSnapshot); + _deleteItemSelection(selectedItems, undoSnapshot); } }; -const splitPathRetainSelection = function (path, index, deselectSplitSegments) { - 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, undoSnapshot) { - if (item.children) { - for (let i = 0; i < item.children.length; i++) { - const child = item.children[i]; - deleteSegments(child, undoSnapshot); - } - } 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(Modes.RESHAPE, undoSnapshot); - 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, undoSnapshot) { - for (let i = 0; i < items.length; i++) { - deleteSegments(items[i], undoSnapshot); - } - - // @todo: Update toolbar state on change - paper.project.view.update(); - performSnapshot(undoSnapshot); -}; - const cloneSelection = function (recursive, undoSnapshot) { const selectedItems = recursive ? getSelectedLeafItems() : getSelectedRootItems(); for (let i = 0; i < selectedItems.length; i++) { @@ -335,21 +228,7 @@ const cloneSelection = function (recursive, undoSnapshot) { performSnapshot(undoSnapshot); }; -// Only returns paths, no compound paths, groups or any other stuff -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 _checkBoundsItem = function (selectionRect, item, event) { const itemBounds = new paper.Path([ item.localToGlobal(item.internalBounds.topLeft), item.localToGlobal(item.internalBounds.topRight), @@ -438,7 +317,7 @@ const _handleRectangularSelectionItems = function (item, event, rect, mode, root // @todo: Update toolbar state on change } else if (isBoundsItem(item)) { - if (checkBoundsItem(rect, item, event)) { + if (_checkBoundsItem(rect, item, event)) { return false; } } @@ -521,16 +400,10 @@ export { selectAllSegments, clearSelection, deleteSelection, - deleteItemSelection, - deleteSegmentSelection, - splitPathAtSelectedSegments, cloneSelection, setItemSelection, - setGroupSelection, getSelectedLeafItems, - getSelectedPaths, getSelectedRootItems, - removeSelectedSegments, processRectangularSelection, selectRootItem, shouldShowIfSelection, From 345a43e12710b1a4b8e0b4e47cbdeb0276b68ce5 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Oct 2017 18:12:22 -0400 Subject: [PATCH 04/13] merge onUpdateSvg and undoSnapshot --- src/components/paint-editor.jsx | 12 ++- src/containers/blob/blob.js | 11 +-- src/containers/brush-mode.jsx | 9 +- src/containers/eraser-mode.jsx | 9 +- src/containers/fill-color-indicator.jsx | 36 ++++++-- src/containers/line-mode.jsx | 13 +-- src/containers/paint-editor.jsx | 9 +- src/containers/reshape-mode.jsx | 10 +-- src/containers/select-mode.jsx | 10 +-- src/containers/stroke-color-indicator.jsx | 36 ++++++-- src/containers/stroke-width-indicator.jsx | 36 ++++++-- .../selection-tools/bounding-box-tool.js | 8 +- src/helper/selection-tools/handle-tool.js | 5 +- src/helper/selection-tools/move-tool.js | 7 +- src/helper/selection-tools/point-tool.js | 5 +- src/helper/selection-tools/reshape-tool.js | 12 ++- src/helper/selection-tools/rotate-tool.js | 5 +- src/helper/selection-tools/scale-tool.js | 5 +- src/helper/selection-tools/select-tool.js | 7 +- src/helper/selection.js | 21 +++-- src/helper/style-path.js | 85 +++++++++++++------ 21 files changed, 209 insertions(+), 142 deletions(-) diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 46f9cc72..f799b9df 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -103,11 +103,17 @@ class PaintEditorComponent extends React.Component { {/* Second Row */}
{/* fill */} - + {/* stroke */} - + {/* stroke width */} - +
Mode tools diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index 3a24597f..33c2607f 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -4,7 +4,6 @@ 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 @@ -27,15 +26,14 @@ 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 */ - constructor (updateCallback, clearSelectedItems, undoSnapshot) { + constructor (onUpdateSvg, clearSelectedItems) { this.broadBrushHelper = new BroadBrushHelper(); this.segmentBrushHelper = new SegmentBrushHelper(); - this.updateCallback = updateCallback; + this.onUpdateSvg = onUpdateSvg; 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; @@ -145,8 +143,7 @@ class Blobbiness { } blob.cursorPreview.visible = false; - blob.updateCallback(); - performSnapshot(blob.undoSnapshot); + blob.onUpdateSvg(); blob.cursorPreview.visible = true; blob.cursorPreview.bringToFront(); blob.cursorPreview.position = event.point; diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index 6c4a0b7c..b4f899ab 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -8,7 +8,6 @@ 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'; @@ -22,7 +21,7 @@ class BrushMode extends React.Component { 'onScroll' ]); this.blob = new Blobbiness( - this.props.onUpdateSvg, this.props.clearSelectedItems, this.props.undoSnapshot); + this.props.onUpdateSvg, this.props.clearSelectedItems); } componentDidMount () { if (this.props.isBrushModeActive) { @@ -91,8 +90,7 @@ BrushMode.propTypes = { }).isRequired, handleMouseDown: PropTypes.func.isRequired, isBrushModeActive: PropTypes.bool.isRequired, - onUpdateSvg: PropTypes.func.isRequired, - undoSnapshot: PropTypes.func.isRequired + onUpdateSvg: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -109,9 +107,6 @@ 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 5d5f6baa..efd920fe 100644 --- a/src/containers/eraser-mode.jsx +++ b/src/containers/eraser-mode.jsx @@ -6,7 +6,6 @@ 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'; @@ -19,7 +18,7 @@ class EraserMode extends React.Component { 'onScroll' ]); this.blob = new Blobbiness( - this.props.onUpdateSvg, this.props.clearSelectedItems, this.props.undoSnapshot); + this.props.onUpdateSvg, this.props.clearSelectedItems); } componentDidMount () { if (this.props.isEraserModeActive) { @@ -74,8 +73,7 @@ EraserMode.propTypes = { }), handleMouseDown: PropTypes.func.isRequired, isEraserModeActive: PropTypes.bool.isRequired, - onUpdateSvg: PropTypes.func.isRequired, - undoSnapshot: PropTypes.func.isRequired + onUpdateSvg: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -91,9 +89,6 @@ 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 8c3cb5aa..904071a8 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -1,22 +1,48 @@ 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 FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; import {applyFillColorToSelection} from '../helper/style-path'; -import {performSnapshot} from '../helper/undo'; -import {undoSnapshot} from '../reducers/undo'; + +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 ( + + ); + } +} const mapStateToProps = state => ({ fillColor: state.scratchPaint.color.fillColor }); const mapDispatchToProps = dispatch => ({ onChangeFillColor: fillColor => { - applyFillColorToSelection(fillColor, undoSnapshot); - performSnapshot(snapshot => dispatch(undoSnapshot(snapshot))); dispatch(changeFillColor(fillColor)); } }); +FillColorIndicator.propTypes = { + fillColor: PropTypes.string, + onChangeFillColor: PropTypes.func.isRequired, + onUpdateSvg: PropTypes.func.isRequired +}; + export default connect( mapStateToProps, mapDispatchToProps -)(FillColorIndicatorComponent); +)(FillColorIndicator); diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 0e11e3d4..bd7a1980 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -10,8 +10,6 @@ import {MIXED} from '../helper/style-path'; import {changeMode} from '../reducers/modes'; 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'; @@ -210,11 +208,10 @@ class LineMode extends React.Component { } this.hitResult = null; } - this.props.onUpdateSvg(); + this.props.setSelectedItems(); - if (this.path) { - performSnapshot(this.props.undoSnapshot); + this.props.onUpdateSvg(); } } toleranceSquared () { @@ -287,8 +284,7 @@ LineMode.propTypes = { handleMouseDown: PropTypes.func.isRequired, isLineModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, - setSelectedItems: PropTypes.func.isRequired, - undoSnapshot: PropTypes.func.isRequired + setSelectedItems: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -307,9 +303,6 @@ 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 232a2b43..a7be96b2 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -3,10 +3,10 @@ import React from 'react'; import PaintEditorComponent from '../components/paint-editor.jsx'; import {changeMode} from '../reducers/modes'; -import {undo, redo} from '../reducers/undo'; +import {undo, redo, undoSnapshot} from '../reducers/undo'; import {getGuideLayer} from '../helper/layer'; -import {performUndo, performRedo} from '../helper/undo'; +import {performUndo, performRedo, performSnapshot} from '../helper/undo'; import Modes from '../modes/modes'; import {connect} from 'react-redux'; @@ -39,6 +39,7 @@ class PaintEditor extends React.Component { }), paper.project.view.center.x - bounds.x, paper.project.view.center.y - bounds.y); + performSnapshot(this.props.undoSnapshot); getGuideLayer().visible = true; } handleUndo () { @@ -69,6 +70,7 @@ PaintEditor.propTypes = { rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, svg: PropTypes.string, + undoSnapshot: PropTypes.func.isRequired, undoState: PropTypes.shape({ stack: PropTypes.arrayOf(PropTypes.object).isRequired, pointer: PropTypes.number.isRequired @@ -95,6 +97,9 @@ const mapDispatchToProps = dispatch => ({ }, onRedo: () => { dispatch(redo()); + }, + undoSnapshot: snapshot => { + dispatch(undoSnapshot(snapshot)); } }); diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx index f192b454..bffb65bd 100644 --- a/src/containers/reshape-mode.jsx +++ b/src/containers/reshape-mode.jsx @@ -8,7 +8,6 @@ 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'; @@ -46,8 +45,7 @@ class ReshapeMode extends React.Component { this.props.clearHoveredItem, this.props.setSelectedItems, this.props.clearSelectedItems, - this.props.onUpdateSvg, - this.props.undoSnapshot + this.props.onUpdateSvg ); this.tool.setPrevHoveredItemId(this.props.hoveredItemId); this.tool.activate(); @@ -73,8 +71,7 @@ ReshapeMode.propTypes = { isReshapeModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, setHoveredItem: PropTypes.func.isRequired, - setSelectedItems: PropTypes.func.isRequired, - undoSnapshot: PropTypes.func.isRequired + setSelectedItems: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -96,9 +93,6 @@ 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 0d946f60..572aa12a 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -7,7 +7,6 @@ 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'; @@ -46,8 +45,7 @@ class SelectMode extends React.Component { this.props.clearHoveredItem, this.props.setSelectedItems, this.props.clearSelectedItems, - this.props.onUpdateSvg, - this.props.undoSnapshot + this.props.onUpdateSvg ); this.tool.activate(); } @@ -71,8 +69,7 @@ SelectMode.propTypes = { isSelectModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, setHoveredItem: PropTypes.func.isRequired, - setSelectedItems: PropTypes.func.isRequired, - undoSnapshot: PropTypes.func.isRequired + setSelectedItems: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -94,9 +91,6 @@ 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 20bd9fee..74619b76 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -1,22 +1,48 @@ 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 StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx'; import {applyStrokeColorToSelection} from '../helper/style-path'; -import {performSnapshot} from '../helper/undo'; -import {undoSnapshot} from '../reducers/undo'; + +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 ( + + ); + } +} const mapStateToProps = state => ({ strokeColor: state.scratchPaint.color.strokeColor }); const mapDispatchToProps = dispatch => ({ onChangeStrokeColor: strokeColor => { - applyStrokeColorToSelection(strokeColor, undoSnapshot); - performSnapshot(snapshot => dispatch(undoSnapshot(snapshot))); dispatch(changeStrokeColor(strokeColor)); } }); +StrokeColorIndicator.propTypes = { + onChangeStrokeColor: PropTypes.func.isRequired, + onUpdateSvg: PropTypes.func.isRequired, + strokeColor: PropTypes.string +}; + export default connect( mapStateToProps, mapDispatchToProps -)(StrokeColorIndicatorComponent); +)(StrokeColorIndicator); diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx index 105263c9..fe836b8a 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -1,22 +1,48 @@ 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 StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx'; import {applyStrokeWidthToSelection} from '../helper/style-path'; -import {performSnapshot} from '../helper/undo'; -import {undoSnapshot} from '../reducers/undo'; + +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 ( + + ); + } +} const mapStateToProps = state => ({ strokeWidth: state.scratchPaint.color.strokeWidth }); const mapDispatchToProps = dispatch => ({ onChangeStrokeWidth: strokeWidth => { - applyStrokeWidthToSelection(strokeWidth, undoSnapshot); - performSnapshot(snapshot => dispatch(undoSnapshot(snapshot))); dispatch(changeStrokeWidth(strokeWidth)); } }); +StrokeWidthIndicator.propTypes = { + onChangeStrokeWidth: PropTypes.func.isRequired, + onUpdateSvg: PropTypes.func.isRequired, + strokeWidth: PropTypes.number +}; + export default connect( mapStateToProps, mapDispatchToProps -)(StrokeWidthIndicatorComponent); +)(StrokeWidthIndicator); diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index 46d94aa7..1826f365 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, undoSnapshot) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { this.onUpdateSvg = onUpdateSvg; this.mode = null; this.boundsPath = null; this.boundsScaleHandles = []; this.boundsRotHandles = []; this._modeMap = {}; - 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); + this._modeMap[Modes.SCALE] = new ScaleTool(onUpdateSvg); + this._modeMap[Modes.ROTATE] = new RotateTool(onUpdateSvg); + this._modeMap[Modes.MOVE] = new MoveTool(setSelectedItems, clearSelectedItems, onUpdateSvg); } /** diff --git a/src/helper/selection-tools/handle-tool.js b/src/helper/selection-tools/handle-tool.js index fd55b0f7..62e0b345 100644 --- a/src/helper/selection-tools/handle-tool.js +++ b/src/helper/selection-tools/handle-tool.js @@ -1,5 +1,4 @@ import {clearSelection, getSelectedLeafItems} from '../selection'; -import {performSnapshot} from '../undo'; /** Sub tool of the Reshape tool for moving handles, which adjust bezier curves. */ class HandleTool { @@ -8,12 +7,11 @@ 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, undoSnapshot) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { this.hitType = null; this.setSelectedItems = setSelectedItems; this.clearSelectedItems = clearSelectedItems; this.onUpdateSvg = onUpdateSvg; - this.undoSnapshot = undoSnapshot; this.selectedItems = []; } /** @@ -83,7 +81,6 @@ class HandleTool { } } 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 c55cf6db..6e39673d 100644 --- a/src/helper/selection-tools/move-tool.js +++ b/src/helper/selection-tools/move-tool.js @@ -2,7 +2,6 @@ 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. @@ -13,12 +12,11 @@ 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, undoSnapshot) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { this.setSelectedItems = setSelectedItems; this.clearSelectedItems = clearSelectedItems; this.selectedItems = null; this.onUpdateSvg = onUpdateSvg; - this.undoSnapshot = undoSnapshot; } /** @@ -53,7 +51,7 @@ class MoveTool { } this._select(item, true, hitProperties.subselect); } - if (hitProperties.clone) cloneSelection(hitProperties.subselect, this.undoSnapshot); + if (hitProperties.clone) cloneSelection(hitProperties.subselect, this.onUpdateSvg); this.selectedItems = getSelectedLeafItems(); } /** @@ -107,7 +105,6 @@ class MoveTool { this.selectedItems = null; 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 73bcbd27..f964f2eb 100644 --- a/src/helper/selection-tools/point-tool.js +++ b/src/helper/selection-tools/point-tool.js @@ -1,7 +1,6 @@ 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 { @@ -10,7 +9,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, undoSnapshot) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { /** * 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. @@ -30,7 +29,6 @@ class PointTool { this.setSelectedItems = setSelectedItems; this.clearSelectedItems = clearSelectedItems; this.onUpdateSvg = onUpdateSvg; - this.undoSnapshot = undoSnapshot; } /** @@ -200,7 +198,6 @@ class PointTool { this.selectedItems = null; this.setSelectedItems(); 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 b7e428a7..38ecebae 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -45,19 +45,18 @@ 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, undoSnapshot) { + constructor (setHoveredItem, clearHoveredItem, setSelectedItems, clearSelectedItems, onUpdateSvg) { 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, undoSnapshot); - this._modeMap[ReshapeModes.POINT] = new PointTool(setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot); - this._modeMap[ReshapeModes.HANDLE] = new HandleTool(setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot); + 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.SELECTION_BOX] = new SelectionBoxTool(Modes.RESHAPE, setSelectedItems, clearSelectedItems); @@ -222,8 +221,7 @@ class ReshapeTool extends paper.Tool { handleKeyUp (event) { // Backspace, delete if (event.key === 'delete' || event.key === 'backspace') { - deleteSelection(Modes.RESHAPE, this.undoSnapshot); - this.onUpdateSvg(); + deleteSelection(Modes.RESHAPE, this.onUpdateSvg); } } deactivateTool () { diff --git a/src/helper/selection-tools/rotate-tool.js b/src/helper/selection-tools/rotate-tool.js index 0b197c7a..902eaf36 100644 --- a/src/helper/selection-tools/rotate-tool.js +++ b/src/helper/selection-tools/rotate-tool.js @@ -1,5 +1,4 @@ import paper from 'paper'; -import {performSnapshot} from '../undo'; /** * Tool to handle rotation when dragging the rotation handle in the bounding box tool. @@ -8,12 +7,11 @@ class RotateTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ - constructor (onUpdateSvg, undoSnapshot) { + constructor (onUpdateSvg) { this.rotItems = []; this.rotGroupPivot = null; this.prevRot = []; this.onUpdateSvg = onUpdateSvg; - this.undoSnapshot = undoSnapshot; } /** @@ -65,7 +63,6 @@ class RotateTool { this.rotGroupPivot = null; this.prevRot = []; - performSnapshot(this.undoSnapshot); this.onUpdateSvg(); } } diff --git a/src/helper/selection-tools/scale-tool.js b/src/helper/selection-tools/scale-tool.js index 7518ca9b..e58e7bd9 100644 --- a/src/helper/selection-tools/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -1,5 +1,4 @@ import paper from 'paper'; -import {performSnapshot} from '../undo'; /** * Tool to handle scaling items by pulling on the handles around the edges of the bounding @@ -9,7 +8,7 @@ class ScaleTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ - constructor (onUpdateSvg, undoSnapshot) { + constructor (onUpdateSvg) { this.pivot = null; this.origPivot = null; this.corner = null; @@ -23,7 +22,6 @@ class ScaleTool { this.boundsScaleHandles = []; this.boundsRotHandles = []; this.onUpdateSvg = onUpdateSvg; - this.undoSnapshot = undoSnapshot; } /** @@ -159,7 +157,6 @@ class ScaleTool { } this.itemGroup.remove(); - 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 f0592cd1..14f9e5b2 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, undoSnapshot) { + constructor (setHoveredItem, clearHoveredItem, setSelectedItems, clearSelectedItems, onUpdateSvg) { super(); this.setHoveredItem = setHoveredItem; this.clearHoveredItem = clearHoveredItem; this.onUpdateSvg = onUpdateSvg; - this.boundingBoxTool = new BoundingBoxTool(setSelectedItems, clearSelectedItems, onUpdateSvg, undoSnapshot); + this.boundingBoxTool = new BoundingBoxTool(setSelectedItems, clearSelectedItems, onUpdateSvg); this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems); this.selectionBoxMode = false; this.prevHoveredItemId = null; @@ -126,10 +126,9 @@ class SelectTool extends paper.Tool { handleKeyUp (event) { // Backspace, delete if (event.key === 'delete' || event.key === 'backspace') { - deleteSelection(Modes.SELECT, this.undoSnapshot); + deleteSelection(Modes.SELECT, this.onUpdateSvg); this.clearHoveredItem(); this.boundingBoxTool.removeBoundsPath(); - this.onUpdateSvg(); } } deactivateTool () { diff --git a/src/helper/selection.js b/src/helper/selection.js index 69d19177..a7bff794 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -4,7 +4,6 @@ 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 @@ -166,7 +165,7 @@ const getSelectedLeafItems = function () { return items; }; -const _deleteItemSelection = function (items, undoSnapshot) { +const _deleteItemSelection = function (items, onUpdateSvg) { for (let i = 0; i < items.length; i++) { items[i].remove(); } @@ -174,11 +173,11 @@ const _deleteItemSelection = function (items, undoSnapshot) { // @todo: Update toolbar state on change if (items.lenth > 0) { paper.project.view.update(); - performSnapshot(undoSnapshot); + onUpdateSvg(); } }; -const _removeSelectedSegments = function (items, undoSnapshot) { +const _removeSelectedSegments = function (items, onUpdateSvg) { performSnapshot(undoSnapshot); const segmentsToRemove = []; @@ -200,32 +199,32 @@ const _removeSelectedSegments = function (items, undoSnapshot) { } if (removedSegments) { paper.project.view.update(); - performSnapshot(undoSnapshot); + onUpdateSvg(); } return removedSegments; }; -const deleteSelection = function (mode, undoSnapshot) { +const deleteSelection = function (mode, onUpdateSvg) { if (mode === Modes.RESHAPE) { const selectedItems = getSelectedLeafItems(); // If there are points selected remove them. If not delete the item selected. - if (!_removeSelectedSegments(selectedItems, undoSnapshot)) { - _deleteItemSelection(selectedItems, undoSnapshot); + if (!_removeSelectedSegments(selectedItems, onUpdateSvg)) { + _deleteItemSelection(selectedItems, onUpdateSvg); } } else { const selectedItems = getSelectedRootItems(); - _deleteItemSelection(selectedItems, undoSnapshot); + _deleteItemSelection(selectedItems, onUpdateSvg); } }; -const cloneSelection = function (recursive, undoSnapshot) { +const cloneSelection = function (recursive, onUpdateSvg) { const selectedItems = recursive ? getSelectedLeafItems() : getSelectedRootItems(); for (let i = 0; i < selectedItems.length; i++) { const item = selectedItems[i]; item.clone(); item.selected = false; } - performSnapshot(undoSnapshot); + onUpdateSvg(); }; const _checkBoundsItem = function (selectionRect, item, event) { diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 92c79d67..6910b573 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -1,46 +1,63 @@ +import paper from 'paper'; import {getSelectedLeafItems} from './selection'; import {isPGTextItem, isPointTextItem} from './item'; import {isGroup} from './group'; -import {performSnapshot} from './undo'; const MIXED = 'scratch-paint/style-path/mixed'; /** * Called when setting fill color * @param {string} colorString New color, css format + * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ -const applyFillColorToSelection = function (colorString, undoSnapshot) { +const applyFillColorToSelection = function (colorString, onUpdateSvg) { const items = getSelectedLeafItems(); + let changed = false; for (const item of items) { if (isPGTextItem(item)) { for (const child of item.children) { if (child.children) { for (const path of child.children) { 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) { - child.fillColor = colorString; + if ((child.fillColor === null && colorString) || + child.fillColor.toCSS() !== new paper.Color(colorString).toCSS()) { + changed = true; + child.fillColor = colorString; + } } } } else { if (isPointTextItem(item) && !colorString) { 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; + } } } - performSnapshot(undoSnapshot); + if (changed) { + onUpdateSvg(); + } }; /** * Called when setting stroke color * @param {string} colorString New color, css format + * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ -const applyStrokeColorToSelection = function (colorString, undoSnapshot) { +const applyStrokeColorToSelection = function (colorString, onUpdateSvg) { const items = getSelectedLeafItems(); - + let changed = false; for (const item of items) { if (isPGTextItem(item)) { if (item.children) { @@ -48,37 +65,53 @@ const applyStrokeColorToSelection = function (colorString, undoSnapshot) { if (child.children) { for (const path of child.children) { 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) { - child.strokeColor = colorString; + if (child.strokeColor !== colorString) { + changed = true; + child.strokeColor = colorString; + } } } } 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; } } - performSnapshot(undoSnapshot); + if (changed) { + onUpdateSvg(); + } }; /** * Called when setting stroke width * @param {number} value New stroke width + * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ -const applyStrokeWidthToSelection = function (value, undoSnapshot) { +const applyStrokeWidthToSelection = function (value, onUpdateSvg) { const items = getSelectedLeafItems(); for (const item of items) { if (isGroup(item)) { continue; - } else { + } else if (item.strokeWidth !== value) { item.strokeWidth = value; + onUpdateSvg(); } } - performSnapshot(undoSnapshot); }; /** @@ -168,13 +201,11 @@ const getColorsFromSelection = function (selectedItems) { const stylePath = function (path, options) { if (options.isEraser) { path.fillColor = 'white'; + } else if (options.fillColor) { + path.fillColor = options.fillColor; } else { - if (options.fillColor) { - path.fillColor = options.fillColor; - } else { - // Make sure something visible is drawn - path.fillColor = 'black'; - } + // Make sure something visible is drawn + path.fillColor = 'black'; } }; @@ -183,13 +214,11 @@ const styleCursorPreview = function (path, options) { path.fillColor = 'white'; path.strokeColor = 'cornflowerblue'; path.strokeWidth = 1; + } else if (options.fillColor) { + path.fillColor = options.fillColor; } else { - if (options.fillColor) { - path.fillColor = options.fillColor; - } else { - // Make sure something visible is drawn - path.fillColor = 'black'; - } + // Make sure something visible is drawn + path.fillColor = 'black'; } }; From 7523af09f0b799c2b5cf4ee6fe7f0855fe880978 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Oct 2017 18:12:28 -0400 Subject: [PATCH 05/13] add missing file --- src/helper/undo.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/helper/undo.js diff --git a/src/helper/undo.js b/src/helper/undo.js new file mode 100644 index 00000000..449aa834 --- /dev/null +++ b/src/helper/undo.js @@ -0,0 +1,45 @@ +// 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) { + paper.project.clear(); + paper.project.importJSON(entry.json); + paper.view.update(); +}; + +const performUndo = function (undoState, dispatchPerformUndo) { + if (undoState.pointer > 0) { + _restore(undoState.stack[undoState.pointer - 1]); + dispatchPerformUndo(); + + // @todo enable/disable buttons + // updateButtonVisibility(); + } +}; + + +const performRedo = function (undoState, dispatchPerformRedo) { + if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) { + _restore(undoState.stack[undoState.pointer + 1]); + dispatchPerformRedo(); + + // @todo enable/disable buttons + // updateButtonVisibility(); + } +}; + +export { + performSnapshot, + performUndo, + performRedo +}; From 87eb78ad49e8e94888e9e3c3fd827a3eba5fec63 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Oct 2017 18:24:52 -0400 Subject: [PATCH 06/13] update stage on undo and redo --- src/containers/paint-editor.jsx | 10 ++++++---- src/helper/undo.js | 11 ++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index a7be96b2..c5154c0f 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -28,7 +28,7 @@ class PaintEditor extends React.Component { componentWillUnmount () { document.removeEventListener('keydown', this.props.onKeyPress); } - handleUpdateSvg () { + handleUpdateSvg (skipSnapshot) { // Hide bounding box getGuideLayer().visible = false; const bounds = paper.project.activeLayer.bounds; @@ -39,14 +39,16 @@ class PaintEditor extends React.Component { }), paper.project.view.center.x - bounds.x, paper.project.view.center.y - bounds.y); - performSnapshot(this.props.undoSnapshot); + if (!skipSnapshot) { + performSnapshot(this.props.undoSnapshot); + } getGuideLayer().visible = true; } handleUndo () { - performUndo(this.props.undoState, this.props.onUndo); + performUndo(this.props.undoState, this.props.onUndo, this.handleUpdateSvg); } handleRedo () { - performRedo(this.props.undoState, this.props.onRedo); + performRedo(this.props.undoState, this.props.onRedo, this.handleUpdateSvg); } render () { return ( diff --git a/src/helper/undo.js b/src/helper/undo.js index 449aa834..45c13826 100644 --- a/src/helper/undo.js +++ b/src/helper/undo.js @@ -11,15 +11,16 @@ const performSnapshot = function (dispatchPerformSnapshot) { // updateButtonVisibility(); }; -const _restore = function (entry) { +const _restore = function (entry, onUpdateSvg) { paper.project.clear(); paper.project.importJSON(entry.json); paper.view.update(); + onUpdateSvg(true /* skipSnapshot */); }; -const performUndo = function (undoState, dispatchPerformUndo) { +const performUndo = function (undoState, dispatchPerformUndo, onUpdateSvg) { if (undoState.pointer > 0) { - _restore(undoState.stack[undoState.pointer - 1]); + _restore(undoState.stack[undoState.pointer - 1], onUpdateSvg); dispatchPerformUndo(); // @todo enable/disable buttons @@ -28,9 +29,9 @@ const performUndo = function (undoState, dispatchPerformUndo) { }; -const performRedo = function (undoState, dispatchPerformRedo) { +const performRedo = function (undoState, dispatchPerformRedo, onUpdateSvg) { if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) { - _restore(undoState.stack[undoState.pointer + 1]); + _restore(undoState.stack[undoState.pointer + 1], onUpdateSvg); dispatchPerformRedo(); // @todo enable/disable buttons From 01d1ddec9df6f59aa3522dffd46ac1ecd75c6628 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Oct 2017 18:39:48 -0400 Subject: [PATCH 07/13] Make unimplemented buttons grey --- src/components/paint-editor.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index f799b9df..459bcd9d 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -76,12 +76,12 @@ class PaintEditorComponent extends React.Component {
@@ -90,12 +90,12 @@ class PaintEditorComponent extends React.Component {
From 4d0e2b4c447ed9c22caf0ccf0614fb610a79bf93 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Oct 2017 18:53:35 -0400 Subject: [PATCH 08/13] remove the default imported costume --- src/playground/playground.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/playground/playground.jsx b/src/playground/playground.jsx index f59ef2b0..161e2f70 100644 --- a/src/playground/playground.jsx +++ b/src/playground/playground.jsx @@ -32,7 +32,6 @@ ReactDOM.render(( From 4d9ecb2a2cc8ba192229f8adbe202e23b980c5b1 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 10 Oct 2017 17:27:18 -0400 Subject: [PATCH 09/13] fix lint --- src/components/paint-editor.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 459bcd9d..2982d86b 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -76,12 +76,12 @@ class PaintEditorComponent extends React.Component { @@ -90,12 +90,12 @@ class PaintEditorComponent extends React.Component { From affe8463be098135ca557061ca30a71973f9b8aa Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 11 Oct 2017 17:17:19 -0400 Subject: [PATCH 10/13] fix undo removes cursor preview --- src/containers/blob/blob.js | 5 ++++- src/containers/line-mode.jsx | 1 - src/helper/selection.js | 1 - src/helper/undo.js | 3 +++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index 33c2607f..3d93ebec 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 {getGuideLayer} from '../../helper/layer'; /** * Shared code for the brush and eraser mode. Adds functions on the paper tool object @@ -166,7 +167,7 @@ class Blobbiness { this.cursorPreviewLastPoint = point; } - if (this.cursorPreview && + if (this.cursorPreview && this.cursorPreview.parent && this.brushSize === this.options.brushSize && this.fillColor === this.options.fillColor && this.strokeColor === this.options.strokeColor) { @@ -176,6 +177,8 @@ class Blobbiness { center: point, radius: this.options.brushSize / 2 }); + newPreview.parent = getGuideLayer(); + newPreview.data.isHelperItem = true; if (this.cursorPreview) { this.cursorPreview.remove(); } diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index bd7a1980..56cf6dc2 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -4,7 +4,6 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; -import {changeStrokeWidth} from '../reducers/stroke-width'; import {clearSelection, getSelectedLeafItems} from '../helper/selection'; import {MIXED} from '../helper/style-path'; import {changeMode} from '../reducers/modes'; diff --git a/src/helper/selection.js b/src/helper/selection.js index a7bff794..5a9e6a06 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -178,7 +178,6 @@ const _deleteItemSelection = function (items, onUpdateSvg) { }; const _removeSelectedSegments = function (items, onUpdateSvg) { - performSnapshot(undoSnapshot); const segmentsToRemove = []; for (let i = 0; i < items.length; i++) { diff --git a/src/helper/undo.js b/src/helper/undo.js index 45c13826..8e0e326e 100644 --- a/src/helper/undo.js +++ b/src/helper/undo.js @@ -12,6 +12,9 @@ const performSnapshot = function (dispatchPerformSnapshot) { }; 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(); From 1d0ffef40f22f0d7d0a3ec4e9d67c06c48df6303 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 12 Oct 2017 11:09:26 -0400 Subject: [PATCH 11/13] Revert "Make unimplemented buttons grey" This reverts commit c320a0b6416887b916853d6265cc123bca776b57. --- src/components/paint-editor.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 2982d86b..f799b9df 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -76,12 +76,12 @@ class PaintEditorComponent extends React.Component { @@ -90,12 +90,12 @@ class PaintEditorComponent extends React.Component { From 49b197eba22e3b5ef6c4cc2503ac688a3447be38 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 12 Oct 2017 11:12:17 -0400 Subject: [PATCH 12/13] Revert "remove the default imported costume" This reverts commit 879297ca95640b41e3e1fbe989a6323912bd7470. --- src/playground/playground.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/playground/playground.jsx b/src/playground/playground.jsx index 161e2f70..f59ef2b0 100644 --- a/src/playground/playground.jsx +++ b/src/playground/playground.jsx @@ -32,6 +32,7 @@ ReactDOM.render(( From 9242faffa66ed1d28deb1061c7396e483224be01 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 12 Oct 2017 11:16:09 -0400 Subject: [PATCH 13/13] fix typo --- src/helper/selection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helper/selection.js b/src/helper/selection.js index 5a9e6a06..95c55984 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -171,7 +171,7 @@ const _deleteItemSelection = function (items, onUpdateSvg) { } // @todo: Update toolbar state on change - if (items.lenth > 0) { + if (items.length > 0) { paper.project.view.update(); onUpdateSvg(); }