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 PaperCanvas from '../containers/paper-canvas.jsx';
|
||||||
import BrushMode from '../containers/brush-mode.jsx';
|
import BrushMode from '../containers/brush-mode.jsx';
|
||||||
import EraserMode from '../containers/eraser-mode.jsx';
|
import EraserMode from '../containers/eraser-mode.jsx';
|
||||||
|
import ReshapeMode from '../containers/reshape-mode.jsx';
|
||||||
import SelectMode from '../containers/select-mode.jsx';
|
import SelectMode from '../containers/select-mode.jsx';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import LineMode from '../containers/line-mode.jsx';
|
import LineMode from '../containers/line-mode.jsx';
|
||||||
|
@ -130,6 +131,9 @@ class PaintEditorComponent extends React.Component {
|
||||||
<SelectMode
|
<SelectMode
|
||||||
onUpdateSvg={this.props.onUpdateSvg}
|
onUpdateSvg={this.props.onUpdateSvg}
|
||||||
/>
|
/>
|
||||||
|
<ReshapeMode
|
||||||
|
onUpdateSvg={this.props.onUpdateSvg}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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 () {
|
componentDidMount () {
|
||||||
paper.setup(this.canvas);
|
paper.setup(this.canvas);
|
||||||
|
// Don't show handles by default
|
||||||
|
paper.settings.handleSize = 0;
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
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