mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-23 05:52:42 -05:00
commit
f7a5111950
25 changed files with 578 additions and 228 deletions
|
@ -58,11 +58,13 @@ class PaintEditorComponent extends React.Component {
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
<button
|
<button
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
|
onClick={this.props.onUndo}
|
||||||
>
|
>
|
||||||
Undo
|
Undo
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
|
onClick={this.props.onRedo}
|
||||||
>
|
>
|
||||||
Redo
|
Redo
|
||||||
</button>
|
</button>
|
||||||
|
@ -101,11 +103,17 @@ class PaintEditorComponent extends React.Component {
|
||||||
{/* Second Row */}
|
{/* Second Row */}
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
{/* fill */}
|
{/* fill */}
|
||||||
<FillColorIndicatorComponent />
|
<FillColorIndicatorComponent
|
||||||
|
onUpdateSvg={this.props.onUpdateSvg}
|
||||||
|
/>
|
||||||
{/* stroke */}
|
{/* stroke */}
|
||||||
<StrokeColorIndicatorComponent />
|
<StrokeColorIndicatorComponent
|
||||||
|
onUpdateSvg={this.props.onUpdateSvg}
|
||||||
|
/>
|
||||||
{/* stroke width */}
|
{/* stroke width */}
|
||||||
<StrokeWidthIndicatorComponent />
|
<StrokeWidthIndicatorComponent
|
||||||
|
onUpdateSvg={this.props.onUpdateSvg}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={styles.inputGroup}>
|
<div className={styles.inputGroup}>
|
||||||
Mode tools
|
Mode tools
|
||||||
|
@ -154,6 +162,8 @@ class PaintEditorComponent extends React.Component {
|
||||||
|
|
||||||
PaintEditorComponent.propTypes = {
|
PaintEditorComponent.propTypes = {
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
|
onRedo: PropTypes.func.isRequired,
|
||||||
|
onUndo: PropTypes.func.isRequired,
|
||||||
onUpdateSvg: PropTypes.func.isRequired,
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
rotationCenterX: PropTypes.number,
|
rotationCenterX: PropTypes.number,
|
||||||
rotationCenterY: PropTypes.number,
|
rotationCenterY: PropTypes.number,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import BroadBrushHelper from './broad-brush-helper';
|
||||||
import SegmentBrushHelper from './segment-brush-helper';
|
import SegmentBrushHelper from './segment-brush-helper';
|
||||||
import {MIXED, styleCursorPreview} from '../../helper/style-path';
|
import {MIXED, styleCursorPreview} from '../../helper/style-path';
|
||||||
import {clearSelection} from '../../helper/selection';
|
import {clearSelection} from '../../helper/selection';
|
||||||
|
import {getGuideLayer} from '../../helper/layer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared code for the brush and eraser mode. Adds functions on the paper tool object
|
* Shared code for the brush and eraser mode. Adds functions on the paper tool object
|
||||||
|
@ -26,13 +27,13 @@ class Blobbiness {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function} updateCallback call when the drawing has changed to let listeners know
|
* @param {function} onUpdateSvg call when the drawing has changed to let listeners know
|
||||||
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
||||||
*/
|
*/
|
||||||
constructor (updateCallback, clearSelectedItems) {
|
constructor (onUpdateSvg, clearSelectedItems) {
|
||||||
this.broadBrushHelper = new BroadBrushHelper();
|
this.broadBrushHelper = new BroadBrushHelper();
|
||||||
this.segmentBrushHelper = new SegmentBrushHelper();
|
this.segmentBrushHelper = new SegmentBrushHelper();
|
||||||
this.updateCallback = updateCallback;
|
this.onUpdateSvg = onUpdateSvg;
|
||||||
this.clearSelectedItems = clearSelectedItems;
|
this.clearSelectedItems = clearSelectedItems;
|
||||||
|
|
||||||
// The following are stored to check whether these have changed and the cursor preview needs to be redrawn.
|
// The following are stored to check whether these have changed and the cursor preview needs to be redrawn.
|
||||||
|
@ -143,7 +144,7 @@ class Blobbiness {
|
||||||
}
|
}
|
||||||
|
|
||||||
blob.cursorPreview.visible = false;
|
blob.cursorPreview.visible = false;
|
||||||
blob.updateCallback();
|
blob.onUpdateSvg();
|
||||||
blob.cursorPreview.visible = true;
|
blob.cursorPreview.visible = true;
|
||||||
blob.cursorPreview.bringToFront();
|
blob.cursorPreview.bringToFront();
|
||||||
blob.cursorPreview.position = event.point;
|
blob.cursorPreview.position = event.point;
|
||||||
|
@ -166,7 +167,7 @@ class Blobbiness {
|
||||||
this.cursorPreviewLastPoint = point;
|
this.cursorPreviewLastPoint = point;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.cursorPreview &&
|
if (this.cursorPreview && this.cursorPreview.parent &&
|
||||||
this.brushSize === this.options.brushSize &&
|
this.brushSize === this.options.brushSize &&
|
||||||
this.fillColor === this.options.fillColor &&
|
this.fillColor === this.options.fillColor &&
|
||||||
this.strokeColor === this.options.strokeColor) {
|
this.strokeColor === this.options.strokeColor) {
|
||||||
|
@ -176,6 +177,8 @@ class Blobbiness {
|
||||||
center: point,
|
center: point,
|
||||||
radius: this.options.brushSize / 2
|
radius: this.options.brushSize / 2
|
||||||
});
|
});
|
||||||
|
newPreview.parent = getGuideLayer();
|
||||||
|
newPreview.data.isHelperItem = true;
|
||||||
if (this.cursorPreview) {
|
if (this.cursorPreview) {
|
||||||
this.cursorPreview.remove();
|
this.cursorPreview.remove();
|
||||||
}
|
}
|
||||||
|
@ -234,8 +237,6 @@ class Blobbiness {
|
||||||
paths.splice(i, 1);
|
paths.splice(i, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: Add back undo
|
|
||||||
// pg.undo.snapshot('broadbrush');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeEraser (lastPath) {
|
mergeEraser (lastPath) {
|
||||||
|
@ -284,8 +285,6 @@ class Blobbiness {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastPath.remove();
|
lastPath.remove();
|
||||||
// TODO add back undo
|
|
||||||
// pg.undo.snapshot('eraser');
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Erase
|
// Erase
|
||||||
|
@ -358,8 +357,6 @@ class Blobbiness {
|
||||||
items[i].remove();
|
items[i].remove();
|
||||||
}
|
}
|
||||||
lastPath.remove();
|
lastPath.remove();
|
||||||
// TODO: Add back undo handling
|
|
||||||
// pg.undo.snapshot('eraser');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
colorMatch (existingPath, addedPath) {
|
colorMatch (existingPath, addedPath) {
|
||||||
|
|
|
@ -4,10 +4,12 @@ import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Modes from '../modes/modes';
|
import Modes from '../modes/modes';
|
||||||
import Blobbiness from './blob/blob';
|
import Blobbiness from './blob/blob';
|
||||||
|
|
||||||
import {changeBrushSize} from '../reducers/brush-mode';
|
import {changeBrushSize} from '../reducers/brush-mode';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {clearSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems} from '../reducers/selected-items';
|
||||||
import {clearSelection} from '../helper/selection';
|
import {clearSelection} from '../helper/selection';
|
||||||
|
|
||||||
import BrushModeComponent from '../components/brush-mode.jsx';
|
import BrushModeComponent from '../components/brush-mode.jsx';
|
||||||
|
|
||||||
class BrushMode extends React.Component {
|
class BrushMode extends React.Component {
|
||||||
|
@ -18,7 +20,8 @@ class BrushMode extends React.Component {
|
||||||
'deactivateTool',
|
'deactivateTool',
|
||||||
'onScroll'
|
'onScroll'
|
||||||
]);
|
]);
|
||||||
this.blob = new Blobbiness(this.props.onUpdateSvg, this.props.clearSelectedItems);
|
this.blob = new Blobbiness(
|
||||||
|
this.props.onUpdateSvg, this.props.clearSelectedItems);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
if (this.props.isBrushModeActive) {
|
if (this.props.isBrushModeActive) {
|
||||||
|
|
|
@ -17,7 +17,8 @@ class EraserMode extends React.Component {
|
||||||
'deactivateTool',
|
'deactivateTool',
|
||||||
'onScroll'
|
'onScroll'
|
||||||
]);
|
]);
|
||||||
this.blob = new Blobbiness(this.props.onUpdateSvg, this.props.clearSelectedItems);
|
this.blob = new Blobbiness(
|
||||||
|
this.props.onUpdateSvg, this.props.clearSelectedItems);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
if (this.props.isEraserModeActive) {
|
if (this.props.isEraserModeActive) {
|
||||||
|
|
|
@ -1,19 +1,48 @@
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import bindAll from 'lodash.bindall';
|
||||||
import {changeFillColor} from '../reducers/fill-color';
|
import {changeFillColor} from '../reducers/fill-color';
|
||||||
import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
|
import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
|
||||||
import {applyFillColorToSelection} from '../helper/style-path';
|
import {applyFillColorToSelection} from '../helper/style-path';
|
||||||
|
|
||||||
|
class FillColorIndicator extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleChangeFillColor'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
handleChangeFillColor (newColor) {
|
||||||
|
applyFillColorToSelection(newColor, this.props.onUpdateSvg);
|
||||||
|
this.props.onChangeFillColor(newColor);
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<FillColorIndicatorComponent
|
||||||
|
fillColor={this.props.fillColor}
|
||||||
|
onChangeFillColor={this.handleChangeFillColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
fillColor: state.scratchPaint.color.fillColor
|
fillColor: state.scratchPaint.color.fillColor
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onChangeFillColor: fillColor => {
|
onChangeFillColor: fillColor => {
|
||||||
applyFillColorToSelection(fillColor);
|
|
||||||
dispatch(changeFillColor(fillColor));
|
dispatch(changeFillColor(fillColor));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
FillColorIndicator.propTypes = {
|
||||||
|
fillColor: PropTypes.string,
|
||||||
|
onChangeFillColor: PropTypes.func.isRequired,
|
||||||
|
onUpdateSvg: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(FillColorIndicatorComponent);
|
)(FillColorIndicator);
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
|
import paper from 'paper';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Modes from '../modes/modes';
|
import Modes from '../modes/modes';
|
||||||
import {changeStrokeWidth} from '../reducers/stroke-width';
|
|
||||||
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
|
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
|
||||||
import {MIXED} from '../helper/style-path';
|
import {MIXED} from '../helper/style-path';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
|
||||||
import LineModeComponent from '../components/line-mode.jsx';
|
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import paper from 'paper';
|
import {changeStrokeWidth} from '../reducers/stroke-width';
|
||||||
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
|
|
||||||
|
import LineModeComponent from '../components/line-mode.jsx';
|
||||||
|
|
||||||
class LineMode extends React.Component {
|
class LineMode extends React.Component {
|
||||||
static get SNAP_TOLERANCE () {
|
static get SNAP_TOLERANCE () {
|
||||||
|
@ -206,13 +207,11 @@ class LineMode extends React.Component {
|
||||||
}
|
}
|
||||||
this.hitResult = null;
|
this.hitResult = null;
|
||||||
}
|
}
|
||||||
this.props.onUpdateSvg();
|
|
||||||
this.props.setSelectedItems();
|
|
||||||
|
|
||||||
// TODO add back undo
|
this.props.setSelectedItems();
|
||||||
// if (this.path) {
|
if (this.path) {
|
||||||
// pg.undo.snapshot('line');
|
this.props.onUpdateSvg();
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
toleranceSquared () {
|
toleranceSquared () {
|
||||||
return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2);
|
return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2);
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PaintEditorComponent from '../components/paint-editor.jsx';
|
import PaintEditorComponent from '../components/paint-editor.jsx';
|
||||||
|
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
|
import {undo, redo, undoSnapshot} from '../reducers/undo';
|
||||||
|
|
||||||
import {getGuideLayer} from '../helper/layer';
|
import {getGuideLayer} from '../helper/layer';
|
||||||
|
import {performUndo, performRedo, performSnapshot} from '../helper/undo';
|
||||||
|
|
||||||
import Modes from '../modes/modes';
|
import Modes from '../modes/modes';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
|
@ -12,7 +17,9 @@ class PaintEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handleUpdateSvg'
|
'handleUpdateSvg',
|
||||||
|
'handleUndo',
|
||||||
|
'handleRedo'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
@ -21,7 +28,7 @@ class PaintEditor extends React.Component {
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
document.removeEventListener('keydown', this.props.onKeyPress);
|
document.removeEventListener('keydown', this.props.onKeyPress);
|
||||||
}
|
}
|
||||||
handleUpdateSvg () {
|
handleUpdateSvg (skipSnapshot) {
|
||||||
// Hide bounding box
|
// Hide bounding box
|
||||||
getGuideLayer().visible = false;
|
getGuideLayer().visible = false;
|
||||||
const bounds = paper.project.activeLayer.bounds;
|
const bounds = paper.project.activeLayer.bounds;
|
||||||
|
@ -32,14 +39,25 @@ class PaintEditor extends React.Component {
|
||||||
}),
|
}),
|
||||||
paper.project.view.center.x - bounds.x,
|
paper.project.view.center.x - bounds.x,
|
||||||
paper.project.view.center.y - bounds.y);
|
paper.project.view.center.y - bounds.y);
|
||||||
|
if (!skipSnapshot) {
|
||||||
|
performSnapshot(this.props.undoSnapshot);
|
||||||
|
}
|
||||||
getGuideLayer().visible = true;
|
getGuideLayer().visible = true;
|
||||||
}
|
}
|
||||||
|
handleUndo () {
|
||||||
|
performUndo(this.props.undoState, this.props.onUndo, this.handleUpdateSvg);
|
||||||
|
}
|
||||||
|
handleRedo () {
|
||||||
|
performRedo(this.props.undoState, this.props.onRedo, this.handleUpdateSvg);
|
||||||
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<PaintEditorComponent
|
<PaintEditorComponent
|
||||||
rotationCenterX={this.props.rotationCenterX}
|
rotationCenterX={this.props.rotationCenterX}
|
||||||
rotationCenterY={this.props.rotationCenterY}
|
rotationCenterY={this.props.rotationCenterY}
|
||||||
svg={this.props.svg}
|
svg={this.props.svg}
|
||||||
|
onRedo={this.handleRedo}
|
||||||
|
onUndo={this.handleUndo}
|
||||||
onUpdateSvg={this.handleUpdateSvg}
|
onUpdateSvg={this.handleUpdateSvg}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -48,12 +66,22 @@ class PaintEditor extends React.Component {
|
||||||
|
|
||||||
PaintEditor.propTypes = {
|
PaintEditor.propTypes = {
|
||||||
onKeyPress: PropTypes.func.isRequired,
|
onKeyPress: PropTypes.func.isRequired,
|
||||||
|
onRedo: PropTypes.func.isRequired,
|
||||||
|
onUndo: PropTypes.func.isRequired,
|
||||||
onUpdateSvg: PropTypes.func.isRequired,
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
rotationCenterX: PropTypes.number,
|
rotationCenterX: PropTypes.number,
|
||||||
rotationCenterY: PropTypes.number,
|
rotationCenterY: PropTypes.number,
|
||||||
svg: PropTypes.string
|
svg: PropTypes.string,
|
||||||
|
undoSnapshot: PropTypes.func.isRequired,
|
||||||
|
undoState: PropTypes.shape({
|
||||||
|
stack: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
pointer: PropTypes.number.isRequired
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
undoState: state.scratchPaint.undo
|
||||||
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onKeyPress: event => {
|
onKeyPress: event => {
|
||||||
if (event.key === 'e') {
|
if (event.key === 'e') {
|
||||||
|
@ -65,10 +93,19 @@ const mapDispatchToProps = dispatch => ({
|
||||||
} else if (event.key === 's') {
|
} else if (event.key === 's') {
|
||||||
dispatch(changeMode(Modes.SELECT));
|
dispatch(changeMode(Modes.SELECT));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onUndo: () => {
|
||||||
|
dispatch(undo());
|
||||||
|
},
|
||||||
|
onRedo: () => {
|
||||||
|
dispatch(redo());
|
||||||
|
},
|
||||||
|
undoSnapshot: snapshot => {
|
||||||
|
dispatch(undoSnapshot(snapshot));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
null,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(PaintEditor);
|
)(PaintEditor);
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
import paper from 'paper';
|
import paper from 'paper';
|
||||||
|
|
||||||
|
import {performSnapshot} from '../helper/undo';
|
||||||
|
import {undoSnapshot} from '../reducers/undo';
|
||||||
|
|
||||||
import styles from './paper-canvas.css';
|
import styles from './paper-canvas.css';
|
||||||
|
|
||||||
class PaperCanvas extends React.Component {
|
class PaperCanvas extends React.Component {
|
||||||
|
@ -20,6 +24,7 @@ class PaperCanvas extends React.Component {
|
||||||
if (this.props.svg) {
|
if (this.props.svg) {
|
||||||
this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY);
|
this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY);
|
||||||
}
|
}
|
||||||
|
performSnapshot(this.props.undoSnapshot);
|
||||||
}
|
}
|
||||||
componentWillReceiveProps (newProps) {
|
componentWillReceiveProps (newProps) {
|
||||||
paper.project.activeLayer.removeChildren();
|
paper.project.activeLayer.removeChildren();
|
||||||
|
@ -85,7 +90,16 @@ PaperCanvas.propTypes = {
|
||||||
canvasRef: PropTypes.func,
|
canvasRef: PropTypes.func,
|
||||||
rotationCenterX: PropTypes.number,
|
rotationCenterX: PropTypes.number,
|
||||||
rotationCenterY: PropTypes.number,
|
rotationCenterY: PropTypes.number,
|
||||||
svg: PropTypes.string
|
svg: PropTypes.string,
|
||||||
|
undoSnapshot: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
undoSnapshot: snapshot => {
|
||||||
|
dispatch(undoSnapshot(snapshot));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default PaperCanvas;
|
export default connect(
|
||||||
|
null,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(PaperCanvas);
|
||||||
|
|
|
@ -45,7 +45,8 @@ class ReshapeMode extends React.Component {
|
||||||
this.props.clearHoveredItem,
|
this.props.clearHoveredItem,
|
||||||
this.props.setSelectedItems,
|
this.props.setSelectedItems,
|
||||||
this.props.clearSelectedItems,
|
this.props.clearSelectedItems,
|
||||||
this.props.onUpdateSvg);
|
this.props.onUpdateSvg
|
||||||
|
);
|
||||||
this.tool.setPrevHoveredItemId(this.props.hoveredItemId);
|
this.tool.setPrevHoveredItemId(this.props.hoveredItemId);
|
||||||
this.tool.activate();
|
this.tool.activate();
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,8 @@ class SelectMode extends React.Component {
|
||||||
this.props.clearHoveredItem,
|
this.props.clearHoveredItem,
|
||||||
this.props.setSelectedItems,
|
this.props.setSelectedItems,
|
||||||
this.props.clearSelectedItems,
|
this.props.clearSelectedItems,
|
||||||
this.props.onUpdateSvg);
|
this.props.onUpdateSvg
|
||||||
|
);
|
||||||
this.tool.activate();
|
this.tool.activate();
|
||||||
}
|
}
|
||||||
deactivateTool () {
|
deactivateTool () {
|
||||||
|
|
|
@ -1,19 +1,48 @@
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import bindAll from 'lodash.bindall';
|
||||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
import {changeStrokeColor} from '../reducers/stroke-color';
|
||||||
import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
|
import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
|
||||||
import {applyStrokeColorToSelection} from '../helper/style-path';
|
import {applyStrokeColorToSelection} from '../helper/style-path';
|
||||||
|
|
||||||
|
class StrokeColorIndicator extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleChangeStrokeColor'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
handleChangeStrokeColor (newColor) {
|
||||||
|
applyStrokeColorToSelection(newColor, this.props.onUpdateSvg);
|
||||||
|
this.props.onChangeStrokeColor(newColor);
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<StrokeColorIndicatorComponent
|
||||||
|
strokeColor={this.props.strokeColor}
|
||||||
|
onChangeStrokeColor={this.handleChangeStrokeColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
strokeColor: state.scratchPaint.color.strokeColor
|
strokeColor: state.scratchPaint.color.strokeColor
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onChangeStrokeColor: strokeColor => {
|
onChangeStrokeColor: strokeColor => {
|
||||||
applyStrokeColorToSelection(strokeColor);
|
|
||||||
dispatch(changeStrokeColor(strokeColor));
|
dispatch(changeStrokeColor(strokeColor));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
StrokeColorIndicator.propTypes = {
|
||||||
|
onChangeStrokeColor: PropTypes.func.isRequired,
|
||||||
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
|
strokeColor: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(StrokeColorIndicatorComponent);
|
)(StrokeColorIndicator);
|
||||||
|
|
|
@ -1,19 +1,48 @@
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import bindAll from 'lodash.bindall';
|
||||||
import {changeStrokeWidth} from '../reducers/stroke-width';
|
import {changeStrokeWidth} from '../reducers/stroke-width';
|
||||||
import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx';
|
import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx';
|
||||||
import {applyStrokeWidthToSelection} from '../helper/style-path';
|
import {applyStrokeWidthToSelection} from '../helper/style-path';
|
||||||
|
|
||||||
|
class StrokeWidthIndicator extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleChangeStrokeWidth'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
handleChangeStrokeWidth (newWidth) {
|
||||||
|
applyStrokeWidthToSelection(newWidth, this.props.onUpdateSvg);
|
||||||
|
this.props.onChangeStrokeWidth(newWidth);
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<StrokeWidthIndicatorComponent
|
||||||
|
strokeWidth={this.props.strokeWidth}
|
||||||
|
onChangeStrokeWidth={this.handleChangeStrokeWidth}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
strokeWidth: state.scratchPaint.color.strokeWidth
|
strokeWidth: state.scratchPaint.color.strokeWidth
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onChangeStrokeWidth: strokeWidth => {
|
onChangeStrokeWidth: strokeWidth => {
|
||||||
applyStrokeWidthToSelection(strokeWidth);
|
|
||||||
dispatch(changeStrokeWidth(strokeWidth));
|
dispatch(changeStrokeWidth(strokeWidth));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
StrokeWidthIndicator.propTypes = {
|
||||||
|
onChangeStrokeWidth: PropTypes.func.isRequired,
|
||||||
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
|
strokeWidth: PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(StrokeWidthIndicatorComponent);
|
)(StrokeWidthIndicator);
|
||||||
|
|
|
@ -12,6 +12,7 @@ class HandleTool {
|
||||||
this.setSelectedItems = setSelectedItems;
|
this.setSelectedItems = setSelectedItems;
|
||||||
this.clearSelectedItems = clearSelectedItems;
|
this.clearSelectedItems = clearSelectedItems;
|
||||||
this.onUpdateSvg = onUpdateSvg;
|
this.onUpdateSvg = onUpdateSvg;
|
||||||
|
this.selectedItems = [];
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @param {!object} hitProperties Describes the mouse event
|
* @param {!object} hitProperties Describes the mouse event
|
||||||
|
@ -28,9 +29,9 @@ class HandleTool {
|
||||||
this.hitType = hitProperties.hitResult.type;
|
this.hitType = hitProperties.hitResult.type;
|
||||||
}
|
}
|
||||||
onMouseDrag (event) {
|
onMouseDrag (event) {
|
||||||
const selectedItems = getSelectedLeafItems();
|
this.selectedItems = getSelectedLeafItems();
|
||||||
|
|
||||||
for (const item of selectedItems) {
|
for (const item of this.selectedItems) {
|
||||||
for (const seg of item.segments) {
|
for (const seg of item.segments) {
|
||||||
// add the point of the segment before the drag started
|
// add the point of the segment before the drag started
|
||||||
// for later use in the snap calculation
|
// for later use in the snap calculation
|
||||||
|
@ -66,8 +67,23 @@ class HandleTool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onMouseUp () {
|
onMouseUp () {
|
||||||
// @todo add back undo
|
// resetting the items and segments origin points for the next usage
|
||||||
this.onUpdateSvg();
|
let moved = false;
|
||||||
|
for (const item of this.selectedItems) {
|
||||||
|
if (!item.segments) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const seg of item.segments) {
|
||||||
|
if (seg.origPoint && !seg.equals(seg.origPoint)) {
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
seg.origPoint = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (moved) {
|
||||||
|
this.onUpdateSvg();
|
||||||
|
}
|
||||||
|
this.selectedItems = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ class MoveTool {
|
||||||
}
|
}
|
||||||
this._select(item, true, hitProperties.subselect);
|
this._select(item, true, hitProperties.subselect);
|
||||||
}
|
}
|
||||||
if (hitProperties.clone) cloneSelection(hitProperties.subselect);
|
if (hitProperties.clone) cloneSelection(hitProperties.subselect, this.onUpdateSvg);
|
||||||
this.selectedItems = getSelectedLeafItems();
|
this.selectedItems = getSelectedLeafItems();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -94,15 +94,19 @@ class MoveTool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onMouseUp () {
|
onMouseUp () {
|
||||||
|
let moved = false;
|
||||||
// resetting the items origin point for the next usage
|
// resetting the items origin point for the next usage
|
||||||
for (const item of this.selectedItems) {
|
for (const item of this.selectedItems) {
|
||||||
|
if (item.data.origPos && !item.position.equals(item.data.origPos)) {
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
item.data.origPos = null;
|
item.data.origPos = null;
|
||||||
}
|
}
|
||||||
this.selectedItems = null;
|
this.selectedItems = null;
|
||||||
|
|
||||||
// @todo add back undo
|
if (moved) {
|
||||||
// pg.undo.snapshot('moveSelection');
|
this.onUpdateSvg();
|
||||||
this.onUpdateSvg();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -166,11 +166,15 @@ class PointTool {
|
||||||
}
|
}
|
||||||
onMouseUp () {
|
onMouseUp () {
|
||||||
// resetting the items and segments origin points for the next usage
|
// resetting the items and segments origin points for the next usage
|
||||||
|
let moved = false;
|
||||||
for (const item of this.selectedItems) {
|
for (const item of this.selectedItems) {
|
||||||
if (!item.segments) {
|
if (!item.segments) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const seg of item.segments) {
|
for (const seg of item.segments) {
|
||||||
|
if (seg.origPoint && !seg.equals(seg.origPoint)) {
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
seg.origPoint = null;
|
seg.origPoint = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,8 +197,9 @@ class PointTool {
|
||||||
}
|
}
|
||||||
this.selectedItems = null;
|
this.selectedItems = null;
|
||||||
this.setSelectedItems();
|
this.setSelectedItems();
|
||||||
// @todo add back undo
|
if (moved) {
|
||||||
this.onUpdateSvg();
|
this.onUpdateSvg();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -221,8 +221,7 @@ class ReshapeTool extends paper.Tool {
|
||||||
handleKeyUp (event) {
|
handleKeyUp (event) {
|
||||||
// Backspace, delete
|
// Backspace, delete
|
||||||
if (event.key === 'delete' || event.key === 'backspace') {
|
if (event.key === 'delete' || event.key === 'backspace') {
|
||||||
deleteSelection(Modes.RESHAPE);
|
deleteSelection(Modes.RESHAPE, this.onUpdateSvg);
|
||||||
this.onUpdateSvg();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deactivateTool () {
|
deactivateTool () {
|
||||||
|
|
|
@ -63,7 +63,6 @@ class RotateTool {
|
||||||
this.rotGroupPivot = null;
|
this.rotGroupPivot = null;
|
||||||
this.prevRot = [];
|
this.prevRot = [];
|
||||||
|
|
||||||
// @todo add back undo
|
|
||||||
this.onUpdateSvg();
|
this.onUpdateSvg();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,7 +157,6 @@ class ScaleTool {
|
||||||
}
|
}
|
||||||
this.itemGroup.remove();
|
this.itemGroup.remove();
|
||||||
|
|
||||||
// @todo add back undo
|
|
||||||
this.onUpdateSvg();
|
this.onUpdateSvg();
|
||||||
}
|
}
|
||||||
_getRectCornerNameByIndex (index) {
|
_getRectCornerNameByIndex (index) {
|
||||||
|
|
|
@ -126,10 +126,9 @@ class SelectTool extends paper.Tool {
|
||||||
handleKeyUp (event) {
|
handleKeyUp (event) {
|
||||||
// Backspace, delete
|
// Backspace, delete
|
||||||
if (event.key === 'delete' || event.key === 'backspace') {
|
if (event.key === 'delete' || event.key === 'backspace') {
|
||||||
deleteSelection(Modes.SELECT);
|
deleteSelection(Modes.SELECT, this.onUpdateSvg);
|
||||||
this.clearHoveredItem();
|
this.clearHoveredItem();
|
||||||
this.boundingBoxTool.removeBoundsPath();
|
this.boundingBoxTool.removeBoundsPath();
|
||||||
this.onUpdateSvg();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deactivateTool () {
|
deactivateTool () {
|
||||||
|
|
|
@ -57,7 +57,7 @@ const selectItemSegments = function (item, state) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setGroupSelection = function (root, selected, fullySelected) {
|
const _setGroupSelection = function (root, selected, fullySelected) {
|
||||||
root.fullySelected = fullySelected;
|
root.fullySelected = fullySelected;
|
||||||
root.selected = selected;
|
root.selected = selected;
|
||||||
// select children of compound-path or group
|
// select children of compound-path or group
|
||||||
|
@ -66,7 +66,7 @@ const setGroupSelection = function (root, selected, fullySelected) {
|
||||||
if (children) {
|
if (children) {
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
if (isGroup(child)) {
|
if (isGroup(child)) {
|
||||||
setGroupSelection(child, selected, fullySelected);
|
_setGroupSelection(child, selected, fullySelected);
|
||||||
} else {
|
} else {
|
||||||
child.fullySelected = fullySelected;
|
child.fullySelected = fullySelected;
|
||||||
child.selected = selected;
|
child.selected = selected;
|
||||||
|
@ -85,12 +85,12 @@ const setItemSelection = function (item, state, fullySelected) {
|
||||||
// do it recursive
|
// do it recursive
|
||||||
setItemSelection(parentGroup, state, fullySelected);
|
setItemSelection(parentGroup, state, fullySelected);
|
||||||
} else if (itemsCompoundPath) {
|
} else if (itemsCompoundPath) {
|
||||||
setGroupSelection(itemsCompoundPath, state, fullySelected);
|
_setGroupSelection(itemsCompoundPath, state, fullySelected);
|
||||||
} else {
|
} else {
|
||||||
if (item.data && item.data.noSelect) {
|
if (item.data && item.data.noSelect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setGroupSelection(item, state, fullySelected);
|
_setGroupSelection(item, state, fullySelected);
|
||||||
}
|
}
|
||||||
// @todo: Update toolbar state on change
|
// @todo: Update toolbar state on change
|
||||||
|
|
||||||
|
@ -165,21 +165,19 @@ const getSelectedLeafItems = function () {
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteItemSelection = function (items) {
|
const _deleteItemSelection = function (items, onUpdateSvg) {
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
items[i].remove();
|
items[i].remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo: Update toolbar state on change
|
// @todo: Update toolbar state on change
|
||||||
paper.project.view.update();
|
if (items.length > 0) {
|
||||||
// @todo add back undo
|
paper.project.view.update();
|
||||||
// pg.undo.snapshot('deleteItemSelection');
|
onUpdateSvg();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeSelectedSegments = function (items) {
|
const _removeSelectedSegments = function (items, onUpdateSvg) {
|
||||||
// @todo add back undo
|
|
||||||
// pg.undo.snapshot('removeSelectedSegments');
|
|
||||||
|
|
||||||
const segmentsToRemove = [];
|
const segmentsToRemove = [];
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
@ -198,161 +196,37 @@ const removeSelectedSegments = function (items) {
|
||||||
seg.remove();
|
seg.remove();
|
||||||
removedSegments = true;
|
removedSegments = true;
|
||||||
}
|
}
|
||||||
|
if (removedSegments) {
|
||||||
|
paper.project.view.update();
|
||||||
|
onUpdateSvg();
|
||||||
|
}
|
||||||
return removedSegments;
|
return removedSegments;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSelection = function (mode) {
|
const deleteSelection = function (mode, onUpdateSvg) {
|
||||||
if (mode === Modes.RESHAPE) {
|
if (mode === Modes.RESHAPE) {
|
||||||
const selectedItems = getSelectedLeafItems();
|
const selectedItems = getSelectedLeafItems();
|
||||||
// If there are points selected remove them. If not delete the item selected.
|
// If there are points selected remove them. If not delete the item selected.
|
||||||
if (!removeSelectedSegments(selectedItems)) {
|
if (!_removeSelectedSegments(selectedItems, onUpdateSvg)) {
|
||||||
deleteItemSelection(selectedItems);
|
_deleteItemSelection(selectedItems, onUpdateSvg);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const selectedItems = getSelectedRootItems();
|
const selectedItems = getSelectedRootItems();
|
||||||
deleteItemSelection(selectedItems);
|
_deleteItemSelection(selectedItems, onUpdateSvg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const splitPathRetainSelection = function (path, index, deselectSplitSegments) {
|
const cloneSelection = function (recursive, onUpdateSvg) {
|
||||||
const selectedPoints = [];
|
|
||||||
|
|
||||||
// collect points of selected segments, so we can reselect them
|
|
||||||
// once the path is split.
|
|
||||||
for (let i = 0; i < path.segments.length; i++) {
|
|
||||||
const seg = path.segments[i];
|
|
||||||
if (seg.selected) {
|
|
||||||
if (deselectSplitSegments && i === index) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
selectedPoints.push(seg.point);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPath = path.split(index, 0);
|
|
||||||
if (!newPath) return;
|
|
||||||
|
|
||||||
// reselect all of the newPaths segments that are in the exact same location
|
|
||||||
// as the ones that are stored in selectedPoints
|
|
||||||
for (let i = 0; i < newPath.segments.length; i++) {
|
|
||||||
const seg = newPath.segments[i];
|
|
||||||
for (let j = 0; j < selectedPoints.length; j++) {
|
|
||||||
const point = selectedPoints[j];
|
|
||||||
if (point.x === seg.point.x && point.y === seg.point.y) {
|
|
||||||
seg.selected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// only do this if path and newPath are different
|
|
||||||
// (split at more than one point)
|
|
||||||
if (path !== newPath) {
|
|
||||||
for (let i = 0; i < path.segments.length; i++) {
|
|
||||||
const seg = path.segments[i];
|
|
||||||
for (let j = 0; j < selectedPoints.length; j++) {
|
|
||||||
const point = selectedPoints[j];
|
|
||||||
if (point.x === seg.point.x && point.y === seg.point.y) {
|
|
||||||
seg.selected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const splitPathAtSelectedSegments = function () {
|
|
||||||
const items = getSelectedRootItems();
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i];
|
|
||||||
const segments = item.segments;
|
|
||||||
for (let j = 0; j < segments.length; j++) {
|
|
||||||
const segment = segments[j];
|
|
||||||
if (segment.selected) {
|
|
||||||
if (item.closed ||
|
|
||||||
(segment.next &&
|
|
||||||
!segment.next.selected &&
|
|
||||||
segment.previous &&
|
|
||||||
!segment.previous.selected)) {
|
|
||||||
splitPathRetainSelection(item, j, true);
|
|
||||||
splitPathAtSelectedSegments();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteSegments = function (item) {
|
|
||||||
if (item.children) {
|
|
||||||
for (let i = 0; i < item.children.length; i++) {
|
|
||||||
const child = item.children[i];
|
|
||||||
deleteSegments(child);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const segments = item.segments;
|
|
||||||
for (let j = 0; j < segments.length; j++) {
|
|
||||||
const segment = segments[j];
|
|
||||||
if (segment.selected) {
|
|
||||||
if (item.closed ||
|
|
||||||
(segment.next &&
|
|
||||||
!segment.next.selected &&
|
|
||||||
segment.previous &&
|
|
||||||
!segment.previous.selected)) {
|
|
||||||
|
|
||||||
splitPathRetainSelection(item, j);
|
|
||||||
deleteSelection();
|
|
||||||
return;
|
|
||||||
|
|
||||||
} else if (!item.closed) {
|
|
||||||
segment.remove();
|
|
||||||
j--; // decrease counter if we removed one from the loop
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// remove items with no segments left
|
|
||||||
if (item.segments.length <= 0) {
|
|
||||||
item.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteSegmentSelection = function (items) {
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
deleteSegments(items[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @todo: Update toolbar state on change
|
|
||||||
paper.project.view.update();
|
|
||||||
// @todo add back undo
|
|
||||||
// pg.undo.snapshot('deleteSegmentSelection');
|
|
||||||
};
|
|
||||||
|
|
||||||
const cloneSelection = function (recursive) {
|
|
||||||
const selectedItems = recursive ? getSelectedLeafItems() : getSelectedRootItems();
|
const selectedItems = recursive ? getSelectedLeafItems() : getSelectedRootItems();
|
||||||
for (let i = 0; i < selectedItems.length; i++) {
|
for (let i = 0; i < selectedItems.length; i++) {
|
||||||
const item = selectedItems[i];
|
const item = selectedItems[i];
|
||||||
item.clone();
|
item.clone();
|
||||||
item.selected = false;
|
item.selected = false;
|
||||||
}
|
}
|
||||||
// @todo add back undo
|
onUpdateSvg();
|
||||||
// pg.undo.snapshot('cloneSelection');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only returns paths, no compound paths, groups or any other stuff
|
const _checkBoundsItem = function (selectionRect, item, event) {
|
||||||
const getSelectedPaths = function () {
|
|
||||||
const allPaths = getSelectedRootItems();
|
|
||||||
const paths = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < allPaths.length; i++) {
|
|
||||||
const path = allPaths[i];
|
|
||||||
if (path.className === 'Path') {
|
|
||||||
paths.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkBoundsItem = function (selectionRect, item, event) {
|
|
||||||
const itemBounds = new paper.Path([
|
const itemBounds = new paper.Path([
|
||||||
item.localToGlobal(item.internalBounds.topLeft),
|
item.localToGlobal(item.internalBounds.topLeft),
|
||||||
item.localToGlobal(item.internalBounds.topRight),
|
item.localToGlobal(item.internalBounds.topRight),
|
||||||
|
@ -441,7 +315,7 @@ const _handleRectangularSelectionItems = function (item, event, rect, mode, root
|
||||||
// @todo: Update toolbar state on change
|
// @todo: Update toolbar state on change
|
||||||
|
|
||||||
} else if (isBoundsItem(item)) {
|
} else if (isBoundsItem(item)) {
|
||||||
if (checkBoundsItem(rect, item, event)) {
|
if (_checkBoundsItem(rect, item, event)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -524,16 +398,10 @@ export {
|
||||||
selectAllSegments,
|
selectAllSegments,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
deleteSelection,
|
deleteSelection,
|
||||||
deleteItemSelection,
|
|
||||||
deleteSegmentSelection,
|
|
||||||
splitPathAtSelectedSegments,
|
|
||||||
cloneSelection,
|
cloneSelection,
|
||||||
setItemSelection,
|
setItemSelection,
|
||||||
setGroupSelection,
|
|
||||||
getSelectedLeafItems,
|
getSelectedLeafItems,
|
||||||
getSelectedPaths,
|
|
||||||
getSelectedRootItems,
|
getSelectedRootItems,
|
||||||
removeSelectedSegments,
|
|
||||||
processRectangularSelection,
|
processRectangularSelection,
|
||||||
selectRootItem,
|
selectRootItem,
|
||||||
shouldShowIfSelection,
|
shouldShowIfSelection,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import paper from 'paper';
|
||||||
import {getSelectedLeafItems} from './selection';
|
import {getSelectedLeafItems} from './selection';
|
||||||
import {isPGTextItem, isPointTextItem} from './item';
|
import {isPGTextItem, isPointTextItem} from './item';
|
||||||
import {isGroup} from './group';
|
import {isGroup} from './group';
|
||||||
|
@ -7,39 +8,56 @@ const MIXED = 'scratch-paint/style-path/mixed';
|
||||||
/**
|
/**
|
||||||
* Called when setting fill color
|
* Called when setting fill color
|
||||||
* @param {string} colorString New color, css format
|
* @param {string} colorString New color, css format
|
||||||
|
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
||||||
*/
|
*/
|
||||||
const applyFillColorToSelection = function (colorString) {
|
const applyFillColorToSelection = function (colorString, onUpdateSvg) {
|
||||||
const items = getSelectedLeafItems();
|
const items = getSelectedLeafItems();
|
||||||
|
let changed = false;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (isPGTextItem(item)) {
|
if (isPGTextItem(item)) {
|
||||||
for (const child of item.children) {
|
for (const child of item.children) {
|
||||||
if (child.children) {
|
if (child.children) {
|
||||||
for (const path of child.children) {
|
for (const path of child.children) {
|
||||||
if (!path.data.isPGGlyphRect) {
|
if (!path.data.isPGGlyphRect) {
|
||||||
path.fillColor = colorString;
|
if ((path.fillColor === null && colorString) ||
|
||||||
|
path.fillColor.toCSS() !== new paper.Color(colorString).toCSS()) {
|
||||||
|
changed = true;
|
||||||
|
path.fillColor = colorString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!child.data.isPGGlyphRect) {
|
} else if (!child.data.isPGGlyphRect) {
|
||||||
child.fillColor = colorString;
|
if ((child.fillColor === null && colorString) ||
|
||||||
|
child.fillColor.toCSS() !== new paper.Color(colorString).toCSS()) {
|
||||||
|
changed = true;
|
||||||
|
child.fillColor = colorString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isPointTextItem(item) && !colorString) {
|
if (isPointTextItem(item) && !colorString) {
|
||||||
colorString = 'rgba(0,0,0,0)';
|
colorString = 'rgba(0,0,0,0)';
|
||||||
}
|
}
|
||||||
item.fillColor = colorString;
|
if ((item.fillColor === null && colorString) ||
|
||||||
|
item.fillColor.toCSS() !== new paper.Color(colorString).toCSS()) {
|
||||||
|
changed = true;
|
||||||
|
item.fillColor = colorString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// @todo add back undo
|
if (changed) {
|
||||||
|
onUpdateSvg();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when setting stroke color
|
* Called when setting stroke color
|
||||||
* @param {string} colorString New color, css format
|
* @param {string} colorString New color, css format
|
||||||
|
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
||||||
*/
|
*/
|
||||||
const applyStrokeColorToSelection = function (colorString) {
|
const applyStrokeColorToSelection = function (colorString, onUpdateSvg) {
|
||||||
const items = getSelectedLeafItems();
|
const items = getSelectedLeafItems();
|
||||||
|
let changed = false;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (isPGTextItem(item)) {
|
if (isPGTextItem(item)) {
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
|
@ -47,37 +65,53 @@ const applyStrokeColorToSelection = function (colorString) {
|
||||||
if (child.children) {
|
if (child.children) {
|
||||||
for (const path of child.children) {
|
for (const path of child.children) {
|
||||||
if (!path.data.isPGGlyphRect) {
|
if (!path.data.isPGGlyphRect) {
|
||||||
path.strokeColor = colorString;
|
if ((path.strokeColor === null && colorString) ||
|
||||||
|
path.strokeColor.toCSS() !== new paper.Color(colorString).toCSS()) {
|
||||||
|
changed = true;
|
||||||
|
path.strokeColor = colorString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!child.data.isPGGlyphRect) {
|
} else if (!child.data.isPGGlyphRect) {
|
||||||
child.strokeColor = colorString;
|
if (child.strokeColor !== colorString) {
|
||||||
|
changed = true;
|
||||||
|
child.strokeColor = colorString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!item.data.isPGGlyphRect) {
|
} else if (!item.data.isPGGlyphRect) {
|
||||||
item.strokeColor = colorString;
|
if ((item.strokeColor === null && colorString) ||
|
||||||
|
item.strokeColor.toCSS() !== new paper.Color(colorString).toCSS()) {
|
||||||
|
changed = true;
|
||||||
|
item.strokeColor = colorString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if ((item.strokeColor === null && colorString) ||
|
||||||
|
item.strokeColor.toCSS() !== new paper.Color(colorString).toCSS()) {
|
||||||
|
changed = true;
|
||||||
item.strokeColor = colorString;
|
item.strokeColor = colorString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// @todo add back undo
|
if (changed) {
|
||||||
|
onUpdateSvg();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when setting stroke width
|
* Called when setting stroke width
|
||||||
* @param {number} value New stroke width
|
* @param {number} value New stroke width
|
||||||
|
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
||||||
*/
|
*/
|
||||||
const applyStrokeWidthToSelection = function (value) {
|
const applyStrokeWidthToSelection = function (value, onUpdateSvg) {
|
||||||
const items = getSelectedLeafItems();
|
const items = getSelectedLeafItems();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (isGroup(item)) {
|
if (isGroup(item)) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else if (item.strokeWidth !== value) {
|
||||||
item.strokeWidth = value;
|
item.strokeWidth = value;
|
||||||
|
onUpdateSvg();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// @todo add back undo
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -167,8 +201,11 @@ const getColorsFromSelection = function (selectedItems) {
|
||||||
const stylePath = function (path, options) {
|
const stylePath = function (path, options) {
|
||||||
if (options.isEraser) {
|
if (options.isEraser) {
|
||||||
path.fillColor = 'white';
|
path.fillColor = 'white';
|
||||||
} else {
|
} else if (options.fillColor) {
|
||||||
path.fillColor = options.fillColor;
|
path.fillColor = options.fillColor;
|
||||||
|
} else {
|
||||||
|
// Make sure something visible is drawn
|
||||||
|
path.fillColor = 'black';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -177,8 +214,11 @@ const styleCursorPreview = function (path, options) {
|
||||||
path.fillColor = 'white';
|
path.fillColor = 'white';
|
||||||
path.strokeColor = 'cornflowerblue';
|
path.strokeColor = 'cornflowerblue';
|
||||||
path.strokeWidth = 1;
|
path.strokeWidth = 1;
|
||||||
} else {
|
} else if (options.fillColor) {
|
||||||
path.fillColor = options.fillColor;
|
path.fillColor = options.fillColor;
|
||||||
|
} else {
|
||||||
|
// Make sure something visible is drawn
|
||||||
|
path.fillColor = 'black';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
49
src/helper/undo.js
Normal file
49
src/helper/undo.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// undo functionality
|
||||||
|
// modifed from https://github.com/memononen/stylii
|
||||||
|
import paper from 'paper';
|
||||||
|
|
||||||
|
const performSnapshot = function (dispatchPerformSnapshot) {
|
||||||
|
dispatchPerformSnapshot({
|
||||||
|
json: paper.project.exportJSON({asString: false})
|
||||||
|
});
|
||||||
|
|
||||||
|
// @todo enable/disable buttons
|
||||||
|
// updateButtonVisibility();
|
||||||
|
};
|
||||||
|
|
||||||
|
const _restore = function (entry, onUpdateSvg) {
|
||||||
|
for (const layer of paper.project.layers) {
|
||||||
|
layer.removeChildren();
|
||||||
|
}
|
||||||
|
paper.project.clear();
|
||||||
|
paper.project.importJSON(entry.json);
|
||||||
|
paper.view.update();
|
||||||
|
onUpdateSvg(true /* skipSnapshot */);
|
||||||
|
};
|
||||||
|
|
||||||
|
const performUndo = function (undoState, dispatchPerformUndo, onUpdateSvg) {
|
||||||
|
if (undoState.pointer > 0) {
|
||||||
|
_restore(undoState.stack[undoState.pointer - 1], onUpdateSvg);
|
||||||
|
dispatchPerformUndo();
|
||||||
|
|
||||||
|
// @todo enable/disable buttons
|
||||||
|
// updateButtonVisibility();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const performRedo = function (undoState, dispatchPerformRedo, onUpdateSvg) {
|
||||||
|
if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) {
|
||||||
|
_restore(undoState.stack[undoState.pointer + 1], onUpdateSvg);
|
||||||
|
dispatchPerformRedo();
|
||||||
|
|
||||||
|
// @todo enable/disable buttons
|
||||||
|
// updateButtonVisibility();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
performSnapshot,
|
||||||
|
performUndo,
|
||||||
|
performRedo
|
||||||
|
};
|
|
@ -5,6 +5,7 @@ import eraserModeReducer from './eraser-mode';
|
||||||
import colorReducer from './color';
|
import colorReducer from './color';
|
||||||
import hoverReducer from './hover';
|
import hoverReducer from './hover';
|
||||||
import selectedItemReducer from './selected-items';
|
import selectedItemReducer from './selected-items';
|
||||||
|
import undoReducer from './undo';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
mode: modeReducer,
|
mode: modeReducer,
|
||||||
|
@ -12,5 +13,6 @@ export default combineReducers({
|
||||||
eraserMode: eraserModeReducer,
|
eraserMode: eraserModeReducer,
|
||||||
color: colorReducer,
|
color: colorReducer,
|
||||||
hoveredItemId: hoverReducer,
|
hoveredItemId: hoverReducer,
|
||||||
selectedItems: selectedItemReducer
|
selectedItems: selectedItemReducer,
|
||||||
|
undo: undoReducer
|
||||||
});
|
});
|
||||||
|
|
79
src/reducers/undo.js
Normal file
79
src/reducers/undo.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import log from '../log/log';
|
||||||
|
|
||||||
|
const UNDO = 'scratch-paint/undo/UNDO';
|
||||||
|
const REDO = 'scratch-paint/undo/REDO';
|
||||||
|
const SNAPSHOT = 'scratch-paint/undo/SNAPSHOT';
|
||||||
|
const CLEAR = 'scratch-paint/undo/CLEAR';
|
||||||
|
const initialState = {
|
||||||
|
stack: [],
|
||||||
|
pointer: -1
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = function (state, action) {
|
||||||
|
if (typeof state === 'undefined') state = initialState;
|
||||||
|
switch (action.type) {
|
||||||
|
case UNDO:
|
||||||
|
if (state.pointer <= 0) {
|
||||||
|
log.warn(`Can't undo, undo stack is empty`);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stack: state.stack,
|
||||||
|
pointer: state.pointer - 1
|
||||||
|
};
|
||||||
|
case REDO:
|
||||||
|
if (state.pointer <= -1 || state.pointer === state.stack.length - 1) {
|
||||||
|
log.warn(`Can't redo, redo stack is empty`);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stack: state.stack,
|
||||||
|
pointer: state.pointer + 1
|
||||||
|
};
|
||||||
|
case SNAPSHOT:
|
||||||
|
if (!action.snapshot) {
|
||||||
|
log.warn(`Couldn't create undo snapshot, no data provided`);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
// Performing an action clears the redo stack
|
||||||
|
stack: state.stack.slice(0, state.pointer + 1).concat(action.snapshot),
|
||||||
|
pointer: state.pointer + 1
|
||||||
|
};
|
||||||
|
case CLEAR:
|
||||||
|
return initialState;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action creators ==================================
|
||||||
|
const undoSnapshot = function (snapshot) {
|
||||||
|
return {
|
||||||
|
type: SNAPSHOT,
|
||||||
|
snapshot: snapshot
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const undo = function () {
|
||||||
|
return {
|
||||||
|
type: UNDO
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const redo = function () {
|
||||||
|
return {
|
||||||
|
type: REDO
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const clearUndoState = function () {
|
||||||
|
return {
|
||||||
|
type: CLEAR
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
reducer as default,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
undoSnapshot,
|
||||||
|
clearUndoState
|
||||||
|
};
|
141
test/unit/undo-reducer.test.js
Normal file
141
test/unit/undo-reducer.test.js
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/* eslint-env jest */
|
||||||
|
import undoReducer from '../../src/reducers/undo';
|
||||||
|
import {undoSnapshot, undo, redo, clearUndoState} from '../../src/reducers/undo';
|
||||||
|
|
||||||
|
test('initialState', () => {
|
||||||
|
let defaultState;
|
||||||
|
|
||||||
|
expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
|
||||||
|
expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */).pointer).toEqual(-1);
|
||||||
|
expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */).stack).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('snapshot', () => {
|
||||||
|
let defaultState;
|
||||||
|
const state1 = {state: 1};
|
||||||
|
const state2 = {state: 2};
|
||||||
|
|
||||||
|
let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
|
||||||
|
expect(reduxState.pointer).toEqual(0);
|
||||||
|
expect(reduxState.stack).toHaveLength(1);
|
||||||
|
expect(reduxState.stack[0]).toEqual(state1);
|
||||||
|
|
||||||
|
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
|
||||||
|
expect(reduxState.pointer).toEqual(1);
|
||||||
|
expect(reduxState.stack).toHaveLength(2);
|
||||||
|
expect(reduxState.stack[0]).toEqual(state1);
|
||||||
|
expect(reduxState.stack[1]).toEqual(state2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalidSnapshot', () => {
|
||||||
|
let defaultState;
|
||||||
|
const state1 = {state: 1};
|
||||||
|
|
||||||
|
const reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
|
||||||
|
const newReduxState = undoReducer(reduxState /* state */, undoSnapshot() /* action */); // No snapshot provided
|
||||||
|
expect(reduxState).toEqual(newReduxState);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearUndoState', () => {
|
||||||
|
let defaultState;
|
||||||
|
const state1 = {state: 1};
|
||||||
|
const state2 = {state: 2};
|
||||||
|
|
||||||
|
// Push 2 states then clear
|
||||||
|
const reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
|
||||||
|
undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
|
||||||
|
const newReduxState = undoReducer(reduxState /* state */, clearUndoState() /* action */);
|
||||||
|
|
||||||
|
expect(newReduxState.pointer).toEqual(-1);
|
||||||
|
expect(newReduxState.stack).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cantUndo', () => {
|
||||||
|
let defaultState;
|
||||||
|
const state1 = {state: 1};
|
||||||
|
|
||||||
|
// Undo when there's no undo stack
|
||||||
|
let reduxState = undoReducer(defaultState /* state */, undo() /* action */);
|
||||||
|
|
||||||
|
expect(reduxState.pointer).toEqual(-1);
|
||||||
|
expect(reduxState.stack).toHaveLength(0);
|
||||||
|
|
||||||
|
// Undo when there's only one state
|
||||||
|
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state1]) /* action */);
|
||||||
|
reduxState = undoReducer(reduxState /* state */, undo() /* action */);
|
||||||
|
|
||||||
|
expect(reduxState.pointer).toEqual(0);
|
||||||
|
expect(reduxState.stack).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cantRedo', () => {
|
||||||
|
let defaultState;
|
||||||
|
const state1 = {state: 1};
|
||||||
|
|
||||||
|
let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
|
||||||
|
|
||||||
|
// Redo when there's no redo stack
|
||||||
|
reduxState = undoReducer(reduxState /* state */, redo() /* action */);
|
||||||
|
|
||||||
|
expect(reduxState.pointer).toEqual(0);
|
||||||
|
expect(reduxState.stack).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('undo', () => {
|
||||||
|
let defaultState;
|
||||||
|
const state1 = {state: 1};
|
||||||
|
const state2 = {state: 2};
|
||||||
|
|
||||||
|
// Push 2 states then undo one
|
||||||
|
let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
|
||||||
|
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
|
||||||
|
reduxState = undoReducer(reduxState /* state */, undo() /* action */);
|
||||||
|
|
||||||
|
expect(reduxState.pointer).toEqual(0);
|
||||||
|
expect(reduxState.stack).toHaveLength(2);
|
||||||
|
expect(reduxState.stack[0]).toEqual(state1);
|
||||||
|
expect(reduxState.stack[1]).toEqual(state2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('redo', () => {
|
||||||
|
let defaultState;
|
||||||
|
const state1 = {state: 1};
|
||||||
|
const state2 = {state: 2};
|
||||||
|
|
||||||
|
// Push 2 states then undo one
|
||||||
|
let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
|
||||||
|
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
|
||||||
|
let newReduxState = undoReducer(reduxState /* state */, undo() /* action */);
|
||||||
|
|
||||||
|
// Now redo and check equality with previous state
|
||||||
|
newReduxState = undoReducer(newReduxState /* state */, redo() /* action */);
|
||||||
|
expect(newReduxState.pointer).toEqual(reduxState.pointer);
|
||||||
|
expect(newReduxState.stack).toHaveLength(reduxState.stack.length);
|
||||||
|
expect(newReduxState.stack[0]).toEqual(reduxState.stack[0]);
|
||||||
|
expect(reduxState.stack[1]).toEqual(reduxState.stack[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('undoSnapshotCantRedo', () => {
|
||||||
|
let defaultState;
|
||||||
|
const state1 = {state: 1};
|
||||||
|
const state2 = {state: 2};
|
||||||
|
const state3 = {state: 3};
|
||||||
|
|
||||||
|
// Push 2 states then undo
|
||||||
|
let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */);
|
||||||
|
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */);
|
||||||
|
reduxState = undoReducer(reduxState /* state */, undo() /* action */);
|
||||||
|
|
||||||
|
expect(reduxState.pointer).toEqual(0);
|
||||||
|
expect(reduxState.stack).toHaveLength(2);
|
||||||
|
|
||||||
|
// Snapshot
|
||||||
|
reduxState = undoReducer(reduxState /* state */, undoSnapshot([state3]) /* action */);
|
||||||
|
// Redo should do nothing
|
||||||
|
const newReduxState = undoReducer(reduxState /* state */, redo() /* action */);
|
||||||
|
|
||||||
|
expect(newReduxState.pointer).toEqual(reduxState.pointer);
|
||||||
|
expect(newReduxState.stack).toHaveLength(reduxState.stack.length);
|
||||||
|
expect(newReduxState.stack[0]).toEqual(reduxState.stack[0]);
|
||||||
|
expect(newReduxState.stack[1]).toEqual(state3);
|
||||||
|
});
|
Loading…
Reference in a new issue