mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-10 06:32:07 -05:00
285 lines
10 KiB
JavaScript
285 lines
10 KiB
JavaScript
import paper from '@scratch/paper';
|
|
import PropTypes from 'prop-types';
|
|
import React from 'react';
|
|
import {connect} from 'react-redux';
|
|
import bindAll from 'lodash.bindall';
|
|
import Modes from '../lib/modes';
|
|
import {clearSelection} from '../helper/selection';
|
|
import {endPointHit, touching} from '../helper/snapping';
|
|
import {drawHitPoint, removeHitPoint} from '../helper/guides';
|
|
import {stylePath} from '../helper/style-path';
|
|
import {changeStrokeColor} from '../reducers/stroke-color';
|
|
import {changeStrokeWidth} from '../reducers/stroke-width';
|
|
import {changeMode} from '../reducers/modes';
|
|
import {clearSelectedItems} from '../reducers/selected-items';
|
|
import {MIXED} from '../helper/style-path';
|
|
import {snapDeltaToAngle} from '../helper/math';
|
|
|
|
import LineModeComponent from '../components/line-mode/line-mode.jsx';
|
|
|
|
class LineMode extends React.Component {
|
|
static get SNAP_TOLERANCE () {
|
|
return 6;
|
|
}
|
|
static get DEFAULT_COLOR () {
|
|
return '#000000';
|
|
}
|
|
constructor (props) {
|
|
super(props);
|
|
bindAll(this, [
|
|
'activateTool',
|
|
'deactivateTool',
|
|
'drawHitPoint',
|
|
'onMouseDown',
|
|
'onMouseMove',
|
|
'onMouseDrag',
|
|
'onMouseUp'
|
|
]);
|
|
}
|
|
componentDidMount () {
|
|
if (this.props.isLineModeActive) {
|
|
this.activateTool();
|
|
}
|
|
}
|
|
componentWillReceiveProps (nextProps) {
|
|
if (nextProps.isLineModeActive && !this.props.isLineModeActive) {
|
|
this.activateTool();
|
|
} else if (!nextProps.isLineModeActive && this.props.isLineModeActive) {
|
|
this.deactivateTool();
|
|
}
|
|
}
|
|
shouldComponentUpdate (nextProps) {
|
|
return nextProps.isLineModeActive !== this.props.isLineModeActive;
|
|
}
|
|
activateTool () {
|
|
clearSelection(this.props.clearSelectedItems);
|
|
|
|
// Force the default line color if stroke is MIXED or transparent
|
|
const {strokeColor} = this.props.colorState;
|
|
if (strokeColor === MIXED || strokeColor === null) {
|
|
this.props.onChangeStrokeColor(LineMode.DEFAULT_COLOR);
|
|
}
|
|
// Force a minimum stroke width
|
|
if (!this.props.colorState.strokeWidth) {
|
|
this.props.onChangeStrokeWidth(1);
|
|
}
|
|
this.tool = new paper.Tool();
|
|
this.active = false;
|
|
|
|
this.path = null;
|
|
this.hitResult = null;
|
|
|
|
const lineMode = this;
|
|
this.tool.onMouseDown = function (event) {
|
|
if (event.event.button > 0) return; // only first mouse button
|
|
lineMode.onMouseDown(event);
|
|
};
|
|
this.tool.onMouseMove = function (event) {
|
|
lineMode.onMouseMove(event);
|
|
};
|
|
this.tool.onMouseDrag = function (event) {
|
|
if (event.event.button > 0) return; // only first mouse button
|
|
lineMode.onMouseDrag(event);
|
|
};
|
|
this.tool.onMouseUp = function (event) {
|
|
if (event.event.button > 0) return; // only first mouse button
|
|
lineMode.onMouseUp(event);
|
|
};
|
|
|
|
this.tool.activate();
|
|
}
|
|
onMouseDown (event) {
|
|
if (event.event.button > 0) return; // only first mouse button
|
|
this.active = true;
|
|
|
|
// If you click near a point, continue that line instead of making a new line
|
|
this.hitResult = endPointHit(event.point, LineMode.SNAP_TOLERANCE);
|
|
if (this.hitResult) {
|
|
this.path = this.hitResult.path;
|
|
stylePath(this.path, this.props.colorState.strokeColor, this.props.colorState.strokeWidth);
|
|
if (this.hitResult.isFirst) {
|
|
this.path.reverse();
|
|
}
|
|
|
|
this.path.lastSegment.handleOut = null; // Make sure added line isn't made curvy
|
|
this.path.add(this.hitResult.segment.point); // Add second point, which is what will move when dragged
|
|
}
|
|
|
|
// If not near other path, start a new path
|
|
if (!this.path) {
|
|
this.path = new paper.Path();
|
|
stylePath(this.path, this.props.colorState.strokeColor, this.props.colorState.strokeWidth);
|
|
|
|
this.path.add(event.point);
|
|
this.path.add(event.point); // Add second point, which is what will move when dragged
|
|
}
|
|
}
|
|
drawHitPoint (hitResult) {
|
|
// If near another path's endpoint, draw hit point to indicate that paths would merge
|
|
if (hitResult) {
|
|
const hitPath = hitResult.path;
|
|
if (hitResult.isFirst) {
|
|
drawHitPoint(hitPath.firstSegment.point);
|
|
} else {
|
|
drawHitPoint(hitPath.lastSegment.point);
|
|
}
|
|
}
|
|
}
|
|
onMouseMove (event) {
|
|
if (this.hitResult) {
|
|
removeHitPoint();
|
|
}
|
|
this.hitResult = endPointHit(event.point, LineMode.SNAP_TOLERANCE);
|
|
this.drawHitPoint(this.hitResult);
|
|
}
|
|
onMouseDrag (event) {
|
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
|
|
|
// Clear the last hit result
|
|
if (this.hitResult) {
|
|
removeHitPoint();
|
|
this.hitResult = null;
|
|
}
|
|
|
|
// If shift is held, act like event.point always lies on a straight or 45 degree line from the last point
|
|
let endPoint = event.point;
|
|
if (event.modifiers.shift) {
|
|
const line = event.point.subtract(this.path.lastSegment.previous.point);
|
|
endPoint = this.path.lastSegment.previous.point.add(snapDeltaToAngle(line, Math.PI / 4));
|
|
}
|
|
|
|
// Find an end point that endPoint is close to (to snap lines together)
|
|
if (this.path &&
|
|
!this.path.closed &&
|
|
this.path.segments.length > 3 &&
|
|
touching(this.path.firstSegment.point, endPoint, LineMode.SNAP_TOLERANCE)) {
|
|
this.hitResult = {
|
|
path: this.path,
|
|
segment: this.path.firstSegment,
|
|
isFirst: true
|
|
};
|
|
} else {
|
|
this.hitResult = endPointHit(endPoint, LineMode.SNAP_TOLERANCE, this.path);
|
|
}
|
|
|
|
// If shift is being held, we shouldn't snap to end points that change the slope by too much.
|
|
// In that case, clear the hit result.
|
|
if (this.hitResult && event.modifiers.shift) {
|
|
const lineToSnap = this.hitResult.segment.point.subtract(this.path.lastSegment.previous.point);
|
|
const lineToEndPoint = endPoint.subtract(this.path.lastSegment.previous.point);
|
|
if (lineToSnap.normalize().getDistance(lineToEndPoint.normalize()) > 1e-2) {
|
|
this.hitResult = null;
|
|
}
|
|
}
|
|
|
|
// If near another path's endpoint, or this path's beginpoint, clip to it to suggest
|
|
// joining/closing the paths.
|
|
if (this.hitResult) {
|
|
this.drawHitPoint(this.hitResult);
|
|
this.path.lastSegment.point = this.hitResult.segment.point;
|
|
} else {
|
|
this.path.lastSegment.point = endPoint;
|
|
}
|
|
}
|
|
onMouseUp (event) {
|
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
|
|
|
// If I single clicked, don't do anything
|
|
if (this.path.segments.length < 2 ||
|
|
(this.path.segments.length === 2 &&
|
|
touching(this.path.firstSegment.point, event.point, LineMode.SNAP_TOLERANCE) &&
|
|
!this.hitResult)) { // Let lines be short if you're connecting them
|
|
this.path.remove();
|
|
this.path = null;
|
|
return;
|
|
} else if (!this.hitResult &&
|
|
touching(this.path.lastSegment.point, this.path.segments[this.path.segments.length - 2].point,
|
|
LineMode.SNAP_TOLERANCE)) {
|
|
// Single click or short drag on an existing path end point
|
|
this.path.removeSegment(this.path.segments.length - 1);
|
|
this.path = null;
|
|
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.point.equals(this.hitResult.segment.point)) {
|
|
this.path.firstSegment.handleIn = null; // Make sure added line isn't made curvy
|
|
// close path
|
|
this.path.closed = true;
|
|
} else {
|
|
// joining two paths
|
|
if (!this.hitResult.isFirst) {
|
|
this.hitResult.path.reverse();
|
|
}
|
|
this.hitResult.path.firstSegment.handleIn = null; // Make sure added line isn't made curvy
|
|
this.path.join(this.hitResult.path);
|
|
}
|
|
removeHitPoint();
|
|
this.hitResult = null;
|
|
}
|
|
|
|
if (this.path) {
|
|
this.props.onUpdateSvg();
|
|
this.path = null;
|
|
}
|
|
this.active = false;
|
|
}
|
|
deactivateTool () {
|
|
this.tool.remove();
|
|
this.tool = null;
|
|
if (this.hitResult) {
|
|
removeHitPoint();
|
|
this.hitResult = null;
|
|
}
|
|
if (this.path) {
|
|
this.path = null;
|
|
}
|
|
}
|
|
render () {
|
|
return (
|
|
<LineModeComponent
|
|
isSelected={this.props.isLineModeActive}
|
|
onMouseDown={this.props.handleMouseDown}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
LineMode.propTypes = {
|
|
clearSelectedItems: PropTypes.func.isRequired,
|
|
colorState: PropTypes.shape({
|
|
fillColor: PropTypes.string,
|
|
strokeColor: PropTypes.string,
|
|
strokeWidth: PropTypes.number
|
|
}).isRequired,
|
|
handleMouseDown: PropTypes.func.isRequired,
|
|
isLineModeActive: PropTypes.bool.isRequired,
|
|
onChangeStrokeColor: PropTypes.func.isRequired,
|
|
onChangeStrokeWidth: PropTypes.func.isRequired,
|
|
onUpdateSvg: PropTypes.func.isRequired
|
|
};
|
|
|
|
const mapStateToProps = state => ({
|
|
colorState: state.scratchPaint.color,
|
|
isLineModeActive: state.scratchPaint.mode === Modes.LINE
|
|
});
|
|
const mapDispatchToProps = dispatch => ({
|
|
clearSelectedItems: () => {
|
|
dispatch(clearSelectedItems());
|
|
},
|
|
handleMouseDown: () => {
|
|
dispatch(changeMode(Modes.LINE));
|
|
},
|
|
onChangeStrokeColor: strokeColor => {
|
|
dispatch(changeStrokeColor(strokeColor));
|
|
},
|
|
onChangeStrokeWidth: strokeWidth => {
|
|
dispatch(changeStrokeWidth(strokeWidth));
|
|
}
|
|
});
|
|
|
|
export default connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps
|
|
)(LineMode);
|