From 2c374444b3d2aa428bd1bfc55d5a123a31e99aa7 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 21 Sep 2017 18:20:44 -0400 Subject: [PATCH] Pull in changes from reshape branch --- src/containers/paper-canvas.jsx | 8 ++ src/containers/select-mode.jsx | 106 ++---------------- src/helper/bounding-box/move-tool.js | 59 ---------- .../bounding-box-tool.js | 19 +++- src/helper/selection-tools/move-tool.js | 101 +++++++++++++++++ .../rotate-tool.js | 8 +- .../scale-tool.js | 8 +- .../selection-tools/selection-box-tool.js | 32 ++++++ src/helper/selection.js | 52 +++++---- 9 files changed, 202 insertions(+), 191 deletions(-) delete mode 100644 src/helper/bounding-box/move-tool.js rename src/helper/{bounding-box => selection-tools}/bounding-box-tool.js (91%) create mode 100644 src/helper/selection-tools/move-tool.js rename src/helper/{bounding-box => selection-tools}/rotate-tool.js (90%) rename src/helper/{bounding-box => selection-tools}/scale-tool.js (97%) create mode 100644 src/helper/selection-tools/selection-box-tool.js diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 50e95b51..7539bb0d 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -33,7 +33,15 @@ class PaperCanvas extends React.Component { onLoad: function (item) { // Remove viewbox if (item.clipped) { + let mask; + for (const child of item.children) { + if (child.isClipMask()) { + mask = child; + break; + } + } item.clipped = false; + mask.remove(); // Consider removing clip mask here? } while (item.reduce() !== item) { diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 572f6885..731ba0e6 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -7,35 +7,17 @@ import Modes from '../modes/modes'; import {changeMode} from '../reducers/modes'; import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; -import {getHoveredItem} from '../helper/hover'; -import {rectSelect} from '../helper/guides'; -import {selectRootItem, processRectangularSelection} from '../helper/selection'; - +import SelectTool from '../helper/selection-tools/select-tool'; import SelectModeComponent from '../components/select-mode.jsx'; -import BoundingBoxTool from '../helper/bounding-box/bounding-box-tool'; import paper from 'paper'; class SelectMode extends React.Component { - static get TOLERANCE () { - return 6; - } constructor (props) { super(props); bindAll(this, [ 'activateTool', - 'deactivateTool', - 'getHitOptions' + 'deactivateTool' ]); - this._hitOptions = { - segments: true, - stroke: true, - curves: true, - fill: true, - guide: false - }; - this.boundingBoxTool = new BoundingBoxTool(); - this.selectionBoxMode = false; - this.selectionRect = null; } componentDidMount () { if (this.props.isSelectModeActive) { @@ -43,6 +25,10 @@ class SelectMode extends React.Component { } } componentWillReceiveProps (nextProps) { + if (this.tool && nextProps.hoveredItem !== this.props.hoveredItem) { + this.tool.setPrevHoveredItem(nextProps.hoveredItem); + } + if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) { this.activateTool(); } else if (!nextProps.isSelectModeActive && this.props.isSelectModeActive) { @@ -52,84 +38,14 @@ class SelectMode extends React.Component { shouldComponentUpdate () { return false; // Static component, for now } - getHitOptions (preselectedOnly) { - this._hitOptions.tolerance = SelectMode.TOLERANCE / paper.view.zoom; - if (preselectedOnly) { - this._hitOptions.selected = true; - } else { - delete this._hitOptions.selected; - } - return this._hitOptions; - } activateTool () { - selectRootItem(); - this.boundingBoxTool.setSelectionBounds(); - this.tool = new paper.Tool(); - - // Define these to sate linter - const selectMode = this; - - this.tool.onMouseDown = function (event) { - if (event.event.button > 0) return; // only first mouse button - - selectMode.props.clearHoveredItem(); - if (!selectMode.boundingBoxTool - .onMouseDown( - event, - event.modifiers.alt, - event.modifiers.shift, - selectMode.getHitOptions(false /* preseelectedOnly */))) { - selectMode.selectionBoxMode = true; - } - }; - - this.tool.onMouseMove = function (event) { - const hoveredItem = getHoveredItem(event, selectMode.getHitOptions()); - const oldHoveredItem = selectMode.props.hoveredItem; - if ((!hoveredItem && oldHoveredItem) || // There is no longer a hovered item - (hoveredItem && !oldHoveredItem) || // There is now a hovered item - (hoveredItem && oldHoveredItem && hoveredItem.id !== oldHoveredItem.id)) { // hovered item changed - selectMode.props.setHoveredItem(hoveredItem); - } - }; - - - this.tool.onMouseDrag = function (event) { - if (event.event.button > 0) return; // only first mouse button - - if (selectMode.selectionBoxMode) { - selectMode.selectionRect = rectSelect(event); - // Remove this rect on the next drag and up event - selectMode.selectionRect.removeOnDrag(); - } else { - selectMode.boundingBoxTool.onMouseDrag(event); - } - }; - - this.tool.onMouseUp = function (event) { - if (event.event.button > 0) return; // only first mouse button - - if (selectMode.selectionBoxMode) { - if (selectMode.selectionRect) { - processRectangularSelection(event, selectMode.selectionRect, Modes.SELECT); - selectMode.selectionRect.remove(); - } - selectMode.boundingBoxTool.setSelectionBounds(); - } else { - selectMode.boundingBoxTool.onMouseUp(event); - selectMode.props.onUpdateSvg(); - } - selectMode.selectionBoxMode = false; - selectMode.selectionRect = null; - }; + this.tool = new SelectTool(this.props.setHoveredItem, this.props.clearHoveredItem, this.props.onUpdateSvg); this.tool.activate(); } deactivateTool () { - this.props.clearHoveredItem(); - this.boundingBoxTool.removeBoundsPath(); + this.tool.deactivateTool(); this.tool.remove(); this.tool = null; - this.hitResult = null; } render () { return ( @@ -141,10 +57,10 @@ class SelectMode extends React.Component { SelectMode.propTypes = { clearHoveredItem: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, - hoveredItem: PropTypes.instanceOf(paper.Item), // eslint-disable-line react/no-unused-prop-types + hoveredItem: PropTypes.instanceOf(paper.Item), isSelectModeActive: PropTypes.bool.isRequired, - onUpdateSvg: PropTypes.func.isRequired, // eslint-disable-line react/no-unused-prop-types - setHoveredItem: PropTypes.func.isRequired // eslint-disable-line react/no-unused-prop-types + onUpdateSvg: PropTypes.func.isRequired, + setHoveredItem: PropTypes.func.isRequired }; const mapStateToProps = state => ({ diff --git a/src/helper/bounding-box/move-tool.js b/src/helper/bounding-box/move-tool.js deleted file mode 100644 index 6303d6cd..00000000 --- a/src/helper/bounding-box/move-tool.js +++ /dev/null @@ -1,59 +0,0 @@ -import {isGroup} from '../group'; -import {isCompoundPathItem, getRootItem} from '../item'; -import {snapDeltaToAngle} from '../math'; -import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; - -class MoveTool { - constructor () { - this.selectedItems = null; - } - - /** - * @param {!paper.HitResult} hitResult Data about the location of the mouse click - * @param {boolean} clone Whether to clone on mouse down (e.g. alt key held) - * @param {boolean} multiselect Whether to multiselect on mouse down (e.g. shift key held) - */ - onMouseDown (hitResult, clone, multiselect) { - const root = getRootItem(hitResult.item); - const item = isCompoundPathItem(root) || isGroup(root) ? root : hitResult.item; - if (!item.selected) { - // deselect all by default if multiselect isn't on - if (!multiselect) { - clearSelection(); - } - setItemSelection(item, true); - } else if (multiselect) { - setItemSelection(item, false); - } - if (clone) cloneSelection(); - this.selectedItems = getSelectedItems(); - } - onMouseDrag (event) { - const dragVector = event.point.subtract(event.downPoint); - for (const item of this.selectedItems) { - // add the position of the item before the drag started - // for later use in the snap calculation - if (!item.data.origPos) { - item.data.origPos = item.position; - } - - if (event.modifiers.shift) { - item.position = item.data.origPos.add(snapDeltaToAngle(dragVector, Math.PI / 4)); - } else { - item.position = item.data.origPos.add(dragVector); - } - } - } - onMouseUp () { - // resetting the items origin point for the next usage - for (const item of this.selectedItems) { - item.data.origPos = null; - } - this.selectedItems = null; - - // @todo add back undo - // pg.undo.snapshot('moveSelection'); - } -} - -export default MoveTool; diff --git a/src/helper/bounding-box/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js similarity index 91% rename from src/helper/bounding-box/bounding-box-tool.js rename to src/helper/selection-tools/bounding-box-tool.js index 32d96918..c77a5dcd 100644 --- a/src/helper/bounding-box/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -28,17 +28,19 @@ const Modes = keyMirror({ * On mouse down, the type of function (move, scale, rotate) is determined based on what is clicked * (scale handle, rotate handle, the object itself). This determines the mode of the tool, which then * delegates actions to the MoveTool, RotateTool or ScaleTool accordingly. + * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ class BoundingBoxTool { - constructor () { + constructor (onUpdateSvg) { + this.onUpdateSvg = onUpdateSvg; this.mode = null; this.boundsPath = null; this.boundsScaleHandles = []; this.boundsRotHandles = []; this._modeMap = {}; - this._modeMap[Modes.SCALE] = new ScaleTool(); - this._modeMap[Modes.ROTATE] = new RotateTool(); - this._modeMap[Modes.MOVE] = new MoveTool(); + this._modeMap[Modes.SCALE] = new ScaleTool(onUpdateSvg); + this._modeMap[Modes.ROTATE] = new RotateTool(onUpdateSvg); + this._modeMap[Modes.MOVE] = new MoveTool(onUpdateSvg); } /** @@ -75,8 +77,13 @@ class BoundingBoxTool { this.mode = Modes.MOVE; } + const hitProperties = { + hitResult: hitResult, + clone: event.modifiers.alt, + multiselect: event.modifiers.shift + }; if (this.mode === Modes.MOVE) { - this._modeMap[this.mode].onMouseDown(hitResult, clone, multiselect); + this._modeMap[this.mode].onMouseDown(hitProperties); } else if (this.mode === Modes.SCALE) { this._modeMap[this.mode].onMouseDown( hitResult, this.boundsPath, this.boundsScaleHandles, this.boundsRotHandles, getSelectedItems()); @@ -102,7 +109,7 @@ class BoundingBoxTool { setSelectionBounds () { this.removeBoundsPath(); - const items = getSelectedItems(); + const items = getSelectedItems(true /* recursive */); if (items.length <= 0) return; let rect = null; diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js new file mode 100644 index 00000000..e6470a92 --- /dev/null +++ b/src/helper/selection-tools/move-tool.js @@ -0,0 +1,101 @@ +import {isGroup} from '../group'; +import {isCompoundPathItem, getRootItem} from '../item'; +import {snapDeltaToAngle} from '../math'; +import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; + +class MoveTool { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { + this.selectedItems = null; + this.onUpdateSvg = onUpdateSvg; + } + + /** + * @param {!object} hitProperties Describes the mouse event + * @param {!paper.HitResult} hitProperties.hitResult Data about the location of the mouse click + * @param {?boolean} hitProperties.clone Whether to clone on mouse down (e.g. alt key held) + * @param {?boolean} hitProperties.multiselect Whether to multiselect on mouse down (e.g. shift key held) + * @param {?boolean} hitProperties.doubleClicked True if this is the second click in a short amout of time + * @param {?boolean} hitProperties.subselect True if we allow selection of subgroups, false if we should + * select the whole group. + */ + onMouseDown (hitProperties) { + let item = hitProperties.hitResult.item; + if (!hitProperties.subselect) { + const root = getRootItem(hitProperties.hitResult.item); + item = isCompoundPathItem(root) || isGroup(root) ? root : hitProperties.hitResult.item; + } + if (item.selected) { + // Double click causes all points to be selected in subselect mode. + if (hitProperties.doubleClicked) { + if (!hitProperties.multiselect) { + clearSelection(); + } + this._select(item, true /* state */, hitProperties.subselect, true /* fullySelect */); + } else if (hitProperties.multiselect) { + this._select(item, false /* state */, hitProperties.subselect); + } + } else { + // deselect all by default if multiselect isn't on + if (!hitProperties.multiselect) { + clearSelection(); + } + this._select(item, true, hitProperties.subselect); + } + if (hitProperties.clone) cloneSelection(hitProperties.subselect); + this.selectedItems = getSelectedItems(true /* subselect */); + } + /** + * Sets the selection state of an item. + * @param {!paper.Item} item Item to select or deselect + * @param {?boolean} state True if item should be selected, false if deselected + * @param {?boolean} subselect True if a subset of all points in an item are allowed to be + * selected, false if items must be selected all or nothing. + * @param {?boolean} fullySelect True if in addition to the item being selected, all of its + * control points should be selected. False if the item should be selected but not its + * points. Only relevant when subselect is true. + */ + _select (item, state, subselect, fullySelect) { + if (subselect) { + item.selected = false; + if (fullySelect) { + item.fullySelected = state; + } else { + item.selected = state; + } + } else { + setItemSelection(item, state); + } + } + onMouseDrag (event) { + const dragVector = event.point.subtract(event.downPoint); + for (const item of this.selectedItems) { + // add the position of the item before the drag started + // for later use in the snap calculation + if (!item.data.origPos) { + item.data.origPos = item.position; + } + + if (event.modifiers.shift) { + item.position = item.data.origPos.add(snapDeltaToAngle(dragVector, Math.PI / 4)); + } else { + item.position = item.data.origPos.add(dragVector); + } + } + } + onMouseUp () { + // resetting the items origin point for the next usage + for (const item of this.selectedItems) { + item.data.origPos = null; + } + this.selectedItems = null; + + // @todo add back undo + // pg.undo.snapshot('moveSelection'); + this.onUpdateSvg(); + } +} + +export default MoveTool; diff --git a/src/helper/bounding-box/rotate-tool.js b/src/helper/selection-tools/rotate-tool.js similarity index 90% rename from src/helper/bounding-box/rotate-tool.js rename to src/helper/selection-tools/rotate-tool.js index 64bf7b50..4893ede1 100644 --- a/src/helper/bounding-box/rotate-tool.js +++ b/src/helper/selection-tools/rotate-tool.js @@ -1,10 +1,14 @@ import paper from 'paper'; class RotateTool { - constructor () { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { this.rotItems = []; this.rotGroupPivot = null; this.prevRot = []; + this.onUpdateSvg = onUpdateSvg; } /** @@ -57,7 +61,7 @@ class RotateTool { this.prevRot = []; // @todo add back undo - // pg.undo.snapshot('rotateSelection'); + this.onUpdateSvg(); } } diff --git a/src/helper/bounding-box/scale-tool.js b/src/helper/selection-tools/scale-tool.js similarity index 97% rename from src/helper/bounding-box/scale-tool.js rename to src/helper/selection-tools/scale-tool.js index 23cb96eb..085395d8 100644 --- a/src/helper/bounding-box/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -1,7 +1,10 @@ import paper from 'paper'; class ScaleTool { - constructor () { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { this.pivot = null; this.origPivot = null; this.corner = null; @@ -14,6 +17,7 @@ class ScaleTool { this.scaleItems = []; this.boundsScaleHandles = []; this.boundsRotHandles = []; + this.onUpdateSvg = onUpdateSvg; } /** @@ -150,7 +154,7 @@ class ScaleTool { this.itemGroup.remove(); // @todo add back undo - // pg.undo.snapshot('scaleSelection'); + this.onUpdateSvg(); } getRectCornerNameByIndex (index) { switch (index) { diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js new file mode 100644 index 00000000..68f014a9 --- /dev/null +++ b/src/helper/selection-tools/selection-box-tool.js @@ -0,0 +1,32 @@ +import Modes from '../../modes/modes'; +import {rectSelect} from '../guides'; +import {clearSelection, processRectangularSelection} from '../selection'; + +class SelectionBoxTool { + constructor (mode) { + this.selectionRect = null; + this.mode = mode; + } + /** + * @param {boolean} multiselect Whether to multiselect on mouse down (e.g. shift key held) + */ + onMouseDown (multiselect) { + if (!multiselect) { + clearSelection(); + } + } + onMouseDrag (event) { + this.selectionRect = rectSelect(event); + // Remove this rect on the next drag and up event + this.selectionRect.removeOnDrag(); + } + onMouseUp (event) { + if (this.selectionRect) { + processRectangularSelection(event, this.selectionRect, Modes.RESHAPE); + this.selectionRect.remove(); + this.selectionRect = null; + } + } +} + +export default SelectionBoxTool; diff --git a/src/helper/selection.js b/src/helper/selection.js index 6ecba05c..226ab205 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -34,37 +34,40 @@ const selectItemSegments = function (item, state) { } }; -const setGroupSelection = function (root, selected) { - // fully selected segments need to be unselected first - root.fullySelected = false; - // then the item can be normally selected +const setGroupSelection = function (root, selected, fullySelected) { + root.fullySelected = fullySelected; root.selected = selected; // select children of compound-path or group if (isCompoundPath(root) || isGroup(root)) { const children = root.children; if (children) { - for (let i = 0; i < children.length; i++) { - children[i].selected = selected; + for (const child of children) { + if (isGroup(child)) { + setGroupSelection(child, selected, fullySelected); + } else { + child.fullySelected = fullySelected; + child.selected = selected; + } } } } }; -const setItemSelection = function (item, state) { +const setItemSelection = function (item, state, fullySelected) { const parentGroup = getItemsGroup(item); const itemsCompoundPath = getItemsCompoundPath(item); // if selection is in a group, select group not individual items if (parentGroup) { // do it recursive - setItemSelection(parentGroup, state); + setItemSelection(parentGroup, state, fullySelected); } else if (itemsCompoundPath) { - setItemSelection(itemsCompoundPath, state); + setGroupSelection(itemsCompoundPath, state, fullySelected); } else { if (item.data && item.data.noSelect) { return; } - setGroupSelection(item, state); + setGroupSelection(item, state, fullySelected); } // @todo: Update toolbar state on change @@ -355,11 +358,9 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { seg.selected = true; } segmentMode = true; - } else { if (event.modifiers.shift && item.selected) { setItemSelection(item, false); - } else { setItemSelection(item, true); } @@ -371,7 +372,7 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { // second round checks for path intersections const intersections = item.getIntersections(rect); if (intersections.length > 0 && !segmentMode) { - // if in detail select mode, select the curves that intersect + // if in reshape mode, select the curves that intersect // with the selectionRect if (mode === Modes.RESHAPE) { for (let k = 0; k < intersections.length; k++) { @@ -389,7 +390,6 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { curve.selected = true; } } - } else { if (event.modifiers.shift && item.selected) { setItemSelection(item, false); @@ -417,9 +417,8 @@ const rectangularSelectionGroupLoop = function (group, rect, root, event, mode) if (isGroup(child) || isCompoundPathItem(child)) { rectangularSelectionGroupLoop(child, rect, root, event, mode); - - } else if (!handleRectangularSelectionItems(child, event, rect, mode)) { - return false; + } else { + handleRectangularSelectionItems(child, event, rect, mode); } } return true; @@ -428,20 +427,16 @@ const rectangularSelectionGroupLoop = function (group, rect, root, event, mode) const processRectangularSelection = function (event, rect, mode) { const allItems = getAllSelectableItems(); - itemLoop: for (let i = 0; i < allItems.length; i++) { const item = allItems[i]; if (mode === Modes.RESHAPE && isPGTextItem(getRootItem(item))) { - continue itemLoop; + continue; } // check for item segment points inside selectionRect if (isGroup(item) || isCompoundPathItem(item)) { - if (!rectangularSelectionGroupLoop(item, rect, item, event, mode)) { - continue itemLoop; - } - - } else if (!handleRectangularSelectionItems(item, event, rect, mode)) { - continue itemLoop; + rectangularSelectionGroupLoop(item, rect, item, event, mode); + } else { + handleRectangularSelectionItems(item, event, rect, mode); } } }; @@ -454,8 +449,11 @@ const selectRootItem = function () { for (const item of items) { if (isCompoundPathChild(item)) { const cp = getItemsCompoundPath(item); - setItemSelection(item, false); - setItemSelection(cp, true); + setItemSelection(cp, true, true /* fullySelected */); + } + const rootItem = getRootItem(item); + if (item !== rootItem) { + setItemSelection(rootItem, true, true /* fullySelected */); } } };