mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 13:32:28 -05:00
Add reshape button
This commit is contained in:
parent
9a09c4324d
commit
b8de3dcc3a
5 changed files with 243 additions and 0 deletions
|
@ -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 {
|
|||
<SelectMode
|
||||
onUpdateSvg={this.props.onUpdateSvg}
|
||||
/>
|
||||
<ReshapeMode
|
||||
onUpdateSvg={this.props.onUpdateSvg}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
19
src/components/reshape-mode.jsx
Normal file
19
src/components/reshape-mode.jsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
const ReshapeModeComponent = props => (
|
||||
<button onClick={props.onMouseDown}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Reshape"
|
||||
description="Label for the reshape tool, which allows changing the points in the lines of the vectors"
|
||||
id="paint.reshapeMode.reshape"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
ReshapeModeComponent.propTypes = {
|
||||
onMouseDown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ReshapeModeComponent;
|
|
@ -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);
|
||||
}
|
||||
|
|
203
src/containers/reshape-mode.jsx
Normal file
203
src/containers/reshape-mode.jsx
Normal file
|
@ -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 (
|
||||
<ReshapeModeComponent onMouseDown={this.props.handleMouseDown} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
15
test/unit/components/reshape-mode.test.jsx
Normal file
15
test/unit/components/reshape-mode.test.jsx
Normal file
|
@ -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(
|
||||
<ReshapeModeComponent onMouseDown={onClick}/>
|
||||
);
|
||||
componentShallowWrapper.simulate('click');
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue