From 99e023eea615732e4af1b8907b653370a30de761 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 21 Sep 2017 18:39:18 -0400 Subject: [PATCH 1/6] add select tool file --- src/helper/selection-tools/select-tool.js | 108 ++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/helper/selection-tools/select-tool.js diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js new file mode 100644 index 00000000..3cb574f5 --- /dev/null +++ b/src/helper/selection-tools/select-tool.js @@ -0,0 +1,108 @@ +import Modes from '../../modes/modes'; + +import {getHoveredItem} from '../hover'; +import {deleteSelection, selectRootItem} from '../selection'; +import BoundingBoxTool from './bounding-box-tool'; +import SelectionBoxTool from './selection-box-tool'; +import paper from 'paper'; + +class SelectTool extends paper.Tool { + static get TOLERANCE () { + return 6; + } + constructor (setHoveredItem, clearHoveredItem, onUpdateSvg) { + super(); + this.setHoveredItem = setHoveredItem; + this.clearHoveredItem = clearHoveredItem; + this.onUpdateSvg = onUpdateSvg; + this.boundingBoxTool = new BoundingBoxTool(onUpdateSvg); + this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT); + this.selectionBoxMode = false; + this._hitOptions = { + segments: true, + stroke: true, + curves: true, + fill: true, + guide: false + }; + + // We have to set these functions instead of just declaring them because + // paper.js tools hook up the listeners in the setter functions. + this.onMouseDown = this.handleMouseDown; + this.onMouseMove = this.handleMouseMove; + this.onMouseDrag = this.handleMouseDrag; + this.onMouseUp = this.handleMouseUp; + this.onKeyUp = this.handleKeyUp; + + selectRootItem(); + this.boundingBoxTool.setSelectionBounds(); + } + setPrevHoveredItem (prevHoveredItem) { + this.prevHoveredItem = prevHoveredItem; + } + getHitOptions (preselectedOnly) { + this._hitOptions.tolerance = SelectTool.TOLERANCE / paper.view.zoom; + if (preselectedOnly) { + this._hitOptions.selected = true; + } else { + delete this._hitOptions.selected; + } + return this._hitOptions; + } + handleMouseDown (event) { + if (event.event.button > 0) return; // only first mouse button + + this.clearHoveredItem(); + if (!this.boundingBoxTool + .onMouseDown( + event, + event.modifiers.alt, + event.modifiers.shift, + this.getHitOptions(false /* preseelectedOnly */))) { + this.selectionBoxMode = true; + this.selectionBoxTool.onMouseDown(event.modifiers.shift); + } + } + handleMouseMove (event) { + const hoveredItem = getHoveredItem(event, this.getHitOptions()); + if ((!hoveredItem && this.prevHoveredItem) || // There is no longer a hovered item + (hoveredItem && !this.prevHoveredItem) || // There is now a hovered item + (hoveredItem && this.prevHoveredItem && + hoveredItem.id !== this.prevHoveredItem.id)) { // hovered item changed + this.setHoveredItem(hoveredItem); + } + } + handleMouseDrag (event) { + if (event.event.button > 0) return; // only first mouse button + + if (this.selectionBoxMode) { + this.selectionBoxTool.onMouseDrag(event); + } else { + this.boundingBoxTool.onMouseDrag(event); + } + } + handleMouseUp (event) { + if (event.event.button > 0) return; // only first mouse button + + if (this.selectionBoxMode) { + this.selectionBoxTool.onMouseUp(event); + this.boundingBoxTool.setSelectionBounds(); + } else { + this.boundingBoxTool.onMouseUp(event); + } + this.selectionBoxMode = false; + } + handleKeyUp (event) { + // Backspace, delete + if (event.key === 'delete' || event.key === 'backspace') { + deleteSelection(Modes.SELECT); + this.onUpdateSvg(); + } + } + deactivateTool () { + this.clearHoveredItem(); + this.boundingBoxTool.removeBoundsPath(); + } +} + +export default SelectTool; From 2c374444b3d2aa428bd1bfc55d5a123a31e99aa7 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 21 Sep 2017 18:20:44 -0400 Subject: [PATCH 2/6] Pull in changes from reshape branch --- src/containers/paper-canvas.jsx | 8 ++ src/containers/select-mode.jsx | 106 ++---------------- src/helper/bounding-box/move-tool.js | 59 ---------- .../bounding-box-tool.js | 19 +++- src/helper/selection-tools/move-tool.js | 101 +++++++++++++++++ .../rotate-tool.js | 8 +- .../scale-tool.js | 8 +- .../selection-tools/selection-box-tool.js | 32 ++++++ src/helper/selection.js | 52 +++++---- 9 files changed, 202 insertions(+), 191 deletions(-) delete mode 100644 src/helper/bounding-box/move-tool.js rename src/helper/{bounding-box => selection-tools}/bounding-box-tool.js (91%) create mode 100644 src/helper/selection-tools/move-tool.js rename src/helper/{bounding-box => selection-tools}/rotate-tool.js (90%) rename src/helper/{bounding-box => selection-tools}/scale-tool.js (97%) create mode 100644 src/helper/selection-tools/selection-box-tool.js diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 50e95b51..7539bb0d 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -33,7 +33,15 @@ class PaperCanvas extends React.Component { onLoad: function (item) { // Remove viewbox if (item.clipped) { + let mask; + for (const child of item.children) { + if (child.isClipMask()) { + mask = child; + break; + } + } item.clipped = false; + mask.remove(); // Consider removing clip mask here? } while (item.reduce() !== item) { diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 572f6885..731ba0e6 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -7,35 +7,17 @@ import Modes from '../modes/modes'; import {changeMode} from '../reducers/modes'; import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; -import {getHoveredItem} from '../helper/hover'; -import {rectSelect} from '../helper/guides'; -import {selectRootItem, processRectangularSelection} from '../helper/selection'; - +import SelectTool from '../helper/selection-tools/select-tool'; import SelectModeComponent from '../components/select-mode.jsx'; -import BoundingBoxTool from '../helper/bounding-box/bounding-box-tool'; import paper from 'paper'; class SelectMode extends React.Component { - static get TOLERANCE () { - return 6; - } constructor (props) { super(props); bindAll(this, [ 'activateTool', - 'deactivateTool', - 'getHitOptions' + 'deactivateTool' ]); - this._hitOptions = { - segments: true, - stroke: true, - curves: true, - fill: true, - guide: false - }; - this.boundingBoxTool = new BoundingBoxTool(); - this.selectionBoxMode = false; - this.selectionRect = null; } componentDidMount () { if (this.props.isSelectModeActive) { @@ -43,6 +25,10 @@ class SelectMode extends React.Component { } } componentWillReceiveProps (nextProps) { + if (this.tool && nextProps.hoveredItem !== this.props.hoveredItem) { + this.tool.setPrevHoveredItem(nextProps.hoveredItem); + } + if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) { this.activateTool(); } else if (!nextProps.isSelectModeActive && this.props.isSelectModeActive) { @@ -52,84 +38,14 @@ class SelectMode extends React.Component { shouldComponentUpdate () { return false; // Static component, for now } - getHitOptions (preselectedOnly) { - this._hitOptions.tolerance = SelectMode.TOLERANCE / paper.view.zoom; - if (preselectedOnly) { - this._hitOptions.selected = true; - } else { - delete this._hitOptions.selected; - } - return this._hitOptions; - } activateTool () { - selectRootItem(); - this.boundingBoxTool.setSelectionBounds(); - this.tool = new paper.Tool(); - - // Define these to sate linter - const selectMode = this; - - this.tool.onMouseDown = function (event) { - if (event.event.button > 0) return; // only first mouse button - - selectMode.props.clearHoveredItem(); - if (!selectMode.boundingBoxTool - .onMouseDown( - event, - event.modifiers.alt, - event.modifiers.shift, - selectMode.getHitOptions(false /* preseelectedOnly */))) { - selectMode.selectionBoxMode = true; - } - }; - - this.tool.onMouseMove = function (event) { - const hoveredItem = getHoveredItem(event, selectMode.getHitOptions()); - const oldHoveredItem = selectMode.props.hoveredItem; - if ((!hoveredItem && oldHoveredItem) || // There is no longer a hovered item - (hoveredItem && !oldHoveredItem) || // There is now a hovered item - (hoveredItem && oldHoveredItem && hoveredItem.id !== oldHoveredItem.id)) { // hovered item changed - selectMode.props.setHoveredItem(hoveredItem); - } - }; - - - this.tool.onMouseDrag = function (event) { - if (event.event.button > 0) return; // only first mouse button - - if (selectMode.selectionBoxMode) { - selectMode.selectionRect = rectSelect(event); - // Remove this rect on the next drag and up event - selectMode.selectionRect.removeOnDrag(); - } else { - selectMode.boundingBoxTool.onMouseDrag(event); - } - }; - - this.tool.onMouseUp = function (event) { - if (event.event.button > 0) return; // only first mouse button - - if (selectMode.selectionBoxMode) { - if (selectMode.selectionRect) { - processRectangularSelection(event, selectMode.selectionRect, Modes.SELECT); - selectMode.selectionRect.remove(); - } - selectMode.boundingBoxTool.setSelectionBounds(); - } else { - selectMode.boundingBoxTool.onMouseUp(event); - selectMode.props.onUpdateSvg(); - } - selectMode.selectionBoxMode = false; - selectMode.selectionRect = null; - }; + this.tool = new SelectTool(this.props.setHoveredItem, this.props.clearHoveredItem, this.props.onUpdateSvg); this.tool.activate(); } deactivateTool () { - this.props.clearHoveredItem(); - this.boundingBoxTool.removeBoundsPath(); + this.tool.deactivateTool(); this.tool.remove(); this.tool = null; - this.hitResult = null; } render () { return ( @@ -141,10 +57,10 @@ class SelectMode extends React.Component { SelectMode.propTypes = { clearHoveredItem: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, - hoveredItem: PropTypes.instanceOf(paper.Item), // eslint-disable-line react/no-unused-prop-types + hoveredItem: PropTypes.instanceOf(paper.Item), isSelectModeActive: PropTypes.bool.isRequired, - onUpdateSvg: PropTypes.func.isRequired, // eslint-disable-line react/no-unused-prop-types - setHoveredItem: PropTypes.func.isRequired // eslint-disable-line react/no-unused-prop-types + onUpdateSvg: PropTypes.func.isRequired, + setHoveredItem: PropTypes.func.isRequired }; const mapStateToProps = state => ({ diff --git a/src/helper/bounding-box/move-tool.js b/src/helper/bounding-box/move-tool.js deleted file mode 100644 index 6303d6cd..00000000 --- a/src/helper/bounding-box/move-tool.js +++ /dev/null @@ -1,59 +0,0 @@ -import {isGroup} from '../group'; -import {isCompoundPathItem, getRootItem} from '../item'; -import {snapDeltaToAngle} from '../math'; -import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; - -class MoveTool { - constructor () { - this.selectedItems = null; - } - - /** - * @param {!paper.HitResult} hitResult Data about the location of the mouse click - * @param {boolean} clone Whether to clone on mouse down (e.g. alt key held) - * @param {boolean} multiselect Whether to multiselect on mouse down (e.g. shift key held) - */ - onMouseDown (hitResult, clone, multiselect) { - const root = getRootItem(hitResult.item); - const item = isCompoundPathItem(root) || isGroup(root) ? root : hitResult.item; - if (!item.selected) { - // deselect all by default if multiselect isn't on - if (!multiselect) { - clearSelection(); - } - setItemSelection(item, true); - } else if (multiselect) { - setItemSelection(item, false); - } - if (clone) cloneSelection(); - this.selectedItems = getSelectedItems(); - } - onMouseDrag (event) { - const dragVector = event.point.subtract(event.downPoint); - for (const item of this.selectedItems) { - // add the position of the item before the drag started - // for later use in the snap calculation - if (!item.data.origPos) { - item.data.origPos = item.position; - } - - if (event.modifiers.shift) { - item.position = item.data.origPos.add(snapDeltaToAngle(dragVector, Math.PI / 4)); - } else { - item.position = item.data.origPos.add(dragVector); - } - } - } - onMouseUp () { - // resetting the items origin point for the next usage - for (const item of this.selectedItems) { - item.data.origPos = null; - } - this.selectedItems = null; - - // @todo add back undo - // pg.undo.snapshot('moveSelection'); - } -} - -export default MoveTool; diff --git a/src/helper/bounding-box/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js similarity index 91% rename from src/helper/bounding-box/bounding-box-tool.js rename to src/helper/selection-tools/bounding-box-tool.js index 32d96918..c77a5dcd 100644 --- a/src/helper/bounding-box/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -28,17 +28,19 @@ const Modes = keyMirror({ * On mouse down, the type of function (move, scale, rotate) is determined based on what is clicked * (scale handle, rotate handle, the object itself). This determines the mode of the tool, which then * delegates actions to the MoveTool, RotateTool or ScaleTool accordingly. + * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ class BoundingBoxTool { - constructor () { + constructor (onUpdateSvg) { + this.onUpdateSvg = onUpdateSvg; this.mode = null; this.boundsPath = null; this.boundsScaleHandles = []; this.boundsRotHandles = []; this._modeMap = {}; - this._modeMap[Modes.SCALE] = new ScaleTool(); - this._modeMap[Modes.ROTATE] = new RotateTool(); - this._modeMap[Modes.MOVE] = new MoveTool(); + this._modeMap[Modes.SCALE] = new ScaleTool(onUpdateSvg); + this._modeMap[Modes.ROTATE] = new RotateTool(onUpdateSvg); + this._modeMap[Modes.MOVE] = new MoveTool(onUpdateSvg); } /** @@ -75,8 +77,13 @@ class BoundingBoxTool { this.mode = Modes.MOVE; } + const hitProperties = { + hitResult: hitResult, + clone: event.modifiers.alt, + multiselect: event.modifiers.shift + }; if (this.mode === Modes.MOVE) { - this._modeMap[this.mode].onMouseDown(hitResult, clone, multiselect); + 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()); @@ -102,7 +109,7 @@ class BoundingBoxTool { setSelectionBounds () { this.removeBoundsPath(); - const items = getSelectedItems(); + const items = getSelectedItems(true /* recursive */); if (items.length <= 0) return; let rect = null; diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js new file mode 100644 index 00000000..e6470a92 --- /dev/null +++ b/src/helper/selection-tools/move-tool.js @@ -0,0 +1,101 @@ +import {isGroup} from '../group'; +import {isCompoundPathItem, getRootItem} from '../item'; +import {snapDeltaToAngle} from '../math'; +import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; + +class MoveTool { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { + this.selectedItems = null; + this.onUpdateSvg = onUpdateSvg; + } + + /** + * @param {!object} hitProperties Describes the mouse event + * @param {!paper.HitResult} hitProperties.hitResult Data about the location of the mouse click + * @param {?boolean} hitProperties.clone Whether to clone on mouse down (e.g. alt key held) + * @param {?boolean} hitProperties.multiselect Whether to multiselect on mouse down (e.g. shift key held) + * @param {?boolean} hitProperties.doubleClicked True if this is the second click in a short amout of time + * @param {?boolean} hitProperties.subselect True if we allow selection of subgroups, false if we should + * select the whole group. + */ + onMouseDown (hitProperties) { + let item = hitProperties.hitResult.item; + if (!hitProperties.subselect) { + const root = getRootItem(hitProperties.hitResult.item); + item = isCompoundPathItem(root) || isGroup(root) ? root : hitProperties.hitResult.item; + } + if (item.selected) { + // Double click causes all points to be selected in subselect mode. + if (hitProperties.doubleClicked) { + if (!hitProperties.multiselect) { + clearSelection(); + } + this._select(item, true /* state */, hitProperties.subselect, true /* fullySelect */); + } else if (hitProperties.multiselect) { + this._select(item, false /* state */, hitProperties.subselect); + } + } else { + // deselect all by default if multiselect isn't on + if (!hitProperties.multiselect) { + clearSelection(); + } + this._select(item, true, hitProperties.subselect); + } + if (hitProperties.clone) cloneSelection(hitProperties.subselect); + this.selectedItems = getSelectedItems(true /* subselect */); + } + /** + * Sets the selection state of an item. + * @param {!paper.Item} item Item to select or deselect + * @param {?boolean} state True if item should be selected, false if deselected + * @param {?boolean} subselect True if a subset of all points in an item are allowed to be + * selected, false if items must be selected all or nothing. + * @param {?boolean} fullySelect True if in addition to the item being selected, all of its + * control points should be selected. False if the item should be selected but not its + * points. Only relevant when subselect is true. + */ + _select (item, state, subselect, fullySelect) { + if (subselect) { + item.selected = false; + if (fullySelect) { + item.fullySelected = state; + } else { + item.selected = state; + } + } else { + setItemSelection(item, state); + } + } + onMouseDrag (event) { + const dragVector = event.point.subtract(event.downPoint); + for (const item of this.selectedItems) { + // add the position of the item before the drag started + // for later use in the snap calculation + if (!item.data.origPos) { + item.data.origPos = item.position; + } + + if (event.modifiers.shift) { + item.position = item.data.origPos.add(snapDeltaToAngle(dragVector, Math.PI / 4)); + } else { + item.position = item.data.origPos.add(dragVector); + } + } + } + onMouseUp () { + // resetting the items origin point for the next usage + for (const item of this.selectedItems) { + item.data.origPos = null; + } + this.selectedItems = null; + + // @todo add back undo + // pg.undo.snapshot('moveSelection'); + this.onUpdateSvg(); + } +} + +export default MoveTool; diff --git a/src/helper/bounding-box/rotate-tool.js b/src/helper/selection-tools/rotate-tool.js similarity index 90% rename from src/helper/bounding-box/rotate-tool.js rename to src/helper/selection-tools/rotate-tool.js index 64bf7b50..4893ede1 100644 --- a/src/helper/bounding-box/rotate-tool.js +++ b/src/helper/selection-tools/rotate-tool.js @@ -1,10 +1,14 @@ import paper from 'paper'; class RotateTool { - constructor () { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { this.rotItems = []; this.rotGroupPivot = null; this.prevRot = []; + this.onUpdateSvg = onUpdateSvg; } /** @@ -57,7 +61,7 @@ class RotateTool { this.prevRot = []; // @todo add back undo - // pg.undo.snapshot('rotateSelection'); + this.onUpdateSvg(); } } diff --git a/src/helper/bounding-box/scale-tool.js b/src/helper/selection-tools/scale-tool.js similarity index 97% rename from src/helper/bounding-box/scale-tool.js rename to src/helper/selection-tools/scale-tool.js index 23cb96eb..085395d8 100644 --- a/src/helper/bounding-box/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -1,7 +1,10 @@ import paper from 'paper'; class ScaleTool { - constructor () { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { this.pivot = null; this.origPivot = null; this.corner = null; @@ -14,6 +17,7 @@ class ScaleTool { this.scaleItems = []; this.boundsScaleHandles = []; this.boundsRotHandles = []; + this.onUpdateSvg = onUpdateSvg; } /** @@ -150,7 +154,7 @@ class ScaleTool { this.itemGroup.remove(); // @todo add back undo - // pg.undo.snapshot('scaleSelection'); + this.onUpdateSvg(); } getRectCornerNameByIndex (index) { switch (index) { diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js new file mode 100644 index 00000000..68f014a9 --- /dev/null +++ b/src/helper/selection-tools/selection-box-tool.js @@ -0,0 +1,32 @@ +import Modes from '../../modes/modes'; +import {rectSelect} from '../guides'; +import {clearSelection, processRectangularSelection} from '../selection'; + +class SelectionBoxTool { + constructor (mode) { + this.selectionRect = null; + this.mode = mode; + } + /** + * @param {boolean} multiselect Whether to multiselect on mouse down (e.g. shift key held) + */ + onMouseDown (multiselect) { + if (!multiselect) { + clearSelection(); + } + } + onMouseDrag (event) { + this.selectionRect = rectSelect(event); + // Remove this rect on the next drag and up event + this.selectionRect.removeOnDrag(); + } + onMouseUp (event) { + if (this.selectionRect) { + processRectangularSelection(event, this.selectionRect, Modes.RESHAPE); + this.selectionRect.remove(); + this.selectionRect = null; + } + } +} + +export default SelectionBoxTool; diff --git a/src/helper/selection.js b/src/helper/selection.js index 6ecba05c..226ab205 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -34,37 +34,40 @@ const selectItemSegments = function (item, state) { } }; -const setGroupSelection = function (root, selected) { - // fully selected segments need to be unselected first - root.fullySelected = false; - // then the item can be normally selected +const setGroupSelection = function (root, selected, fullySelected) { + root.fullySelected = fullySelected; root.selected = selected; // select children of compound-path or group if (isCompoundPath(root) || isGroup(root)) { const children = root.children; if (children) { - for (let i = 0; i < children.length; i++) { - children[i].selected = selected; + for (const child of children) { + if (isGroup(child)) { + setGroupSelection(child, selected, fullySelected); + } else { + child.fullySelected = fullySelected; + child.selected = selected; + } } } } }; -const setItemSelection = function (item, state) { +const setItemSelection = function (item, state, fullySelected) { const parentGroup = getItemsGroup(item); const itemsCompoundPath = getItemsCompoundPath(item); // if selection is in a group, select group not individual items if (parentGroup) { // do it recursive - setItemSelection(parentGroup, state); + setItemSelection(parentGroup, state, fullySelected); } else if (itemsCompoundPath) { - setItemSelection(itemsCompoundPath, state); + setGroupSelection(itemsCompoundPath, state, fullySelected); } else { if (item.data && item.data.noSelect) { return; } - setGroupSelection(item, state); + setGroupSelection(item, state, fullySelected); } // @todo: Update toolbar state on change @@ -355,11 +358,9 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { seg.selected = true; } segmentMode = true; - } else { if (event.modifiers.shift && item.selected) { setItemSelection(item, false); - } else { setItemSelection(item, true); } @@ -371,7 +372,7 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { // second round checks for path intersections const intersections = item.getIntersections(rect); if (intersections.length > 0 && !segmentMode) { - // if in detail select mode, select the curves that intersect + // if in reshape mode, select the curves that intersect // with the selectionRect if (mode === Modes.RESHAPE) { for (let k = 0; k < intersections.length; k++) { @@ -389,7 +390,6 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { curve.selected = true; } } - } else { if (event.modifiers.shift && item.selected) { setItemSelection(item, false); @@ -417,9 +417,8 @@ const rectangularSelectionGroupLoop = function (group, rect, root, event, mode) if (isGroup(child) || isCompoundPathItem(child)) { rectangularSelectionGroupLoop(child, rect, root, event, mode); - - } else if (!handleRectangularSelectionItems(child, event, rect, mode)) { - return false; + } else { + handleRectangularSelectionItems(child, event, rect, mode); } } return true; @@ -428,20 +427,16 @@ const rectangularSelectionGroupLoop = function (group, rect, root, event, mode) const processRectangularSelection = function (event, rect, mode) { const allItems = getAllSelectableItems(); - itemLoop: for (let i = 0; i < allItems.length; i++) { const item = allItems[i]; if (mode === Modes.RESHAPE && isPGTextItem(getRootItem(item))) { - continue itemLoop; + continue; } // check for item segment points inside selectionRect if (isGroup(item) || isCompoundPathItem(item)) { - if (!rectangularSelectionGroupLoop(item, rect, item, event, mode)) { - continue itemLoop; - } - - } else if (!handleRectangularSelectionItems(item, event, rect, mode)) { - continue itemLoop; + rectangularSelectionGroupLoop(item, rect, item, event, mode); + } else { + handleRectangularSelectionItems(item, event, rect, mode); } } }; @@ -454,8 +449,11 @@ const selectRootItem = function () { for (const item of items) { if (isCompoundPathChild(item)) { const cp = getItemsCompoundPath(item); - setItemSelection(item, false); - setItemSelection(cp, true); + setItemSelection(cp, true, true /* fullySelected */); + } + const rootItem = getRootItem(item); + if (item !== rootItem) { + setItemSelection(rootItem, true, true /* fullySelected */); } } }; From 060ff0ab15d73e36d5a58671ae18438ffe123172 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 11:10:17 -0400 Subject: [PATCH 3/6] Fix select tool being able to select sub pieces of groups, fix bounding box showing after delete --- src/helper/helper.js | 5 +++ src/helper/selection-tools/select-tool.js | 1 + .../selection-tools/selection-box-tool.js | 3 +- src/helper/selection.js | 32 +++++++++++++++---- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/helper/helper.js b/src/helper/helper.js index 7a041959..c7428106 100644 --- a/src/helper/helper.js +++ b/src/helper/helper.js @@ -1,5 +1,10 @@ import paper from 'paper'; +/** + * @param {boolean} includeGuides True if guide layer items like the bounding box should + * be included in the returned items. + * @return {Array} all top-level (direct descendants of a paper.Layer) items + */ const getAllPaperItems = function (includeGuides) { includeGuides = includeGuides || false; const allItems = []; diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index 3cb574f5..68a4adf2 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -96,6 +96,7 @@ class SelectTool extends paper.Tool { // Backspace, delete if (event.key === 'delete' || event.key === 'backspace') { deleteSelection(Modes.SELECT); + this.boundingBoxTool.removeBoundsPath(); this.onUpdateSvg(); } } diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js index 68f014a9..f057200f 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -1,4 +1,3 @@ -import Modes from '../../modes/modes'; import {rectSelect} from '../guides'; import {clearSelection, processRectangularSelection} from '../selection'; @@ -22,7 +21,7 @@ class SelectionBoxTool { } onMouseUp (event) { if (this.selectionRect) { - processRectangularSelection(event, this.selectionRect, Modes.RESHAPE); + processRectangularSelection(event, this.selectionRect, this.mode); this.selectionRect.remove(); this.selectionRect = null; } diff --git a/src/helper/selection.js b/src/helper/selection.js index 226ab205..db076952 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -6,6 +6,10 @@ import {getItemsGroup, isGroup} from './group'; import {getRootItem, isBoundsItem, isCompoundPathItem, isPathItem, isPGTextItem} from './item'; import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compound-path'; +/** + * @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 selectables = []; @@ -411,12 +415,12 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { }; // if the rectangular selection found a group, drill into it recursively -const rectangularSelectionGroupLoop = function (group, rect, root, event, mode) { +const _rectangularSelectionGroupLoop = function (group, rect, root, event, mode) { for (let i = 0; i < group.children.length; i++) { const child = group.children[i]; if (isGroup(child) || isCompoundPathItem(child)) { - rectangularSelectionGroupLoop(child, rect, root, event, mode); + _rectangularSelectionGroupLoop(child, rect, root, event, mode); } else { handleRectangularSelectionItems(child, event, rect, mode); } @@ -424,6 +428,14 @@ const rectangularSelectionGroupLoop = function (group, rect, root, event, mode) return true; }; +/** + * Called after drawing a selection rectangle in a select mode. In reshape mode, this + * selects all control points and curves within the rectangle. In select mode, this + * selects all items and groups that intersect the rectangle + * @param {!MouseEvent} event The mouse event to draw the rectangle + * @param {!paper.Rect} rect The selection rectangle + * @param {Modes} mode The mode of the paint editor when drawing the rectangle + */ const processRectangularSelection = function (event, rect, mode) { const allItems = getAllSelectableItems(); @@ -432,19 +444,25 @@ const processRectangularSelection = function (event, rect, mode) { if (mode === Modes.RESHAPE && isPGTextItem(getRootItem(item))) { continue; } - // check for item segment points inside selectionRect if (isGroup(item) || isCompoundPathItem(item)) { - rectangularSelectionGroupLoop(item, rect, item, event, mode); + // Drill into the group in reshape mode; check for item segment points inside + if (mode === Modes.RESHAPE) { + _rectangularSelectionGroupLoop(item, rect, item, event, mode); + } else { + setGroupSelection(item, true, true /* fullySelected */); + } } else { handleRectangularSelectionItems(item, event, rect, mode); } } }; +/** + * When switching to the select tool while having a child object of a + * compound path selected, deselect the child and select the compound path + * instead. (otherwise the compound path breaks because of scale-grouping) + */ const selectRootItem = function () { - // when switching to the select tool while having a child object of a - // compound path selected, deselect the child and select the compound path - // instead. (otherwise the compound path breaks because of scale-grouping) const items = getSelectedItems(); for (const item of items) { if (isCompoundPathChild(item)) { From fd9a4af83f859565ed4c1e37993592c4f9bf2bb8 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 12:12:07 -0400 Subject: [PATCH 4/6] add comments and clean up --- src/containers/paper-canvas.css | 2 + .../{selection-hov.jsx => selection-hoc.jsx} | 4 +- src/helper/guides.js | 28 ++++++++- src/helper/helper.js | 63 ------------------- .../selection-tools/bounding-box-tool.js | 2 +- src/helper/selection-tools/move-tool.js | 3 + src/helper/selection-tools/rotate-tool.js | 3 + src/helper/selection-tools/scale-tool.js | 18 +++--- src/helper/selection-tools/select-tool.js | 51 +++++++++++---- .../selection-tools/selection-box-tool.js | 1 + src/helper/selection.js | 22 ++++++- src/index.js | 2 +- 12 files changed, 109 insertions(+), 90 deletions(-) rename src/containers/{selection-hov.jsx => selection-hoc.jsx} (94%) delete mode 100644 src/helper/helper.js diff --git a/src/containers/paper-canvas.css b/src/containers/paper-canvas.css index 16edbb7a..82e8e028 100644 --- a/src/containers/paper-canvas.css +++ b/src/containers/paper-canvas.css @@ -4,5 +4,7 @@ margin: auto; position: relative; background-color: #fff; + /* Turn off anti-aliasing for the drawing canvas. Each time it's updated it switches + back and forth from aliased to unaliased and that looks bad */ image-rendering: pixelated; } diff --git a/src/containers/selection-hov.jsx b/src/containers/selection-hoc.jsx similarity index 94% rename from src/containers/selection-hov.jsx rename to src/containers/selection-hoc.jsx index 81a4f7df..ca1179ab 100644 --- a/src/containers/selection-hov.jsx +++ b/src/containers/selection-hoc.jsx @@ -3,7 +3,7 @@ import React from 'react'; import {connect} from 'react-redux'; import paper from 'paper'; -const SelectionHOV = function (WrappedComponent) { +const SelectionHOC = function (WrappedComponent) { class SelectionComponent extends React.Component { componentDidMount () { if (this.props.hoveredItem) { @@ -43,4 +43,4 @@ const SelectionHOV = function (WrappedComponent) { )(SelectionComponent); }; -export default SelectionHOV; +export default SelectionHOC; diff --git a/src/helper/guides.js b/src/helper/guides.js index ffd65d76..c6f917b8 100644 --- a/src/helper/guides.js +++ b/src/helper/guides.js @@ -1,6 +1,6 @@ import paper from 'paper'; import {getGuideLayer} from './layer'; -import {removePaperItemsByTags, removePaperItemsByDataTags} from './helper'; +import {getAllPaperItems} from './selection'; const GUIDE_BLUE = '#009dec'; const GUIDE_GREY = '#aaaaaa'; @@ -65,12 +65,34 @@ const getGuideColor = function (colorName) { } }; +const _removePaperItemsByDataTags = function (tags) { + const allItems = getAllPaperItems(true); + for (const item of allItems) { + for (const tag of tags) { + if (item.data && item.data[tag]) { + item.remove(); + } + } + } +}; + +const _removePaperItemsByTags = function (tags) { + const allItems = getAllPaperItems(true); + for (const item of allItems) { + for (const tag of tags) { + if (item[tag]) { + item.remove(); + } + } + } +}; + const removeHelperItems = function () { - removePaperItemsByDataTags(['isHelperItem']); + _removePaperItemsByDataTags(['isHelperItem']); }; const removeAllGuides = function () { - removePaperItemsByTags(['guide']); + _removePaperItemsByTags(['guide']); }; export { diff --git a/src/helper/helper.js b/src/helper/helper.js deleted file mode 100644 index c7428106..00000000 --- a/src/helper/helper.js +++ /dev/null @@ -1,63 +0,0 @@ -import paper from 'paper'; - -/** - * @param {boolean} includeGuides True if guide layer items like the bounding box should - * be included in the returned items. - * @return {Array} all top-level (direct descendants of a paper.Layer) items - */ -const getAllPaperItems = function (includeGuides) { - includeGuides = includeGuides || false; - const allItems = []; - for (const layer of paper.project.layers) { - for (const child of layer.children) { - // don't give guides back - if (!includeGuides && child.guide) { - continue; - } - allItems.push(child); - } - } - return allItems; -}; - -const getPaperItemsByTags = function (tags) { - const allItems = getAllPaperItems(true); - const foundItems = []; - for (const item of allItems) { - for (const tag of tags) { - if (item[tag] && foundItems.indexOf(item) === -1) { - foundItems.push(item); - } - } - } - return foundItems; -}; - -const removePaperItemsByDataTags = function (tags) { - const allItems = getAllPaperItems(true); - for (const item of allItems) { - for (const tag of tags) { - if (item.data && item.data[tag]) { - item.remove(); - } - } - } -}; - -const removePaperItemsByTags = function (tags) { - const allItems = getAllPaperItems(true); - for (const item of allItems) { - for (const tag of tags) { - if (item[tag]) { - item.remove(); - } - } - } -}; - -export { - getAllPaperItems, - getPaperItemsByTags, - removePaperItemsByDataTags, - removePaperItemsByTags -}; diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index c77a5dcd..dca535fd 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -24,7 +24,7 @@ const Modes = keyMirror({ }); /** - * A paper.Tool that handles transforming the selection and drawing a bounding box with handles. + * Tool that handles transforming the selection and drawing a bounding box with handles. * On mouse down, the type of function (move, scale, rotate) is determined based on what is clicked * (scale handle, rotate handle, the object itself). This determines the mode of the tool, which then * delegates actions to the MoveTool, RotateTool or ScaleTool accordingly. diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js index e6470a92..8853e631 100644 --- a/src/helper/selection-tools/move-tool.js +++ b/src/helper/selection-tools/move-tool.js @@ -3,6 +3,9 @@ import {isCompoundPathItem, getRootItem} from '../item'; import {snapDeltaToAngle} from '../math'; import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; +/** + * Tool to handle dragging an item to reposition it in a selection mode. + */ class MoveTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes diff --git a/src/helper/selection-tools/rotate-tool.js b/src/helper/selection-tools/rotate-tool.js index 4893ede1..2006cebf 100644 --- a/src/helper/selection-tools/rotate-tool.js +++ b/src/helper/selection-tools/rotate-tool.js @@ -1,5 +1,8 @@ import paper from 'paper'; +/** + * Tool to handle rotation when dragging the rotation handle in the bounding box tool. + */ class RotateTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes diff --git a/src/helper/selection-tools/scale-tool.js b/src/helper/selection-tools/scale-tool.js index 085395d8..8744ab72 100644 --- a/src/helper/selection-tools/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -1,5 +1,9 @@ import paper from 'paper'; +/** + * Tool to handle scaling items by pulling on the handles around the edges of the bounding + * box when in the bounding box tool. + */ class ScaleTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes @@ -34,9 +38,9 @@ class ScaleTool { this.boundsPath = boundsPath; this.boundsScaleHandles = boundsScaleHandles; this.boundsRotHandles = boundsRotHandles; - this.pivot = this.boundsPath.bounds[this.getOpposingRectCornerNameByIndex(index)].clone(); - this.origPivot = this.boundsPath.bounds[this.getOpposingRectCornerNameByIndex(index)].clone(); - this.corner = this.boundsPath.bounds[this.getRectCornerNameByIndex(index)].clone(); + this.pivot = this.boundsPath.bounds[this._getOpposingRectCornerNameByIndex(index)].clone(); + this.origPivot = this.boundsPath.bounds[this._getOpposingRectCornerNameByIndex(index)].clone(); + this.corner = this.boundsPath.bounds[this._getRectCornerNameByIndex(index)].clone(); this.origSize = this.corner.subtract(this.pivot); this.origCenter = this.boundsPath.bounds.center; for (const item of selectedItems) { @@ -105,14 +109,14 @@ class ScaleTool { for (let i = 0; i < this.boundsScaleHandles.length; i++) { const handle = this.boundsScaleHandles[i]; - handle.position = this.itemGroup.bounds[this.getRectCornerNameByIndex(i)]; + handle.position = this.itemGroup.bounds[this._getRectCornerNameByIndex(i)]; handle.bringToFront(); } for (let i = 0; i < this.boundsRotHandles.length; i++) { const handle = this.boundsRotHandles[i]; if (handle) { - handle.position = this.itemGroup.bounds[this.getRectCornerNameByIndex(i)] + handle.data.offset; + handle.position = this.itemGroup.bounds[this._getRectCornerNameByIndex(i)] + handle.data.offset; handle.bringToFront(); } } @@ -156,7 +160,7 @@ class ScaleTool { // @todo add back undo this.onUpdateSvg(); } - getRectCornerNameByIndex (index) { + _getRectCornerNameByIndex (index) { switch (index) { case 0: return 'bottomLeft'; @@ -176,7 +180,7 @@ class ScaleTool { return 'bottomCenter'; } } - getOpposingRectCornerNameByIndex (index) { + _getOpposingRectCornerNameByIndex (index) { switch (index) { case 0: return 'topRight'; diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index 68a4adf2..e2f561ed 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -6,10 +6,23 @@ import BoundingBoxTool from './bounding-box-tool'; import SelectionBoxTool from './selection-box-tool'; import paper from 'paper'; +/** + * paper.Tool that handles select mode. This is made up of 2 subtools. + * - The selection box tool is active when the user clicks an empty space and drags. + * It selects all items in the rectangle. + * - The bounding box tool is active if the user clicks on a non-empty space. It handles + * reshaping the item that was clicked. + */ class SelectTool extends paper.Tool { + /** The distance within which mouse events count as a hit against an item */ static get TOLERANCE () { return 6; } + /** + * @param {function} setHoveredItem Callback to set the hovered item + * @param {function} clearHoveredItem Callback to clear the hovered item + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ constructor (setHoveredItem, clearHoveredItem, onUpdateSvg) { super(); this.setHoveredItem = setHoveredItem; @@ -18,13 +31,6 @@ class SelectTool extends paper.Tool { this.boundingBoxTool = new BoundingBoxTool(onUpdateSvg); this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT); this.selectionBoxMode = false; - this._hitOptions = { - segments: true, - stroke: true, - curves: true, - fill: true, - guide: false - }; // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. @@ -37,21 +43,42 @@ class SelectTool extends paper.Tool { selectRootItem(); this.boundingBoxTool.setSelectionBounds(); } + /** + * To be called when the hovered item changes. When the select tool hovers over a + * new item, it compares against this to see if a hover item change event needs to + * be fired. + * @param {paper.Item} prevHoveredItem The highlight that indicates the mouse is over + * a given item currently + */ setPrevHoveredItem (prevHoveredItem) { this.prevHoveredItem = prevHoveredItem; } + /** + * Returns the hit options to use when conducting hit tests. + * @param {boolean} preselectedOnly True if we should only return results that are already + * selected. + * @return {object} See paper.Item.hitTest for definition of options + */ getHitOptions (preselectedOnly) { - this._hitOptions.tolerance = SelectTool.TOLERANCE / paper.view.zoom; + // Tolerance needs to be scaled when the view is zoomed in in order to represent the same + // distance for the user to move the mouse. + const hitOptions = { + segments: true, + stroke: true, + curves: true, + fill: true, + guide: false, + tolerance: SelectTool.TOLERANCE / paper.view.zoom + }; if (preselectedOnly) { - this._hitOptions.selected = true; - } else { - delete this._hitOptions.selected; + hitOptions.selected = true; } - return this._hitOptions; + return hitOptions; } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button + // If bounding box tool does not find an item that was hit, use selection box tool. this.clearHoveredItem(); if (!this.boundingBoxTool .onMouseDown( diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js index f057200f..bc787c51 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -1,6 +1,7 @@ import {rectSelect} from '../guides'; import {clearSelection, processRectangularSelection} from '../selection'; +/** Tool to handle drag selection. A dotted line box appears and everything enclosed is selected. */ class SelectionBoxTool { constructor (mode) { this.selectionRect = null; diff --git a/src/helper/selection.js b/src/helper/selection.js index db076952..db57ae92 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -1,11 +1,30 @@ import paper from 'paper'; import Modes from '../modes/modes'; -import {getAllPaperItems} from './helper'; import {getItemsGroup, isGroup} from './group'; import {getRootItem, isBoundsItem, isCompoundPathItem, isPathItem, isPGTextItem} from './item'; import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compound-path'; +/** + * @param {boolean} includeGuides True if guide layer items like the bounding box should + * be included in the returned items. + * @return {Array} all top-level (direct descendants of a paper.Layer) items + */ +const getAllPaperItems = function (includeGuides) { + includeGuides = includeGuides || false; + const allItems = []; + for (const layer of paper.project.layers) { + for (const child of layer.children) { + // don't give guides back + if (!includeGuides && child.guide) { + continue; + } + allItems.push(child); + } + } + return allItems; +}; + /** * @return {Array} all top-level (direct descendants of a paper.Layer) items * that aren't guide items or helper items. @@ -489,6 +508,7 @@ const shouldShowSelectAll = function () { }; export { + getAllPaperItems, selectAllItems, selectAllSegments, clearSelection, diff --git a/src/index.js b/src/index.js index 2c655e14..bbcbba68 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import PaintEditor from './containers/paint-editor.jsx'; -import SelectionHOV from './containers/selection-hov.jsx'; +import SelectionHOV from './containers/selection-hoc.jsx'; import ScratchPaintReducer from './reducers/scratch-paint-reducer'; const Wrapped = SelectionHOV(PaintEditor); From 05ad64fd015880785e2da17ccc9513b446ca42a0 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 12:22:09 -0400 Subject: [PATCH 5/6] fix being able to select subgroups again --- src/helper/selection.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/helper/selection.js b/src/helper/selection.js index db57ae92..4abd8b31 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -366,7 +366,7 @@ const checkBoundsItem = function (selectionRect, item, event) { itemBounds.remove(); }; -const handleRectangularSelectionItems = function (item, event, rect, mode) { +const _handleRectangularSelectionItems = function (item, event, rect, mode, root) { if (isPathItem(item)) { let segmentMode = false; @@ -383,9 +383,9 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { segmentMode = true; } else { if (event.modifiers.shift && item.selected) { - setItemSelection(item, false); + setItemSelection(root, false); } else { - setItemSelection(item, true); + setItemSelection(root, true, true /* fullySelected */); } return false; } @@ -441,7 +441,7 @@ const _rectangularSelectionGroupLoop = function (group, rect, root, event, mode) if (isGroup(child) || isCompoundPathItem(child)) { _rectangularSelectionGroupLoop(child, rect, root, event, mode); } else { - handleRectangularSelectionItems(child, event, rect, mode); + _handleRectangularSelectionItems(child, event, rect, mode, root); } } return true; @@ -464,14 +464,10 @@ const processRectangularSelection = function (event, rect, mode) { continue; } if (isGroup(item) || isCompoundPathItem(item)) { - // Drill into the group in reshape mode; check for item segment points inside - if (mode === Modes.RESHAPE) { - _rectangularSelectionGroupLoop(item, rect, item, event, mode); - } else { - setGroupSelection(item, true, true /* fullySelected */); - } + // check for item segment points inside + _rectangularSelectionGroupLoop(item, rect, item, event, mode); } else { - handleRectangularSelectionItems(item, event, rect, mode); + _handleRectangularSelectionItems(item, event, rect, mode, item); } } }; From e5303784a00ac4441f15c272b265376b9e18d893 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 13:48:18 -0400 Subject: [PATCH 6/6] Switch state to track hover item ID instead of item itself --- package.json | 1 - src/containers/select-mode.jsx | 13 ++++---- src/containers/selection-hoc.jsx | 36 ++++++++++++++++------- src/helper/selection-tools/select-tool.js | 18 ++++++------ src/reducers/hover.js | 20 ++++++++----- src/reducers/scratch-paint-reducer.js | 2 +- test/unit/hover-reducer.test.js | 13 +++----- 7 files changed, 58 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 7fabae78..3e56fbb8 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "babel-plugin-transform-object-rest-spread": "^6.22.0", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.22.0", - "canvas-prebuilt": "^1.6.5-prerelease.1", "classnames": "2.2.5", "css-loader": "0.28.3", "enzyme": "^2.8.2", diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 731ba0e6..3992d3f9 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -9,7 +9,6 @@ import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; import SelectTool from '../helper/selection-tools/select-tool'; import SelectModeComponent from '../components/select-mode.jsx'; -import paper from 'paper'; class SelectMode extends React.Component { constructor (props) { @@ -25,8 +24,8 @@ class SelectMode extends React.Component { } } componentWillReceiveProps (nextProps) { - if (this.tool && nextProps.hoveredItem !== this.props.hoveredItem) { - this.tool.setPrevHoveredItem(nextProps.hoveredItem); + if (this.tool && nextProps.hoveredItemId !== this.props.hoveredItemId) { + this.tool.setPrevHoveredItemId(nextProps.hoveredItemId); } if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) { @@ -57,7 +56,7 @@ class SelectMode extends React.Component { SelectMode.propTypes = { clearHoveredItem: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, - hoveredItem: PropTypes.instanceOf(paper.Item), + hoveredItemId: PropTypes.number, isSelectModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, setHoveredItem: PropTypes.func.isRequired @@ -65,11 +64,11 @@ SelectMode.propTypes = { const mapStateToProps = state => ({ isSelectModeActive: state.scratchPaint.mode === Modes.SELECT, - hoveredItem: state.scratchPaint.hoveredItem + hoveredItemId: state.scratchPaint.hoveredItemId }); const mapDispatchToProps = dispatch => ({ - setHoveredItem: hoveredItem => { - dispatch(setHoveredItem(hoveredItem)); + setHoveredItem: hoveredItemId => { + dispatch(setHoveredItem(hoveredItemId)); }, clearHoveredItem: () => { dispatch(clearHoveredItem()); diff --git a/src/containers/selection-hoc.jsx b/src/containers/selection-hoc.jsx index ca1179ab..e468a179 100644 --- a/src/containers/selection-hoc.jsx +++ b/src/containers/selection-hoc.jsx @@ -1,29 +1,43 @@ 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 { + constructor (props) { + super(props); + bindAll(this, [ + 'removeItemById' + ]); + } componentDidMount () { - if (this.props.hoveredItem) { + if (this.props.hoveredItemId) { paper.view.update(); } } componentDidUpdate (prevProps) { - if (this.props.hoveredItem && this.props.hoveredItem !== prevProps.hoveredItem) { - // A hover item has been added. Update the view - if (prevProps.hoveredItem) { - prevProps.hoveredItem.remove(); + // Hovered item has changed + if ((this.props.hoveredItemId && this.props.hoveredItemId !== prevProps.hoveredItemId) || + (!this.props.hoveredItemId && prevProps.hoveredItemId)) { + // Remove the old hover item if any + this.removeItemById(prevProps.hoveredItemId); + } + } + removeItemById (itemId) { + if (itemId) { + const match = paper.project.getItem({ + match: item => (item.id === itemId) + }); + if (match) { + match.remove(); } - } else if (!this.props.hoveredItem && prevProps.hoveredItem) { - // Remove the hover item - prevProps.hoveredItem.remove(); } } render () { const { - hoveredItem, // eslint-disable-line no-unused-vars + hoveredItemId, // eslint-disable-line no-unused-vars ...props } = this.props; return ( @@ -32,11 +46,11 @@ const SelectionHOC = function (WrappedComponent) { } } SelectionComponent.propTypes = { - hoveredItem: PropTypes.instanceOf(paper.Item) + hoveredItemId: PropTypes.number }; const mapStateToProps = state => ({ - hoveredItem: state.scratchPaint.hoveredItem + hoveredItemId: state.scratchPaint.hoveredItemId }); return connect( mapStateToProps diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index e2f561ed..22320a23 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -47,11 +47,11 @@ class SelectTool extends paper.Tool { * To be called when the hovered item changes. When the select tool hovers over a * new item, it compares against this to see if a hover item change event needs to * be fired. - * @param {paper.Item} prevHoveredItem The highlight that indicates the mouse is over - * a given item currently + * @param {paper.Item} prevHoveredItemId ID of the highlight item that indicates the mouse is + * over a given item currently */ - setPrevHoveredItem (prevHoveredItem) { - this.prevHoveredItem = prevHoveredItem; + setPrevHoveredItemId (prevHoveredItemId) { + this.prevHoveredItemId = prevHoveredItemId; } /** * Returns the hit options to use when conducting hit tests. @@ -92,11 +92,11 @@ class SelectTool extends paper.Tool { } handleMouseMove (event) { const hoveredItem = getHoveredItem(event, this.getHitOptions()); - if ((!hoveredItem && this.prevHoveredItem) || // There is no longer a hovered item - (hoveredItem && !this.prevHoveredItem) || // There is now a hovered item - (hoveredItem && this.prevHoveredItem && - hoveredItem.id !== this.prevHoveredItem.id)) { // hovered item changed - this.setHoveredItem(hoveredItem); + if ((!hoveredItem && this.prevHoveredItemId) || // There is no longer a hovered item + (hoveredItem && !this.prevHoveredItemId) || // There is now a hovered item + (hoveredItem && this.prevHoveredItemId && + hoveredItem.id !== this.prevHoveredItemId)) { // hovered item changed + this.setHoveredItem(hoveredItem ? hoveredItem.id : null); } } handleMouseDrag (event) { diff --git a/src/reducers/hover.js b/src/reducers/hover.js index 2a3e31ee..fe5d5cab 100644 --- a/src/reducers/hover.js +++ b/src/reducers/hover.js @@ -1,4 +1,3 @@ -import paper from 'paper'; import log from '../log/log'; const CHANGE_HOVERED = 'scratch-paint/hover/CHANGE_HOVERED'; @@ -8,29 +7,36 @@ const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { case CHANGE_HOVERED: - if (typeof action.hoveredItem === 'undefined' || - (action.hoveredItem !== null && !(action.hoveredItem instanceof paper.Item))) { + if (typeof action.hoveredItemId === 'undefined') { log.warn(`Hovered item should not be set to undefined. Use null.`); return state; + } else if (typeof action.hoveredItemId === 'undefined' || isNaN(action.hoveredItemId)) { + log.warn(`Hovered item should be an item ID number. Got: ${action.hoveredItemId}`); + return state; } - return action.hoveredItem; + return action.hoveredItemId; default: return state; } }; // Action creators ================================== -const setHoveredItem = function (hoveredItem) { +/** + * Set the hovered item state to the given item ID + * @param {number} hoveredItemId The paper.Item ID of the hover indicator item. + * @return {object} Redux action to change the hovered item. + */ +const setHoveredItem = function (hoveredItemId) { return { type: CHANGE_HOVERED, - hoveredItem: hoveredItem + hoveredItemId: hoveredItemId }; }; const clearHoveredItem = function () { return { type: CHANGE_HOVERED, - hoveredItem: null + hoveredItemId: null }; }; diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 3dfabe6b..0d44903b 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -10,5 +10,5 @@ export default combineReducers({ brushMode: brushModeReducer, eraserMode: eraserModeReducer, color: colorReducer, - hoveredItem: hoverReducer + hoveredItemId: hoverReducer }); diff --git a/test/unit/hover-reducer.test.js b/test/unit/hover-reducer.test.js index b8c63cd0..58f469b8 100644 --- a/test/unit/hover-reducer.test.js +++ b/test/unit/hover-reducer.test.js @@ -1,12 +1,7 @@ /* eslint-env jest */ -import paper from 'paper'; import reducer from '../../src/reducers/hover'; import {clearHoveredItem, setHoveredItem} from '../../src/reducers/hover'; -beforeEach(() => { - paper.setup(); -}); - test('initialState', () => { let defaultState; expect(reducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeNull(); @@ -14,22 +9,22 @@ test('initialState', () => { test('setHoveredItem', () => { let defaultState; - const item1 = new paper.Path(); - const item2 = new paper.Path(); + const item1 = 1; + const item2 = 2; expect(reducer(defaultState /* state */, setHoveredItem(item1) /* action */)).toBe(item1); expect(reducer(item1 /* state */, setHoveredItem(item2) /* action */)).toBe(item2); }); test('clearHoveredItem', () => { let defaultState; - const item = new paper.Path(); + const item = 1; expect(reducer(defaultState /* state */, clearHoveredItem() /* action */)).toBeNull(); expect(reducer(item /* state */, clearHoveredItem() /* action */)).toBeNull(); }); test('invalidSetHoveredItem', () => { let defaultState; - const item = new paper.Path(); + const item = 1; const nonItem = {random: 'object'}; let undef; expect(reducer(defaultState /* state */, setHoveredItem(nonItem) /* action */)).toBeNull();