diff --git a/src/components/fill-color-indicator.jsx b/src/components/fill-color-indicator.jsx index 2cad237a..3e555fda 100644 --- a/src/components/fill-color-indicator.jsx +++ b/src/components/fill-color-indicator.jsx @@ -6,6 +6,8 @@ import BufferedInputHOC from './forms/buffered-input-hoc.jsx'; import Label from './forms/label.jsx'; import Input from './forms/input.jsx'; +import {MIXED} from '../helper/style-path'; + import styles from './paint-editor.css'; const BufferedInput = BufferedInputHOC(Input); @@ -21,7 +23,7 @@ const FillColorIndicatorComponent = props => ( @@ -29,7 +31,7 @@ const FillColorIndicatorComponent = props => ( ); FillColorIndicatorComponent.propTypes = { - fillColor: PropTypes.string.isRequired, + fillColor: PropTypes.string, intl: intlShape, onChangeFillColor: PropTypes.func.isRequired }; diff --git a/src/components/stroke-color-indicator.jsx b/src/components/stroke-color-indicator.jsx index 71531314..45fb4824 100644 --- a/src/components/stroke-color-indicator.jsx +++ b/src/components/stroke-color-indicator.jsx @@ -6,6 +6,8 @@ import BufferedInputHOC from './forms/buffered-input-hoc.jsx'; import Label from './forms/label.jsx'; import Input from './forms/input.jsx'; +import {MIXED} from '../helper/style-path'; + import styles from './paint-editor.css'; const BufferedInput = BufferedInputHOC(Input); @@ -21,7 +23,8 @@ const StrokeColorIndicatorComponent = props => ( @@ -31,7 +34,7 @@ const StrokeColorIndicatorComponent = props => ( StrokeColorIndicatorComponent.propTypes = { intl: intlShape, onChangeStrokeColor: PropTypes.func.isRequired, - strokeColor: PropTypes.string.isRequired + strokeColor: PropTypes.string }; export default injectIntl(StrokeColorIndicatorComponent); diff --git a/src/components/stroke-width-indicator.jsx b/src/components/stroke-width-indicator.jsx index 6b5774ed..6bdc28bc 100644 --- a/src/components/stroke-width-indicator.jsx +++ b/src/components/stroke-width-indicator.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import BufferedInputHOC from './forms/buffered-input-hoc.jsx'; import Input from './forms/input.jsx'; + import {MAX_STROKE_WIDTH} from '../reducers/stroke-width'; import styles from './paint-editor.css'; @@ -15,7 +16,7 @@ const StrokeWidthIndicatorComponent = props => ( max={MAX_STROKE_WIDTH} min="0" type="number" - value={props.strokeWidth} + value={props.strokeWidth ? props.strokeWidth : 0} onSubmit={props.onChangeStrokeWidth} /> @@ -23,7 +24,7 @@ const StrokeWidthIndicatorComponent = props => ( StrokeWidthIndicatorComponent.propTypes = { onChangeStrokeWidth: PropTypes.func.isRequired, - strokeWidth: PropTypes.number.isRequired + strokeWidth: PropTypes.number }; export default StrokeWidthIndicatorComponent; diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index a10565c2..62b126d8 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -2,7 +2,7 @@ import paper from 'paper'; import log from '../../log/log'; import BroadBrushHelper from './broad-brush-helper'; import SegmentBrushHelper from './segment-brush-helper'; -import {styleCursorPreview} from './style-path'; +import {MIXED, styleCursorPreview} from '../../helper/style-path'; import {clearSelection} from '../../helper/selection'; /** @@ -32,6 +32,11 @@ class Blobbiness { this.broadBrushHelper = new BroadBrushHelper(); this.segmentBrushHelper = new SegmentBrushHelper(); this.updateCallback = updateCallback; + + // The following are stored to check whether these have changed and the cursor preview needs to be redrawn. + this.strokeColor = null; + this.brushSize = null; + this.fillColor = null; } /** @@ -45,7 +50,15 @@ class Blobbiness { * @param {?number} options.strokeWidth Width of the brush outline. */ setOptions (options) { - this.options = options; + const oldFillColor = this.options ? this.options.fillColor : null; + const oldStrokeColor = this.options ? this.options.strokeColor : null; + const oldStrokeWidth = this.options ? this.options.strokeWidth : null; + this.options = { + ...options, + fillColor: options.fillColor === MIXED ? oldFillColor : options.fillColor, + strokeColor: options.strokeColor === MIXED ? oldStrokeColor : options.strokeColor, + strokeWidth: options.strokeWidth === null ? oldStrokeWidth : options.strokeWidth + }; this.resizeCursorIfNeeded(); } @@ -150,8 +163,8 @@ class Blobbiness { if (this.cursorPreview && this.brushSize === this.options.brushSize && - this.fillColor === this.options.fillColor && - this.strokeColor === this.options.strokeColor) { + (this.options.fillColor === MIXED || this.fillColor === this.options.fillColor) && + (this.options.strokeColor === MIXED || this.strokeColor === this.options.strokeColor)) { return; } const newPreview = new paper.Path.Circle({ @@ -162,8 +175,12 @@ class Blobbiness { this.cursorPreview.remove(); } this.brushSize = this.options.brushSize; - this.fillColor = this.options.fillColor; - this.strokeColor = this.options.strokeColor; + if (this.options.fillColor !== MIXED) { + this.fillColor = this.options.fillColor; + } + if (this.options.strokeColor !== MIXED) { + this.strokeColor = this.options.strokeColor; + } this.cursorPreview = newPreview; styleCursorPreview(this.cursorPreview, this.options); } @@ -233,7 +250,7 @@ class Blobbiness { // Eraser didn't hit anything selected, so assume they meant to erase from all instead of from subset // and deselect the selection if (items.length === 0) { - clearSelection(); + clearSelection(this.clearSelectedItems); items = paper.project.getItems({ match: function (item) { return blob.isMergeable(lastPath, item) && blob.touches(lastPath, item); diff --git a/src/containers/blob/broad-brush-helper.js b/src/containers/blob/broad-brush-helper.js index 87af4f43..7ccef404 100644 --- a/src/containers/blob/broad-brush-helper.js +++ b/src/containers/blob/broad-brush-helper.js @@ -1,6 +1,6 @@ // Broadbrush based on http://paperjs.org/tutorials/interaction/working-with-mouse-vectors/ import paper from 'paper'; -import {stylePath} from './style-path'; +import {stylePath} from '../../helper/style-path'; /** * Broad brush functions to add as listeners on the mouse. Call them when the corresponding mouse event happens diff --git a/src/containers/blob/segment-brush-helper.js b/src/containers/blob/segment-brush-helper.js index 6ccd48ce..aa29ec5f 100644 --- a/src/containers/blob/segment-brush-helper.js +++ b/src/containers/blob/segment-brush-helper.js @@ -1,5 +1,5 @@ import paper from 'paper'; -import {stylePath} from './style-path'; +import {stylePath} from '../../helper/style-path'; /** * Segment brush functions to add as listeners on the mouse. Call them when the corresponding mouse event happens diff --git a/src/containers/blob/style-path.js b/src/containers/blob/style-path.js deleted file mode 100644 index 26a2527b..00000000 --- a/src/containers/blob/style-path.js +++ /dev/null @@ -1,22 +0,0 @@ -const stylePath = function (path, options) { - if (options.isEraser) { - path.fillColor = 'white'; - } else { - path.fillColor = options.fillColor; - } -}; - -const styleCursorPreview = function (path, options) { - if (options.isEraser) { - path.fillColor = 'white'; - path.strokeColor = 'cornflowerblue'; - path.strokeWidth = 1; - } else { - path.fillColor = options.fillColor; - } -}; - -export { - stylePath, - styleCursorPreview -}; diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index bbf36f11..799ada60 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -6,6 +6,7 @@ import Modes from '../modes/modes'; import Blobbiness from './blob/blob'; import {changeBrushSize} from '../reducers/brush-mode'; import {changeMode} from '../reducers/modes'; +import {clearSelectedItems} from '../reducers/selected-items'; import {clearSelection} from '../helper/selection'; import BrushModeComponent from '../components/brush-mode.jsx'; @@ -43,7 +44,7 @@ class BrushMode extends React.Component { activateTool () { // TODO: Instead of clearing selection, consider a kind of "draw inside" // analogous to how selection works with eraser - clearSelection(); + clearSelection(this.props.clearSelectedItems); // TODO: This is temporary until a component that provides the brush size is hooked up this.props.canvas.addEventListener('mousewheel', this.onScroll); @@ -78,10 +79,11 @@ BrushMode.propTypes = { }), canvas: PropTypes.instanceOf(Element).isRequired, changeBrushSize: PropTypes.func.isRequired, + clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.string.isRequired, - strokeColor: PropTypes.string.isRequired, - strokeWidth: PropTypes.number.isRequired + fillColor: PropTypes.string, + strokeColor: PropTypes.string, + strokeWidth: PropTypes.number }).isRequired, handleMouseDown: PropTypes.func.isRequired, isBrushModeActive: PropTypes.bool.isRequired, @@ -94,6 +96,9 @@ const mapStateToProps = state => ({ isBrushModeActive: state.scratchPaint.mode === Modes.BRUSH }); const mapDispatchToProps = dispatch => ({ + clearSelectedItems: () => { + dispatch(clearSelectedItems()); + }, changeBrushSize: brushSize => { dispatch(changeBrushSize(brushSize)); }, diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 494be917..35611f45 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -1,12 +1,14 @@ import {connect} from 'react-redux'; import {changeFillColor} from '../reducers/fill-color'; import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; +import {applyFillColorToSelection} from '../helper/style-path'; const mapStateToProps = state => ({ fillColor: state.scratchPaint.color.fillColor }); const mapDispatchToProps = dispatch => ({ onChangeFillColor: fillColor => { + applyFillColorToSelection(fillColor); dispatch(changeFillColor(fillColor)); } }); diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 0af6f939..e1ba1c36 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -4,7 +4,9 @@ import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; import {changeStrokeWidth} from '../reducers/stroke-width'; -import {clearSelection} from '../helper/selection'; +import {clearSelection, getSelectedItems} from '../helper/selection'; +import {MIXED} from '../helper/style-path'; +import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import LineModeComponent from '../components/line-mode.jsx'; import {changeMode} from '../reducers/modes'; import paper from 'paper'; @@ -43,7 +45,7 @@ class LineMode extends React.Component { return false; // Static component, for now } activateTool () { - clearSelection(); + clearSelection(this.props.clearSelectedItems); this.props.canvas.addEventListener('mousewheel', this.onScroll); this.tool = new paper.Tool(); @@ -93,9 +95,12 @@ class LineMode extends React.Component { if (!this.path) { this.path = new paper.Path(); - this.path.setStrokeColor(this.props.colorState.strokeColor); + this.path.setStrokeColor( + this.props.colorState.strokeColor === MIXED ? 'black' : this.props.colorState.strokeColor); // Make sure a visible line is drawn - this.path.setStrokeWidth(Math.max(1, this.props.colorState.strokeWidth)); + this.path.setStrokeWidth( + this.props.colorState.strokeWidth === null || this.props.colorState.strokeWidth === 0 ? + 1 : this.props.colorState.strokeWidth); this.path.setSelected(true); this.path.add(event.point); @@ -202,6 +207,7 @@ class LineMode extends React.Component { this.hitResult = null; } this.props.onUpdateSvg(); + this.props.setSelectedItems(); // TODO add back undo // if (this.path) { @@ -269,14 +275,16 @@ class LineMode extends React.Component { LineMode.propTypes = { canvas: PropTypes.instanceOf(Element).isRequired, changeStrokeWidth: PropTypes.func.isRequired, + clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.string.isRequired, - strokeColor: PropTypes.string.isRequired, - strokeWidth: PropTypes.number.isRequired + fillColor: PropTypes.string, + strokeColor: PropTypes.string, + strokeWidth: PropTypes.number }).isRequired, handleMouseDown: PropTypes.func.isRequired, isLineModeActive: PropTypes.bool.isRequired, - onUpdateSvg: PropTypes.func.isRequired + onUpdateSvg: PropTypes.func.isRequired, + setSelectedItems: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -287,6 +295,12 @@ const mapDispatchToProps = dispatch => ({ changeStrokeWidth: strokeWidth => { dispatch(changeStrokeWidth(strokeWidth)); }, + clearSelectedItems: () => { + dispatch(clearSelectedItems()); + }, + setSelectedItems: () => { + dispatch(setSelectedItems(getSelectedItems(true /* recursive */))); + }, handleMouseDown: () => { dispatch(changeMode(Modes.LINE)); } diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx index 340c2789..392989f9 100644 --- a/src/containers/reshape-mode.jsx +++ b/src/containers/reshape-mode.jsx @@ -5,12 +5,12 @@ import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; import {changeMode} from '../reducers/modes'; -import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; +import {clearHoveredItem, setHoveredItem} from '../reducers/hover'; +import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; +import {getSelectedItems} from '../helper/selection'; import ReshapeTool from '../helper/selection-tools/reshape-tool'; import ReshapeModeComponent from '../components/reshape-mode.jsx'; -import paper from 'paper'; - class ReshapeMode extends React.Component { constructor (props) { @@ -40,7 +40,12 @@ class ReshapeMode extends React.Component { return false; // Static component, for now } activateTool () { - this.tool = new ReshapeTool(this.props.setHoveredItem, this.props.clearHoveredItem, this.props.onUpdateSvg); + this.tool = new ReshapeTool( + this.props.setHoveredItem, + this.props.clearHoveredItem, + this.props.setSelectedItems, + this.props.clearSelectedItems, + this.props.onUpdateSvg); this.tool.setPrevHoveredItemId(this.props.hoveredItemId); this.tool.activate(); } @@ -59,11 +64,13 @@ class ReshapeMode extends React.Component { ReshapeMode.propTypes = { clearHoveredItem: PropTypes.func.isRequired, + clearSelectedItems: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, hoveredItemId: PropTypes.number, isReshapeModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, - setHoveredItem: PropTypes.func.isRequired + setHoveredItem: PropTypes.func.isRequired, + setSelectedItems: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -77,6 +84,12 @@ const mapDispatchToProps = dispatch => ({ clearHoveredItem: () => { dispatch(clearHoveredItem()); }, + clearSelectedItems: () => { + dispatch(clearSelectedItems()); + }, + setSelectedItems: () => { + dispatch(setSelectedItems(getSelectedItems(true /* recursive */))); + }, handleMouseDown: () => { dispatch(changeMode(Modes.RESHAPE)); } diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 3992d3f9..b3b43e0c 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -5,8 +5,10 @@ import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; import {changeMode} from '../reducers/modes'; -import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; +import {clearHoveredItem, setHoveredItem} from '../reducers/hover'; +import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; +import {getSelectedItems} from '../helper/selection'; import SelectTool from '../helper/selection-tools/select-tool'; import SelectModeComponent from '../components/select-mode.jsx'; @@ -38,7 +40,12 @@ class SelectMode extends React.Component { return false; // Static component, for now } activateTool () { - this.tool = new SelectTool(this.props.setHoveredItem, this.props.clearHoveredItem, this.props.onUpdateSvg); + this.tool = new SelectTool( + this.props.setHoveredItem, + this.props.clearHoveredItem, + this.props.setSelectedItems, + this.props.clearSelectedItems, + this.props.onUpdateSvg); this.tool.activate(); } deactivateTool () { @@ -55,11 +62,13 @@ class SelectMode extends React.Component { SelectMode.propTypes = { clearHoveredItem: PropTypes.func.isRequired, + clearSelectedItems: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, hoveredItemId: PropTypes.number, isSelectModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, - setHoveredItem: PropTypes.func.isRequired + setHoveredItem: PropTypes.func.isRequired, + setSelectedItems: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -73,6 +82,12 @@ const mapDispatchToProps = dispatch => ({ clearHoveredItem: () => { dispatch(clearHoveredItem()); }, + clearSelectedItems: () => { + dispatch(clearSelectedItems()); + }, + setSelectedItems: () => { + dispatch(setSelectedItems(getSelectedItems(true /* recursive */))); + }, handleMouseDown: () => { dispatch(changeMode(Modes.SELECT)); } diff --git a/src/containers/selection-hoc.jsx b/src/containers/selection-hoc.jsx index e468a179..cb7af472 100644 --- a/src/containers/selection-hoc.jsx +++ b/src/containers/selection-hoc.jsx @@ -1,8 +1,15 @@ +import paper from 'paper'; + import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; -import paper from 'paper'; + +import {getSelectedItems} from '../helper/selection'; +import {getColorsFromSelection} from '../helper/style-path'; +import {changeStrokeColor} from '../reducers/stroke-color'; +import {changeStrokeWidth} from '../reducers/stroke-width'; +import {changeFillColor} from '../reducers/fill-color'; const SelectionHOC = function (WrappedComponent) { class SelectionComponent extends React.Component { @@ -52,8 +59,21 @@ const SelectionHOC = function (WrappedComponent) { const mapStateToProps = state => ({ hoveredItemId: state.scratchPaint.hoveredItemId }); + const mapDispatchToProps = dispatch => ({ + onUpdateColors: (() => { + const selectedItems = getSelectedItems(true /* recursive */); + if (selectedItems.length === 0) { + return; + } + const colorState = getColorsFromSelection(); + dispatch(changeFillColor(colorState.fillColor)); + dispatch(changeStrokeColor(colorState.strokeColor)); + dispatch(changeStrokeWidth(colorState.strokeWidth)); + }) + }); return connect( - mapStateToProps + mapStateToProps, + mapDispatchToProps )(SelectionComponent); }; diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index add989bf..f7ffcbab 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -1,12 +1,14 @@ import {connect} from 'react-redux'; import {changeStrokeColor} from '../reducers/stroke-color'; import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx'; +import {applyStrokeColorToSelection} from '../helper/style-path'; const mapStateToProps = state => ({ strokeColor: state.scratchPaint.color.strokeColor }); const mapDispatchToProps = dispatch => ({ onChangeStrokeColor: strokeColor => { + applyStrokeColorToSelection(strokeColor); dispatch(changeStrokeColor(strokeColor)); } }); diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx index 5e5fa967..d1b0def3 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -1,12 +1,14 @@ import {connect} from 'react-redux'; import {changeStrokeWidth} from '../reducers/stroke-width'; import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx'; +import {applyStrokeWidthToSelection} from '../helper/style-path'; const mapStateToProps = state => ({ strokeWidth: state.scratchPaint.color.strokeWidth }); const mapDispatchToProps = dispatch => ({ onChangeStrokeWidth: strokeWidth => { + applyStrokeWidthToSelection(strokeWidth); dispatch(changeStrokeWidth(strokeWidth)); } }); diff --git a/src/helper/group.js b/src/helper/group.js index e6d5e63e..20bac775 100644 --- a/src/helper/group.js +++ b/src/helper/group.js @@ -6,11 +6,11 @@ const isGroup = function (item) { return isGroupItem(item); }; -const groupSelection = function () { +const groupSelection = function (clearSelectedItems) { const items = getSelectedItems(); if (items.length > 0) { const group = new paper.Group(items); - clearSelection(); + clearSelection(clearSelectedItems); setItemSelection(group, true); for (let i = 0; i < group.children.length; i++) { group.children[i].selected = true; @@ -47,8 +47,8 @@ const ungroupLoop = function (group, recursive) { }; // ungroup items (only top hierarchy) -const ungroupItems = function (items) { - clearSelection(); +const ungroupItems = function (items, clearSelectedItems) { + clearSelection(clearSelectedItems); const emptyGroups = []; for (let i = 0; i < items.length; i++) { const item = items[i]; diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index dca535fd..df427f1d 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -31,7 +31,14 @@ const Modes = keyMirror({ * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ class BoundingBoxTool { - constructor (onUpdateSvg) { + /** + * @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 (setSelectedItems, clearSelectedItems, onUpdateSvg) { + this.setSelectedItems = setSelectedItems; + this.clearSelectedItems = clearSelectedItems; this.onUpdateSvg = onUpdateSvg; this.mode = null; this.boundsPath = null; @@ -40,7 +47,7 @@ class BoundingBoxTool { this._modeMap = {}; this._modeMap[Modes.SCALE] = new ScaleTool(onUpdateSvg); this._modeMap[Modes.ROTATE] = new RotateTool(onUpdateSvg); - this._modeMap[Modes.MOVE] = new MoveTool(onUpdateSvg); + this._modeMap[Modes.MOVE] = new MoveTool(setSelectedItems, clearSelectedItems, onUpdateSvg); } /** @@ -56,7 +63,6 @@ class BoundingBoxTool { if (!hitResults || hitResults.length === 0) { if (!multiselect) { this.removeBoundsPath(); - clearSelection(); } return false; } diff --git a/src/helper/selection-tools/handle-tool.js b/src/helper/selection-tools/handle-tool.js index 26aead16..4636116d 100644 --- a/src/helper/selection-tools/handle-tool.js +++ b/src/helper/selection-tools/handle-tool.js @@ -3,10 +3,14 @@ import {clearSelection, getSelectedItems} from '../selection'; /** Sub tool of the Reshape tool for moving handles, which adjust bezier curves. */ class HandleTool { /** + * @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 (onUpdateSvg) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { this.hitType = null; + this.setSelectedItems = setSelectedItems; + this.clearSelectedItems = clearSelectedItems; this.onUpdateSvg = onUpdateSvg; } /** @@ -16,7 +20,7 @@ class HandleTool { */ onMouseDown (hitProperties) { if (!hitProperties.multiselect) { - clearSelection(); + clearSelection(this.clearSelectedItems); } hitProperties.hitResult.segment.handleIn.selected = true; diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js index 8853e631..34212708 100644 --- a/src/helper/selection-tools/move-tool.js +++ b/src/helper/selection-tools/move-tool.js @@ -8,9 +8,13 @@ import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from */ class MoveTool { /** + * @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 (onUpdateSvg) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { + this.setSelectedItems = setSelectedItems; + this.clearSelectedItems = clearSelectedItems; this.selectedItems = null; this.onUpdateSvg = onUpdateSvg; } @@ -34,7 +38,7 @@ class MoveTool { // Double click causes all points to be selected in subselect mode. if (hitProperties.doubleClicked) { if (!hitProperties.multiselect) { - clearSelection(); + clearSelection(this.clearSelectedItems); } this._select(item, true /* state */, hitProperties.subselect, true /* fullySelect */); } else if (hitProperties.multiselect) { @@ -43,7 +47,7 @@ class MoveTool { } else { // deselect all by default if multiselect isn't on if (!hitProperties.multiselect) { - clearSelection(); + clearSelection(this.clearSelectedItems); } this._select(item, true, hitProperties.subselect); } @@ -71,6 +75,7 @@ class MoveTool { } else { setItemSelection(item, state); } + this.setSelectedItems(); } onMouseDrag (event) { const dragVector = event.point.subtract(event.downPoint); diff --git a/src/helper/selection-tools/point-tool.js b/src/helper/selection-tools/point-tool.js index 12aa53ae..20eb4da2 100644 --- a/src/helper/selection-tools/point-tool.js +++ b/src/helper/selection-tools/point-tool.js @@ -5,9 +5,11 @@ import {clearSelection, getSelectedItems} from '../selection'; /** Subtool of ReshapeTool for moving control points. */ class PointTool { /** + * @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 (onUpdateSvg) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { /** * Deselection often does not happen until mouse up. If the mouse is dragged before * mouse up, deselection is cancelled. This variable keeps track of which paper.Item to deselect. @@ -24,6 +26,8 @@ class PointTool { */ this.invertDeselect = false; this.selectedItems = null; + this.setSelectedItems = setSelectedItems; + this.clearSelectedItems = clearSelectedItems; this.onUpdateSvg = onUpdateSvg; } @@ -49,7 +53,7 @@ class PointTool { } } else { if (!hitProperties.multiselect) { - clearSelection(); + clearSelection(this.clearSelectedItems); } hitProperties.hitResult.segment.selected = true; } @@ -86,7 +90,7 @@ class PointTool { hitProperties.hitResult.item.insert(hitProperties.hitResult.location.index + 1, newSegment); hitProperties.hitResult.segment = newSegment; if (!hitProperties.multiselect) { - clearSelection(); + clearSelection(this.clearSelectedItems); } newSegment.selected = true; @@ -175,7 +179,7 @@ class PointTool { // and delete if (this.deselectOnMouseUp) { if (this.invertDeselect) { - clearSelection(); + clearSelection(this.clearSelectedItems); this.deselectOnMouseUp.selected = true; } else { this.deselectOnMouseUp.selected = false; @@ -188,6 +192,7 @@ class PointTool { this.deleteOnMouseUp = null; } this.selectedItems = null; + this.setSelectedItems(); // @todo add back undo this.onUpdateSvg(); } diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js index 596ec0f3..6d469b23 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -41,9 +41,11 @@ class ReshapeTool 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, onUpdateSvg) { + constructor (setHoveredItem, clearHoveredItem, setSelectedItems, clearSelectedItems, onUpdateSvg) { super(); this.setHoveredItem = setHoveredItem; this.clearHoveredItem = clearHoveredItem; @@ -52,10 +54,10 @@ class ReshapeTool extends paper.Tool { this.lastEvent = null; this.mode = ReshapeModes.SELECTION_BOX; this._modeMap = {}; - this._modeMap[ReshapeModes.FILL] = new MoveTool(onUpdateSvg); - this._modeMap[ReshapeModes.POINT] = new PointTool(onUpdateSvg); - this._modeMap[ReshapeModes.HANDLE] = new HandleTool(onUpdateSvg); - this._modeMap[ReshapeModes.SELECTION_BOX] = new SelectionBoxTool(Modes.RESHAPE); + this._modeMap[ReshapeModes.FILL] = new MoveTool(setSelectedItems, clearSelectedItems, onUpdateSvg); + this._modeMap[ReshapeModes.POINT] = new PointTool(setSelectedItems, clearSelectedItems, onUpdateSvg); + this._modeMap[ReshapeModes.HANDLE] = new HandleTool(setSelectedItems, clearSelectedItems, onUpdateSvg); + this._modeMap[ReshapeModes.SELECTION_BOX] = new SelectionBoxTool(Modes.RESHAPE, setSelectedItems, clearSelectedItems); // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index bc33eea3..93fc31cf 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -21,15 +21,17 @@ class SelectTool 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, onUpdateSvg) { + constructor (setHoveredItem, clearHoveredItem, setSelectedItems, clearSelectedItems, onUpdateSvg) { super(); this.setHoveredItem = setHoveredItem; this.clearHoveredItem = clearHoveredItem; this.onUpdateSvg = onUpdateSvg; - this.boundingBoxTool = new BoundingBoxTool(onUpdateSvg); - this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT); + this.boundingBoxTool = new BoundingBoxTool(setSelectedItems, clearSelectedItems, onUpdateSvg); + this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems); this.selectionBoxMode = false; this.prevHoveredItemId = null; @@ -42,6 +44,7 @@ class SelectTool extends paper.Tool { this.onKeyUp = this.handleKeyUp; selectRootItem(); + setSelectedItems(); this.boundingBoxTool.setSelectionBounds(); } /** diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js index bc787c51..14ce3eb8 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -3,16 +3,24 @@ import {clearSelection, processRectangularSelection} from '../selection'; /** Tool to handle drag selection. A dotted line box appears and everything enclosed is selected. */ class SelectionBoxTool { - constructor (mode) { + /** + * @param {!Modes} mode Current paint editor mode + * @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 + */ + constructor (mode, setSelectedItems, clearSelectedItems) { this.selectionRect = null; this.mode = mode; + this.setSelectedItems = setSelectedItems; + this.clearSelectedItems = clearSelectedItems; } /** * @param {boolean} multiselect Whether to multiselect on mouse down (e.g. shift key held) */ onMouseDown (multiselect) { if (!multiselect) { - clearSelection(); + clearSelection(this.clearSelectedItems); + this.clearSelectedItems(); } } onMouseDrag (event) { @@ -25,6 +33,7 @@ class SelectionBoxTool { processRectangularSelection(event, this.selectionRect, this.mode); this.selectionRect.remove(); this.selectionRect = null; + this.setSelectedItems(); } } } diff --git a/src/helper/selection.js b/src/helper/selection.js index 6d318f1d..443fa5cb 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -112,9 +112,11 @@ const selectAllSegments = function () { } }; -const clearSelection = function () { +/** @param {!function} dispatchClearSelect Function to update the Redux select state */ +const clearSelection = function (dispatchClearSelect) { paper.project.deselectAll(); // @todo: Update toolbar state on change + dispatchClearSelect(); }; // This gets all selected non-grouped items and groups diff --git a/src/helper/style-path.js b/src/helper/style-path.js new file mode 100644 index 00000000..00b8a979 --- /dev/null +++ b/src/helper/style-path.js @@ -0,0 +1,193 @@ +import {getSelectedItems} from './selection'; +import {isPGTextItem, isPointTextItem} from './item'; +import {isGroup} from './group'; + +const MIXED = 'scratch-paint/style-path/mixed'; + +/** + * Called when setting fill color + * @param {string} colorString New color, css format + */ +const applyFillColorToSelection = function (colorString) { + const items = getSelectedItems(true /* recursive */); + for (const item of items) { + if (isPGTextItem(item)) { + for (const child of item.children) { + if (child.children) { + for (const path of child.children) { + if (!path.data.isPGGlyphRect) { + path.fillColor = colorString; + } + } + } else if (!child.data.isPGGlyphRect) { + child.fillColor = colorString; + } + } + } else { + if (isPointTextItem(item) && !colorString) { + colorString = 'rgba(0,0,0,0)'; + } + item.fillColor = colorString; + } + } + // @todo add back undo +}; + +/** + * Called when setting stroke color + * @param {string} colorString New color, css format + */ +const applyStrokeColorToSelection = function (colorString) { + const items = getSelectedItems(true /* recursive */); + + for (const item of items) { + if (isPGTextItem(item)) { + if (item.children) { + for (const child of item.children) { + if (child.children) { + for (const path of child.children) { + if (!path.data.isPGGlyphRect) { + path.strokeColor = colorString; + } + } + } else if (!child.data.isPGGlyphRect) { + child.strokeColor = colorString; + } + } + } else if (!item.data.isPGGlyphRect) { + item.strokeColor = colorString; + } + } else { + item.strokeColor = colorString; + } + } + // @todo add back undo +}; + +/** + * Called when setting stroke width + * @param {number} value New stroke width + */ +const applyStrokeWidthToSelection = function (value) { + const items = getSelectedItems(true /* recursive */); + for (const item of items) { + if (isGroup(item)) { + continue; + } else { + item.strokeWidth = value; + } + } + // @todo add back undo +}; + +/** + * Get state of colors and stroke width for selection + * @return {object} Object of strokeColor, strokeWidth, fillColor of the selection. + * Gives MIXED when there are mixed values for a color, and null for transparent. + * Gives null when there are mixed values for stroke width. + */ +const getColorsFromSelection = function () { + const selectedItems = getSelectedItems(true /* recursive */); + let selectionFillColorString; + let selectionStrokeColorString; + let selectionStrokeWidth; + let firstChild = true; + + for (const item of selectedItems) { + let itemFillColorString; + let itemStrokeColorString; + + // handle pgTextItems differently by going through their children + if (isPGTextItem(item)) { + for (const child of item.children) { + for (const path of child.children) { + if (!path.data.isPGGlyphRect) { + if (path.fillColor) { + itemFillColorString = path.fillColor.toCSS(); + } + if (path.strokeColor) { + itemStrokeColorString = path.strokeColor.toCSS(); + } + // check every style against the first of the items + if (firstChild) { + firstChild = false; + selectionFillColorString = itemFillColorString; + selectionStrokeColorString = itemStrokeColorString; + selectionStrokeWidth = path.strokeWidth; + } + if (itemFillColorString !== selectionFillColorString) { + selectionFillColorString = MIXED; + } + if (itemStrokeColorString !== selectionStrokeColorString) { + selectionStrokeColorString = MIXED; + } + if (selectionStrokeWidth !== path.strokeWidth) { + selectionStrokeWidth = null; + } + } + } + } + } else if (!isGroup(item)) { + if (item.fillColor) { + // hack bc text items with null fill can't be detected by fill-hitTest anymore + if (isPointTextItem(item) && item.fillColor.toCSS() === 'rgba(0,0,0,0)') { + itemFillColorString = null; + } else { + itemFillColorString = item.fillColor.toCSS(); + } + } + if (item.strokeColor) { + itemStrokeColorString = item.strokeColor.toCSS(); + } + // check every style against the first of the items + if (firstChild) { + firstChild = false; + selectionFillColorString = itemFillColorString; + selectionStrokeColorString = itemStrokeColorString; + selectionStrokeWidth = item.strokeWidth; + } + if (itemFillColorString !== selectionFillColorString) { + selectionFillColorString = MIXED; + } + if (itemStrokeColorString !== selectionStrokeColorString) { + selectionStrokeColorString = MIXED; + } + if (selectionStrokeWidth !== item.strokeWidth) { + selectionStrokeWidth = null; + } + } + } + return { + fillColor: selectionFillColorString ? selectionFillColorString : null, + strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null, + strokeWidth: selectionStrokeWidth || (selectionStrokeWidth === null) ? selectionStrokeWidth : 0 //todo why is this 0 for arrow + }; +}; + +const stylePath = function (path, options) { + if (options.isEraser) { + path.fillColor = 'white'; + } else { + path.fillColor = options.fillColor; + } +}; + +const styleCursorPreview = function (path, options) { + if (options.isEraser) { + path.fillColor = 'white'; + path.strokeColor = 'cornflowerblue'; + path.strokeWidth = 1; + } else { + path.fillColor = options.fillColor; + } +}; + +export { + applyFillColorToSelection, + applyStrokeColorToSelection, + applyStrokeWidthToSelection, + getColorsFromSelection, + MIXED, + stylePath, + styleCursorPreview +}; diff --git a/src/reducers/fill-color.js b/src/reducers/fill-color.js index 74bf4d6c..783ef18b 100644 --- a/src/reducers/fill-color.js +++ b/src/reducers/fill-color.js @@ -1,4 +1,6 @@ import log from '../log/log'; +import {CHANGE_SELECTED_ITEMS} from './selected-items'; +import {getColorsFromSelection} from '../helper/style-path'; const CHANGE_FILL_COLOR = 'scratch-paint/fill-color/CHANGE_FILL_COLOR'; const initialState = '#000'; @@ -14,6 +16,12 @@ const reducer = function (state, action) { return state; } return action.fillColor; + case CHANGE_SELECTED_ITEMS: + // Don't change state if no selection + if (!action.selectedItems || !action.selectedItems.length) { + return state; + } + return getColorsFromSelection().fillColor; default: return state; } diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 0d44903b..2ac4a2ee 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -4,11 +4,13 @@ import brushModeReducer from './brush-mode'; import eraserModeReducer from './eraser-mode'; import colorReducer from './color'; import hoverReducer from './hover'; +import selectedItemReducer from './selected-items'; export default combineReducers({ mode: modeReducer, brushMode: brushModeReducer, eraserMode: eraserModeReducer, color: colorReducer, - hoveredItemId: hoverReducer + hoveredItemId: hoverReducer, + selectedItems: selectedItemReducer }); diff --git a/src/reducers/selected-items.js b/src/reducers/selected-items.js new file mode 100644 index 00000000..2ebb8140 --- /dev/null +++ b/src/reducers/selected-items.js @@ -0,0 +1,48 @@ +const CHANGE_SELECTED_ITEMS = 'scratch-paint/select/CHANGE_SELECTED_ITEMS'; +const initialState = []; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case CHANGE_SELECTED_ITEMS: + // If they are not equal, return the new list of items. Else return old list + if (action.selectedItems.length !== state.length) { + return action.selectedItems; + } + // Shallow equality check + for (let i = 0; i < action.selectedItems.length; i++) { + if (action.selectedItems[i] !== state[i]) { + return action.selectedItems; + } + } + return state; + default: + return state; + } +}; + +// Action creators ================================== +/** + * Set the selected item state to the given array of items + * @param {Array} selectedItems from paper.project.selectedItems + * @return {object} Redux action to change the selected items. + */ +const setSelectedItems = function (selectedItems) { + return { + type: CHANGE_SELECTED_ITEMS, + selectedItems: selectedItems + }; +}; +const clearSelectedItems = function () { + return { + type: CHANGE_SELECTED_ITEMS, + selectedItems: [] + }; +}; + +export { + reducer as default, + setSelectedItems, + clearSelectedItems, + CHANGE_SELECTED_ITEMS +}; diff --git a/src/reducers/stroke-color.js b/src/reducers/stroke-color.js index 15efc21e..4d414447 100644 --- a/src/reducers/stroke-color.js +++ b/src/reducers/stroke-color.js @@ -1,4 +1,6 @@ import log from '../log/log'; +import {CHANGE_SELECTED_ITEMS} from './selected-items'; +import {getColorsFromSelection} from '../helper/style-path'; const CHANGE_STROKE_COLOR = 'scratch-paint/stroke-color/CHANGE_STROKE_COLOR'; const initialState = '#000'; @@ -14,6 +16,12 @@ const reducer = function (state, action) { return state; } return action.strokeColor; + case CHANGE_SELECTED_ITEMS: + // Don't change state if no selection + if (!action.selectedItems || !action.selectedItems.length) { + return state; + } + return getColorsFromSelection().strokeColor; default: return state; } diff --git a/src/reducers/stroke-width.js b/src/reducers/stroke-width.js index 615cab12..57c13926 100644 --- a/src/reducers/stroke-width.js +++ b/src/reducers/stroke-width.js @@ -1,4 +1,6 @@ import log from '../log/log'; +import {CHANGE_SELECTED_ITEMS} from './selected-items'; +import {getColorsFromSelection} from '../helper/style-path'; const CHANGE_STROKE_WIDTH = 'scratch-paint/stroke-width/CHANGE_STROKE_WIDTH'; const MAX_STROKE_WIDTH = 400; @@ -13,6 +15,12 @@ const reducer = function (state, action) { return state; } return Math.min(MAX_STROKE_WIDTH, Math.max(0, action.strokeWidth)); + case CHANGE_SELECTED_ITEMS: + // Don't change state if no selection + if (!action.selectedItems || !action.selectedItems.length) { + return state; + } + return getColorsFromSelection().strokeWidth; default: return state; }