From 131193ef730df14208d6c0f539e0599953554bd9 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 18 Oct 2017 19:22:02 -0400 Subject: [PATCH] Pen tool --- src/containers/pen-mode.jsx | 26 ++++-- src/helper/tools/pen-tool.js | 156 ++++++++++++++++++++++++++++++++--- 2 files changed, 164 insertions(+), 18 deletions(-) diff --git a/src/containers/pen-mode.jsx b/src/containers/pen-mode.jsx index 0519f963..eaa0421b 100644 --- a/src/containers/pen-mode.jsx +++ b/src/containers/pen-mode.jsx @@ -6,9 +6,9 @@ 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.jsx'; @@ -29,6 +29,11 @@ class PenMode extends React.Component { 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) { this.activateTool(); @@ -40,13 +45,15 @@ class PenMode extends React.Component { return false; // Static component, for now } 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.setPrevHoveredItemId(this.props.hoveredItemId); + this.tool.setColorState(this.props.colorState); this.tool.activate(); } deactivateTool () { @@ -64,17 +71,23 @@ 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 + setHoveredItem: PropTypes.func.isRequired }; const mapStateToProps = state => ({ + colorState: state.scratchPaint.color, isPenModeActive: state.scratchPaint.mode === Modes.PEN, hoveredItemId: state.scratchPaint.hoveredItemId + }); const mapDispatchToProps = dispatch => ({ setHoveredItem: hoveredItemId => { @@ -86,9 +99,6 @@ const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, - setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); - }, handleMouseDown: () => { dispatch(changeMode(Modes.PEN)); }, diff --git a/src/helper/tools/pen-tool.js b/src/helper/tools/pen-tool.js index 1baffd89..86d0f7b6 100644 --- a/src/helper/tools/pen-tool.js +++ b/src/helper/tools/pen-tool.js @@ -1,32 +1,49 @@ 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 (setHoveredItem, clearHoveredItem, 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. this.onMouseDown = this.handleMouseDown; 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 @@ -38,16 +55,135 @@ class PenTool extends paper.Tool { setPrevHoveredItemId (prevHoveredItemId) { this.prevHoveredItemId = prevHoveredItemId; } - handleMouseDown () { - log.warn('Pen not yet implemented'); + setColorState (colorState) { + this.colorState = colorState; } - handleMouseMove () { + 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); + } + } } - handleMouseDrag () { + 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(); + } } - handleMouseUp () { + 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); + } + 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; } }