From b8de3dcc3a8ceda4159f81b90c4c52287c92e3c7 Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 18 Sep 2017 11:28:34 -0400 Subject: [PATCH 01/14] Add reshape button --- src/components/paint-editor.jsx | 4 + src/components/reshape-mode.jsx | 19 ++ src/containers/paper-canvas.jsx | 2 + src/containers/reshape-mode.jsx | 203 +++++++++++++++++++++ test/unit/components/reshape-mode.test.jsx | 15 ++ 5 files changed, 243 insertions(+) create mode 100644 src/components/reshape-mode.jsx create mode 100644 src/containers/reshape-mode.jsx create mode 100644 test/unit/components/reshape-mode.test.jsx diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index b565eff8..da5fcd3f 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -3,6 +3,7 @@ import React from 'react'; import PaperCanvas from '../containers/paper-canvas.jsx'; import BrushMode from '../containers/brush-mode.jsx'; import EraserMode from '../containers/eraser-mode.jsx'; +import ReshapeMode from '../containers/reshape-mode.jsx'; import SelectMode from '../containers/select-mode.jsx'; import PropTypes from 'prop-types'; import LineMode from '../containers/line-mode.jsx'; @@ -130,6 +131,9 @@ class PaintEditorComponent extends React.Component { + ) : null} diff --git a/src/components/reshape-mode.jsx b/src/components/reshape-mode.jsx new file mode 100644 index 00000000..a2b06f40 --- /dev/null +++ b/src/components/reshape-mode.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +const ReshapeModeComponent = props => ( + +); + +ReshapeModeComponent.propTypes = { + onMouseDown: PropTypes.func.isRequired +}; + +export default ReshapeModeComponent; diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 50e95b51..ab6dc3e2 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -15,6 +15,8 @@ class PaperCanvas extends React.Component { } componentDidMount () { paper.setup(this.canvas); + // Don't show handles by default + paper.settings.handleSize = 0; if (this.props.svg) { this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY); } diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx new file mode 100644 index 00000000..1f729841 --- /dev/null +++ b/src/containers/reshape-mode.jsx @@ -0,0 +1,203 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; +import bindAll from 'lodash.bindall'; +import Modes from '../modes/modes'; + +import {changeMode} from '../reducers/modes'; +import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; + +import {getHoveredItem} from '../helper/hover'; +import {rectSelect} from '../helper/guides'; +import {processRectangularSelection} from '../helper/selection'; + +import ReshapeModeComponent from '../components/reshape-mode.jsx'; +import BoundingBoxTool from '../helper/bounding-box/bounding-box-tool'; +import paper from 'paper'; + +class ReshapeMode extends React.Component { + static get TOLERANCE () { + return 8; + } + constructor (props) { + super(props); + bindAll(this, [ + 'activateTool', + 'deactivateTool', + 'getHitOptions' + ]); + + this._hitOptionsSelected = { + match: function (item) { + if (!item.item || !item.item.selected) return; + if (item.type === 'handle-out' || item.type === 'handle-in') { + // Only hit test against handles that are visible, that is, + // their segment is selected + if (!item.segment.selected) { + return false; + } + // If the entire shape is selected, handles are hidden + if (item.item.fullySelected) { + return false; + } + } + return true; + }, + segments: true, + stroke: true, + curves: true, + handles: true, + fill: true, + guide: false + }; + this._hitOptions = { + match: function (item) { + if (item.type === 'handle-out' || item.type === 'handle-in') { + // Only hit test against handles that are visible, that is, + // their segment is selected + if (!item.segment.selected) { + return false; + } + // If the entire shape is selected, handles are hidden + if (item.item.fullySelected) { + return false; + } + } + return true; + }, + segments: true, + stroke: true, + curves: true, + handles: true, + fill: true, + guide: false + }; + this.boundingBoxTool = new BoundingBoxTool(); + this.selectionBoxMode = false; + this.selectionRect = null; + } + componentDidMount () { + if (this.props.isReshapeModeActive) { + this.activateTool(this.props); + } + } + componentWillReceiveProps (nextProps) { + if (nextProps.isReshapeModeActive && !this.props.isReshapeModeActive) { + this.activateTool(); + } else if (!nextProps.isReshapeModeActive && this.props.isReshapeModeActive) { + this.deactivateTool(); + } + } + shouldComponentUpdate () { + return false; // Static component, for now + } + getHitOptions (preselectedOnly) { + this._hitOptions.tolerance = ReshapeMode.TOLERANCE / paper.view.zoom; + this._hitOptionsSelected.tolerance = ReshapeMode.TOLERANCE / paper.view.zoom; + return preselectedOnly ? this._hitOptionsSelected : this._hitOptions; + } + activateTool () { + paper.settings.handleSize = 8; + this.boundingBoxTool.setSelectionBounds(); + this.tool = new paper.Tool(); + + const reshapeMode = this; + + this.tool.onMouseDown = function (event) { + if (event.event.button > 0) return; // only first mouse button + + reshapeMode.props.clearHoveredItem(); + if (!reshapeMode.boundingBoxTool + .onMouseDown( + event, + event.modifiers.alt, + event.modifiers.shift, + reshapeMode.getHitOptions(false /* preseelectedOnly */))) { + reshapeMode.selectionBoxMode = true; + } + }; + + this.tool.onMouseMove = function (event) { + const hoveredItem = getHoveredItem(event, reshapeMode.getHitOptions()); + const oldHoveredItem = reshapeMode.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 + reshapeMode.props.setHoveredItem(hoveredItem); + } + }; + + + this.tool.onMouseDrag = function (event) { + if (event.event.button > 0) return; // only first mouse button + + if (reshapeMode.selectionBoxMode) { + reshapeMode.selectionRect = rectSelect(event); + // Remove this rect on the next drag and up event + reshapeMode.selectionRect.removeOnDrag(); + } else { + reshapeMode.boundingBoxTool.onMouseDrag(event); + } + }; + + this.tool.onMouseUp = function (event) { + if (event.event.button > 0) return; // only first mouse button + + if (reshapeMode.selectionBoxMode) { + if (reshapeMode.selectionRect) { + processRectangularSelection(event, reshapeMode.selectionRect, Modes.RESHAPE); + reshapeMode.selectionRect.remove(); + } + reshapeMode.boundingBoxTool.setSelectionBounds(); + } else { + reshapeMode.boundingBoxTool.onMouseUp(event); + reshapeMode.props.onUpdateSvg(); + } + reshapeMode.selectionBoxMode = false; + reshapeMode.selectionRect = null; + }; + this.tool.activate(); + } + deactivateTool () { + paper.settings.handleSize = 0; + this.props.clearHoveredItem(); + this.tool.remove(); + this.tool = null; + this.hitResult = null; + } + render () { + return ( + + ); + } +} + +ReshapeMode.propTypes = { + clearHoveredItem: PropTypes.func.isRequired, + handleMouseDown: PropTypes.func.isRequired, + hoveredItem: PropTypes.instanceOf(paper.Item), // eslint-disable-line react/no-unused-prop-types + isReshapeModeActive: 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 +}; + +const mapStateToProps = state => ({ + isReshapeModeActive: state.scratchPaint.mode === Modes.RESHAPE, + hoveredItem: state.scratchPaint.hoveredItem +}); +const mapDispatchToProps = dispatch => ({ + setHoveredItem: hoveredItem => { + dispatch(setHoveredItem(hoveredItem)); + }, + clearHoveredItem: () => { + dispatch(clearHoveredItem()); + }, + handleMouseDown: () => { + dispatch(changeMode(Modes.RESHAPE)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ReshapeMode); diff --git a/test/unit/components/reshape-mode.test.jsx b/test/unit/components/reshape-mode.test.jsx new file mode 100644 index 00000000..a6c71a3a --- /dev/null +++ b/test/unit/components/reshape-mode.test.jsx @@ -0,0 +1,15 @@ +/* eslint-env jest */ +import React from 'react'; // eslint-disable-line no-unused-vars +import {shallow} from 'enzyme'; +import ReshapeModeComponent from '../../../src/components/reshape-mode.jsx'; // eslint-disable-line no-unused-vars + +describe('ReshapeModeComponent', () => { + test('triggers callback when clicked', () => { + const onClick = jest.fn(); + const componentShallowWrapper = shallow( + + ); + componentShallowWrapper.simulate('click'); + expect(onClick).toHaveBeenCalled(); + }); +}); From f15a3dbe02131b53ea70b54b96937e922e065b8c Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 21 Sep 2017 10:36:26 -0400 Subject: [PATCH 02/14] Add reshape --- src/containers/reshape-mode.jsx | 135 ++---------- src/containers/select-mode.jsx | 18 +- src/helper/bounding-box/move-tool.js | 59 ------ src/helper/hover.js | 6 +- .../bounding-box-tool.js | 2 + src/helper/selection-tools/handle-tool.js | 64 ++++++ src/helper/selection-tools/move-tool.js | 96 +++++++++ src/helper/selection-tools/point-tool.js | 193 +++++++++++++++++ src/helper/selection-tools/reshape-tool.js | 200 ++++++++++++++++++ .../rotate-tool.js | 0 .../scale-tool.js | 2 +- .../selection-tools/selection-box-tool.js | 32 +++ src/helper/selection.js | 99 +++++---- 13 files changed, 671 insertions(+), 235 deletions(-) delete mode 100644 src/helper/bounding-box/move-tool.js rename src/helper/{bounding-box => selection-tools}/bounding-box-tool.js (99%) create mode 100644 src/helper/selection-tools/handle-tool.js create mode 100644 src/helper/selection-tools/move-tool.js create mode 100644 src/helper/selection-tools/point-tool.js create mode 100644 src/helper/selection-tools/reshape-tool.js rename src/helper/{bounding-box => selection-tools}/rotate-tool.js (100%) rename src/helper/{bounding-box => selection-tools}/scale-tool.js (99%) create mode 100644 src/helper/selection-tools/selection-box-tool.js diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx index 1f729841..41e18cf6 100644 --- a/src/containers/reshape-mode.jsx +++ b/src/containers/reshape-mode.jsx @@ -7,74 +7,19 @@ 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 {processRectangularSelection} from '../helper/selection'; - +import {selectSubItems} from '../helper/selection'; +import ReshapeTool from '../helper/selection-tools/reshape-tool'; import ReshapeModeComponent from '../components/reshape-mode.jsx'; -import BoundingBoxTool from '../helper/bounding-box/bounding-box-tool'; import paper from 'paper'; + class ReshapeMode extends React.Component { - static get TOLERANCE () { - return 8; - } constructor (props) { super(props); bindAll(this, [ 'activateTool', - 'deactivateTool', - 'getHitOptions' + 'deactivateTool' ]); - - this._hitOptionsSelected = { - match: function (item) { - if (!item.item || !item.item.selected) return; - if (item.type === 'handle-out' || item.type === 'handle-in') { - // Only hit test against handles that are visible, that is, - // their segment is selected - if (!item.segment.selected) { - return false; - } - // If the entire shape is selected, handles are hidden - if (item.item.fullySelected) { - return false; - } - } - return true; - }, - segments: true, - stroke: true, - curves: true, - handles: true, - fill: true, - guide: false - }; - this._hitOptions = { - match: function (item) { - if (item.type === 'handle-out' || item.type === 'handle-in') { - // Only hit test against handles that are visible, that is, - // their segment is selected - if (!item.segment.selected) { - return false; - } - // If the entire shape is selected, handles are hidden - if (item.item.fullySelected) { - return false; - } - } - return true; - }, - segments: true, - stroke: true, - curves: true, - handles: true, - fill: true, - guide: false - }; - this.boundingBoxTool = new BoundingBoxTool(); - this.selectionBoxMode = false; - this.selectionRect = null; } componentDidMount () { if (this.props.isReshapeModeActive) { @@ -82,6 +27,10 @@ class ReshapeMode extends React.Component { } } componentWillReceiveProps (nextProps) { + if (this.tool && nextProps.hoveredItem !== this.props.hoveredItem) { + this.tool.setPrevHoveredItem(nextProps.hoveredItem); + } + if (nextProps.isReshapeModeActive && !this.props.isReshapeModeActive) { this.activateTool(); } else if (!nextProps.isReshapeModeActive && this.props.isReshapeModeActive) { @@ -91,72 +40,12 @@ class ReshapeMode extends React.Component { shouldComponentUpdate () { return false; // Static component, for now } - getHitOptions (preselectedOnly) { - this._hitOptions.tolerance = ReshapeMode.TOLERANCE / paper.view.zoom; - this._hitOptionsSelected.tolerance = ReshapeMode.TOLERANCE / paper.view.zoom; - return preselectedOnly ? this._hitOptionsSelected : this._hitOptions; - } activateTool () { - paper.settings.handleSize = 8; - this.boundingBoxTool.setSelectionBounds(); - this.tool = new paper.Tool(); - - const reshapeMode = this; - - this.tool.onMouseDown = function (event) { - if (event.event.button > 0) return; // only first mouse button - - reshapeMode.props.clearHoveredItem(); - if (!reshapeMode.boundingBoxTool - .onMouseDown( - event, - event.modifiers.alt, - event.modifiers.shift, - reshapeMode.getHitOptions(false /* preseelectedOnly */))) { - reshapeMode.selectionBoxMode = true; - } - }; - - this.tool.onMouseMove = function (event) { - const hoveredItem = getHoveredItem(event, reshapeMode.getHitOptions()); - const oldHoveredItem = reshapeMode.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 - reshapeMode.props.setHoveredItem(hoveredItem); - } - }; - - - this.tool.onMouseDrag = function (event) { - if (event.event.button > 0) return; // only first mouse button - - if (reshapeMode.selectionBoxMode) { - reshapeMode.selectionRect = rectSelect(event); - // Remove this rect on the next drag and up event - reshapeMode.selectionRect.removeOnDrag(); - } else { - reshapeMode.boundingBoxTool.onMouseDrag(event); - } - }; - - this.tool.onMouseUp = function (event) { - if (event.event.button > 0) return; // only first mouse button - - if (reshapeMode.selectionBoxMode) { - if (reshapeMode.selectionRect) { - processRectangularSelection(event, reshapeMode.selectionRect, Modes.RESHAPE); - reshapeMode.selectionRect.remove(); - } - reshapeMode.boundingBoxTool.setSelectionBounds(); - } else { - reshapeMode.boundingBoxTool.onMouseUp(event); - reshapeMode.props.onUpdateSvg(); - } - reshapeMode.selectionBoxMode = false; - reshapeMode.selectionRect = null; - }; + selectSubItems(); + this.tool = new ReshapeTool(this.props.setHoveredItem, this.props.clearHoveredItem); + this.tool.setPrevHoveredItem(this.props.hoveredItem); this.tool.activate(); + paper.settings.handleSize = 8; } deactivateTool () { paper.settings.handleSize = 0; diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 572f6885..1b3eaa4a 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -12,7 +12,8 @@ import {rectSelect} from '../helper/guides'; import {selectRootItem, processRectangularSelection} from '../helper/selection'; import SelectModeComponent from '../components/select-mode.jsx'; -import BoundingBoxTool from '../helper/bounding-box/bounding-box-tool'; +import BoundingBoxTool from '../helper/selection-tools/bounding-box-tool'; +import SelectionBoxTool from '../helper/selection-tools/selection-box-tool'; import paper from 'paper'; class SelectMode extends React.Component { @@ -34,8 +35,8 @@ class SelectMode extends React.Component { guide: false }; this.boundingBoxTool = new BoundingBoxTool(); + this.selectionBoxTool = new SelectionBoxTool(); this.selectionBoxMode = false; - this.selectionRect = null; } componentDidMount () { if (this.props.isSelectModeActive) { @@ -62,6 +63,7 @@ class SelectMode extends React.Component { return this._hitOptions; } activateTool () { + debugger; selectRootItem(); this.boundingBoxTool.setSelectionBounds(); this.tool = new paper.Tool(); @@ -69,17 +71,18 @@ class SelectMode extends React.Component { // Define these to sate linter const selectMode = this; - this.tool.onMouseDown = function (event) { + this.tool.onMouseDown = event => { if (event.event.button > 0) return; // only first mouse button - selectMode.props.clearHoveredItem(); - if (!selectMode.boundingBoxTool + this.props.clearHoveredItem(); + if (!this.boundingBoxTool .onMouseDown( event, event.modifiers.alt, event.modifiers.shift, - selectMode.getHitOptions(false /* preseelectedOnly */))) { - selectMode.selectionBoxMode = true; + this.getHitOptions(false /* preseelectedOnly */))) { + this.selectionBoxMode = true; + this.selectionBoxTool.onMouseDown(event.modifiers.shift); } }; @@ -125,6 +128,7 @@ class SelectMode extends React.Component { this.tool.activate(); } deactivateTool () { + debugger; this.props.clearHoveredItem(); this.boundingBoxTool.removeBoundsPath(); this.tool.remove(); 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/hover.js b/src/helper/hover.js index b4c0bc1f..eee1b3b5 100644 --- a/src/helper/hover.js +++ b/src/helper/hover.js @@ -6,9 +6,11 @@ import {isGroupChild} from './group'; /** * @param {!MouseEvent} event mouse event * @param {?object} hitOptions hit options to use + * @param {?boolean} subselect Whether items within groups can be hovered. If false, the + * entire group should be hovered. * @return {paper.Item} the hovered item or null if there is none */ -const getHoveredItem = function (event, hitOptions) { +const getHoveredItem = function (event, hitOptions, subselect) { const hitResults = paper.project.hitTestAll(event.point, hitOptions); if (hitResults.length === 0) { return null; @@ -27,7 +29,7 @@ const getHoveredItem = function (event, hitOptions) { if (isBoundsItem(hitResult.item)) { return hoverBounds(hitResult.item); - } else if (isGroupChild(hitResult.item)) { + } else if (!subselect && isGroupChild(hitResult.item)) { return hoverBounds(getRootItem(hitResult.item)); } return hoverItem(hitResult); diff --git a/src/helper/bounding-box/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js similarity index 99% rename from src/helper/bounding-box/bounding-box-tool.js rename to src/helper/selection-tools/bounding-box-tool.js index 32d96918..22cb8ade 100644 --- a/src/helper/bounding-box/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -100,6 +100,7 @@ class BoundingBoxTool { this.setSelectionBounds(); } setSelectionBounds () { + debugger; this.removeBoundsPath(); const items = getSelectedItems(); @@ -180,6 +181,7 @@ class BoundingBoxTool { } } removeBoundsPath () { + debugger; removeHelperItems(); this.boundsPath = null; this.boundsScaleHandles.length = 0; diff --git a/src/helper/selection-tools/handle-tool.js b/src/helper/selection-tools/handle-tool.js new file mode 100644 index 00000000..b05c2e21 --- /dev/null +++ b/src/helper/selection-tools/handle-tool.js @@ -0,0 +1,64 @@ +import {clearSelection, getSelectedItems} from '../selection'; + +class HandleTool { + constructor () { + this.hitType = null; + } + /** + * @param {!object} hitProperties Describes the mouse event + * @param {?boolean} hitProperties.multiselect Whether to multiselect on mouse down (e.g. shift key held) + * select the whole group. + */ + onMouseDown (hitProperties) { + if (!hitProperties.multiselect) { + clearSelection(); + } + + hitProperties.hitResult.segment.handleIn.selected = true; + hitProperties.hitResult.segment.handleOut.selected = true; + this.hitType = hitProperties.hitResult.type; + } + onMouseDrag (event) { + const selectedItems = getSelectedItems(true /* recursive */); + + for (const item of selectedItems) { + for (const seg of item.segments) { + // add the point of the segment before the drag started + // for later use in the snap calculation + if (!seg.origPoint) { + seg.origPoint = seg.point.clone(); + } + + if (seg.handleOut.selected && this.hitType === 'handle-out'){ + // if option is pressed or handles have been split, + // they're no longer parallel and move independently + if (event.modifiers.option || + !seg.handleOut.isColinear(seg.handleIn)) { + seg.handleOut = seg.handleOut.add(event.delta); + } else { + const oldLength = seg.handleOut.length; + seg.handleOut = seg.handleOut.add(event.delta); + seg.handleIn = seg.handleOut.multiply(-seg.handleIn.length / oldLength); + } + } else if (seg.handleIn.selected && this.hitType === 'handle-in') { + // if option is pressed or handles have been split, + // they're no longer parallel and move independently + if (event.modifiers.option || + !seg.handleOut.isColinear(seg.handleIn)) { + seg.handleIn = seg.handleIn.add(event.delta); + + } else { + const oldLength = seg.handleIn.length; + seg.handleIn = seg.handleIn.add(event.delta); + seg.handleOut = seg.handleIn.multiply(-seg.handleOut.length / oldLength); + } + } + } + } + } + onMouseUp () { + // @todo add back undo + } +} + +export default HandleTool; diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js new file mode 100644 index 00000000..99fbb661 --- /dev/null +++ b/src/helper/selection-tools/move-tool.js @@ -0,0 +1,96 @@ +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 {!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(hitProperties.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'); + } +} + +export default MoveTool; diff --git a/src/helper/selection-tools/point-tool.js b/src/helper/selection-tools/point-tool.js new file mode 100644 index 00000000..2072dbb3 --- /dev/null +++ b/src/helper/selection-tools/point-tool.js @@ -0,0 +1,193 @@ +import paper from 'paper'; +import {snapDeltaToAngle} from '../math'; +import {clearSelection, getSelectedItems} from '../selection'; + +/** Subtool of ReshapeTool for moving control points. */ +class PointTool { + constructor () { + /** + * 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. + */ + this.deselectOnMouseUp = null; + /** + * Delete control point does not happen until mouse up. If the mouse is dragged before + * mouse up, delete is cancelled. This variable keeps track of the hitResult that triggers delete. + */ + this.deleteOnMouseUp = null; + /** + * There are 2 cases for deselection: Deselect this, or deselect everything but this. + * When invert deselect is true, deselect everything but the item in deselectOnMouseUp. + */ + this.invertDeselect = false; + this.selectedItems = null; + } + + /** + * @param {!object} hitProperties Describes the mouse event + * @param {!paper.HitResult} hitProperties.hitResult Data about the location of the mouse click + * @param {?boolean} hitProperties.multiselect Whether to multiselect on mouse down (e.g. shift key held) + * @param {?boolean} hitProperties.doubleClicked Whether this is the second click in a short time + */ + onMouseDown (hitProperties) { + // Remove point + if (hitProperties.doubleClicked) { + this.deleteOnMouseUp = hitProperties.hitResult; + } + if (hitProperties.hitResult.segment.selected) { + // selected points with no handles get handles if selected again + if (hitProperties.multiselect) { + this.deselectOnMouseUp = hitProperties.hitResult.segment; + } else { + this.deselectOnMouseUp = hitProperties.hitResult.segment; + this.invertDeselect = true; + hitProperties.hitResult.segment.selected = true; + } + } else { + if (!hitProperties.multiselect) { + clearSelection(); + } + hitProperties.hitResult.segment.selected = true; + } + + this.selectedItems = getSelectedItems(true /* recursive */); + } + /** + * @param {!object} hitProperties Describes the mouse event + * @param {!paper.HitResult} hitProperties.hitResult Data about the location of the mouse click + * @param {?boolean} hitProperties.multiselect Whether to multiselect on mouse down (e.g. shift key held) + */ + addPoint (hitProperties) { + // Length of curve from previous point to new point + const beforeCurveLength = hitProperties.hitResult.location.curveOffset; + const afterCurveLength = + hitProperties.hitResult.location.curve.length - hitProperties.hitResult.location.curveOffset; + + // Handle length based on curve length until next point + let handleIn = hitProperties.hitResult.location.tangent.multiply(-beforeCurveLength / 2); + let handleOut = hitProperties.hitResult.location.tangent.multiply(afterCurveLength / 2); + // Don't let one handle overwhelm the other (results in path doubling back on itself weirdly) + if (handleIn.length > 3 * handleOut.length) { + handleIn = handleIn.multiply(3 * handleOut.length / handleIn.length); + } + if (handleOut.length > 3 * handleIn.length) { + handleOut = handleOut.multiply(3 * handleIn.length / handleOut.length); + } + + const beforeSegment = hitProperties.hitResult.item.segments[hitProperties.hitResult.location.index]; + const afterSegment = hitProperties.hitResult.item.segments[hitProperties.hitResult.location.index + 1]; + + // Add segment + const newSegment = new paper.Segment(hitProperties.hitResult.location.point, handleIn, handleOut); + hitProperties.hitResult.item.insert(hitProperties.hitResult.location.index + 1, newSegment); + hitProperties.hitResult.segment = newSegment; + if (!hitProperties.multiselect) { + clearSelection(); + } + newSegment.selected = true; + + // Adjust handles of curve before and curve after to account for new curve length + if (beforeSegment && beforeSegment.handleOut) { + if (afterSegment) { + beforeSegment.handleOut = + beforeSegment.handleOut.multiply(beforeCurveLength / 2 / beforeSegment.handleOut.length); + } else { + beforeSegment.handleOut = null; + } + } + if (afterSegment && afterSegment.handleIn) { + if (beforeSegment) { + afterSegment.handleIn = + afterSegment.handleIn.multiply(afterCurveLength / 2 / afterSegment.handleIn.length); + } else { + afterSegment.handleIn = null; + } + } + } + removePoint (hitResult) { + const index = hitResult.segment.index; + hitResult.item.removeSegment(index); + + // Adjust handles of curve before and curve after to account for new curve length + const beforeSegment = hitResult.item.segments[index - 1]; + const afterSegment = hitResult.item.segments[index]; + const curveLength = beforeSegment ? beforeSegment.curve ? beforeSegment.curve.length : null : null; + if (beforeSegment && beforeSegment.handleOut) { + if (afterSegment) { + beforeSegment.handleOut = + beforeSegment.handleOut.multiply(curveLength / 2 / beforeSegment.handleOut.length); + } else { + beforeSegment.handleOut = null; + } + } + if (afterSegment && afterSegment.handleIn) { + if (beforeSegment) { + afterSegment.handleIn = afterSegment.handleIn.multiply(curveLength / 2 / afterSegment.handleIn.length); + } else { + afterSegment.handleIn = null; + } + } + } + onMouseDrag (event) { + // A click will deselect, but a drag will not + this.deselectOnMouseUp = null; + this.invertDeselect = false; + this.deleteOnMouseUp = null; + + const dragVector = event.point.subtract(event.downPoint); + + for (const item of this.selectedItems) { + if (!item.segments) { + return; + } + for (const seg of item.segments) { + // add the point of the segment before the drag started + // for later use in the snap calculation + if (!seg.origPoint) { + seg.origPoint = seg.point.clone(); + } + if (seg.selected) { + if (event.modifiers.shift) { + seg.point = seg.origPoint.add(snapDeltaToAngle(dragVector, Math.PI / 4)); + } else { + seg.point = seg.point.add(event.delta); + } + } + } + } + } + onMouseUp () { + // resetting the items and segments origin points for the next usage + for (const item of this.selectedItems) { + if (!item.segments) { + return; + } + for (const seg of item.segments) { + seg.origPoint = null; + } + } + + // If no drag occurred between mouse down and mouse up, then we can go through with deselect + // and delete + if (this.deselectOnMouseUp) { + if (this.invertDeselect) { + clearSelection(); + this.deselectOnMouseUp.selected = true; + } else { + this.deselectOnMouseUp.selected = false; + } + this.deselectOnMouseUp = null; + this.invertDeselect = false; + } + if (this.deleteOnMouseUp) { + this.removePoint(this.deleteOnMouseUp); + this.deleteOnMouseUp = null; + } + this.selectedItems = null; + // @todo add back undo + } +} + +export default PointTool; + +// - bounding box when switching between select and reshape diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js new file mode 100644 index 00000000..e5ab3cdb --- /dev/null +++ b/src/helper/selection-tools/reshape-tool.js @@ -0,0 +1,200 @@ +import paper from 'paper'; +import log from '../../log/log'; +import keyMirror from 'keymirror'; + +import Modes from '../../modes/modes'; +import {getHoveredItem} from '../hover'; +import {deleteSelection} from '../selection'; +import {getRootItem, isPGTextItem} from '../item'; +import MoveTool from './move-tool'; +import PointTool from './point-tool'; +import HandleTool from './handle-tool'; +import SelectionBoxTool from './selection-box-tool'; + +/** Modes of the reshape tool, which can do many things depending on how it's used. */ +const ReshapeModes = keyMirror({ + FILL: null, + POINT: null, + HANDLE: null, + SELECTION_BOX: null +}); + +class ReshapeTool extends paper.Tool { + static get TOLERANCE () { + return 8; + } + static get DOUBLE_CLICK_MILLIS () { + return 250; + } + constructor (setHoveredItem, clearHoveredItem) { + super(); + this.setHoveredItem = setHoveredItem; + this.clearHoveredItem = clearHoveredItem; + this.prevHoveredItem = null; + this._hitOptionsSelected = { + match: function (item) { + if (!item.item || !item.item.selected) return; + if (item.type === 'handle-out' || item.type === 'handle-in') { + // Only hit test against handles that are visible, that is, + // their segment is selected + if (!item.segment.selected) { + return false; + } + } + return true; + }, + segments: true, + stroke: true, + curves: true, + handles: true, + fill: true, + guide: false + }; + this._hitOptions = { + match: function (item) { + if (item.type === 'handle-out' || item.type === 'handle-in') { + // Only hit test against handles that are visible, that is, + // their segment is selected + if (!item.segment.selected) { + return false; + } + } + return true; + }, + segments: true, + stroke: true, + curves: true, + handles: true, + fill: true, + guide: false + }; + this.lastEvent = null; + this.mode = ReshapeModes.SELECTION_BOX; + this.selectionRect = null; + this._modeMap = {}; + this._modeMap[ReshapeModes.FILL] = new MoveTool(); + this._modeMap[ReshapeModes.POINT] = new PointTool(); + this._modeMap[ReshapeModes.HANDLE] = new HandleTool(); + this._modeMap[ReshapeModes.SELECTION_BOX] = new SelectionBoxTool(); + + // 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; + } + getHitOptions (preselectedOnly) { + this._hitOptions.tolerance = ReshapeTool.TOLERANCE / paper.view.zoom; + this._hitOptionsSelected.tolerance = ReshapeTool.TOLERANCE / paper.view.zoom; + return preselectedOnly ? this._hitOptionsSelected : this._hitOptions; + } + setPrevHoveredItem (prevHoveredItem) { + this.prevHoveredItem = prevHoveredItem; + } + handleMouseDown (event) { + if (event.event.button > 0) return; // only first mouse button + this.clearHoveredItem(); + + // Check if double clicked + let doubleClicked = false; + if (this.lastEvent) { + if ((event.event.timeStamp - this.lastEvent.event.timeStamp) < ReshapeTool.DOUBLE_CLICK_MILLIS) { + doubleClicked = true; + } else { + doubleClicked = false; + } + } + this.lastEvent = event; + + // Choose hit result =========================================================== + // Prefer hits on already selected items + let hitResults = + paper.project.hitTestAll(event.point, this.getHitOptions(true /* preselectedOnly */)); + if (hitResults.length === 0) { + hitResults = paper.project.hitTestAll(event.point, this.getHitOptions()); + } + if (hitResults.length === 0) { + this._modeMap[ReshapeModes.SELECTION_BOX].onMouseDown(event.modifiers.shift); + return; + } + + // Prefer hits on segments to other types of hits, to make sure handles are movable. + let hitResult = hitResults[0]; + for (let i = 0; i < hitResults.length; i++) { + if (hitResults[i].type === 'segment') { + hitResult = hitResults[i]; + break; + } + } + + // Don't allow detail-selection of PGTextItem + if (isPGTextItem(getRootItem(hitResult.item))) { + return; + } + + const hitProperties = { + hitResult: hitResult, + clone: event.modifiers.alt, + multiselect: event.modifiers.shift, + doubleClicked: doubleClicked, + subselect: true + }; + + // If item is not yet selected, don't behave differently depending on if they clicked a segment + // or stroke (since those were invisible), just select the whole thing as if they clicked the fill. + if (!hitResult.item.selected || + hitResult.type === 'fill' || + (hitResult.type !== 'segment' && doubleClicked)) { + this.mode = ReshapeModes.FILL; + this._modeMap[this.mode].onMouseDown(hitProperties); + } else if (hitResult.type === 'segment') { + this.mode = ReshapeModes.POINT; + this._modeMap[this.mode].onMouseDown(hitProperties); + } else if ( + hitResult.type === 'stroke' || + hitResult.type === 'curve') { + this.mode = ReshapeModes.POINT; + this._modeMap[this.mode].addPoint(hitProperties); + this._modeMap[this.mode].onMouseDown(hitProperties); + } else if ( + hitResult.type === 'handle-in' || + hitResult.type === 'handle-out') { + this.mode = ReshapeModes.HANDLE; + this._modeMap[this.mode].onMouseDown(hitProperties); + } else { + log.warn(`Unhandled hit result type: ${hitResult.type}`); + this.mode = ReshapeModes.FILL; + this._modeMap[this.mode].onMouseDown(hitProperties); + } + + // @todo Trigger selection changed. Update styles based on selection. + } + handleMouseMove (event) { + const hoveredItem = getHoveredItem(event, this.getHitOptions(), true /* subselect */); + 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 + this._modeMap[this.mode].onMouseDrag(event); + } + handleMouseUp (event) { + if (event.event.button > 0) return; // only first mouse button + this._modeMap[this.mode].onMouseUp(event); + this.mode = ReshapeModes.SELECTION_BOX; + } + handleKeyUp (event) { + // Backspace, delete + if (event.key === 'delete' || event.key === 'backspace') { + deleteSelection(Modes.RESHAPE); + } + } +} + +export default ReshapeTool; diff --git a/src/helper/bounding-box/rotate-tool.js b/src/helper/selection-tools/rotate-tool.js similarity index 100% rename from src/helper/bounding-box/rotate-tool.js rename to src/helper/selection-tools/rotate-tool.js diff --git a/src/helper/bounding-box/scale-tool.js b/src/helper/selection-tools/scale-tool.js similarity index 99% rename from src/helper/bounding-box/scale-tool.js rename to src/helper/selection-tools/scale-tool.js index 23cb96eb..0b7c5b6f 100644 --- a/src/helper/bounding-box/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -113,7 +113,7 @@ class ScaleTool { } } } - onMouseUp () { + onMouseUp (event) { this.pivot = null; this.origPivot = null; this.corner = null; 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..ab71165e --- /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'; +import {getHoveredItem} from '../hover'; + +class SelectionBoxTool { + constructor () { + this.selectionRect = null; + } + /** + * @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..cd4e5bbe 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -3,7 +3,7 @@ import Modes from '../modes/modes'; import {getAllPaperItems} from './helper'; import {getItemsGroup, isGroup} from './group'; -import {getRootItem, isBoundsItem, isCompoundPathItem, isPathItem, isPGTextItem} from './item'; +import {getRootItem, isGroupItem, isCompoundPathItem, isPathItem, isPGTextItem} from './item'; import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compound-path'; const getAllSelectableItems = function () { @@ -54,12 +54,12 @@ const setItemSelection = function (item, state) { const parentGroup = getItemsGroup(item); const itemsCompoundPath = getItemsCompoundPath(item); - // if selection is in a group, select group not individual items + // if selection is in a group, select group if (parentGroup) { // do it recursive setItemSelection(parentGroup, state); } else if (itemsCompoundPath) { - setItemSelection(itemsCompoundPath, state); + setGroupSelection(itemsCompoundPath, state); } else { if (item.data && item.data.noSelect) { return; @@ -122,8 +122,8 @@ const getSelectedItems = function (recursive) { return itemsAndGroups; }; -const deleteItemSelection = function () { - const items = getSelectedItems(); +const deleteItemSelection = function (recursive) { + const items = getSelectedItems(recursive); for (let i = 0; i < items.length; i++) { items[i].remove(); } @@ -134,11 +134,11 @@ const deleteItemSelection = function () { // pg.undo.snapshot('deleteItemSelection'); }; -const removeSelectedSegments = function () { +const removeSelectedSegments = function (recursive) { // @todo add back undo // pg.undo.snapshot('removeSelectedSegments'); - const items = getSelectedItems(); + const items = getSelectedItems(recursive); const segmentsToRemove = []; for (let i = 0; i < items.length; i++) { @@ -163,8 +163,8 @@ const removeSelectedSegments = function () { const deleteSelection = function (mode) { if (mode === Modes.RESHAPE) { // If there are points selected remove them. If not delete the item selected. - if (!removeSelectedSegments()) { - deleteItemSelection(); + if (!removeSelectedSegments(true /* recursive */)) { + deleteItemSelection(true /* recursive */); } } else { deleteItemSelection(); @@ -286,8 +286,8 @@ const deleteSegmentSelection = function () { // pg.undo.snapshot('deleteSegmentSelection'); }; -const cloneSelection = function () { - const selectedItems = getSelectedItems(); +const cloneSelection = function (recursive) { + const selectedItems = getSelectedItems(recursive); for (let i = 0; i < selectedItems.length; i++) { const item = selectedItems[i]; item.clone(); @@ -311,34 +311,34 @@ const getSelectedPaths = function () { return paths; }; -const checkBoundsItem = function (selectionRect, item, event) { - const itemBounds = new paper.Path([ - item.localToGlobal(item.internalBounds.topLeft), - item.localToGlobal(item.internalBounds.topRight), - item.localToGlobal(item.internalBounds.bottomRight), - item.localToGlobal(item.internalBounds.bottomLeft) - ]); - itemBounds.closed = true; - itemBounds.guide = true; +// const checkBoundsItem = function (selectionRect, item, event) { +// const itemBounds = new paper.Path([ +// item.localToGlobal(item.internalBounds.topLeft), +// item.localToGlobal(item.internalBounds.topRight), +// item.localToGlobal(item.internalBounds.bottomRight), +// item.localToGlobal(item.internalBounds.bottomLeft) +// ]); +// itemBounds.closed = true; +// itemBounds.guide = true; - for (let i = 0; i < itemBounds.segments.length; i++) { - const seg = itemBounds.segments[i]; - if (selectionRect.contains(seg.point) || - (i === 0 && selectionRect.getIntersections(itemBounds).length > 0)) { - if (event.modifiers.shift && item.selected) { - setItemSelection(item, false); +// for (let i = 0; i < itemBounds.segments.length; i++) { +// const seg = itemBounds.segments[i]; +// if (selectionRect.contains(seg.point) || +// (i === 0 && selectionRect.getIntersections(itemBounds).length > 0)) { +// if (event.modifiers.shift && item.selected) { +// setItemSelection(item, false); - } else { - setItemSelection(item, true); - } - itemBounds.remove(); - return true; +// } else { +// setItemSelection(item, true); +// } +// itemBounds.remove(); +// return true; - } - } +// } +// } - itemBounds.remove(); -}; +// itemBounds.remove(); +// }; const handleRectangularSelectionItems = function (item, event, rect, mode) { if (isPathItem(item)) { @@ -402,10 +402,10 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { } // @todo: Update toolbar state on change - } else if (isBoundsItem(item)) { - if (checkBoundsItem(rect, item, event)) { - return false; - } + // } else if (isBoundsItem(item)) { + // if (checkBoundsItem(rect, item, event)) { + // return false; + // } } return true; }; @@ -448,18 +448,30 @@ const processRectangularSelection = function (event, rect, mode) { 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(); + // compound path or group selected, select the whole compound path or + // group instead. (otherwise the compound path breaks because of + // scale-grouping) + const items = getSelectedItems(true /* recursive */); for (const item of items) { if (isCompoundPathChild(item)) { const cp = getItemsCompoundPath(item); - setItemSelection(item, false); setItemSelection(cp, true); } + const rootItem = getRootItem(item); + if (item !== rootItem) { + setItemSelection(item, false); + setItemSelection(rootItem, true); + } } }; +const selectSubItems = function () { + // when switching to the reshape tool while having a compound path or group + // selected, deselect the group and select the children instead. + // TODO +}; + + const shouldShowIfSelection = function () { return getSelectedItems().length > 0; }; @@ -488,6 +500,7 @@ export { removeSelectedSegments, processRectangularSelection, selectRootItem, + selectSubItems, shouldShowIfSelection, shouldShowIfSelectionRecursive, shouldShowSelectAll From 4bc4d924150f53139e4fdfa60924774900258115 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 21 Sep 2017 18:20:44 -0400 Subject: [PATCH 03/14] Move select tool out of select mode --- src/containers/paper-canvas.jsx | 8 ++ src/containers/reshape-mode.jsx | 14 +-- src/containers/select-mode.jsx | 110 ++--------------- .../selection-tools/bounding-box-tool.js | 21 ++-- src/helper/selection-tools/handle-tool.js | 7 +- src/helper/selection-tools/move-tool.js | 9 +- src/helper/selection-tools/point-tool.js | 9 +- src/helper/selection-tools/reshape-tool.js | 18 ++- src/helper/selection-tools/rotate-tool.js | 8 +- src/helper/selection-tools/scale-tool.js | 10 +- .../selection-tools/selection-box-tool.js | 4 +- src/helper/selection.js | 116 ++++++++---------- 12 files changed, 135 insertions(+), 199 deletions(-) diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index ab6dc3e2..ae4eda24 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -35,7 +35,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/reshape-mode.jsx b/src/containers/reshape-mode.jsx index 41e18cf6..9900f571 100644 --- a/src/containers/reshape-mode.jsx +++ b/src/containers/reshape-mode.jsx @@ -7,7 +7,6 @@ import Modes from '../modes/modes'; import {changeMode} from '../reducers/modes'; import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; -import {selectSubItems} from '../helper/selection'; import ReshapeTool from '../helper/selection-tools/reshape-tool'; import ReshapeModeComponent from '../components/reshape-mode.jsx'; import paper from 'paper'; @@ -41,15 +40,12 @@ class ReshapeMode extends React.Component { return false; // Static component, for now } activateTool () { - selectSubItems(); - this.tool = new ReshapeTool(this.props.setHoveredItem, this.props.clearHoveredItem); + this.tool = new ReshapeTool(this.props.setHoveredItem, this.props.clearHoveredItem, this.props.onUpdateSvg); this.tool.setPrevHoveredItem(this.props.hoveredItem); this.tool.activate(); - paper.settings.handleSize = 8; } deactivateTool () { - paper.settings.handleSize = 0; - this.props.clearHoveredItem(); + this.tool.deactivateTool(); this.tool.remove(); this.tool = null; this.hitResult = null; @@ -64,10 +60,10 @@ class ReshapeMode extends React.Component { ReshapeMode.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), isReshapeModeActive: 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/containers/select-mode.jsx b/src/containers/select-mode.jsx index 1b3eaa4a..731ba0e6 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -7,36 +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/selection-tools/bounding-box-tool'; -import SelectionBoxTool from '../helper/selection-tools/selection-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.selectionBoxTool = new SelectionBoxTool(); - this.selectionBoxMode = false; } componentDidMount () { if (this.props.isSelectModeActive) { @@ -44,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) { @@ -53,87 +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 () { - debugger; - selectRootItem(); - this.boundingBoxTool.setSelectionBounds(); - this.tool = new paper.Tool(); - - // Define these to sate linter - const selectMode = this; - - this.tool.onMouseDown = event => { - if (event.event.button > 0) return; // only first mouse button - - this.props.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); - } - }; - - 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 () { - debugger; - this.props.clearHoveredItem(); - this.boundingBoxTool.removeBoundsPath(); + this.tool.deactivateTool(); this.tool.remove(); this.tool = null; - this.hitResult = null; } render () { return ( @@ -145,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/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index 22cb8ade..c77a5dcd 100644 --- a/src/helper/selection-tools/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()); @@ -100,10 +107,9 @@ class BoundingBoxTool { this.setSelectionBounds(); } setSelectionBounds () { - debugger; this.removeBoundsPath(); - const items = getSelectedItems(); + const items = getSelectedItems(true /* recursive */); if (items.length <= 0) return; let rect = null; @@ -181,7 +187,6 @@ class BoundingBoxTool { } } removeBoundsPath () { - debugger; removeHelperItems(); this.boundsPath = null; this.boundsScaleHandles.length = 0; diff --git a/src/helper/selection-tools/handle-tool.js b/src/helper/selection-tools/handle-tool.js index b05c2e21..49757b83 100644 --- a/src/helper/selection-tools/handle-tool.js +++ b/src/helper/selection-tools/handle-tool.js @@ -1,8 +1,12 @@ import {clearSelection, getSelectedItems} from '../selection'; class HandleTool { - constructor () { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { this.hitType = null; + this.onUpdateSvg = onUpdateSvg; } /** * @param {!object} hitProperties Describes the mouse event @@ -58,6 +62,7 @@ class HandleTool { } onMouseUp () { // @todo add back undo + this.onUpdateSvg(); } } diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js index 99fbb661..e6470a92 100644 --- a/src/helper/selection-tools/move-tool.js +++ b/src/helper/selection-tools/move-tool.js @@ -4,8 +4,12 @@ import {snapDeltaToAngle} from '../math'; import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; class MoveTool { - constructor () { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { this.selectedItems = null; + this.onUpdateSvg = onUpdateSvg; } /** @@ -41,7 +45,7 @@ class MoveTool { this._select(item, true, hitProperties.subselect); } if (hitProperties.clone) cloneSelection(hitProperties.subselect); - this.selectedItems = getSelectedItems(hitProperties.subselect); + this.selectedItems = getSelectedItems(true /* subselect */); } /** * Sets the selection state of an item. @@ -90,6 +94,7 @@ class MoveTool { // @todo add back undo // pg.undo.snapshot('moveSelection'); + this.onUpdateSvg(); } } diff --git a/src/helper/selection-tools/point-tool.js b/src/helper/selection-tools/point-tool.js index 2072dbb3..12aa53ae 100644 --- a/src/helper/selection-tools/point-tool.js +++ b/src/helper/selection-tools/point-tool.js @@ -4,7 +4,10 @@ import {clearSelection, getSelectedItems} from '../selection'; /** Subtool of ReshapeTool for moving control points. */ class PointTool { - constructor () { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (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. @@ -21,6 +24,7 @@ class PointTool { */ this.invertDeselect = false; this.selectedItems = null; + this.onUpdateSvg = onUpdateSvg; } /** @@ -185,9 +189,8 @@ class PointTool { } this.selectedItems = null; // @todo add back undo + this.onUpdateSvg(); } } export default PointTool; - -// - bounding box when switching between select and reshape diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js index e5ab3cdb..77b86300 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -26,10 +26,11 @@ class ReshapeTool extends paper.Tool { static get DOUBLE_CLICK_MILLIS () { return 250; } - constructor (setHoveredItem, clearHoveredItem) { + constructor (setHoveredItem, clearHoveredItem, onUpdateSvg) { super(); this.setHoveredItem = setHoveredItem; this.clearHoveredItem = clearHoveredItem; + this.onUpdateSvg = onUpdateSvg; this.prevHoveredItem = null; this._hitOptionsSelected = { match: function (item) { @@ -72,10 +73,10 @@ class ReshapeTool extends paper.Tool { this.mode = ReshapeModes.SELECTION_BOX; this.selectionRect = null; this._modeMap = {}; - this._modeMap[ReshapeModes.FILL] = new MoveTool(); - this._modeMap[ReshapeModes.POINT] = new PointTool(); - this._modeMap[ReshapeModes.HANDLE] = new HandleTool(); - this._modeMap[ReshapeModes.SELECTION_BOX] = new SelectionBoxTool(); + 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); // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. @@ -84,6 +85,8 @@ class ReshapeTool extends paper.Tool { this.onMouseDrag = this.handleMouseDrag; this.onMouseUp = this.handleMouseUp; this.onKeyUp = this.handleKeyUp; + + paper.settings.handleSize = 8; } getHitOptions (preselectedOnly) { this._hitOptions.tolerance = ReshapeTool.TOLERANCE / paper.view.zoom; @@ -193,8 +196,13 @@ class ReshapeTool extends paper.Tool { // Backspace, delete if (event.key === 'delete' || event.key === 'backspace') { deleteSelection(Modes.RESHAPE); + this.onUpdateSvg(); } } + deactivateTool() { + paper.settings.handleSize = 0; + this.clearHoveredItem(); + } } export default ReshapeTool; diff --git a/src/helper/selection-tools/rotate-tool.js b/src/helper/selection-tools/rotate-tool.js index 64bf7b50..4893ede1 100644 --- a/src/helper/selection-tools/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/selection-tools/scale-tool.js b/src/helper/selection-tools/scale-tool.js index 0b7c5b6f..085395d8 100644 --- a/src/helper/selection-tools/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; } /** @@ -113,7 +117,7 @@ class ScaleTool { } } } - onMouseUp (event) { + onMouseUp () { this.pivot = null; this.origPivot = null; this.corner = null; @@ -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 index ab71165e..68f014a9 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -1,11 +1,11 @@ import Modes from '../../modes/modes'; import {rectSelect} from '../guides'; import {clearSelection, processRectangularSelection} from '../selection'; -import {getHoveredItem} from '../hover'; class SelectionBoxTool { - constructor () { + constructor (mode) { this.selectionRect = null; + this.mode = mode; } /** * @param {boolean} multiselect Whether to multiselect on mouse down (e.g. shift key held) diff --git a/src/helper/selection.js b/src/helper/selection.js index cd4e5bbe..c1996270 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -3,7 +3,7 @@ import Modes from '../modes/modes'; import {getAllPaperItems} from './helper'; import {getItemsGroup, isGroup} from './group'; -import {getRootItem, isGroupItem, isCompoundPathItem, isPathItem, isPGTextItem} from './item'; +import {getRootItem, isCompoundPathItem, isBoundsItem, isPathItem, isPGTextItem} from './item'; import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compound-path'; const getAllSelectableItems = function () { @@ -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 if (parentGroup) { // do it recursive - setItemSelection(parentGroup, state); + setItemSelection(parentGroup, state, fullySelected); } else if (itemsCompoundPath) { - setGroupSelection(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 @@ -311,34 +314,34 @@ const getSelectedPaths = function () { return paths; }; -// const checkBoundsItem = function (selectionRect, item, event) { -// const itemBounds = new paper.Path([ -// item.localToGlobal(item.internalBounds.topLeft), -// item.localToGlobal(item.internalBounds.topRight), -// item.localToGlobal(item.internalBounds.bottomRight), -// item.localToGlobal(item.internalBounds.bottomLeft) -// ]); -// itemBounds.closed = true; -// itemBounds.guide = true; +const checkBoundsItem = function (selectionRect, item, event) { + const itemBounds = new paper.Path([ + item.localToGlobal(item.internalBounds.topLeft), + item.localToGlobal(item.internalBounds.topRight), + item.localToGlobal(item.internalBounds.bottomRight), + item.localToGlobal(item.internalBounds.bottomLeft) + ]); + itemBounds.closed = true; + itemBounds.guide = true; -// for (let i = 0; i < itemBounds.segments.length; i++) { -// const seg = itemBounds.segments[i]; -// if (selectionRect.contains(seg.point) || -// (i === 0 && selectionRect.getIntersections(itemBounds).length > 0)) { -// if (event.modifiers.shift && item.selected) { -// setItemSelection(item, false); + for (let i = 0; i < itemBounds.segments.length; i++) { + const seg = itemBounds.segments[i]; + if (selectionRect.contains(seg.point) || + (i === 0 && selectionRect.getIntersections(itemBounds).length > 0)) { + if (event.modifiers.shift && item.selected) { + setItemSelection(item, false); -// } else { -// setItemSelection(item, true); -// } -// itemBounds.remove(); -// return true; + } else { + setItemSelection(item, true); + } + itemBounds.remove(); + return true; -// } -// } + } + } -// itemBounds.remove(); -// }; + itemBounds.remove(); +}; const handleRectangularSelectionItems = function (item, event, rect, mode) { if (isPathItem(item)) { @@ -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); @@ -402,10 +402,10 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { } // @todo: Update toolbar state on change - // } else if (isBoundsItem(item)) { - // if (checkBoundsItem(rect, item, event)) { - // return false; - // } + } else if (isBoundsItem(item)) { + if (checkBoundsItem(rect, item, event)) { + return false; + } } return true; }; @@ -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); } } }; @@ -455,23 +450,15 @@ const selectRootItem = function () { for (const item of items) { if (isCompoundPathChild(item)) { const cp = getItemsCompoundPath(item); - setItemSelection(cp, true); + setItemSelection(cp, true, true /* fullySelected */); } const rootItem = getRootItem(item); if (item !== rootItem) { - setItemSelection(item, false); - setItemSelection(rootItem, true); + setItemSelection(rootItem, true, true /* fullySelected */); } } }; -const selectSubItems = function () { - // when switching to the reshape tool while having a compound path or group - // selected, deselect the group and select the children instead. - // TODO -}; - - const shouldShowIfSelection = function () { return getSelectedItems().length > 0; }; @@ -500,7 +487,6 @@ export { removeSelectedSegments, processRectangularSelection, selectRootItem, - selectSubItems, shouldShowIfSelection, shouldShowIfSelectionRecursive, shouldShowSelectAll From 340316565b0cfa7da631a6de8a2ebf5ccf0655c7 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 21 Sep 2017 18:39:18 -0400 Subject: [PATCH 04/14] 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 dc66283bd2d34ced081956493e0c07641133fa0e Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 11:10:17 -0400 Subject: [PATCH 05/14] 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 | 2 ++ .../selection-tools/selection-box-tool.js | 3 +- src/helper/selection.js | 33 ++++++++++++++----- 4 files changed, 33 insertions(+), 10 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..2dbb015d 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -96,6 +96,8 @@ class SelectTool extends paper.Tool { // Backspace, delete if (event.key === 'delete' || event.key === 'backspace') { deleteSelection(Modes.SELECT); + this.clearHoveredItem(); + 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 c1996270..458b5f41 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -6,6 +6,10 @@ import {getItemsGroup, isGroup} from './group'; import {getRootItem, isCompoundPathItem, isBoundsItem, 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,20 +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 or group selected, select the whole compound path or - // group instead. (otherwise the compound path breaks because of - // scale-grouping) const items = getSelectedItems(true /* recursive */); for (const item of items) { if (isCompoundPathChild(item)) { From 5db66af0a1e8090b99432f02fc39efeb84d1187f Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 12:12:07 -0400 Subject: [PATCH 06/14] 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 2dbb015d..a7f0c775 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 458b5f41..607172e2 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, isCompoundPathItem, isBoundsItem, 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 adbd0235511caa9972ed2922e4d7f6bc956f46d2 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 12:22:09 -0400 Subject: [PATCH 07/14] 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 607172e2..6d318f1d 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 3bb606e16a3fce172e7fa99f1da4d474519fbe9a Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 12:31:39 -0400 Subject: [PATCH 08/14] move hit options declaration into function --- src/helper/selection-tools/reshape-tool.js | 73 ++++++++++------------ 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js index 77b86300..424ee183 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -32,43 +32,6 @@ class ReshapeTool extends paper.Tool { this.clearHoveredItem = clearHoveredItem; this.onUpdateSvg = onUpdateSvg; this.prevHoveredItem = null; - this._hitOptionsSelected = { - match: function (item) { - if (!item.item || !item.item.selected) return; - if (item.type === 'handle-out' || item.type === 'handle-in') { - // Only hit test against handles that are visible, that is, - // their segment is selected - if (!item.segment.selected) { - return false; - } - } - return true; - }, - segments: true, - stroke: true, - curves: true, - handles: true, - fill: true, - guide: false - }; - this._hitOptions = { - match: function (item) { - if (item.type === 'handle-out' || item.type === 'handle-in') { - // Only hit test against handles that are visible, that is, - // their segment is selected - if (!item.segment.selected) { - return false; - } - } - return true; - }, - segments: true, - stroke: true, - curves: true, - handles: true, - fill: true, - guide: false - }; this.lastEvent = null; this.mode = ReshapeModes.SELECTION_BOX; this.selectionRect = null; @@ -89,9 +52,39 @@ class ReshapeTool extends paper.Tool { paper.settings.handleSize = 8; } getHitOptions (preselectedOnly) { - this._hitOptions.tolerance = ReshapeTool.TOLERANCE / paper.view.zoom; - this._hitOptionsSelected.tolerance = ReshapeTool.TOLERANCE / paper.view.zoom; - return preselectedOnly ? this._hitOptionsSelected : this._hitOptions; + const hitOptions = { + segments: true, + stroke: true, + curves: true, + handles: true, + fill: true, + guide: false + }; + if (preselectedOnly) { + hitOptions.match = item => { + if (!item.item || !item.item.selected) return; + if (item.type === 'handle-out' || item.type === 'handle-in') { + // Only hit test against handles that are visible, that is, + // their segment is selected + if (!item.segment.selected) { + return false; + } + } + return true; + }; + } else { + hitOptions.match = item => { + if (item.type === 'handle-out' || item.type === 'handle-in') { + // Only hit test against handles that are visible, that is, + // their segment is selected + if (!item.segment.selected) { + return false; + } + } + return true; + }; + } + return hitOptions; } setPrevHoveredItem (prevHoveredItem) { this.prevHoveredItem = prevHoveredItem; From 7e1375d8554aad3219e8f8401c446f5d7d34ba75 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 13:48:18 -0400 Subject: [PATCH 09/14] 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 a7f0c775..bfc989a6 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(); From 32661dde92786af288e2e2446806c736dc250065 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 13:56:58 -0400 Subject: [PATCH 10/14] switch to use id instead of hovered item --- src/containers/reshape-mode.jsx | 14 ++++++------- src/helper/selection-tools/reshape-tool.js | 24 +++++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx index 9900f571..340c2789 100644 --- a/src/containers/reshape-mode.jsx +++ b/src/containers/reshape-mode.jsx @@ -26,8 +26,8 @@ class ReshapeMode 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.isReshapeModeActive && !this.props.isReshapeModeActive) { @@ -41,7 +41,7 @@ class ReshapeMode extends React.Component { } activateTool () { this.tool = new ReshapeTool(this.props.setHoveredItem, this.props.clearHoveredItem, this.props.onUpdateSvg); - this.tool.setPrevHoveredItem(this.props.hoveredItem); + this.tool.setPrevHoveredItemId(this.props.hoveredItemId); this.tool.activate(); } deactivateTool () { @@ -60,7 +60,7 @@ class ReshapeMode extends React.Component { ReshapeMode.propTypes = { clearHoveredItem: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, - hoveredItem: PropTypes.instanceOf(paper.Item), + hoveredItemId: PropTypes.number, isReshapeModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, setHoveredItem: PropTypes.func.isRequired @@ -68,11 +68,11 @@ ReshapeMode.propTypes = { const mapStateToProps = state => ({ isReshapeModeActive: state.scratchPaint.mode === Modes.RESHAPE, - 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/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js index 424ee183..8fad8c35 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -31,10 +31,9 @@ class ReshapeTool extends paper.Tool { this.setHoveredItem = setHoveredItem; this.clearHoveredItem = clearHoveredItem; this.onUpdateSvg = onUpdateSvg; - this.prevHoveredItem = null; + this.prevHoveredItemId = null; this.lastEvent = null; this.mode = ReshapeModes.SELECTION_BOX; - this.selectionRect = null; this._modeMap = {}; this._modeMap[ReshapeModes.FILL] = new MoveTool(onUpdateSvg); this._modeMap[ReshapeModes.POINT] = new PointTool(onUpdateSvg); @@ -86,8 +85,8 @@ class ReshapeTool extends paper.Tool { } return hitOptions; } - setPrevHoveredItem (prevHoveredItem) { - this.prevHoveredItem = prevHoveredItem; + setPrevHoveredItemId (prevHoveredItemId) { + this.prevHoveredItemId = prevHoveredItemId; } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button @@ -169,11 +168,11 @@ class ReshapeTool extends paper.Tool { } handleMouseMove (event) { const hoveredItem = getHoveredItem(event, this.getHitOptions(), true /* subselect */); - 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) { @@ -192,9 +191,14 @@ class ReshapeTool extends paper.Tool { this.onUpdateSvg(); } } - deactivateTool() { + deactivateTool () { paper.settings.handleSize = 0; this.clearHoveredItem(); + this.setHoveredItem = null; + this.clearHoveredItem = null; + this.onUpdateSvg = null; + this.prevHoveredItemId = null; + this.lastEvent = null; } } From 33d8dd30b5b865ac870b53e6f116ba563435d815 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 14:02:18 -0400 Subject: [PATCH 11/14] release things on deactivate --- src/helper/selection-tools/reshape-tool.js | 1 - src/helper/selection-tools/select-tool.js | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js index 8fad8c35..5c14d06c 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -197,7 +197,6 @@ class ReshapeTool extends paper.Tool { this.setHoveredItem = null; this.clearHoveredItem = null; this.onUpdateSvg = null; - this.prevHoveredItemId = null; this.lastEvent = null; } } diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index bfc989a6..bc33eea3 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -31,6 +31,7 @@ class SelectTool extends paper.Tool { this.boundingBoxTool = new BoundingBoxTool(onUpdateSvg); this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT); this.selectionBoxMode = false; + this.prevHoveredItemId = null; // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. @@ -131,6 +132,11 @@ class SelectTool extends paper.Tool { deactivateTool () { this.clearHoveredItem(); this.boundingBoxTool.removeBoundsPath(); + this.setHoveredItem = null; + this.clearHoveredItem = null; + this.onUpdateSvg = null; + this.boundingBoxTool = null; + this.selectionBoxTool = null; } } From 33236f198d7c0ed6fe4b22b7a370c93d9be2ce0f Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 14:08:19 -0400 Subject: [PATCH 12/14] fix hit testing --- src/helper/selection-tools/reshape-tool.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js index 5c14d06c..e94a7ad6 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -57,7 +57,8 @@ class ReshapeTool extends paper.Tool { curves: true, handles: true, fill: true, - guide: false + guide: false, + tolerance: ReshapeTool.TOLERANCE / paper.view.zoom }; if (preselectedOnly) { hitOptions.match = item => { From 6bcd59f3881a123a032627185a39a5d7ca8c3903 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 14:14:48 -0400 Subject: [PATCH 13/14] add comments --- src/helper/selection-tools/handle-tool.js | 1 + src/helper/selection-tools/reshape-tool.js | 32 +++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/helper/selection-tools/handle-tool.js b/src/helper/selection-tools/handle-tool.js index 49757b83..26aead16 100644 --- a/src/helper/selection-tools/handle-tool.js +++ b/src/helper/selection-tools/handle-tool.js @@ -1,5 +1,6 @@ import {clearSelection, getSelectedItems} from '../selection'; +/** Sub tool of the Reshape tool for moving handles, which adjust bezier curves. */ class HandleTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js index e94a7ad6..596ec0f3 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -19,13 +19,30 @@ const ReshapeModes = keyMirror({ SELECTION_BOX: null }); +/** + * paper.Tool to handle reshape mode, which allows manipulation of control points and + * handles of path items. Can be used to select items within groups and points within items. + * Reshape is made up of 4 tools: + * - Selection box tool, which is activated by clicking an empty area. Draws a box and selects + * points and curves inside it + * - Move tool, which translates items + * - Point tool, which translates, adds and removes points + * - Handle tool, which translates handles, changing the shape of curves + */ class ReshapeTool extends paper.Tool { + /** Distance within which mouse is considered to be hitting an item */ static get TOLERANCE () { return 8; } + /** Clicks registered within this amount of time are registered as double clicks */ static get DOUBLE_CLICK_MILLIS () { return 250; } + /** + * @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; @@ -50,6 +67,12 @@ class ReshapeTool extends paper.Tool { paper.settings.handleSize = 8; } + /** + * 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) { const hitOptions = { segments: true, @@ -86,6 +109,13 @@ class ReshapeTool extends paper.Tool { } return hitOptions; } + /** + * To be called when the hovered item changes. When the select tool hovers over a + * new item, it compares against this to see if a hover item change event needs to + * be fired. + * @param {paper.Item} prevHoveredItemId ID of the highlight item that indicates the mouse is + * over a given item currently + */ setPrevHoveredItemId (prevHoveredItemId) { this.prevHoveredItemId = prevHoveredItemId; } @@ -104,7 +134,7 @@ class ReshapeTool extends paper.Tool { } this.lastEvent = event; - // Choose hit result =========================================================== + // Choose hit result to use =========================================================== // Prefer hits on already selected items let hitResults = paper.project.hitTestAll(event.point, this.getHitOptions(true /* preselectedOnly */)); From 02e4f0b3d04606364958b9f35de68dad25cf6246 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 3 Oct 2017 17:19:22 -0400 Subject: [PATCH 14/14] fix lint --- src/containers/reshape-mode.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx index 340c2789..2fb3dc55 100644 --- a/src/containers/reshape-mode.jsx +++ b/src/containers/reshape-mode.jsx @@ -9,8 +9,6 @@ import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; import ReshapeTool from '../helper/selection-tools/reshape-tool'; import ReshapeModeComponent from '../components/reshape-mode.jsx'; -import paper from 'paper'; - class ReshapeMode extends React.Component { constructor (props) {