From c2cae279b75b8911f68e76379384ff28aa0e9423 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 22 Aug 2017 15:53:35 -0400 Subject: [PATCH] Add line mode --- src/components/line-mode.jsx | 19 ++ src/components/paint-editor.jsx | 2 + src/containers/line-mode.jsx | 265 ++++++++++++++++++ src/containers/paint-editor.jsx | 2 + src/modes/modes.js | 3 +- src/reducers/line-mode.js | 11 + .../messages/src/components/brush-mode.json | 2 +- .../messages/src/components/line-mode.json | 7 + 8 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 src/components/line-mode.jsx create mode 100644 src/containers/line-mode.jsx create mode 100644 src/reducers/line-mode.js create mode 100644 translations/messages/src/components/line-mode.json diff --git a/src/components/line-mode.jsx b/src/components/line-mode.jsx new file mode 100644 index 00000000..17affefa --- /dev/null +++ b/src/components/line-mode.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +const LineModeComponent = props => ( + +); + +LineModeComponent.propTypes = { + onMouseDown: PropTypes.func.isRequired +}; + +export default LineModeComponent; diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 1459fe35..8f7ff57f 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -3,6 +3,7 @@ import React from 'react'; import PaperCanvas from '../containers/paper-canvas.jsx'; import BrushMode from '../containers/brush-mode.jsx'; import EraserMode from '../containers/eraser-mode.jsx'; +import LineMode from '../containers/line-mode.jsx'; class PaintEditorComponent extends React.Component { constructor (props) { @@ -23,6 +24,7 @@ class PaintEditorComponent extends React.Component { + ); } diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx new file mode 100644 index 00000000..0b892bfd --- /dev/null +++ b/src/containers/line-mode.jsx @@ -0,0 +1,265 @@ +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 LineModeComponent from '../components/line-mode.jsx'; +import {changeMode} from '../reducers/modes'; +import paper from 'paper'; + +class LineMode extends React.Component { + static get SNAP_TOLERANCE () { + return 6; + } + constructor (props) { + super(props); + bindAll(this, [ + 'activateTool', + 'deactivateTool', + 'toleranceSquared', + 'findLineEnd' + ]); + } + 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(); + } else if (nextProps.isLineModeActive && this.props.isLineModeActive) { + this.blob.setOptions(nextProps.lineModeState); + } + } + shouldComponentUpdate () { + return false; // Static component, for now + } + activateTool () { + // TODO add back selection + // pg.selection.clearSelection(); + + this.tool = new paper.Tool(); + + this.path = null; + this.hitResult = null; + + // TODO add back colors + // Make sure a stroke color is set on the line tool + // if(!pg.stylebar.getStrokeColor()) { + // pg.stylebar.setStrokeColor(pg.stylebar.getFillColor()); + // pg.stylebar.setFillColor(null); + // } + + const lineMode = this; + this.tool.onMouseDown = function (event) { + if (event.event.button > 0) return; // only first mouse button + + if (this.path) { + this.path.setSelected(false); + this.path = null; + } + + // If you click near a point, continue that line instead of making a new line + this.hitResult = lineMode.findLineEnd(event.point); + if (this.hitResult) { + this.path = this.hitResult.path; + 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.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) { + // 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) < lineMode.toleranceSquared()) { + this.hitResult = { + path: this.path, + segment: this.path.firstSegment, + isFirst: true + }; + } else { + this.hitResult = lineMode.findLineEnd(event.point); + } + + if (this.hitResult) { + const hitPath = this.hitResult.path; + hitPath.setSelected(true); + if (this.hitResult.isFirst) { + hitPath.firstSegment.setSelected(true); + } else { + 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()) { + this.hitResult = { + 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) { + 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 + // if (this.path) { + // pg.undo.snapshot('line'); + // } + + }; + + this.tool.activate(); + } + toleranceSquared () { + return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2); + } + findLineEnd (point, excludePath) { + const lines = paper.project.getItems({ + class: paper.Path + }); + // Prefer more recent lines + for (let i = lines.length - 1; i >= 0; i--) { + if (lines[i].closed) { + continue; + } + if (excludePath && lines[i] === excludePath) { + continue; + } + if (lines[i].firstSegment && lines[i].firstSegment.point.getDistance(point, true) < this.toleranceSquared()) { + return { + path: lines[i], + segment: lines[i].firstSegment, + isFirst: true + }; + } + if (lines[i].lastSegment && lines[i].lastSegment.point.getDistance(point, true) < this.toleranceSquared()) { + return { + path: lines[i], + segment: lines[i].lastSegment, + isFirst: false + }; + } + } + return null; + } + deactivateTool () { + if (this.path) { + this.path.setSelected(false); + this.path = null; + } + } + render () { + return ( + + ); + } +} + +LineMode.propTypes = { + handleMouseDown: PropTypes.func.isRequired, + isLineModeActive: PropTypes.bool.isRequired, + lineModeState: PropTypes.shape({ + lineWidth: PropTypes.number.isRequired + }) +}; + +const mapStateToProps = state => ({ + lineModeState: state.lineMode, + isLineModeActive: state.mode === Modes.LINE +}); +const mapDispatchToProps = dispatch => ({ + handleMouseDown: () => { + dispatch(changeMode(Modes.LINE)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(LineMode); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 303546ba..772d8985 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -29,6 +29,8 @@ const mapDispatchToProps = dispatch => ({ dispatch(changeMode(Modes.ERASER)); } else if (event.key === 'b') { dispatch(changeMode(Modes.BRUSH)); + } else if (event.key === 'l') { + dispatch(changeMode(Modes.LINE)); } } }); diff --git a/src/modes/modes.js b/src/modes/modes.js index c485f0fe..c1b64738 100644 --- a/src/modes/modes.js +++ b/src/modes/modes.js @@ -2,7 +2,8 @@ import keyMirror from 'keymirror'; const Modes = keyMirror({ BRUSH: null, - ERASER: null + ERASER: null, + LINE: null }); export default Modes; diff --git a/src/reducers/line-mode.js b/src/reducers/line-mode.js new file mode 100644 index 00000000..e2c780fe --- /dev/null +++ b/src/reducers/line-mode.js @@ -0,0 +1,11 @@ +const initialState = {lineWidth: 2}; + +const reducer = function (state) { + if (typeof state === 'undefined') state = initialState; + return state; +}; + +// Action creators ================================== + + +export default reducer; diff --git a/translations/messages/src/components/brush-mode.json b/translations/messages/src/components/brush-mode.json index cc898d9b..fb8440ae 100644 --- a/translations/messages/src/components/brush-mode.json +++ b/translations/messages/src/components/brush-mode.json @@ -4,4 +4,4 @@ "description": "Label for the brush tool", "defaultMessage": "Brush" } -] +] \ No newline at end of file diff --git a/translations/messages/src/components/line-mode.json b/translations/messages/src/components/line-mode.json new file mode 100644 index 00000000..8c9c98e1 --- /dev/null +++ b/translations/messages/src/components/line-mode.json @@ -0,0 +1,7 @@ +[ + { + "id": "paint.lineMode.line", + "description": "Label for the line tool, which draws straight line segments", + "defaultMessage": "Line" + } +] \ No newline at end of file