diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx
index b565eff8..da5fcd3f 100644
--- a/src/components/paint-editor.jsx
+++ b/src/components/paint-editor.jsx
@@ -3,6 +3,7 @@ import React from 'react';
import PaperCanvas from '../containers/paper-canvas.jsx';
import BrushMode from '../containers/brush-mode.jsx';
import EraserMode from '../containers/eraser-mode.jsx';
+import ReshapeMode from '../containers/reshape-mode.jsx';
import SelectMode from '../containers/select-mode.jsx';
import PropTypes from 'prop-types';
import LineMode from '../containers/line-mode.jsx';
@@ -130,6 +131,9 @@ class PaintEditorComponent extends React.Component {
+
) : null}
diff --git a/src/components/reshape-mode.jsx b/src/components/reshape-mode.jsx
new file mode 100644
index 00000000..a2b06f40
--- /dev/null
+++ b/src/components/reshape-mode.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {FormattedMessage} from 'react-intl';
+
+const ReshapeModeComponent = props => (
+
+);
+
+ReshapeModeComponent.propTypes = {
+ onMouseDown: PropTypes.func.isRequired
+};
+
+export default ReshapeModeComponent;
diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx
index 50e95b51..ab6dc3e2 100644
--- a/src/containers/paper-canvas.jsx
+++ b/src/containers/paper-canvas.jsx
@@ -15,6 +15,8 @@ class PaperCanvas extends React.Component {
}
componentDidMount () {
paper.setup(this.canvas);
+ // Don't show handles by default
+ paper.settings.handleSize = 0;
if (this.props.svg) {
this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY);
}
diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx
new file mode 100644
index 00000000..1f729841
--- /dev/null
+++ b/src/containers/reshape-mode.jsx
@@ -0,0 +1,203 @@
+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 {changeMode} from '../reducers/modes';
+import {setHoveredItem, clearHoveredItem} from '../reducers/hover';
+
+import {getHoveredItem} from '../helper/hover';
+import {rectSelect} from '../helper/guides';
+import {processRectangularSelection} from '../helper/selection';
+
+import ReshapeModeComponent from '../components/reshape-mode.jsx';
+import BoundingBoxTool from '../helper/bounding-box/bounding-box-tool';
+import paper from 'paper';
+
+class ReshapeMode extends React.Component {
+ static get TOLERANCE () {
+ return 8;
+ }
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'activateTool',
+ 'deactivateTool',
+ 'getHitOptions'
+ ]);
+
+ this._hitOptionsSelected = {
+ match: function (item) {
+ if (!item.item || !item.item.selected) return;
+ if (item.type === 'handle-out' || item.type === 'handle-in') {
+ // Only hit test against handles that are visible, that is,
+ // their segment is selected
+ if (!item.segment.selected) {
+ return false;
+ }
+ // If the entire shape is selected, handles are hidden
+ if (item.item.fullySelected) {
+ return false;
+ }
+ }
+ return true;
+ },
+ segments: true,
+ stroke: true,
+ curves: true,
+ handles: true,
+ fill: true,
+ guide: false
+ };
+ this._hitOptions = {
+ match: function (item) {
+ if (item.type === 'handle-out' || item.type === 'handle-in') {
+ // Only hit test against handles that are visible, that is,
+ // their segment is selected
+ if (!item.segment.selected) {
+ return false;
+ }
+ // If the entire shape is selected, handles are hidden
+ if (item.item.fullySelected) {
+ return false;
+ }
+ }
+ return true;
+ },
+ segments: true,
+ stroke: true,
+ curves: true,
+ handles: true,
+ fill: true,
+ guide: false
+ };
+ this.boundingBoxTool = new BoundingBoxTool();
+ this.selectionBoxMode = false;
+ this.selectionRect = null;
+ }
+ componentDidMount () {
+ if (this.props.isReshapeModeActive) {
+ this.activateTool(this.props);
+ }
+ }
+ componentWillReceiveProps (nextProps) {
+ if (nextProps.isReshapeModeActive && !this.props.isReshapeModeActive) {
+ this.activateTool();
+ } else if (!nextProps.isReshapeModeActive && this.props.isReshapeModeActive) {
+ this.deactivateTool();
+ }
+ }
+ shouldComponentUpdate () {
+ return false; // Static component, for now
+ }
+ getHitOptions (preselectedOnly) {
+ this._hitOptions.tolerance = ReshapeMode.TOLERANCE / paper.view.zoom;
+ this._hitOptionsSelected.tolerance = ReshapeMode.TOLERANCE / paper.view.zoom;
+ return preselectedOnly ? this._hitOptionsSelected : this._hitOptions;
+ }
+ activateTool () {
+ paper.settings.handleSize = 8;
+ this.boundingBoxTool.setSelectionBounds();
+ this.tool = new paper.Tool();
+
+ const reshapeMode = this;
+
+ this.tool.onMouseDown = function (event) {
+ if (event.event.button > 0) return; // only first mouse button
+
+ reshapeMode.props.clearHoveredItem();
+ if (!reshapeMode.boundingBoxTool
+ .onMouseDown(
+ event,
+ event.modifiers.alt,
+ event.modifiers.shift,
+ reshapeMode.getHitOptions(false /* preseelectedOnly */))) {
+ reshapeMode.selectionBoxMode = true;
+ }
+ };
+
+ this.tool.onMouseMove = function (event) {
+ const hoveredItem = getHoveredItem(event, reshapeMode.getHitOptions());
+ const oldHoveredItem = reshapeMode.props.hoveredItem;
+ if ((!hoveredItem && oldHoveredItem) || // There is no longer a hovered item
+ (hoveredItem && !oldHoveredItem) || // There is now a hovered item
+ (hoveredItem && oldHoveredItem && hoveredItem.id !== oldHoveredItem.id)) { // hovered item changed
+ reshapeMode.props.setHoveredItem(hoveredItem);
+ }
+ };
+
+
+ this.tool.onMouseDrag = function (event) {
+ if (event.event.button > 0) return; // only first mouse button
+
+ if (reshapeMode.selectionBoxMode) {
+ reshapeMode.selectionRect = rectSelect(event);
+ // Remove this rect on the next drag and up event
+ reshapeMode.selectionRect.removeOnDrag();
+ } else {
+ reshapeMode.boundingBoxTool.onMouseDrag(event);
+ }
+ };
+
+ this.tool.onMouseUp = function (event) {
+ if (event.event.button > 0) return; // only first mouse button
+
+ if (reshapeMode.selectionBoxMode) {
+ if (reshapeMode.selectionRect) {
+ processRectangularSelection(event, reshapeMode.selectionRect, Modes.RESHAPE);
+ reshapeMode.selectionRect.remove();
+ }
+ reshapeMode.boundingBoxTool.setSelectionBounds();
+ } else {
+ reshapeMode.boundingBoxTool.onMouseUp(event);
+ reshapeMode.props.onUpdateSvg();
+ }
+ reshapeMode.selectionBoxMode = false;
+ reshapeMode.selectionRect = null;
+ };
+ this.tool.activate();
+ }
+ deactivateTool () {
+ paper.settings.handleSize = 0;
+ this.props.clearHoveredItem();
+ this.tool.remove();
+ this.tool = null;
+ this.hitResult = null;
+ }
+ render () {
+ return (
+
+ );
+ }
+}
+
+ReshapeMode.propTypes = {
+ clearHoveredItem: PropTypes.func.isRequired,
+ handleMouseDown: PropTypes.func.isRequired,
+ hoveredItem: PropTypes.instanceOf(paper.Item), // eslint-disable-line react/no-unused-prop-types
+ isReshapeModeActive: PropTypes.bool.isRequired,
+ onUpdateSvg: PropTypes.func.isRequired, // eslint-disable-line react/no-unused-prop-types
+ setHoveredItem: PropTypes.func.isRequired // eslint-disable-line react/no-unused-prop-types
+};
+
+const mapStateToProps = state => ({
+ isReshapeModeActive: state.scratchPaint.mode === Modes.RESHAPE,
+ hoveredItem: state.scratchPaint.hoveredItem
+});
+const mapDispatchToProps = dispatch => ({
+ setHoveredItem: hoveredItem => {
+ dispatch(setHoveredItem(hoveredItem));
+ },
+ clearHoveredItem: () => {
+ dispatch(clearHoveredItem());
+ },
+ handleMouseDown: () => {
+ dispatch(changeMode(Modes.RESHAPE));
+ }
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ReshapeMode);
diff --git a/test/unit/components/reshape-mode.test.jsx b/test/unit/components/reshape-mode.test.jsx
new file mode 100644
index 00000000..a6c71a3a
--- /dev/null
+++ b/test/unit/components/reshape-mode.test.jsx
@@ -0,0 +1,15 @@
+/* eslint-env jest */
+import React from 'react'; // eslint-disable-line no-unused-vars
+import {shallow} from 'enzyme';
+import ReshapeModeComponent from '../../../src/components/reshape-mode.jsx'; // eslint-disable-line no-unused-vars
+
+describe('ReshapeModeComponent', () => {
+ test('triggers callback when clicked', () => {
+ const onClick = jest.fn();
+ const componentShallowWrapper = shallow(
+
+ );
+ componentShallowWrapper.simulate('click');
+ expect(onClick).toHaveBeenCalled();
+ });
+});