From b87c17523a57a4d813f3993c063bdf48821e75b4 Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 2 Oct 2017 15:25:04 -0400 Subject: [PATCH 01/10] Selection sets and gets fill/stroke color/width --- src/components/fill-color-indicator.jsx | 6 +- src/components/stroke-color-indicator.jsx | 7 +- src/components/stroke-width-indicator.jsx | 5 +- src/containers/blob/blob.js | 31 ++- src/containers/blob/broad-brush-helper.js | 2 +- src/containers/blob/segment-brush-helper.js | 2 +- src/containers/blob/style-path.js | 22 -- src/containers/brush-mode.jsx | 13 +- src/containers/fill-color-indicator.jsx | 2 + src/containers/line-mode.jsx | 30 ++- src/containers/reshape-mode.jsx | 23 ++- src/containers/select-mode.jsx | 21 +- src/containers/selection-hoc.jsx | 24 ++- src/containers/stroke-color-indicator.jsx | 2 + src/containers/stroke-width-indicator.jsx | 2 + src/helper/group.js | 8 +- .../selection-tools/bounding-box-tool.js | 12 +- src/helper/selection-tools/handle-tool.js | 8 +- src/helper/selection-tools/move-tool.js | 11 +- src/helper/selection-tools/point-tool.js | 13 +- src/helper/selection-tools/reshape-tool.js | 12 +- src/helper/selection-tools/select-tool.js | 9 +- .../selection-tools/selection-box-tool.js | 13 +- src/helper/selection.js | 4 +- src/helper/style-path.js | 193 ++++++++++++++++++ src/reducers/fill-color.js | 8 + src/reducers/scratch-paint-reducer.js | 4 +- src/reducers/selected-items.js | 48 +++++ src/reducers/stroke-color.js | 8 + src/reducers/stroke-width.js | 8 + 30 files changed, 464 insertions(+), 87 deletions(-) delete mode 100644 src/containers/blob/style-path.js create mode 100644 src/helper/style-path.js create mode 100644 src/reducers/selected-items.js 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; } From 7a9399d0db17cd748263ec9d45b1dc791d65920a Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 3 Oct 2017 13:45:19 -0400 Subject: [PATCH 02/10] fix lint --- src/components/fill-color-indicator.jsx | 3 ++- src/components/stroke-color-indicator.jsx | 5 +++-- src/helper/selection-tools/bounding-box-tool.js | 4 +--- src/helper/selection-tools/reshape-tool.js | 3 ++- src/helper/style-path.js | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/fill-color-indicator.jsx b/src/components/fill-color-indicator.jsx index 3e555fda..5d2f65d2 100644 --- a/src/components/fill-color-indicator.jsx +++ b/src/components/fill-color-indicator.jsx @@ -23,7 +23,8 @@ const FillColorIndicatorComponent = props => ( diff --git a/src/components/stroke-color-indicator.jsx b/src/components/stroke-color-indicator.jsx index 45fb4824..dcb7db5a 100644 --- a/src/components/stroke-color-indicator.jsx +++ b/src/components/stroke-color-indicator.jsx @@ -23,8 +23,9 @@ const StrokeColorIndicatorComponent = props => ( diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index df427f1d..842319dd 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -1,7 +1,7 @@ import paper from 'paper'; import keyMirror from 'keymirror'; -import {clearSelection, getSelectedItems} from '../selection'; +import {getSelectedItems} from '../selection'; import {getGuideColor, removeHelperItems} from '../guides'; import {getGuideLayer} from '../layer'; @@ -37,8 +37,6 @@ class BoundingBoxTool { * @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; diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js index 6d469b23..efa75158 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -57,7 +57,8 @@ class ReshapeTool extends paper.Tool { 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); + 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/style-path.js b/src/helper/style-path.js index 00b8a979..6c660017 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -160,7 +160,7 @@ const getColorsFromSelection = function () { return { fillColor: selectionFillColorString ? selectionFillColorString : null, strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null, - strokeWidth: selectionStrokeWidth || (selectionStrokeWidth === null) ? selectionStrokeWidth : 0 //todo why is this 0 for arrow + strokeWidth: selectionStrokeWidth || (selectionStrokeWidth === null) ? selectionStrokeWidth : 0 }; }; From 720f83605d6395ee7bb76f0713fd5dd6e41a8527 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 3 Oct 2017 15:04:53 -0400 Subject: [PATCH 03/10] add tests --- src/containers/selection-hoc.jsx | 2 +- src/helper/style-path.js | 4 +- src/reducers/fill-color.js | 2 +- src/reducers/selected-items.js | 7 +++- src/reducers/stroke-color.js | 2 +- src/reducers/stroke-width.js | 2 +- test/__mocks__/paperMocks.js | 23 ++++++++++++ test/unit/fill-color-reducer.test.js | 19 ++++++++++ test/unit/selected-items-reducer.test.js | 47 ++++++++++++++++++++++++ test/unit/stroke-color-reducer.test.js | 19 ++++++++++ test/unit/stroke-width-reducer.test.js | 18 +++++++++ 11 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 test/__mocks__/paperMocks.js create mode 100644 test/unit/selected-items-reducer.test.js diff --git a/src/containers/selection-hoc.jsx b/src/containers/selection-hoc.jsx index cb7af472..2066ed09 100644 --- a/src/containers/selection-hoc.jsx +++ b/src/containers/selection-hoc.jsx @@ -65,7 +65,7 @@ const SelectionHOC = function (WrappedComponent) { if (selectedItems.length === 0) { return; } - const colorState = getColorsFromSelection(); + const colorState = getColorsFromSelection(selectedItems); dispatch(changeFillColor(colorState.fillColor)); dispatch(changeStrokeColor(colorState.strokeColor)); dispatch(changeStrokeWidth(colorState.strokeWidth)); diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 6c660017..2e27713f 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -82,12 +82,12 @@ const applyStrokeWidthToSelection = function (value) { /** * Get state of colors and stroke width for selection + * @param {!Array} selectedItems Selected paper items * @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 */); +const getColorsFromSelection = function (selectedItems) { let selectionFillColorString; let selectionStrokeColorString; let selectionStrokeWidth; diff --git a/src/reducers/fill-color.js b/src/reducers/fill-color.js index 783ef18b..fafa3016 100644 --- a/src/reducers/fill-color.js +++ b/src/reducers/fill-color.js @@ -21,7 +21,7 @@ const reducer = function (state, action) { if (!action.selectedItems || !action.selectedItems.length) { return state; } - return getColorsFromSelection().fillColor; + return getColorsFromSelection(action.selectedItems).fillColor; default: return state; } diff --git a/src/reducers/selected-items.js b/src/reducers/selected-items.js index 2ebb8140..67a2d147 100644 --- a/src/reducers/selected-items.js +++ b/src/reducers/selected-items.js @@ -1,3 +1,4 @@ +import log from '../log/log'; const CHANGE_SELECTED_ITEMS = 'scratch-paint/select/CHANGE_SELECTED_ITEMS'; const initialState = []; @@ -5,11 +6,15 @@ const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { case CHANGE_SELECTED_ITEMS: + if (!action.selectedItems || !(action.selectedItems instanceof Array)) { + log.warn(`No selected items or wrong format provided: ${action.selectedItems}`); + return state; + } // 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 + // Shallow equality check (we may need to update this later for more granularity) for (let i = 0; i < action.selectedItems.length; i++) { if (action.selectedItems[i] !== state[i]) { return action.selectedItems; diff --git a/src/reducers/stroke-color.js b/src/reducers/stroke-color.js index 4d414447..a7ecba9e 100644 --- a/src/reducers/stroke-color.js +++ b/src/reducers/stroke-color.js @@ -21,7 +21,7 @@ const reducer = function (state, action) { if (!action.selectedItems || !action.selectedItems.length) { return state; } - return getColorsFromSelection().strokeColor; + return getColorsFromSelection(action.selectedItems).strokeColor; default: return state; } diff --git a/src/reducers/stroke-width.js b/src/reducers/stroke-width.js index 57c13926..43213b8d 100644 --- a/src/reducers/stroke-width.js +++ b/src/reducers/stroke-width.js @@ -20,7 +20,7 @@ const reducer = function (state, action) { if (!action.selectedItems || !action.selectedItems.length) { return state; } - return getColorsFromSelection().strokeWidth; + return getColorsFromSelection(action.selectedItems).strokeWidth; default: return state; } diff --git a/test/__mocks__/paperMocks.js b/test/__mocks__/paperMocks.js new file mode 100644 index 00000000..b8d9ff15 --- /dev/null +++ b/test/__mocks__/paperMocks.js @@ -0,0 +1,23 @@ +/** + * Pretend paper.Item whose parent is a layer. + * @param {object} options Item params + * @param {string} options.strokeColor Value to return for the item's stroke color + * @param {string} options.fillColor Value to return for the item's fill color + * @param {string} options.strokeWidth Value to return for the item's stroke width + * @return {object} mock item + */ +const mockPaperRootItem = function (options) { + return { + strokeColor: {toCSS: function () { + return options.strokeColor; + }}, + fillColor: {toCSS: function () { + return options.fillColor; + }}, + strokeWidth: options.strokeWidth, + parent: {className: 'Layer'}, + data: {} + }; +}; + +export {mockPaperRootItem}; diff --git a/test/unit/fill-color-reducer.test.js b/test/unit/fill-color-reducer.test.js index dddd9008..fb830431 100644 --- a/test/unit/fill-color-reducer.test.js +++ b/test/unit/fill-color-reducer.test.js @@ -1,6 +1,9 @@ /* eslint-env jest */ import fillColorReducer from '../../src/reducers/fill-color'; import {changeFillColor} from '../../src/reducers/fill-color'; +import {setSelectedItems} from '../../src/reducers/selected-items'; +import {MIXED} from '../../src/helper/style-path'; +import {mockPaperRootItem} from '../__mocks__/paperMocks'; test('initialState', () => { let defaultState; @@ -26,6 +29,22 @@ test('changeFillColor', () => { .toEqual(newFillColor); }); +test('changefillColorViaSelectedItems', () => { + let defaultState; + + const fillColor1 = 6; + const fillColor2 = null; // transparent + let selectedItems = [mockPaperRootItem({fillColor: fillColor1})]; + expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + .toEqual(fillColor1); + selectedItems = [mockPaperRootItem({fillColor: fillColor2})]; + expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + .toEqual(fillColor2); + selectedItems = [mockPaperRootItem({fillColor: fillColor1}), mockPaperRootItem({fillColor: fillColor2})]; + expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + .toEqual(MIXED); +}); + test('invalidChangeFillColor', () => { const origState = '#fff'; diff --git a/test/unit/selected-items-reducer.test.js b/test/unit/selected-items-reducer.test.js new file mode 100644 index 00000000..d4a30ba8 --- /dev/null +++ b/test/unit/selected-items-reducer.test.js @@ -0,0 +1,47 @@ +/* eslint-env jest */ +import selectedItemsReducer from '../../src/reducers/selected-items'; +import {setSelectedItems, clearSelectedItems} from '../../src/reducers/selected-items'; + +test('initialState', () => { + let defaultState; + + expect(selectedItemsReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined(); +}); + +test('setSelectedItems', () => { + let defaultState; + + const newSelected1 = ['selected1', 'selected2']; + const newSelected2 = ['selected1', 'selected3']; + const unselected = []; + expect(selectedItemsReducer(defaultState /* state */, setSelectedItems(newSelected1) /* action */)) + .toEqual(newSelected1); + expect(selectedItemsReducer(newSelected1, setSelectedItems(newSelected2) /* action */)) + .toEqual(newSelected2); + expect(selectedItemsReducer(newSelected1, setSelectedItems(unselected) /* action */)) + .toEqual(unselected); + expect(selectedItemsReducer(defaultState, setSelectedItems(unselected) /* action */)) + .toEqual(unselected); +}); + +test('clearSelectedItems', () => { + let defaultState; + + const selectedState = ['selected1', 'selected2']; + const unselectedState = []; + expect(selectedItemsReducer(defaultState /* state */, clearSelectedItems() /* action */)) + .toHaveLength(0); + expect(selectedItemsReducer(selectedState /* state */, clearSelectedItems() /* action */)) + .toHaveLength(0); + expect(selectedItemsReducer(unselectedState /* state */, clearSelectedItems() /* action */)) + .toHaveLength(0); +}); + +test('invalidsetSelectedItems', () => { + const origState = ['selected1', 'selected2']; + + expect(selectedItemsReducer(origState /* state */, setSelectedItems() /* action */)) + .toBe(origState); + expect(selectedItemsReducer(origState /* state */, setSelectedItems('notAnArray') /* action */)) + .toBe(origState); +}); diff --git a/test/unit/stroke-color-reducer.test.js b/test/unit/stroke-color-reducer.test.js index 7f812299..e823d2e1 100644 --- a/test/unit/stroke-color-reducer.test.js +++ b/test/unit/stroke-color-reducer.test.js @@ -1,6 +1,9 @@ /* eslint-env jest */ import strokeColorReducer from '../../src/reducers/stroke-color'; import {changeStrokeColor} from '../../src/reducers/stroke-color'; +import {setSelectedItems} from '../../src/reducers/selected-items'; +import {MIXED} from '../../src/helper/style-path'; +import {mockPaperRootItem} from '../__mocks__/paperMocks'; test('initialState', () => { let defaultState; @@ -26,6 +29,22 @@ test('changeStrokeColor', () => { .toEqual(newStrokeColor); }); +test('changeStrokeColorViaSelectedItems', () => { + let defaultState; + + const strokeColor1 = 6; + const strokeColor2 = null; // transparent + let selectedItems = [mockPaperRootItem({strokeColor: strokeColor1})]; + expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + .toEqual(strokeColor1); + selectedItems = [mockPaperRootItem({strokeColor: strokeColor2})]; + expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + .toEqual(strokeColor2); + selectedItems = [mockPaperRootItem({strokeColor: strokeColor1}), mockPaperRootItem({strokeColor: strokeColor2})]; + expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + .toEqual(MIXED); +}); + test('invalidChangeStrokeColor', () => { const origState = '#fff'; diff --git a/test/unit/stroke-width-reducer.test.js b/test/unit/stroke-width-reducer.test.js index a2189049..03fd9184 100644 --- a/test/unit/stroke-width-reducer.test.js +++ b/test/unit/stroke-width-reducer.test.js @@ -1,6 +1,8 @@ /* eslint-env jest */ import strokeWidthReducer from '../../src/reducers/stroke-width'; import {MAX_STROKE_WIDTH, changeStrokeWidth} from '../../src/reducers/stroke-width'; +import {setSelectedItems} from '../../src/reducers/selected-items'; +import {mockPaperRootItem} from '../__mocks__/paperMocks'; test('initialState', () => { let defaultState; @@ -23,6 +25,22 @@ test('changestrokeWidth', () => { .toEqual(MAX_STROKE_WIDTH); }); +test('changeStrokeWidthViaSelectedItems', () => { + let defaultState; + + const strokeWidth1 = 6; + let strokeWidth2; // no outline + let selectedItems = [mockPaperRootItem({strokeWidth: strokeWidth1})]; + expect(strokeWidthReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + .toEqual(strokeWidth1); + selectedItems = [mockPaperRootItem({strokeWidth: strokeWidth2})]; + expect(strokeWidthReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + .toEqual(0); // Convert no outline to stroke width 0 + selectedItems = [mockPaperRootItem({strokeWidth: strokeWidth1}), mockPaperRootItem({strokeWidth: strokeWidth2})]; + expect(strokeWidthReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + .toEqual(null); // null indicates mixed for stroke width +}); + test('invalidChangestrokeWidth', () => { const origState = {strokeWidth: 1}; From e4f1a5ad289a4c40adeb4e3f7c24aee078e3fe9e Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 3 Oct 2017 15:17:27 -0400 Subject: [PATCH 04/10] clean up and add comment --- src/containers/blob/blob.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index 62b126d8..6a9b871e 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -50,9 +50,12 @@ class Blobbiness { * @param {?number} options.strokeWidth Width of the brush outline. */ setOptions (options) { - const oldFillColor = this.options ? this.options.fillColor : null; + const oldFillColor = this.options ? this.options.fillColor : 'black'; const oldStrokeColor = this.options ? this.options.strokeColor : null; const oldStrokeWidth = this.options ? this.options.strokeWidth : null; + // If values are mixed, it means the color was set by a selection contained multiple values. + // In this case keep drawing with the previous values if any. (For stroke width, null indicates) + // mixed, because stroke width is required to be a number) this.options = { ...options, fillColor: options.fillColor === MIXED ? oldFillColor : options.fillColor, @@ -163,8 +166,8 @@ class Blobbiness { if (this.cursorPreview && this.brushSize === this.options.brushSize && - (this.options.fillColor === MIXED || this.fillColor === this.options.fillColor) && - (this.options.strokeColor === MIXED || this.strokeColor === this.options.strokeColor)) { + this.fillColor === this.options.fillColor && + this.strokeColor === this.options.strokeColor) { return; } const newPreview = new paper.Path.Circle({ @@ -175,12 +178,8 @@ class Blobbiness { this.cursorPreview.remove(); } this.brushSize = this.options.brushSize; - if (this.options.fillColor !== MIXED) { - this.fillColor = this.options.fillColor; - } - if (this.options.strokeColor !== MIXED) { - this.strokeColor = this.options.strokeColor; - } + this.fillColor = this.options.fillColor; + this.strokeColor = this.options.strokeColor; this.cursorPreview = newPreview; styleCursorPreview(this.cursorPreview, this.options); } From aa2b70f1e71318dc09692b091756d52ae735196b Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 3 Oct 2017 17:53:06 -0400 Subject: [PATCH 05/10] Add missing clearSelectedItems function to blob --- src/containers/blob/blob.js | 4 +++- src/containers/brush-mode.jsx | 2 +- src/containers/eraser-mode.jsx | 7 ++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index 6a9b871e..22606723 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -27,11 +27,13 @@ class Blobbiness { /** * @param {function} updateCallback call when the drawing has changed to let listeners know + * @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state */ - constructor (updateCallback) { + constructor (updateCallback, clearSelectedItems) { this.broadBrushHelper = new BroadBrushHelper(); this.segmentBrushHelper = new SegmentBrushHelper(); this.updateCallback = updateCallback; + this.clearSelectedItems = clearSelectedItems; // The following are stored to check whether these have changed and the cursor preview needs to be redrawn. this.strokeColor = null; diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index 799ada60..b7027c14 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -18,7 +18,7 @@ class BrushMode extends React.Component { 'deactivateTool', 'onScroll' ]); - this.blob = new Blobbiness(this.props.onUpdateSvg); + this.blob = new Blobbiness(this.props.onUpdateSvg, this.props.clearSelectedItems); } componentDidMount () { if (this.props.isBrushModeActive) { diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx index 62d5b4fe..423058c2 100644 --- a/src/containers/eraser-mode.jsx +++ b/src/containers/eraser-mode.jsx @@ -5,6 +5,7 @@ import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; import Blobbiness from './blob/blob'; import {changeBrushSize} from '../reducers/eraser-mode'; +import {clearSelectedItems} from '../reducers/selected-items'; import EraserModeComponent from '../components/eraser-mode.jsx'; import {changeMode} from '../reducers/modes'; @@ -16,7 +17,7 @@ class EraserMode extends React.Component { 'deactivateTool', 'onScroll' ]); - this.blob = new Blobbiness(this.props.onUpdateSvg); + this.blob = new Blobbiness(this.props.onUpdateSvg, this.props.clearSelectedItems); } componentDidMount () { if (this.props.isEraserModeActive) { @@ -65,6 +66,7 @@ class EraserMode extends React.Component { EraserMode.propTypes = { canvas: PropTypes.instanceOf(Element).isRequired, changeBrushSize: PropTypes.func.isRequired, + clearSelectedItems: PropTypes.func.isRequired, eraserModeState: PropTypes.shape({ brushSize: PropTypes.number.isRequired }), @@ -78,6 +80,9 @@ const mapStateToProps = state => ({ isEraserModeActive: state.scratchPaint.mode === Modes.ERASER }); const mapDispatchToProps = dispatch => ({ + clearSelectedItems: () => { + dispatch(clearSelectedItems()); + }, changeBrushSize: brushSize => { dispatch(changeBrushSize(brushSize)); }, From 60a310ae853de0a8092b9e44e59ad461a8df7034 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 3 Oct 2017 18:13:12 -0400 Subject: [PATCH 06/10] remove unused --- src/containers/selection-hoc.jsx | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/containers/selection-hoc.jsx b/src/containers/selection-hoc.jsx index 2066ed09..d81a1c07 100644 --- a/src/containers/selection-hoc.jsx +++ b/src/containers/selection-hoc.jsx @@ -5,12 +5,6 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; -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 { constructor (props) { @@ -59,21 +53,8 @@ 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(selectedItems); - dispatch(changeFillColor(colorState.fillColor)); - dispatch(changeStrokeColor(colorState.strokeColor)); - dispatch(changeStrokeWidth(colorState.strokeWidth)); - }) - }); return connect( - mapStateToProps, - mapDispatchToProps + mapStateToProps )(SelectionComponent); }; From c67370ab804eab7eceb2a4eb3aa49aa817214db3 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 6 Oct 2017 13:55:28 -0400 Subject: [PATCH 07/10] attempt 1 at trying to get tests to work on travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index cc8a5822..001cff0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,8 @@ env: global: - NODE_ENV=production install: +- apt-get update && apt-get install -y libcairo2-dev libpango1.0-dev libssl-dev libjpeg62-dev libgif-dev pkg-config +- npm install canvas - npm --production=false install - npm --production=false update before_deploy: From df8c3f66e35ab11d2a4a74d40591a843773ba066 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 6 Oct 2017 14:00:50 -0400 Subject: [PATCH 08/10] attempt 2 at trying to get tests to work on travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 001cff0f..92dcf7da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: node_js -sudo: false +sudo: required dist: trusty node_js: - 6 @@ -10,7 +10,7 @@ env: global: - NODE_ENV=production install: -- apt-get update && apt-get install -y libcairo2-dev libpango1.0-dev libssl-dev libjpeg62-dev libgif-dev pkg-config +- sudo apt-get update && sudo apt-get install -y libcairo2-dev libpango1.0-dev libssl-dev libjpeg62-dev libgif-dev pkg-config - npm install canvas - npm --production=false install - npm --production=false update From ab244a5e4c007f63ccf2edb92db387e92d8a1846 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 11 Oct 2017 11:32:51 -0400 Subject: [PATCH 09/10] convert getSelectedItems(recursive) to 2 functions --- src/containers/blob/blob.js | 2 +- src/containers/line-mode.jsx | 4 +- src/containers/reshape-mode.jsx | 4 +- src/containers/select-mode.jsx | 4 +- src/helper/group.js | 10 +- src/helper/guides.js | 6 +- .../selection-tools/bounding-box-tool.js | 8 +- src/helper/selection-tools/handle-tool.js | 4 +- src/helper/selection-tools/move-tool.js | 4 +- src/helper/selection-tools/point-tool.js | 4 +- src/helper/selection.js | 98 +++++++++++-------- src/helper/style-path.js | 8 +- 12 files changed, 85 insertions(+), 71 deletions(-) diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index 22606723..3025be8d 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -56,7 +56,7 @@ class Blobbiness { const oldStrokeColor = this.options ? this.options.strokeColor : null; const oldStrokeWidth = this.options ? this.options.strokeWidth : null; // If values are mixed, it means the color was set by a selection contained multiple values. - // In this case keep drawing with the previous values if any. (For stroke width, null indicates) + // In this case keep drawing with the previous values if any. (For stroke width, null indicates // mixed, because stroke width is required to be a number) this.options = { ...options, diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index e1ba1c36..69356d62 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -4,7 +4,7 @@ import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; import {changeStrokeWidth} from '../reducers/stroke-width'; -import {clearSelection, getSelectedItems} from '../helper/selection'; +import {clearSelection, getSelectedLeafItems} from '../helper/selection'; import {MIXED} from '../helper/style-path'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import LineModeComponent from '../components/line-mode.jsx'; @@ -299,7 +299,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedItems(true /* recursive */))); + dispatch(setSelectedItems(getSelectedLeafItems())); }, handleMouseDown: () => { dispatch(changeMode(Modes.LINE)); diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx index 392989f9..1683874c 100644 --- a/src/containers/reshape-mode.jsx +++ b/src/containers/reshape-mode.jsx @@ -7,7 +7,7 @@ import Modes from '../modes/modes'; import {changeMode} from '../reducers/modes'; import {clearHoveredItem, setHoveredItem} from '../reducers/hover'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; -import {getSelectedItems} from '../helper/selection'; +import {getSelectedLeafItems} from '../helper/selection'; import ReshapeTool from '../helper/selection-tools/reshape-tool'; import ReshapeModeComponent from '../components/reshape-mode.jsx'; @@ -88,7 +88,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedItems(true /* recursive */))); + dispatch(setSelectedItems(getSelectedLeafItems())); }, handleMouseDown: () => { dispatch(changeMode(Modes.RESHAPE)); diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index b3b43e0c..505bf412 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -8,7 +8,7 @@ import {changeMode} from '../reducers/modes'; import {clearHoveredItem, setHoveredItem} from '../reducers/hover'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; -import {getSelectedItems} from '../helper/selection'; +import {getSelectedLeafItems} from '../helper/selection'; import SelectTool from '../helper/selection-tools/select-tool'; import SelectModeComponent from '../components/select-mode.jsx'; @@ -86,7 +86,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedItems(true /* recursive */))); + dispatch(setSelectedItems(getSelectedLeafItems())); }, handleMouseDown: () => { dispatch(changeMode(Modes.SELECT)); diff --git a/src/helper/group.js b/src/helper/group.js index 20bac775..792753c0 100644 --- a/src/helper/group.js +++ b/src/helper/group.js @@ -1,13 +1,13 @@ import paper from 'paper'; import {getRootItem, isGroupItem} from './item'; -import {clearSelection, getSelectedItems, setItemSelection} from './selection'; +import {clearSelection, getSelectedRootItems, setItemSelection} from './selection'; const isGroup = function (item) { return isGroupItem(item); }; const groupSelection = function (clearSelectedItems) { - const items = getSelectedItems(); + const items = getSelectedRootItems(); if (items.length > 0) { const group = new paper.Group(items); clearSelection(clearSelectedItems); @@ -71,7 +71,7 @@ const ungroupItems = function (items, clearSelectedItems) { }; const ungroupSelection = function () { - const items = getSelectedItems(); + const items = getSelectedRootItems(); ungroupItems(items); }; @@ -102,12 +102,12 @@ const isGroupChild = function (item) { }; const shouldShowGroup = function () { - const items = getSelectedItems(); + const items = getSelectedRootItems(); return items.length > 1; }; const shouldShowUngroup = function () { - const items = getSelectedItems(); + const items = getSelectedRootItems(); for (let i = 0; i < items.length; i++) { const item = items[i]; if (isGroup(item) && !item.data.isPGTextItem && item.children && item.children.length > 0) { diff --git a/src/helper/guides.js b/src/helper/guides.js index c6f917b8..ae5c569e 100644 --- a/src/helper/guides.js +++ b/src/helper/guides.js @@ -1,6 +1,6 @@ import paper from 'paper'; import {getGuideLayer} from './layer'; -import {getAllPaperItems} from './selection'; +import {getAllRootItems} from './selection'; const GUIDE_BLUE = '#009dec'; const GUIDE_GREY = '#aaaaaa'; @@ -66,7 +66,7 @@ const getGuideColor = function (colorName) { }; const _removePaperItemsByDataTags = function (tags) { - const allItems = getAllPaperItems(true); + const allItems = getAllRootItems(true); for (const item of allItems) { for (const tag of tags) { if (item.data && item.data[tag]) { @@ -77,7 +77,7 @@ const _removePaperItemsByDataTags = function (tags) { }; const _removePaperItemsByTags = function (tags) { - const allItems = getAllPaperItems(true); + const allItems = getAllRootItems(true); for (const item of allItems) { for (const tag of tags) { if (item[tag]) { diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index 842319dd..1826f365 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -1,7 +1,7 @@ import paper from 'paper'; import keyMirror from 'keymirror'; -import {getSelectedItems} from '../selection'; +import {getSelectedRootItems} from '../selection'; import {getGuideColor, removeHelperItems} from '../guides'; import {getGuideLayer} from '../layer'; @@ -90,9 +90,9 @@ class BoundingBoxTool { this._modeMap[this.mode].onMouseDown(hitProperties); } else if (this.mode === Modes.SCALE) { this._modeMap[this.mode].onMouseDown( - hitResult, this.boundsPath, this.boundsScaleHandles, this.boundsRotHandles, getSelectedItems()); + hitResult, this.boundsPath, this.boundsScaleHandles, this.boundsRotHandles, getSelectedRootItems()); } else if (this.mode === Modes.ROTATE) { - this._modeMap[this.mode].onMouseDown(hitResult, this.boundsPath, getSelectedItems()); + this._modeMap[this.mode].onMouseDown(hitResult, this.boundsPath, getSelectedRootItems()); } // while transforming object, never show the bounds stuff @@ -113,7 +113,7 @@ class BoundingBoxTool { setSelectionBounds () { this.removeBoundsPath(); - const items = getSelectedItems(true /* recursive */); + const items = getSelectedRootItems(); if (items.length <= 0) return; let rect = null; diff --git a/src/helper/selection-tools/handle-tool.js b/src/helper/selection-tools/handle-tool.js index 4636116d..c3d26a1d 100644 --- a/src/helper/selection-tools/handle-tool.js +++ b/src/helper/selection-tools/handle-tool.js @@ -1,4 +1,4 @@ -import {clearSelection, getSelectedItems} from '../selection'; +import {clearSelection, getSelectedLeafItems} from '../selection'; /** Sub tool of the Reshape tool for moving handles, which adjust bezier curves. */ class HandleTool { @@ -28,7 +28,7 @@ class HandleTool { this.hitType = hitProperties.hitResult.type; } onMouseDrag (event) { - const selectedItems = getSelectedItems(true /* recursive */); + const selectedItems = getSelectedLeafItems(); for (const item of selectedItems) { for (const seg of item.segments) { diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js index 34212708..0a16277b 100644 --- a/src/helper/selection-tools/move-tool.js +++ b/src/helper/selection-tools/move-tool.js @@ -1,7 +1,7 @@ import {isGroup} from '../group'; import {isCompoundPathItem, getRootItem} from '../item'; import {snapDeltaToAngle} from '../math'; -import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; +import {clearSelection, cloneSelection, getSelectedLeafItems, setItemSelection} from '../selection'; /** * Tool to handle dragging an item to reposition it in a selection mode. @@ -52,7 +52,7 @@ class MoveTool { this._select(item, true, hitProperties.subselect); } if (hitProperties.clone) cloneSelection(hitProperties.subselect); - this.selectedItems = getSelectedItems(true /* subselect */); + this.selectedItems = getSelectedLeafItems(); } /** * Sets the selection state of an item. diff --git a/src/helper/selection-tools/point-tool.js b/src/helper/selection-tools/point-tool.js index 20eb4da2..9b54cb7e 100644 --- a/src/helper/selection-tools/point-tool.js +++ b/src/helper/selection-tools/point-tool.js @@ -1,6 +1,6 @@ import paper from 'paper'; import {snapDeltaToAngle} from '../math'; -import {clearSelection, getSelectedItems} from '../selection'; +import {clearSelection, getSelectedLeafItems} from '../selection'; /** Subtool of ReshapeTool for moving control points. */ class PointTool { @@ -58,7 +58,7 @@ class PointTool { hitProperties.hitResult.segment.selected = true; } - this.selectedItems = getSelectedItems(true /* recursive */); + this.selectedItems = getSelectedLeafItems(); } /** * @param {!object} hitProperties Describes the mouse event diff --git a/src/helper/selection.js b/src/helper/selection.js index 443fa5cb..2c12b161 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -10,7 +10,7 @@ import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compo * be included in the returned items. * @return {Array} all top-level (direct descendants of a paper.Layer) items */ -const getAllPaperItems = function (includeGuides) { +const getAllRootItems = function (includeGuides) { includeGuides = includeGuides || false; const allItems = []; for (const layer of paper.project.layers) { @@ -29,8 +29,8 @@ const getAllPaperItems = function (includeGuides) { * @return {Array} all top-level (direct descendants of a paper.Layer) items * that aren't guide items or helper items. */ -const getAllSelectableItems = function () { - const allItems = getAllPaperItems(); +const getAllSelectableRootItems = function () { + const allItems = getAllRootItems(); const selectables = []; for (let i = 0; i < allItems.length; i++) { if (allItems[i].data && !allItems[i].data.isHelperItem) { @@ -97,7 +97,7 @@ const setItemSelection = function (item, state, fullySelected) { }; const selectAllItems = function () { - const items = getAllSelectableItems(); + const items = getAllSelectableRootItems(); for (let i = 0; i < items.length; i++) { setItemSelection(items[i], true); @@ -105,7 +105,7 @@ const selectAllItems = function () { }; const selectAllSegments = function () { - const items = getAllSelectableItems(); + const items = getAllSelectableRootItems(); for (let i = 0; i < items.length; i++) { selectItemSegments(items[i], true); @@ -119,39 +119,53 @@ const clearSelection = function (dispatchClearSelect) { dispatchClearSelect(); }; -// This gets all selected non-grouped items and groups -// (alternative to paper.project.selectedItems, which includes -// group children in addition to the group) -// Returns in increasing Z order -const getSelectedItems = function (recursive) { +/** + * This gets all selected non-grouped items and groups + * (alternative to paper.project.selectedItems, which includes + * group children in addition to the group) + * @return {Array} in increasing Z order. + */ +const getSelectedRootItems = function () { const allItems = paper.project.selectedItems; const itemsAndGroups = []; - if (recursive) { - for (let i = 0; i < allItems.length; i++) { - const item = allItems[i]; + for (let i = 0; i < allItems.length; i++) { + const item = allItems[i]; + if ((isGroup(item) && !isGroup(item.parent)) || + !isGroup(item.parent)) { if (item.data && !item.data.isSelectionBound) { itemsAndGroups.push(item); } } - } else { - for (let i = 0; i < allItems.length; i++) { - const item = allItems[i]; - if ((isGroup(item) && !isGroup(item.parent)) || - !isGroup(item.parent)) { - if (item.data && !item.data.isSelectionBound) { - itemsAndGroups.push(item); - } - } - } } + // sort items by index (0 at bottom) itemsAndGroups.sort((a, b) => parseFloat(a.index) - parseFloat(b.index)); return itemsAndGroups; }; -const deleteItemSelection = function (recursive) { - const items = getSelectedItems(recursive); +/** + * This gets all selected items that are as deeply nested as possible. Does not + * return the parent groups. + * @return {Array} in increasing Z order. + */ +const getSelectedLeafItems = function () { + const allItems = paper.project.selectedItems; + const items = []; + + for (let i = 0; i < allItems.length; i++) { + const item = allItems[i]; + if (!isGroup(item) && item.data && !item.data.isSelectionBound) { + items.push(item); + } + } + + // sort items by index (0 at bottom) + items.sort((a, b) => parseFloat(a.index) - parseFloat(b.index)); + return items; +}; + +const deleteItemSelection = function (items) { for (let i = 0; i < items.length; i++) { items[i].remove(); } @@ -162,11 +176,10 @@ const deleteItemSelection = function (recursive) { // pg.undo.snapshot('deleteItemSelection'); }; -const removeSelectedSegments = function (recursive) { +const removeSelectedSegments = function (items) { // @todo add back undo // pg.undo.snapshot('removeSelectedSegments'); - const items = getSelectedItems(recursive); const segmentsToRemove = []; for (let i = 0; i < items.length; i++) { @@ -190,12 +203,14 @@ const removeSelectedSegments = function (recursive) { const deleteSelection = function (mode) { if (mode === Modes.RESHAPE) { + const selectedItems = getSelectedLeafItems(); // If there are points selected remove them. If not delete the item selected. - if (!removeSelectedSegments(true /* recursive */)) { - deleteItemSelection(true /* recursive */); + if (!removeSelectedSegments(selectedItems)) { + deleteItemSelection(selectedItems); } } else { - deleteItemSelection(); + const selectedItems = getSelectedRootItems(); + deleteItemSelection(selectedItems); } }; @@ -245,7 +260,7 @@ const splitPathRetainSelection = function (path, index, deselectSplitSegments) { }; const splitPathAtSelectedSegments = function () { - const items = getSelectedItems(); + const items = getSelectedRootItems(); for (let i = 0; i < items.length; i++) { const item = items[i]; const segments = item.segments; @@ -301,9 +316,7 @@ const deleteSegments = function (item) { } }; -const deleteSegmentSelection = function () { - - const items = getSelectedItems(); +const deleteSegmentSelection = function (items) { for (let i = 0; i < items.length; i++) { deleteSegments(items[i]); } @@ -315,7 +328,7 @@ const deleteSegmentSelection = function () { }; const cloneSelection = function (recursive) { - const selectedItems = getSelectedItems(recursive); + const selectedItems = recursive ? getSelectedLeafItems() : getSelectedRootItems(); for (let i = 0; i < selectedItems.length; i++) { const item = selectedItems[i]; item.clone(); @@ -327,7 +340,7 @@ const cloneSelection = function (recursive) { // Only returns paths, no compound paths, groups or any other stuff const getSelectedPaths = function () { - const allPaths = getSelectedItems(); + const allPaths = getSelectedRootItems(); const paths = []; for (let i = 0; i < allPaths.length; i++) { @@ -458,7 +471,7 @@ const _rectangularSelectionGroupLoop = function (group, rect, root, event, mode) * @param {Modes} mode The mode of the paint editor when drawing the rectangle */ const processRectangularSelection = function (event, rect, mode) { - const allItems = getAllSelectableItems(); + const allItems = getAllSelectableRootItems(); for (let i = 0; i < allItems.length; i++) { const item = allItems[i]; @@ -480,7 +493,7 @@ const processRectangularSelection = function (event, rect, mode) { * instead. (otherwise the compound path breaks because of scale-grouping) */ const selectRootItem = function () { - const items = getSelectedItems(true /* recursive */); + const items = getSelectedLeafItems(); for (const item of items) { if (isCompoundPathChild(item)) { const cp = getItemsCompoundPath(item); @@ -494,11 +507,11 @@ const selectRootItem = function () { }; const shouldShowIfSelection = function () { - return getSelectedItems().length > 0; + return getSelectedRootItems().length > 0; }; const shouldShowIfSelectionRecursive = function () { - return getSelectedItems(true /* recursive */).length > 0; + return getSelectedRootItems().length > 0; }; const shouldShowSelectAll = function () { @@ -506,7 +519,7 @@ const shouldShowSelectAll = function () { }; export { - getAllPaperItems, + getAllRootItems, selectAllItems, selectAllSegments, clearSelection, @@ -517,8 +530,9 @@ export { cloneSelection, setItemSelection, setGroupSelection, - getSelectedItems, + getSelectedLeafItems, getSelectedPaths, + getSelectedRootItems, removeSelectedSegments, processRectangularSelection, selectRootItem, diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 2e27713f..aab1aee1 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -1,4 +1,4 @@ -import {getSelectedItems} from './selection'; +import {getSelectedLeafItems} from './selection'; import {isPGTextItem, isPointTextItem} from './item'; import {isGroup} from './group'; @@ -9,7 +9,7 @@ const MIXED = 'scratch-paint/style-path/mixed'; * @param {string} colorString New color, css format */ const applyFillColorToSelection = function (colorString) { - const items = getSelectedItems(true /* recursive */); + const items = getSelectedLeafItems; for (const item of items) { if (isPGTextItem(item)) { for (const child of item.children) { @@ -38,7 +38,7 @@ const applyFillColorToSelection = function (colorString) { * @param {string} colorString New color, css format */ const applyStrokeColorToSelection = function (colorString) { - const items = getSelectedItems(true /* recursive */); + const items = getSelectedLeafItems(); for (const item of items) { if (isPGTextItem(item)) { @@ -69,7 +69,7 @@ const applyStrokeColorToSelection = function (colorString) { * @param {number} value New stroke width */ const applyStrokeWidthToSelection = function (value) { - const items = getSelectedItems(true /* recursive */); + const items = getSelectedLeafItems(); for (const item of items) { if (isGroup(item)) { continue; From f7cd7fc54dd6eb01f7cf8fa11d98d8008c550f5c Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 11 Oct 2017 11:58:05 -0400 Subject: [PATCH 10/10] fix function --- src/helper/style-path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helper/style-path.js b/src/helper/style-path.js index aab1aee1..6e85b9f6 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -9,7 +9,7 @@ const MIXED = 'scratch-paint/style-path/mixed'; * @param {string} colorString New color, css format */ const applyFillColorToSelection = function (colorString) { - const items = getSelectedLeafItems; + const items = getSelectedLeafItems(); for (const item of items) { if (isPGTextItem(item)) { for (const child of item.children) {