Merge pull request #71 from fsih/penTool

Pen tool
This commit is contained in:
DD Liu 2017-10-20 14:18:04 -04:00 committed by GitHub
commit 4580fcc7d8
2 changed files with 162 additions and 48 deletions

View file

@ -5,10 +5,9 @@ import bindAll from 'lodash.bindall';
import Modes from '../modes/modes';
import {changeMode} from '../reducers/modes';
import {clearHoveredItem, setHoveredItem} from '../reducers/hover';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {clearSelectedItems} from '../reducers/selected-items';
import {getSelectedLeafItems} from '../helper/selection';
import {clearSelection} from '../helper/selection';
import PenTool from '../helper/tools/pen-tool';
import PenModeComponent from '../components/pen-mode/pen-mode.jsx';
@ -26,8 +25,10 @@ class PenMode extends React.Component {
}
}
componentWillReceiveProps (nextProps) {
if (this.tool && nextProps.hoveredItemId !== this.props.hoveredItemId) {
this.tool.setPrevHoveredItemId(nextProps.hoveredItemId);
if (this.tool &&
(nextProps.colorState.strokeColor !== this.props.colorState.strokeColor ||
nextProps.colorState.strokeWidth !== this.props.colorState.strokeWidth)) {
this.tool.setColorState(nextProps.colorState);
}
if (nextProps.isPenModeActive && !this.props.isPenModeActive) {
@ -40,13 +41,12 @@ class PenMode extends React.Component {
return nextProps.isPenModeActive !== this.props.isPenModeActive;
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
this.tool = new PenTool(
this.props.setHoveredItem,
this.props.clearHoveredItem,
this.props.setSelectedItems,
this.props.clearSelectedItems,
this.props.onUpdateSvg
);
this.tool.setColorState(this.props.colorState);
this.tool.activate();
}
deactivateTool () {
@ -65,33 +65,26 @@ class PenMode extends React.Component {
}
PenMode.propTypes = {
clearHoveredItem: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({
fillColor: PropTypes.string,
strokeColor: PropTypes.string,
strokeWidth: PropTypes.number
}).isRequired,
handleMouseDown: PropTypes.func.isRequired,
hoveredItemId: PropTypes.number,
isPenModeActive: PropTypes.bool.isRequired,
onUpdateSvg: PropTypes.func.isRequired,
setHoveredItem: PropTypes.func.isRequired,
setSelectedItems: PropTypes.func.isRequired
onUpdateSvg: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
isPenModeActive: state.scratchPaint.mode === Modes.PEN,
hoveredItemId: state.scratchPaint.hoveredItemId
colorState: state.scratchPaint.color,
isPenModeActive: state.scratchPaint.mode === Modes.PEN
});
const mapDispatchToProps = dispatch => ({
setHoveredItem: hoveredItemId => {
dispatch(setHoveredItem(hoveredItemId));
},
clearHoveredItem: () => {
dispatch(clearHoveredItem());
},
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems()));
},
handleMouseDown: () => {
dispatch(changeMode(Modes.PEN));
},

View file

@ -1,25 +1,35 @@
import paper from '@scratch/paper';
import log from '../../log/log';
import {stylePath} from '../style-path';
import {endPointHit, touching} from '../snapping';
import {drawHitPoint, removeHitPoint} from '../guides';
/**
* Tool to handle freehand drawing of lines.
*/
class PenTool extends paper.Tool {
static get SNAP_TOLERANCE () {
return 5;
}
/** Smaller numbers match the line more closely, larger numbers for smoother curves */
static get SMOOTHING () {
return 2;
}
/**
* @param {function} setHoveredItem Callback to set the hovered item
* @param {function} clearHoveredItem Callback to clear the hovered item
* @param {function} setSelectedItems Callback to set the set of selected items in the Redux state
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
*/
constructor (setHoveredItem, clearHoveredItem, setSelectedItems, clearSelectedItems, onUpdateSvg) {
constructor (clearSelectedItems, onUpdateSvg) {
super();
this.setHoveredItem = setHoveredItem;
this.clearHoveredItem = clearHoveredItem;
this.setSelectedItems = setSelectedItems;
this.clearSelectedItems = clearSelectedItems;
this.onUpdateSvg = onUpdateSvg;
this.prevHoveredItemId = null;
this.colorState = null;
this.path = null;
this.hitResult = null;
// Piece of whole path that was added by last stroke. Used to smooth just the added part.
this.subpath = null;
this.subpathIndex = 0;
// We have to set these functions instead of just declaring them because
// paper.js tools hook up the listeners in the setter functions.
@ -27,27 +37,138 @@ class PenTool extends paper.Tool {
this.onMouseMove = this.handleMouseMove;
this.onMouseDrag = this.handleMouseDrag;
this.onMouseUp = this.handleMouseUp;
this.fixedDistance = 2;
}
/**
* To be called when the hovered item changes. When the select tool hovers over a
* new item, it compares against this to see if a hover item change event needs to
* be fired.
* @param {paper.Item} prevHoveredItemId ID of the highlight item that indicates the mouse is
* over a given item currently
*/
setPrevHoveredItemId (prevHoveredItemId) {
this.prevHoveredItemId = prevHoveredItemId;
setColorState (colorState) {
this.colorState = colorState;
}
handleMouseDown () {
log.warn('Pen not yet implemented');
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);
}
}
}
handleMouseMove () {
handleMouseDown (event) {
if (event.event.button > 0) return; // only first mouse button
this.subpath = new paper.Path({insert: false});
// If you click near a point, continue that line instead of making a new line
this.hitResult = endPointHit(event.point, PenTool.SNAP_TOLERANCE);
if (this.hitResult) {
this.path = this.hitResult.path;
stylePath(this.path, this.colorState.strokeColor, this.colorState.strokeWidth);
if (this.hitResult.isFirst) {
this.path.reverse();
}
this.subpathIndex = this.path.segments.length;
this.path.lastSegment.handleOut = null; // Don't interfere with the curvature of the existing path
this.path.lastSegment.handleIn = null;
}
// If not near other path, start a new path
if (!this.path) {
this.path = new paper.Path();
stylePath(this.path, this.colorState.strokeColor, this.colorState.strokeWidth);
this.path.add(event.point);
this.subpath.add(event.point);
paper.view.draw();
}
}
handleMouseDrag () {
handleMouseMove (event) {
// If near another path's endpoint, or this path's beginpoint, clip to it to suggest
// joining/closing the paths.
if (this.hitResult) {
removeHitPoint();
}
this.hitResult = endPointHit(event.point, PenTool.SNAP_TOLERANCE);
this.drawHitPoint(this.hitResult);
}
handleMouseUp () {
handleMouseDrag (event) {
if (event.event.button > 0) return; // only first mouse button
// If near another path's endpoint, or this path's beginpoint, highlight it to suggest
// joining/closing the paths.
if (this.hitResult) {
removeHitPoint();
this.hitResult = null;
}
if (this.path &&
!this.path.closed &&
this.path.segments.length > 3 &&
touching(this.path.firstSegment.point, event.point, PenTool.SNAP_TOLERANCE)) {
this.hitResult = {
path: this.path,
segment: this.path.firstSegment,
isFirst: true
};
} else {
this.hitResult = endPointHit(event.point, PenTool.SNAP_TOLERANCE, this.path);
}
if (this.hitResult) {
this.drawHitPoint(this.hitResult);
}
this.path.add(event.point);
this.subpath.add(event.point);
}
handleMouseUp (event) {
if (event.event.button > 0) return; // only first mouse button
// If I single clicked, don't do anything
if (!this.hitResult && // Might be connecting 2 points that are very close
(this.path.segments.length < 2 ||
(this.path.segments.length === 2 &&
touching(this.path.firstSegment.point, event.point, PenTool.SNAP_TOLERANCE)))) {
this.path.remove();
this.path = null;
return;
}
// Smooth only the added portion
const hasStartConnection = this.subpathIndex > 0;
const hasEndConnection = !!this.hitResult;
this.path.removeSegments(this.subpathIndex);
this.subpath.simplify(this.SMOOTHING);
if (hasStartConnection && this.subpath.length > 0) {
this.subpath.removeSegment(0);
}
if (hasEndConnection && this.subpath.length > 0) {
this.subpath.removeSegment(this.subpath.length - 1);
}
this.path.insertSegments(this.subpathIndex, this.subpath.segments);
this.subpath = null;
this.subpathIndex = 0;
// If I intersect other line end points, join or close
if (this.hitResult) {
if (touching(this.path.firstSegment.point, this.hitResult.segment.point, PenTool.SNAP_TOLERANCE)) {
// close path
this.path.closed = true;
} else {
// joining two paths
if (!this.hitResult.isFirst) {
this.hitResult.path.reverse();
}
this.path.join(this.hitResult.path);
}
removeHitPoint();
this.hitResult = null;
}
if (this.path) {
this.onUpdateSvg();
this.path = null;
}
}
deactivateTool () {
this.fixedDistance = 1;
}
}