diff --git a/.travis.yml b/.travis.yml
index cc8a5822..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,6 +10,8 @@ env:
global:
- NODE_ENV=production
install:
+- 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
before_deploy:
diff --git a/src/components/fill-color-indicator.jsx b/src/components/fill-color-indicator.jsx
index 2cad237a..5d2f65d2 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,8 @@ const FillColorIndicatorComponent = props => (
@@ -29,7 +32,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..dcb7db5a 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,9 @@ const StrokeColorIndicatorComponent = props => (
@@ -31,7 +35,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..3025be8d 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';
/**
@@ -27,11 +27,18 @@ 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;
+ this.brushSize = null;
+ this.fillColor = null;
}
/**
@@ -45,7 +52,18 @@ class Blobbiness {
* @param {?number} options.strokeWidth Width of the brush outline.
*/
setOptions (options) {
- this.options = options;
+ 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,
+ strokeColor: options.strokeColor === MIXED ? oldStrokeColor : options.strokeColor,
+ strokeWidth: options.strokeWidth === null ? oldStrokeWidth : options.strokeWidth
+ };
this.resizeCursorIfNeeded();
}
@@ -233,7 +251,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..b7027c14 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';
@@ -17,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) {
@@ -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/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));
},
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..69356d62 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, getSelectedLeafItems} 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(getSelectedLeafItems()));
+ },
handleMouseDown: () => {
dispatch(changeMode(Modes.LINE));
}
diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx
index 2fb3dc55..1683874c 100644
--- a/src/containers/reshape-mode.jsx
+++ b/src/containers/reshape-mode.jsx
@@ -5,7 +5,9 @@ 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 {getSelectedLeafItems} from '../helper/selection';
import ReshapeTool from '../helper/selection-tools/reshape-tool';
import ReshapeModeComponent from '../components/reshape-mode.jsx';
@@ -38,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();
}
@@ -57,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 => ({
@@ -75,6 +84,12 @@ const mapDispatchToProps = dispatch => ({
clearHoveredItem: () => {
dispatch(clearHoveredItem());
},
+ clearSelectedItems: () => {
+ dispatch(clearSelectedItems());
+ },
+ setSelectedItems: () => {
+ dispatch(setSelectedItems(getSelectedLeafItems()));
+ },
handleMouseDown: () => {
dispatch(changeMode(Modes.RESHAPE));
}
diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx
index 3992d3f9..505bf412 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 {getSelectedLeafItems} 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(getSelectedLeafItems()));
+ },
handleMouseDown: () => {
dispatch(changeMode(Modes.SELECT));
}
diff --git a/src/containers/selection-hoc.jsx b/src/containers/selection-hoc.jsx
index e468a179..d81a1c07 100644
--- a/src/containers/selection-hoc.jsx
+++ b/src/containers/selection-hoc.jsx
@@ -1,8 +1,9 @@
+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';
const SelectionHOC = function (WrappedComponent) {
class SelectionComponent extends React.Component {
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..792753c0 100644
--- a/src/helper/group.js
+++ b/src/helper/group.js
@@ -1,16 +1,16 @@
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 () {
- const items = getSelectedItems();
+const groupSelection = function (clearSelectedItems) {
+ const items = getSelectedRootItems();
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];
@@ -71,7 +71,7 @@ const ungroupItems = function (items) {
};
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 dca535fd..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 {clearSelection, getSelectedItems} from '../selection';
+import {getSelectedRootItems} from '../selection';
import {getGuideColor, removeHelperItems} from '../guides';
import {getGuideLayer} from '../layer';
@@ -31,7 +31,12 @@ 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.onUpdateSvg = onUpdateSvg;
this.mode = null;
this.boundsPath = null;
@@ -40,7 +45,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 +61,6 @@ class BoundingBoxTool {
if (!hitResults || hitResults.length === 0) {
if (!multiselect) {
this.removeBoundsPath();
- clearSelection();
}
return false;
}
@@ -86,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
@@ -109,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 26aead16..c3d26a1d 100644
--- a/src/helper/selection-tools/handle-tool.js
+++ b/src/helper/selection-tools/handle-tool.js
@@ -1,12 +1,16 @@
-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 {
/**
+ * @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;
@@ -24,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 8853e631..0a16277b 100644
--- a/src/helper/selection-tools/move-tool.js
+++ b/src/helper/selection-tools/move-tool.js
@@ -1,16 +1,20 @@
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.
*/
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,12 +47,12 @@ 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);
}
if (hitProperties.clone) cloneSelection(hitProperties.subselect);
- this.selectedItems = getSelectedItems(true /* subselect */);
+ this.selectedItems = getSelectedLeafItems();
}
/**
* Sets the selection state of an item.
@@ -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..9b54cb7e 100644
--- a/src/helper/selection-tools/point-tool.js
+++ b/src/helper/selection-tools/point-tool.js
@@ -1,13 +1,15 @@
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 {
/**
+ * @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,12 +53,12 @@ class PointTool {
}
} else {
if (!hitProperties.multiselect) {
- clearSelection();
+ clearSelection(this.clearSelectedItems);
}
hitProperties.hitResult.segment.selected = true;
}
- this.selectedItems = getSelectedItems(true /* recursive */);
+ this.selectedItems = getSelectedLeafItems();
}
/**
* @param {!object} hitProperties Describes the mouse event
@@ -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..efa75158 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,11 @@ 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..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,51 +105,67 @@ const selectAllItems = function () {
};
const selectAllSegments = function () {
- const items = getAllSelectableItems();
+ const items = getAllSelectableRootItems();
for (let i = 0; i < items.length; i++) {
selectItemSegments(items[i], true);
}
};
-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
-// (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();
}
@@ -160,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++) {
@@ -188,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);
}
};
@@ -243,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;
@@ -299,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]);
}
@@ -313,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();
@@ -325,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++) {
@@ -456,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];
@@ -478,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);
@@ -492,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 () {
@@ -504,7 +519,7 @@ const shouldShowSelectAll = function () {
};
export {
- getAllPaperItems,
+ getAllRootItems,
selectAllItems,
selectAllSegments,
clearSelection,
@@ -515,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
new file mode 100644
index 00000000..6e85b9f6
--- /dev/null
+++ b/src/helper/style-path.js
@@ -0,0 +1,193 @@
+import {getSelectedLeafItems} 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 = getSelectedLeafItems();
+ 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 = getSelectedLeafItems();
+
+ 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 = getSelectedLeafItems();
+ 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
+ * @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 (selectedItems) {
+ 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
+ };
+};
+
+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..fafa3016 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(action.selectedItems).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..67a2d147
--- /dev/null
+++ b/src/reducers/selected-items.js
@@ -0,0 +1,53 @@
+import log from '../log/log';
+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 (!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 (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;
+ }
+ }
+ 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..a7ecba9e 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(action.selectedItems).strokeColor;
default:
return state;
}
diff --git a/src/reducers/stroke-width.js b/src/reducers/stroke-width.js
index 615cab12..43213b8d 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(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};