mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 13:32:28 -05:00
add tests
This commit is contained in:
parent
c2cae279b7
commit
a622d0d3e9
5 changed files with 185 additions and 146 deletions
|
@ -44,7 +44,7 @@
|
||||||
"eslint-config-import": "^0.13.0",
|
"eslint-config-import": "^0.13.0",
|
||||||
"eslint-config-scratch": "^3.0.0",
|
"eslint-config-scratch": "^3.0.0",
|
||||||
"eslint-plugin-import": "^2.7.0",
|
"eslint-plugin-import": "^2.7.0",
|
||||||
"eslint-plugin-react": "^7.0.1",
|
"eslint-plugin-react": "^7.3.0",
|
||||||
"gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder",
|
"gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder",
|
||||||
"html-webpack-plugin": "2.28.0",
|
"html-webpack-plugin": "2.28.0",
|
||||||
"jest": "^20.0.4",
|
"jest": "^20.0.4",
|
||||||
|
|
|
@ -24,7 +24,7 @@ class PaintEditorComponent extends React.Component {
|
||||||
<PaperCanvas canvasRef={this.setCanvas} />
|
<PaperCanvas canvasRef={this.setCanvas} />
|
||||||
<BrushMode canvas={this.state.canvas} />
|
<BrushMode canvas={this.state.canvas} />
|
||||||
<EraserMode canvas={this.state.canvas} />
|
<EraserMode canvas={this.state.canvas} />
|
||||||
<LineMode />
|
<LineMode canvas={this.state.canvas} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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 {changeLineWidth} from '../reducers/line-mode';
|
||||||
import LineModeComponent from '../components/line-mode.jsx';
|
import LineModeComponent from '../components/line-mode.jsx';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import paper from 'paper';
|
import paper from 'paper';
|
||||||
|
@ -16,8 +17,13 @@ class LineMode extends React.Component {
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'activateTool',
|
'activateTool',
|
||||||
'deactivateTool',
|
'deactivateTool',
|
||||||
|
'onMouseDown',
|
||||||
|
'onMouseMove',
|
||||||
|
'onMouseDrag',
|
||||||
|
'onMouseUp',
|
||||||
'toleranceSquared',
|
'toleranceSquared',
|
||||||
'findLineEnd'
|
'findLineEnd',
|
||||||
|
'onScroll'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
@ -30,8 +36,6 @@ class LineMode extends React.Component {
|
||||||
this.activateTool();
|
this.activateTool();
|
||||||
} else if (!nextProps.isLineModeActive && this.props.isLineModeActive) {
|
} else if (!nextProps.isLineModeActive && this.props.isLineModeActive) {
|
||||||
this.deactivateTool();
|
this.deactivateTool();
|
||||||
} else if (nextProps.isLineModeActive && this.props.isLineModeActive) {
|
|
||||||
this.blob.setOptions(nextProps.lineModeState);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shouldComponentUpdate () {
|
shouldComponentUpdate () {
|
||||||
|
@ -40,7 +44,7 @@ class LineMode extends React.Component {
|
||||||
activateTool () {
|
activateTool () {
|
||||||
// TODO add back selection
|
// TODO add back selection
|
||||||
// pg.selection.clearSelection();
|
// pg.selection.clearSelection();
|
||||||
|
this.props.canvas.addEventListener('mousewheel', this.onScroll);
|
||||||
this.tool = new paper.Tool();
|
this.tool = new paper.Tool();
|
||||||
|
|
||||||
this.path = null;
|
this.path = null;
|
||||||
|
@ -56,58 +60,103 @@ class LineMode extends React.Component {
|
||||||
const lineMode = this;
|
const lineMode = this;
|
||||||
this.tool.onMouseDown = function (event) {
|
this.tool.onMouseDown = function (event) {
|
||||||
if (event.event.button > 0) return; // only first mouse button
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
lineMode.onMouseDown(event);
|
||||||
if (this.path) {
|
};
|
||||||
this.path.setSelected(false);
|
this.tool.onMouseMove = function (event) {
|
||||||
this.path = null;
|
lineMode.onMouseMove(event);
|
||||||
}
|
};
|
||||||
|
this.tool.onMouseDrag = function (event) {
|
||||||
// If you click near a point, continue that line instead of making a new line
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
this.hitResult = lineMode.findLineEnd(event.point);
|
lineMode.onMouseDrag(event);
|
||||||
if (this.hitResult) {
|
};
|
||||||
this.path = this.hitResult.path;
|
this.tool.onMouseUp = function (event) {
|
||||||
if (this.hitResult.isFirst) {
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
this.path.reverse();
|
lineMode.onMouseUp(event);
|
||||||
}
|
|
||||||
this.path.lastSegment.setSelected(true);
|
|
||||||
this.path.add(this.hitResult.segment); // Add second point, which is what will move when dragged
|
|
||||||
this.path.lastSegment.handleOut = null; // Make sure line isn't curvy
|
|
||||||
this.path.lastSegment.handleIn = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not near other path, start a new path
|
|
||||||
if (!this.path) {
|
|
||||||
this.path = new paper.Path();
|
|
||||||
|
|
||||||
// TODO add back style
|
|
||||||
// this.path = pg.stylebar.applyActiveToolbarStyle(path);
|
|
||||||
this.path.setStrokeColor('black');
|
|
||||||
|
|
||||||
this.path.setSelected(true);
|
|
||||||
this.path.add(event.point);
|
|
||||||
this.path.add(event.point); // Add second point, which is what will move when dragged
|
|
||||||
paper.view.draw();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.tool.onMouseMove = function (event) {
|
this.tool.activate();
|
||||||
// If near another path's endpoint, or this path's beginpoint, clip to it to suggest
|
}
|
||||||
// joining/closing the paths.
|
onMouseDown (event) {
|
||||||
if (this.hitResult) {
|
// Deselect old path
|
||||||
this.hitResult.path.setSelected(false);
|
if (this.path) {
|
||||||
this.hitResult = null;
|
this.path.setSelected(false);
|
||||||
}
|
this.path = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.path && !this.path.closed && this.path.firstSegment.point.getDistance(event.point, true) < lineMode.toleranceSquared()) {
|
// If you click near a point, continue that line instead of making a new line
|
||||||
this.hitResult = {
|
this.hitResult = this.findLineEnd(event.point);
|
||||||
path: this.path,
|
if (this.hitResult) {
|
||||||
segment: this.path.firstSegment,
|
this.path = this.hitResult.path;
|
||||||
isFirst: true
|
if (this.hitResult.isFirst) {
|
||||||
};
|
this.path.reverse();
|
||||||
|
}
|
||||||
|
this.path.lastSegment.setSelected(true);
|
||||||
|
this.path.add(this.hitResult.segment); // Add second point, which is what will move when dragged
|
||||||
|
this.path.lastSegment.handleOut = null; // Make sure line isn't curvy
|
||||||
|
this.path.lastSegment.handleIn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not near other path, start a new path
|
||||||
|
if (!this.path) {
|
||||||
|
this.path = new paper.Path();
|
||||||
|
|
||||||
|
// TODO add back style
|
||||||
|
// this.path = pg.stylebar.applyActiveToolbarStyle(path);
|
||||||
|
this.path.setStrokeColor('black');
|
||||||
|
this.path.setStrokeWidth(this.props.lineModeState.lineWidth);
|
||||||
|
|
||||||
|
this.path.setSelected(true);
|
||||||
|
this.path.add(event.point);
|
||||||
|
this.path.add(event.point); // Add second point, which is what will move when dragged
|
||||||
|
paper.view.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMouseMove (event) {
|
||||||
|
// If near another path's endpoint, or this path's beginpoint, clip to it to suggest
|
||||||
|
// joining/closing the paths.
|
||||||
|
if (this.hitResult) {
|
||||||
|
this.hitResult.path.setSelected(false);
|
||||||
|
this.hitResult = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.path &&
|
||||||
|
!this.path.closed &&
|
||||||
|
this.path.firstSegment.point.getDistance(event.point, true) < this.toleranceSquared()) {
|
||||||
|
this.hitResult = {
|
||||||
|
path: this.path,
|
||||||
|
segment: this.path.firstSegment,
|
||||||
|
isFirst: true
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.hitResult = this.findLineEnd(event.point);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hitResult) {
|
||||||
|
const hitPath = this.hitResult.path;
|
||||||
|
hitPath.setSelected(true);
|
||||||
|
if (this.hitResult.isFirst) {
|
||||||
|
hitPath.firstSegment.setSelected(true);
|
||||||
} else {
|
} else {
|
||||||
this.hitResult = lineMode.findLineEnd(event.point);
|
hitPath.lastSegment.setSelected(true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMouseDrag (event) {
|
||||||
|
// If near another path's endpoint, or this path's beginpoint, clip to it to suggest
|
||||||
|
// joining/closing the paths.
|
||||||
|
if (this.hitResult && this.hitResult.path !== this.path) this.hitResult.path.setSelected(false);
|
||||||
|
this.hitResult = null;
|
||||||
|
|
||||||
|
if (this.path &&
|
||||||
|
this.path.segments.length > 3 &&
|
||||||
|
this.path.firstSegment.point.getDistance(event.point, true) < this.toleranceSquared()) {
|
||||||
|
this.hitResult = {
|
||||||
|
path: this.path,
|
||||||
|
segment: this.path.firstSegment,
|
||||||
|
isFirst: true
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.hitResult = this.findLineEnd(event.point, this.path);
|
||||||
if (this.hitResult) {
|
if (this.hitResult) {
|
||||||
const hitPath = this.hitResult.path;
|
const hitPath = this.hitResult.path;
|
||||||
hitPath.setSelected(true);
|
hitPath.setSelected(true);
|
||||||
|
@ -117,84 +166,54 @@ class LineMode extends React.Component {
|
||||||
hitPath.lastSegment.setSelected(true);
|
hitPath.lastSegment.setSelected(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
this.tool.onMouseDrag = function (event) {
|
|
||||||
if (event.event.button > 0) return; // only first mouse button
|
|
||||||
// If near another path's endpoint, or this path's beginpoint, clip to it to suggest
|
|
||||||
// joining/closing the paths.
|
|
||||||
if (this.hitResult && this.hitResult.path !== this.path) this.hitResult.path.setSelected(false);
|
|
||||||
this.hitResult = null;
|
|
||||||
|
|
||||||
if (this.path && this.path.segments.length > 3 && this.path.firstSegment.point.getDistance(event.point, true) < lineMode.toleranceSquared()) {
|
// snapping
|
||||||
this.hitResult = {
|
if (this.path) {
|
||||||
path: this.path,
|
|
||||||
segment: this.path.firstSegment,
|
|
||||||
isFirst: true
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.hitResult = lineMode.findLineEnd(event.point, this.path);
|
|
||||||
if (this.hitResult) {
|
|
||||||
const hitPath = this.hitResult.path;
|
|
||||||
hitPath.setSelected(true);
|
|
||||||
if (this.hitResult.isFirst) {
|
|
||||||
hitPath.firstSegment.setSelected(true);
|
|
||||||
} else {
|
|
||||||
hitPath.lastSegment.setSelected(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// snapping
|
|
||||||
if (this.path) {
|
|
||||||
if (this.hitResult) {
|
|
||||||
this.path.lastSegment.point = this.hitResult.segment.point;
|
|
||||||
} else {
|
|
||||||
this.path.lastSegment.point = event.point;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.tool.onMouseUp = function (event) {
|
|
||||||
if (event.event.button > 0) return; // only first mouse button
|
|
||||||
|
|
||||||
// If I single clicked, don't do anything
|
|
||||||
if (this.path.segments.length < 2 || (this.path.segments.length === 2 && this.path.firstSegment.point.getDistance(event.point, true) < lineMode.toleranceSquared())) {
|
|
||||||
this.path.remove();
|
|
||||||
this.path = null;
|
|
||||||
// TODO don't erase the line if both ends are snapped to different points
|
|
||||||
return;
|
|
||||||
} else if (this.path.lastSegment.point.getDistance(this.path.segments[this.path.segments.length - 2].point, true) < lineMode.toleranceSquared()) {
|
|
||||||
this.path.removeSegment(this.path.segments.length - 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If I intersect other line end points, join or close
|
|
||||||
if (this.hitResult) {
|
if (this.hitResult) {
|
||||||
this.path.removeSegment(this.path.segments.length - 1);
|
this.path.lastSegment.point = this.hitResult.segment.point;
|
||||||
if (this.path.firstSegment === this.hitResult.segment) {
|
} else {
|
||||||
// close path
|
this.path.lastSegment.point = event.point;
|
||||||
this.path.closed = true;
|
|
||||||
this.path.setSelected(false);
|
|
||||||
} else {
|
|
||||||
// joining two paths
|
|
||||||
if (!this.hitResult.isFirst) {
|
|
||||||
this.hitResult.path.reverse();
|
|
||||||
}
|
|
||||||
this.path.join(this.hitResult.path);
|
|
||||||
}
|
|
||||||
this.hitResult = null;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMouseUp (event) {
|
||||||
|
// If I single clicked, don't do anything
|
||||||
|
if (this.path.segments.length < 2 ||
|
||||||
|
(this.path.segments.length === 2 &&
|
||||||
|
this.path.firstSegment.point.getDistance(event.point, true) < this.toleranceSquared())) {
|
||||||
|
this.path.remove();
|
||||||
|
this.path = null;
|
||||||
|
// TODO don't erase the line if both ends are snapped to different points
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
|
this.path.lastSegment.point.getDistance(this.path.segments[this.path.segments.length - 2].point, true) <
|
||||||
|
this.toleranceSquared()) {
|
||||||
|
this.path.removeSegment(this.path.segments.length - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If I intersect other line end points, join or close
|
||||||
|
if (this.hitResult) {
|
||||||
|
this.path.removeSegment(this.path.segments.length - 1);
|
||||||
|
if (this.path.firstSegment === this.hitResult.segment) {
|
||||||
|
// close path
|
||||||
|
this.path.closed = true;
|
||||||
|
this.path.setSelected(false);
|
||||||
|
} else {
|
||||||
|
// joining two paths
|
||||||
|
if (!this.hitResult.isFirst) {
|
||||||
|
this.hitResult.path.reverse();
|
||||||
|
}
|
||||||
|
this.path.join(this.hitResult.path);
|
||||||
|
}
|
||||||
|
this.hitResult = null;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO add back undo
|
// TODO add back undo
|
||||||
// if (this.path) {
|
// if (this.path) {
|
||||||
// pg.undo.snapshot('line');
|
// pg.undo.snapshot('line');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
this.tool.activate();
|
|
||||||
}
|
}
|
||||||
toleranceSquared () {
|
toleranceSquared () {
|
||||||
return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2);
|
return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2);
|
||||||
|
@ -211,7 +230,8 @@ class LineMode extends React.Component {
|
||||||
if (excludePath && lines[i] === excludePath) {
|
if (excludePath && lines[i] === excludePath) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (lines[i].firstSegment && lines[i].firstSegment.point.getDistance(point, true) < this.toleranceSquared()) {
|
if (lines[i].firstSegment &&
|
||||||
|
lines[i].firstSegment.point.getDistance(point, true) < this.toleranceSquared()) {
|
||||||
return {
|
return {
|
||||||
path: lines[i],
|
path: lines[i],
|
||||||
segment: lines[i].firstSegment,
|
segment: lines[i].firstSegment,
|
||||||
|
@ -229,11 +249,20 @@ class LineMode extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
deactivateTool () {
|
deactivateTool () {
|
||||||
|
this.props.canvas.removeEventListener('mousewheel', this.onScroll);
|
||||||
if (this.path) {
|
if (this.path) {
|
||||||
this.path.setSelected(false);
|
this.path.setSelected(false);
|
||||||
this.path = null;
|
this.path = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onScroll (event) {
|
||||||
|
if (event.deltaY < 0) {
|
||||||
|
this.props.changeLineWidth(this.props.lineModeState.lineWidth + 1);
|
||||||
|
} else if (event.deltaY > 0 && this.props.lineModeState.lineWidth > 1) {
|
||||||
|
this.props.changeLineWidth(this.props.lineModeState.lineWidth - 1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<LineModeComponent onMouseDown={this.props.handleMouseDown} />
|
<LineModeComponent onMouseDown={this.props.handleMouseDown} />
|
||||||
|
@ -242,6 +271,8 @@ class LineMode extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
LineMode.propTypes = {
|
LineMode.propTypes = {
|
||||||
|
canvas: PropTypes.instanceOf(Element).isRequired,
|
||||||
|
changeLineWidth: PropTypes.func.isRequired,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
isLineModeActive: PropTypes.bool.isRequired,
|
isLineModeActive: PropTypes.bool.isRequired,
|
||||||
lineModeState: PropTypes.shape({
|
lineModeState: PropTypes.shape({
|
||||||
|
@ -254,6 +285,9 @@ const mapStateToProps = state => ({
|
||||||
isLineModeActive: state.mode === Modes.LINE
|
isLineModeActive: state.mode === Modes.LINE
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
changeLineWidth: lineWidth => {
|
||||||
|
dispatch(changeLineWidth(lineWidth));
|
||||||
|
},
|
||||||
handleMouseDown: () => {
|
handleMouseDown: () => {
|
||||||
dispatch(changeMode(Modes.LINE));
|
dispatch(changeMode(Modes.LINE));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,31 @@
|
||||||
|
import log from '../log/log';
|
||||||
|
|
||||||
|
const CHANGE_LINE_WIDTH = 'scratch-paint/line-mode/CHANGE_LINE_WIDTH';
|
||||||
const initialState = {lineWidth: 2};
|
const initialState = {lineWidth: 2};
|
||||||
|
|
||||||
const reducer = function (state) {
|
const reducer = function (state, action) {
|
||||||
if (typeof state === 'undefined') state = initialState;
|
if (typeof state === 'undefined') state = initialState;
|
||||||
return state;
|
switch (action.type) {
|
||||||
|
case CHANGE_LINE_WIDTH:
|
||||||
|
if (isNaN(action.lineWidth)) {
|
||||||
|
log.warn(`Invalid line width: ${action.lineWidth}`);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {lineWidth: Math.max(1, action.lineWidth)};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Action creators ==================================
|
// Action creators ==================================
|
||||||
|
const changeLineWidth = function (lineWidth) {
|
||||||
|
return {
|
||||||
|
type: CHANGE_LINE_WIDTH,
|
||||||
|
lineWidth: lineWidth
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
export default reducer;
|
reducer as default,
|
||||||
|
changeLineWidth
|
||||||
|
};
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
/* eslint-env jest */
|
|
||||||
import React from 'react'; // eslint-disable-line no-unused-vars
|
|
||||||
import {shallow} from 'enzyme';
|
|
||||||
import EraserModeComponent from '../../../src/components/eraser-mode.jsx'; // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
describe('EraserModeComponent', () => {
|
|
||||||
test('triggers callback when clicked', () => {
|
|
||||||
const onClick = jest.fn();
|
|
||||||
const componentShallowWrapper = shallow(
|
|
||||||
<EraserModeComponent onMouseDown={onClick}/>
|
|
||||||
);
|
|
||||||
componentShallowWrapper.simulate('click');
|
|
||||||
expect(onClick).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in a new issue