Mode tools
@@ -154,6 +162,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..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
@@ -26,13 +27,13 @@ class Blobbiness {
}
/**
- * @param {function} updateCallback call when the drawing has changed to let listeners know
+ * @param {function} onUpdateSvg call when the drawing has changed to let listeners know
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
*/
- constructor (updateCallback, clearSelectedItems) {
+ constructor (onUpdateSvg, clearSelectedItems) {
this.broadBrushHelper = new BroadBrushHelper();
this.segmentBrushHelper = new SegmentBrushHelper();
- this.updateCallback = updateCallback;
+ this.onUpdateSvg = onUpdateSvg;
this.clearSelectedItems = clearSelectedItems;
// The following are stored to check whether these have changed and the cursor preview needs to be redrawn.
@@ -143,7 +144,7 @@ class Blobbiness {
}
blob.cursorPreview.visible = false;
- blob.updateCallback();
+ blob.onUpdateSvg();
blob.cursorPreview.visible = true;
blob.cursorPreview.bringToFront();
blob.cursorPreview.position = event.point;
@@ -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();
}
@@ -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..b4f899ab 100644
--- a/src/containers/brush-mode.jsx
+++ b/src/containers/brush-mode.jsx
@@ -4,10 +4,12 @@ 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 {clearSelection} from '../helper/selection';
+
import BrushModeComponent from '../components/brush-mode.jsx';
class BrushMode extends React.Component {
@@ -18,7 +20,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);
}
componentDidMount () {
if (this.props.isBrushModeActive) {
diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx
index 423058c2..efd920fe 100644
--- a/src/containers/eraser-mode.jsx
+++ b/src/containers/eraser-mode.jsx
@@ -17,7 +17,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);
}
componentDidMount () {
if (this.props.isEraserModeActive) {
diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx
index 35611f45..904071a8 100644
--- a/src/containers/fill-color-indicator.jsx
+++ b/src/containers/fill-color-indicator.jsx
@@ -1,19 +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';
+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);
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 69356d62..56cf6dc2 100644
--- a/src/containers/line-mode.jsx
+++ b/src/containers/line-mode.jsx
@@ -1,15 +1,16 @@
+import paper from 'paper';
import PropTypes from 'prop-types';
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 {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 LineModeComponent from '../components/line-mode.jsx';
class LineMode extends React.Component {
static get SNAP_TOLERANCE () {
@@ -206,13 +207,11 @@ class LineMode extends React.Component {
}
this.hitResult = null;
}
- this.props.onUpdateSvg();
+
this.props.setSelectedItems();
-
- // TODO add back undo
- // if (this.path) {
- // pg.undo.snapshot('line');
- // }
+ if (this.path) {
+ this.props.onUpdateSvg();
+ }
}
toleranceSquared () {
return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2);
diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx
index 9d48fa2d..c5154c0f 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, undoSnapshot} from '../reducers/undo';
+
import {getGuideLayer} from '../helper/layer';
+import {performUndo, performRedo, performSnapshot} 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 () {
@@ -21,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;
@@ -32,14 +39,25 @@ class PaintEditor extends React.Component {
}),
paper.project.view.center.x - bounds.x,
paper.project.view.center.y - bounds.y);
+ if (!skipSnapshot) {
+ performSnapshot(this.props.undoSnapshot);
+ }
getGuideLayer().visible = true;
}
+ handleUndo () {
+ performUndo(this.props.undoState, this.props.onUndo, this.handleUpdateSvg);
+ }
+ handleRedo () {
+ performRedo(this.props.undoState, this.props.onRedo, this.handleUpdateSvg);
+ }
render () {
return (
);
@@ -48,12 +66,22 @@ 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,
+ undoSnapshot: PropTypes.func.isRequired,
+ undoState: PropTypes.shape({
+ stack: PropTypes.arrayOf(PropTypes.object).isRequired,
+ pointer: PropTypes.number.isRequired
+ })
};
+const mapStateToProps = state => ({
+ undoState: state.scratchPaint.undo
+});
const mapDispatchToProps = dispatch => ({
onKeyPress: event => {
if (event.key === 'e') {
@@ -65,10 +93,19 @@ const mapDispatchToProps = dispatch => ({
} else if (event.key === 's') {
dispatch(changeMode(Modes.SELECT));
}
+ },
+ onUndo: () => {
+ dispatch(undo());
+ },
+ onRedo: () => {
+ dispatch(redo());
+ },
+ undoSnapshot: snapshot => {
+ dispatch(undoSnapshot(snapshot));
}
});
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..bffb65bd 100644
--- a/src/containers/reshape-mode.jsx
+++ b/src/containers/reshape-mode.jsx
@@ -45,7 +45,8 @@ class ReshapeMode extends React.Component {
this.props.clearHoveredItem,
this.props.setSelectedItems,
this.props.clearSelectedItems,
- this.props.onUpdateSvg);
+ this.props.onUpdateSvg
+ );
this.tool.setPrevHoveredItemId(this.props.hoveredItemId);
this.tool.activate();
}
diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx
index 505bf412..572aa12a 100644
--- a/src/containers/select-mode.jsx
+++ b/src/containers/select-mode.jsx
@@ -45,7 +45,8 @@ class SelectMode extends React.Component {
this.props.clearHoveredItem,
this.props.setSelectedItems,
this.props.clearSelectedItems,
- this.props.onUpdateSvg);
+ this.props.onUpdateSvg
+ );
this.tool.activate();
}
deactivateTool () {
diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx
index f7ffcbab..74619b76 100644
--- a/src/containers/stroke-color-indicator.jsx
+++ b/src/containers/stroke-color-indicator.jsx
@@ -1,19 +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';
+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);
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 d1b0def3..fe836b8a 100644
--- a/src/containers/stroke-width-indicator.jsx
+++ b/src/containers/stroke-width-indicator.jsx
@@ -1,19 +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';
+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);
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/handle-tool.js b/src/helper/selection-tools/handle-tool.js
index c3d26a1d..62e0b345 100644
--- a/src/helper/selection-tools/handle-tool.js
+++ b/src/helper/selection-tools/handle-tool.js
@@ -12,6 +12,7 @@ class HandleTool {
this.setSelectedItems = setSelectedItems;
this.clearSelectedItems = clearSelectedItems;
this.onUpdateSvg = onUpdateSvg;
+ this.selectedItems = [];
}
/**
* @param {!object} hitProperties Describes the mouse event
@@ -28,9 +29,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 +67,23 @@ 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) {
+ this.onUpdateSvg();
+ }
+ this.selectedItems = [];
}
}
diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js
index 0a16277b..6e39673d 100644
--- a/src/helper/selection-tools/move-tool.js
+++ b/src/helper/selection-tools/move-tool.js
@@ -51,7 +51,7 @@ class MoveTool {
}
this._select(item, true, hitProperties.subselect);
}
- if (hitProperties.clone) cloneSelection(hitProperties.subselect);
+ if (hitProperties.clone) cloneSelection(hitProperties.subselect, this.onUpdateSvg);
this.selectedItems = getSelectedLeafItems();
}
/**
@@ -94,15 +94,19 @@ 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) {
+ this.onUpdateSvg();
+ }
}
}
diff --git a/src/helper/selection-tools/point-tool.js b/src/helper/selection-tools/point-tool.js
index 9b54cb7e..f964f2eb 100644
--- a/src/helper/selection-tools/point-tool.js
+++ b/src/helper/selection-tools/point-tool.js
@@ -166,11 +166,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 +197,9 @@ class PointTool {
}
this.selectedItems = null;
this.setSelectedItems();
- // @todo add back undo
- this.onUpdateSvg();
+ if (moved) {
+ this.onUpdateSvg();
+ }
}
}
diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js
index efa75158..38ecebae 100644
--- a/src/helper/selection-tools/reshape-tool.js
+++ b/src/helper/selection-tools/reshape-tool.js
@@ -221,8 +221,7 @@ class ReshapeTool extends paper.Tool {
handleKeyUp (event) {
// Backspace, delete
if (event.key === 'delete' || event.key === 'backspace') {
- deleteSelection(Modes.RESHAPE);
- 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 2006cebf..902eaf36 100644
--- a/src/helper/selection-tools/rotate-tool.js
+++ b/src/helper/selection-tools/rotate-tool.js
@@ -63,7 +63,6 @@ class RotateTool {
this.rotGroupPivot = null;
this.prevRot = [];
- // @todo add back undo
this.onUpdateSvg();
}
}
diff --git a/src/helper/selection-tools/scale-tool.js b/src/helper/selection-tools/scale-tool.js
index 8744ab72..e58e7bd9 100644
--- a/src/helper/selection-tools/scale-tool.js
+++ b/src/helper/selection-tools/scale-tool.js
@@ -157,7 +157,6 @@ class ScaleTool {
}
this.itemGroup.remove();
- // @todo add back undo
this.onUpdateSvg();
}
_getRectCornerNameByIndex (index) {
diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js
index 93fc31cf..14f9e5b2 100644
--- a/src/helper/selection-tools/select-tool.js
+++ b/src/helper/selection-tools/select-tool.js
@@ -126,10 +126,9 @@ class SelectTool extends paper.Tool {
handleKeyUp (event) {
// Backspace, delete
if (event.key === 'delete' || event.key === 'backspace') {
- deleteSelection(Modes.SELECT);
+ 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 2c12b161..95c55984 100644
--- a/src/helper/selection.js
+++ b/src/helper/selection.js
@@ -57,7 +57,7 @@ const selectItemSegments = function (item, state) {
}
};
-const setGroupSelection = function (root, selected, fullySelected) {
+const _setGroupSelection = function (root, selected, fullySelected) {
root.fullySelected = fullySelected;
root.selected = selected;
// select children of compound-path or group
@@ -66,7 +66,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;
@@ -85,12 +85,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
@@ -165,21 +165,19 @@ const getSelectedLeafItems = function () {
return items;
};
-const deleteItemSelection = function (items) {
+const _deleteItemSelection = function (items, onUpdateSvg) {
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');
+ if (items.length > 0) {
+ paper.project.view.update();
+ onUpdateSvg();
+ }
};
-const removeSelectedSegments = function (items) {
- // @todo add back undo
- // pg.undo.snapshot('removeSelectedSegments');
-
+const _removeSelectedSegments = function (items, onUpdateSvg) {
const segmentsToRemove = [];
for (let i = 0; i < items.length; i++) {
@@ -198,161 +196,37 @@ const removeSelectedSegments = function (items) {
seg.remove();
removedSegments = true;
}
+ if (removedSegments) {
+ paper.project.view.update();
+ onUpdateSvg();
+ }
return removedSegments;
};
-const deleteSelection = function (mode) {
+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)) {
- deleteItemSelection(selectedItems);
+ if (!_removeSelectedSegments(selectedItems, onUpdateSvg)) {
+ _deleteItemSelection(selectedItems, onUpdateSvg);
}
} else {
const selectedItems = getSelectedRootItems();
- deleteItemSelection(selectedItems);
+ _deleteItemSelection(selectedItems, onUpdateSvg);
}
};
-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) {
- if (item.children) {
- for (let i = 0; i < item.children.length; i++) {
- const child = item.children[i];
- deleteSegments(child);
- }
- } else {
- const segments = item.segments;
- for (let j = 0; j < segments.length; j++) {
- const segment = segments[j];
- if (segment.selected) {
- if (item.closed ||
- (segment.next &&
- !segment.next.selected &&
- segment.previous &&
- !segment.previous.selected)) {
-
- splitPathRetainSelection(item, j);
- deleteSelection();
- return;
-
- } else if (!item.closed) {
- segment.remove();
- j--; // decrease counter if we removed one from the loop
- }
-
- }
- }
- }
- // remove items with no segments left
- if (item.segments.length <= 0) {
- item.remove();
- }
-};
-
-const deleteSegmentSelection = function (items) {
- for (let i = 0; i < items.length; i++) {
- deleteSegments(items[i]);
- }
-
- // @todo: Update toolbar state on change
- paper.project.view.update();
- // @todo add back undo
- // pg.undo.snapshot('deleteSegmentSelection');
-};
-
-const cloneSelection = function (recursive) {
+const 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;
}
- // @todo add back undo
- // pg.undo.snapshot('cloneSelection');
+ onUpdateSvg();
};
-// 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),
@@ -441,7 +315,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;
}
}
@@ -524,16 +398,10 @@ export {
selectAllSegments,
clearSelection,
deleteSelection,
- deleteItemSelection,
- deleteSegmentSelection,
- splitPathAtSelectedSegments,
cloneSelection,
setItemSelection,
- setGroupSelection,
getSelectedLeafItems,
- getSelectedPaths,
getSelectedRootItems,
- removeSelectedSegments,
processRectangularSelection,
selectRootItem,
shouldShowIfSelection,
diff --git a/src/helper/style-path.js b/src/helper/style-path.js
index 6e85b9f6..6910b573 100644
--- a/src/helper/style-path.js
+++ b/src/helper/style-path.js
@@ -1,3 +1,4 @@
+import paper from 'paper';
import {getSelectedLeafItems} from './selection';
import {isPGTextItem, isPointTextItem} from './item';
import {isGroup} from './group';
@@ -7,39 +8,56 @@ 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) {
+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;
+ }
}
}
- // @todo add back undo
+ 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) {
+const applyStrokeColorToSelection = function (colorString, onUpdateSvg) {
const items = getSelectedLeafItems();
-
+ let changed = false;
for (const item of items) {
if (isPGTextItem(item)) {
if (item.children) {
@@ -47,37 +65,53 @@ const applyStrokeColorToSelection = function (colorString) {
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;
}
}
- // @todo add back undo
+ 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) {
+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();
}
}
- // @todo add back undo
};
/**
@@ -167,8 +201,11 @@ const getColorsFromSelection = function (selectedItems) {
const stylePath = function (path, options) {
if (options.isEraser) {
path.fillColor = 'white';
- } else {
+ } else if (options.fillColor) {
path.fillColor = options.fillColor;
+ } else {
+ // Make sure something visible is drawn
+ path.fillColor = 'black';
}
};
@@ -177,8 +214,11 @@ const styleCursorPreview = function (path, options) {
path.fillColor = 'white';
path.strokeColor = 'cornflowerblue';
path.strokeWidth = 1;
- } else {
+ } else if (options.fillColor) {
path.fillColor = options.fillColor;
+ } else {
+ // Make sure something visible is drawn
+ path.fillColor = 'black';
}
};
diff --git a/src/helper/undo.js b/src/helper/undo.js
new file mode 100644
index 00000000..8e0e326e
--- /dev/null
+++ b/src/helper/undo.js
@@ -0,0 +1,49 @@
+// undo functionality
+// modifed from https://github.com/memononen/stylii
+import paper from 'paper';
+
+const performSnapshot = function (dispatchPerformSnapshot) {
+ dispatchPerformSnapshot({
+ json: paper.project.exportJSON({asString: false})
+ });
+
+ // @todo enable/disable buttons
+ // updateButtonVisibility();
+};
+
+const _restore = function (entry, onUpdateSvg) {
+ for (const layer of paper.project.layers) {
+ layer.removeChildren();
+ }
+ paper.project.clear();
+ paper.project.importJSON(entry.json);
+ paper.view.update();
+ onUpdateSvg(true /* skipSnapshot */);
+};
+
+const performUndo = function (undoState, dispatchPerformUndo, onUpdateSvg) {
+ if (undoState.pointer > 0) {
+ _restore(undoState.stack[undoState.pointer - 1], onUpdateSvg);
+ dispatchPerformUndo();
+
+ // @todo enable/disable buttons
+ // updateButtonVisibility();
+ }
+};
+
+
+const performRedo = function (undoState, dispatchPerformRedo, onUpdateSvg) {
+ if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) {
+ _restore(undoState.stack[undoState.pointer + 1], onUpdateSvg);
+ dispatchPerformRedo();
+
+ // @todo enable/disable buttons
+ // updateButtonVisibility();
+ }
+};
+
+export {
+ performSnapshot,
+ performUndo,
+ performRedo
+};
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
new file mode 100644
index 00000000..2a669383
--- /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 <= 0) {
+ log.warn(`Can't undo, undo stack is empty`);
+ return state;
+ }
+ return {
+ stack: state.stack,
+ pointer: state.pointer - 1
+ };
+ case REDO:
+ if (state.pointer <= -1 || state.pointer === state.stack.length - 1) {
+ log.warn(`Can't redo, redo stack is empty`);
+ return state;
+ }
+ return {
+ stack: state.stack,
+ pointer: state.pointer + 1
+ };
+ case SNAPSHOT:
+ if (!action.snapshot) {
+ log.warn(`Couldn't create undo snapshot, no data provided`);
+ return state;
+ }
+ return {
+ // Performing an action clears the redo stack
+ stack: state.stack.slice(0, state.pointer + 1).concat(action.snapshot),
+ pointer: state.pointer + 1
+ };
+ case CLEAR:
+ return initialState;
+ default:
+ return state;
+ }
+};
+
+// Action creators ==================================
+const undoSnapshot = function (snapshot) {
+ return {
+ type: SNAPSHOT,
+ snapshot: snapshot
+ };
+};
+const undo = function () {
+ return {
+ type: UNDO
+ };
+};
+const redo = function () {
+ return {
+ type: REDO
+ };
+};
+const clearUndoState = function () {
+ return {
+ type: CLEAR
+ };
+};
+
+export {
+ reducer as default,
+ undo,
+ redo,
+ undoSnapshot,
+ clearUndoState
+};
diff --git a/test/unit/undo-reducer.test.js b/test/unit/undo-reducer.test.js
new file mode 100644
index 00000000..6201d9e3
--- /dev/null
+++ b/test/unit/undo-reducer.test.js
@@ -0,0 +1,141 @@
+/* eslint-env jest */
+import undoReducer from '../../src/reducers/undo';
+import {undoSnapshot, undo, redo, clearUndoState} from '../../src/reducers/undo';
+
+test('initialState', () => {
+ let defaultState;
+
+ expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
+ expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */).pointer).toEqual(-1);
+ expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */).stack).toHaveLength(0);
+});
+
+test('snapshot', () => {
+ let defaultState;
+ const state1 = {state: 1};
+ const state2 = {state: 2};
+
+ let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
+ expect(reduxState.pointer).toEqual(0);
+ expect(reduxState.stack).toHaveLength(1);
+ expect(reduxState.stack[0]).toEqual(state1);
+
+ reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
+ expect(reduxState.pointer).toEqual(1);
+ expect(reduxState.stack).toHaveLength(2);
+ expect(reduxState.stack[0]).toEqual(state1);
+ expect(reduxState.stack[1]).toEqual(state2);
+});
+
+test('invalidSnapshot', () => {
+ let defaultState;
+ const state1 = {state: 1};
+
+ const reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
+ const newReduxState = undoReducer(reduxState /* state */, undoSnapshot() /* action */); // No snapshot provided
+ expect(reduxState).toEqual(newReduxState);
+});
+
+test('clearUndoState', () => {
+ let defaultState;
+ const state1 = {state: 1};
+ const state2 = {state: 2};
+
+ // Push 2 states then clear
+ const reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
+ undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
+ const newReduxState = undoReducer(reduxState /* state */, clearUndoState() /* action */);
+
+ expect(newReduxState.pointer).toEqual(-1);
+ expect(newReduxState.stack).toHaveLength(0);
+});
+
+test('cantUndo', () => {
+ let defaultState;
+ const state1 = {state: 1};
+
+ // Undo when there's no undo stack
+ let reduxState = undoReducer(defaultState /* state */, undo() /* action */);
+
+ expect(reduxState.pointer).toEqual(-1);
+ expect(reduxState.stack).toHaveLength(0);
+
+ // Undo when there's only one state
+ reduxState = undoReducer(reduxState /* state */, undoSnapshot([state1]) /* action */);
+ reduxState = undoReducer(reduxState /* state */, undo() /* action */);
+
+ expect(reduxState.pointer).toEqual(0);
+ expect(reduxState.stack).toHaveLength(1);
+});
+
+test('cantRedo', () => {
+ let defaultState;
+ const state1 = {state: 1};
+
+ let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
+
+ // Redo when there's no redo stack
+ reduxState = undoReducer(reduxState /* state */, redo() /* action */);
+
+ expect(reduxState.pointer).toEqual(0);
+ expect(reduxState.stack).toHaveLength(1);
+});
+
+test('undo', () => {
+ let defaultState;
+ const state1 = {state: 1};
+ const state2 = {state: 2};
+
+ // Push 2 states then undo one
+ let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
+ reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
+ reduxState = undoReducer(reduxState /* state */, undo() /* action */);
+
+ expect(reduxState.pointer).toEqual(0);
+ expect(reduxState.stack).toHaveLength(2);
+ expect(reduxState.stack[0]).toEqual(state1);
+ expect(reduxState.stack[1]).toEqual(state2);
+});
+
+test('redo', () => {
+ let defaultState;
+ const state1 = {state: 1};
+ const state2 = {state: 2};
+
+ // Push 2 states then undo one
+ let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
+ reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
+ let newReduxState = undoReducer(reduxState /* state */, undo() /* action */);
+
+ // Now redo and check equality with previous state
+ newReduxState = undoReducer(newReduxState /* state */, redo() /* action */);
+ expect(newReduxState.pointer).toEqual(reduxState.pointer);
+ expect(newReduxState.stack).toHaveLength(reduxState.stack.length);
+ expect(newReduxState.stack[0]).toEqual(reduxState.stack[0]);
+ expect(reduxState.stack[1]).toEqual(reduxState.stack[1]);
+});
+
+test('undoSnapshotCantRedo', () => {
+ let defaultState;
+ const state1 = {state: 1};
+ const state2 = {state: 2};
+ const state3 = {state: 3};
+
+ // Push 2 states then undo
+ let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
+ reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
+ reduxState = undoReducer(reduxState /* state */, undo() /* action */);
+
+ expect(reduxState.pointer).toEqual(0);
+ expect(reduxState.stack).toHaveLength(2);
+
+ // Snapshot
+ reduxState = undoReducer(reduxState /* state */, undoSnapshot([state3]) /* action */);
+ // Redo should do nothing
+ const newReduxState = undoReducer(reduxState /* state */, redo() /* action */);
+
+ expect(newReduxState.pointer).toEqual(reduxState.pointer);
+ expect(newReduxState.stack).toHaveLength(reduxState.stack.length);
+ expect(newReduxState.stack[0]).toEqual(reduxState.stack[0]);
+ expect(newReduxState.stack[1]).toEqual(state3);
+});