diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx
index f799b9df..f81262b5 100644
--- a/src/components/paint-editor.jsx
+++ b/src/components/paint-editor.jsx
@@ -1,12 +1,16 @@
import bindAll from 'lodash.bindall';
import React from 'react';
+import PropTypes from 'prop-types';
+
import PaperCanvas from '../containers/paper-canvas.jsx';
+
import BrushMode from '../containers/brush-mode.jsx';
import EraserMode from '../containers/eraser-mode.jsx';
import ReshapeMode from '../containers/reshape-mode.jsx';
import SelectMode from '../containers/select-mode.jsx';
-import PropTypes from 'prop-types';
import LineMode from '../containers/line-mode.jsx';
+import PenMode from '../containers/pen-mode.jsx';
+
import FillColorIndicatorComponent from '../containers/fill-color-indicator.jsx';
import StrokeColorIndicatorComponent from '../containers/stroke-color-indicator.jsx';
import StrokeWidthIndicatorComponent from '../containers/stroke-width-indicator.jsx';
@@ -132,6 +136,10 @@ class PaintEditorComponent extends React.Component {
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg}
/>
+
(
+
+);
+
+PenModeComponent.propTypes = {
+ onMouseDown: PropTypes.func.isRequired
+};
+
+export default PenModeComponent;
diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx
index b4f899ab..0ffdde41 100644
--- a/src/containers/brush-mode.jsx
+++ b/src/containers/brush-mode.jsx
@@ -3,7 +3,7 @@ import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../modes/modes';
-import Blobbiness from './blob/blob';
+import Blobbiness from '../helper/blob-tools/blob';
import {changeBrushSize} from '../reducers/brush-mode';
import {changeMode} from '../reducers/modes';
diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx
index efd920fe..87a7925d 100644
--- a/src/containers/eraser-mode.jsx
+++ b/src/containers/eraser-mode.jsx
@@ -3,7 +3,7 @@ import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../modes/modes';
-import Blobbiness from './blob/blob';
+import Blobbiness from '../helper/blob-tools/blob';
import {changeBrushSize} from '../reducers/eraser-mode';
import {clearSelectedItems} from '../reducers/selected-items';
import EraserModeComponent from '../components/eraser-mode.jsx';
diff --git a/src/containers/pen-mode.jsx b/src/containers/pen-mode.jsx
new file mode 100644
index 00000000..0519f963
--- /dev/null
+++ b/src/containers/pen-mode.jsx
@@ -0,0 +1,102 @@
+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 {changeMode} from '../reducers/modes';
+import {clearHoveredItem, setHoveredItem} from '../reducers/hover';
+import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
+
+import {getSelectedLeafItems} from '../helper/selection';
+import PenTool from '../helper/tools/pen-tool';
+import PenModeComponent from '../components/pen-mode.jsx';
+
+class PenMode extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'activateTool',
+ 'deactivateTool'
+ ]);
+ }
+ componentDidMount () {
+ if (this.props.isPenModeActive) {
+ this.activateTool(this.props);
+ }
+ }
+ componentWillReceiveProps (nextProps) {
+ if (this.tool && nextProps.hoveredItemId !== this.props.hoveredItemId) {
+ this.tool.setPrevHoveredItemId(nextProps.hoveredItemId);
+ }
+
+ if (nextProps.isPenModeActive && !this.props.isPenModeActive) {
+ this.activateTool();
+ } else if (!nextProps.isPenModeActive && this.props.isPenModeActive) {
+ this.deactivateTool();
+ }
+ }
+ shouldComponentUpdate () {
+ return false; // Static component, for now
+ }
+ activateTool () {
+ this.tool = new PenTool(
+ this.props.setHoveredItem,
+ this.props.clearHoveredItem,
+ this.props.setSelectedItems,
+ this.props.clearSelectedItems,
+ this.props.onUpdateSvg
+ );
+ this.tool.activate();
+ }
+ deactivateTool () {
+ this.tool.deactivateTool();
+ this.tool.remove();
+ this.tool = null;
+ }
+ render () {
+ return (
+
+ );
+ }
+}
+
+PenMode.propTypes = {
+ clearHoveredItem: PropTypes.func.isRequired,
+ clearSelectedItems: PropTypes.func.isRequired,
+ handleMouseDown: PropTypes.func.isRequired,
+ hoveredItemId: PropTypes.number,
+ isPenModeActive: PropTypes.bool.isRequired,
+ onUpdateSvg: PropTypes.func.isRequired,
+ setHoveredItem: PropTypes.func.isRequired,
+ setSelectedItems: PropTypes.func.isRequired
+};
+
+const mapStateToProps = state => ({
+ isPenModeActive: state.scratchPaint.mode === Modes.PEN,
+ hoveredItemId: state.scratchPaint.hoveredItemId
+});
+const mapDispatchToProps = dispatch => ({
+ setHoveredItem: hoveredItemId => {
+ dispatch(setHoveredItem(hoveredItemId));
+ },
+ clearHoveredItem: () => {
+ dispatch(clearHoveredItem());
+ },
+ clearSelectedItems: () => {
+ dispatch(clearSelectedItems());
+ },
+ setSelectedItems: () => {
+ dispatch(setSelectedItems(getSelectedLeafItems()));
+ },
+ handleMouseDown: () => {
+ dispatch(changeMode(Modes.PEN));
+ },
+ deactivateTool () {
+ }
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(PenMode);
diff --git a/src/containers/blob/blob.js b/src/helper/blob-tools/blob.js
similarity index 100%
rename from src/containers/blob/blob.js
rename to src/helper/blob-tools/blob.js
diff --git a/src/containers/blob/broad-brush-helper.js b/src/helper/blob-tools/broad-brush-helper.js
similarity index 100%
rename from src/containers/blob/broad-brush-helper.js
rename to src/helper/blob-tools/broad-brush-helper.js
diff --git a/src/containers/blob/segment-brush-helper.js b/src/helper/blob-tools/segment-brush-helper.js
similarity index 100%
rename from src/containers/blob/segment-brush-helper.js
rename to src/helper/blob-tools/segment-brush-helper.js
diff --git a/src/helper/tools/pen-tool.js b/src/helper/tools/pen-tool.js
new file mode 100644
index 00000000..76d5e838
--- /dev/null
+++ b/src/helper/tools/pen-tool.js
@@ -0,0 +1,52 @@
+import paper from '@scratch/paper';
+import log from '../../log/log';
+
+/**
+ * Tool to handle freehand drawing of lines.
+ */
+class PenTool extends paper.Tool {
+ /**
+ * @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) {
+ super();
+ this.setHoveredItem = setHoveredItem;
+ this.clearHoveredItem = clearHoveredItem;
+ this.setSelectedItems = setSelectedItems;
+ this.clearSelectedItems = clearSelectedItems;
+ this.onUpdateSvg = onUpdateSvg;
+ this.prevHoveredItemId = null;
+
+ // 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;
+ }
+ /**
+ * 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;
+ }
+ handleMouseDown () {
+ log.warn('Pen not yet implemented');
+ }
+ handleMouseMove () {
+ }
+ handleMouseDrag () {
+ }
+ handleMouseUp () {
+ }
+}
+
+export default PenTool;
diff --git a/src/modes/modes.js b/src/modes/modes.js
index a12446e3..f8262e5c 100644
--- a/src/modes/modes.js
+++ b/src/modes/modes.js
@@ -5,7 +5,8 @@ const Modes = keyMirror({
ERASER: null,
LINE: null,
SELECT: null,
- RESHAPE: null
+ RESHAPE: null,
+ PEN: null
});
export default Modes;