From cf75703580bd72438cdb373382787cde3f3a4f23 Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 11 Sep 2017 10:52:00 -0400 Subject: [PATCH 01/17] In-progress add select mode --- src/containers/select-mode.jsx | 128 +++++++++++++ src/helper/bounding-box-tool.js | 326 ++++++++++++++++++++++++++++++++ src/helper/compoundPath.js | 86 +++++++++ src/helper/group.js | 139 ++++++++++++++ src/helper/guides.js | 184 ++++++++++++++++++ src/helper/hover.js | 50 +++++ 6 files changed, 913 insertions(+) create mode 100644 src/containers/select-mode.jsx create mode 100644 src/helper/bounding-box-tool.js create mode 100644 src/helper/compoundPath.js create mode 100644 src/helper/group.js create mode 100644 src/helper/guides.js create mode 100644 src/helper/hover.js diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx new file mode 100644 index 00000000..9cabc75e --- /dev/null +++ b/src/containers/select-mode.jsx @@ -0,0 +1,128 @@ +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 {clearSelection} from '../reducers/selection'; +import {setHoveredItem} from '../reducers/hover'; +import {getHoveredItem} from '../helper/hover'; +import {changeMode} from '../reducers/modes'; +import SelectModeComponent from '../components/select-mode.jsx'; +import paper from 'paper'; + +class SelectMode extends React.Component { + static get TOLERANCE () { + return 6; + } + constructor (props) { + super(props); + bindAll(this, [ + 'activateTool', + 'deactivateTool', + 'getHitOptions', + 'preProcessSelection', + 'onMouseDown', + 'onMouseMove', + 'onMouseDrag', + 'onMouseUp' + ]); + this._hitOptions = { + segments: true, + stroke: true, + curves: true, + fill: true, + guide: false + }; + + } + componentDidMount () { + if (this.props.isSelectModeActive) { + this.activateTool(this.props); + } + } + componentWillReceiveProps (nextProps) { + if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) { + this.activateTool(); + } else if (!nextProps.isSelectModeActive && this.props.isSelectModeActive) { + this.deactivateTool(); + } + } + shouldComponentUpdate () { + return false; // Static component, for now + } + getHitOptions () { + this._hitOptions.tolerance = SelectMode.TOLERANCE / paper.view.zoom; + return this._hitOptions; + } + activateTool () { + clearSelection(); + this.preProcessSelection(); + this.tool = new paper.Tool(); + + + this.tool.onMouseDown = function (event) { + this.onMouseDown(event); + }; + + this.tool.onMouseMove = function (event) { + this.props.setHoveredItem(getHoveredItem(this.getHitOptions())); + }; + + + this.tool.onMouseDrag = function (event) { + this.onMouseDrag(event); + }; + + this.tool.onMouseUp = function (event) { + this.onMouseUp(event); + }; + this.tool.activate(); + } + preProcessSelection () { + // 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 = this.props.selectedItems; + for (let item of items) { + if(isCompoundPathChild(item)) { + var cp = getItemsCompoundPath(item); + setItemSelection(item, false); + setItemSelection(cp, true); + } + }; + }; + deactivateTool () { + this.props.setHoveredItem(); + this.tool.remove(); + this.tool = null; + this.hitResult = null; + } + render () { + return ( + + ); + } +} + +SelectMode.propTypes = { + handleMouseDown: PropTypes.func.isRequired, + isSelectModeActive: PropTypes.bool.isRequired, + onUpdateSvg: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + isSelectModeActive: state.scratchPaint.mode === Modes.SELECT +}); +const mapDispatchToProps = dispatch => ({ + setHoveredItem: hoveredItem => { + dispatch(setHoveredItem(hoveredItem)); + }, + handleMouseDown: () => { + dispatch(changeMode(Modes.SELECT)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(SelectMode); diff --git a/src/helper/bounding-box-tool.js b/src/helper/bounding-box-tool.js new file mode 100644 index 00000000..4479b4a0 --- /dev/null +++ b/src/helper/bounding-box-tool.js @@ -0,0 +1,326 @@ +import paper from 'paper'; + +var mode = 'none'; +var selectionRect; + +var itemGroup; +var pivot; +var corner; +var origPivot; +var origSize; +var origCenter; +var scaleItems; +var scaleItemsInsertBelow; + +var rotItems = []; +var rotGroupPivot; +var prevRot = []; + +class BoundingBoxTool extends paper.Tool { + onMouseDown: If BoundingBoxTool got a hit result, switch to bounding box tool as the primary tool. + Else switch to the default tool. + + Where should the move tool be handled? Might make sense on bounding box tool since whenever the bounding + box is active, move is possible + + Shift button handling? If you shift click, bounding box tool wants to add it to the selection. But shape tools + probably don't. + - If shift is held down during mouse click, don't switch to the bounding box tool even if it gets a hit? + Then we can decide how to deal with it differently for different modes. + + Alt button handling? + - Same as shift? + + + + + onMouseDown (event) { + if(event.event.button > 0) return; // only first mouse button + clearHoveredItem(); + + const hitResults = paper.project.hitTestAll(event.point, this.getHitOptions()); + // Prefer scale to trigger over rotate, since their regions overlap + if (hitResults && hitResults.length > 0) { + let hitResult = hitResults[0]; + for (let i = 0; i < hitResults.length; i++) { + if (hitResults[i].item.data && hitResults[i].item.data.isScaleHandle) { + hitResult = hitResults[i]; + this.mode = 'scale'; + break; + } else if (hitResults[i].item.data && hitResults[i].item.data.isRotHandle) { + hitResult = hitResults[i]; + this.mode = 'rotate'; + } + } + if (mode === 'rotate') { + rotGroupPivot = boundsPath.bounds.center; + rotItems = pg.selection.getSelectedItems(); + + jQuery.each(rotItems, function(i, item) { + prevRot[i] = (event.point - rotGroupPivot).angle; + }); + } else if (mode === 'scale') { + var index = hitResult.item.data.index; + pivot = boundsPath.bounds[getOpposingRectCornerNameByIndex(index)].clone(); + origPivot = boundsPath.bounds[getOpposingRectCornerNameByIndex(index)].clone(); + corner = boundsPath.bounds[getRectCornerNameByIndex(index)].clone(); + origSize = corner.subtract(pivot); + origCenter = boundsPath.bounds.center; + scaleItems = pg.selection.getSelectedItems(); + } + else { // Move mode + // deselect all by default if the shift key isn't pressed + // also needs some special love for compound paths and groups, + // as their children are not marked as "selected" + // deselect a currently selected item if shift is pressed + var root = pg.item.getRootItem(hitResult.item); + if(pg.item.isCompoundPathItem(root) || pg.group.isGroup(root)) { + if(!root.selected) { + if (!event.modifiers.shift) { + pg.selection.clearSelection() + } + root.selected = true; + for (var i = 0; i < root.children.length; i++) { + root.children[i].selected = true; + } + jQuery(document).trigger('SelectionChanged'); + if(event.modifiers.alt) { + mode = 'cloneMove'; + pg.selection.cloneSelection(); + + } else { + mode = 'move'; + } + } else { + if (event.modifiers.shift) { + root.selected = false; + for (var i = 0; i < root.children.length; i++) { + root.children[i].selected = false; + } + } else { + if(event.modifiers.alt) { + mode = 'cloneMove'; + pg.selection.cloneSelection(); + + } else { + mode = 'move'; + } + } + } + } else if(hitResult.item.selected) { + if (event.modifiers.shift) { + pg.selection.setItemSelection(hitResult.item, false); + } else { + if(event.modifiers.alt) { + mode = 'cloneMove'; + pg.selection.cloneSelection(); + + } else { + mode = 'move'; + } + } + } else { + if (!event.modifiers.shift) { + pg.selection.clearSelection() + } + pg.selection.setItemSelection(hitResult.item, true); + + if(event.modifiers.alt) { + mode = 'cloneMove'; + pg.selection.cloneSelection(); + + } else { + mode = 'move'; + } + } + } + // while transforming object, never show the bounds stuff + removeBoundsPath(); + } else { + if (!event.modifiers.shift) { + removeBoundsPath(); + pg.selection.clearSelection(); + } + mode = 'rectSelection'; + } + } + onMouseDrag (event) { + if(event.event.button > 0) return; // only first mouse button + + var modOrigSize = origSize; + + if(mode == 'rectSelection') { + selectionRect = pg.guides.rectSelect(event); + // Remove this rect on the next drag and up event + selectionRect.removeOnDrag(); + + } else if(mode == 'scale') { + // get index of scale items + var items = paper.project.getItems({ + 'match': function(item) { + if (item instanceof Layer) { + return false; + } + for (var i = 0; i < scaleItems.length; i++) { + if (!scaleItems[i].isBelow(item)) { + return false; + } + } + return true; + } + }); + if (items.length > 0) { + // Lowest item above all scale items in z index + scaleItemsInsertBelow = items[0]; + } + + itemGroup = new paper.Group(scaleItems); + itemGroup.insertBelow(scaleItemsInsertBelow); + itemGroup.addChild(boundsPath); + itemGroup.data.isHelperItem = true; + itemGroup.strokeScaling = false; + itemGroup.applyMatrix = false; + + if (event.modifiers.alt) { + pivot = origCenter; + modOrigSize = origSize*0.5; + } else { + pivot = origPivot; + } + + corner = corner.add(event.delta); + var size = corner.subtract(pivot); + var sx = 1.0, sy = 1.0; + if (Math.abs(modOrigSize.x) > 0.0000001) { + sx = size.x / modOrigSize.x; + } + if (Math.abs(modOrigSize.y) > 0.0000001) { + sy = size.y / modOrigSize.y; + } + + if (event.modifiers.shift) { + var signx = sx > 0 ? 1 : -1; + var signy = sy > 0 ? 1 : -1; + sx = sy = Math.max(Math.abs(sx), Math.abs(sy)); + sx *= signx; + sy *= signy; + } + + itemGroup.scale(sx, sy, pivot); + + jQuery.each(boundsScaleHandles, function(index, handle) { + handle.position = itemGroup.bounds[getRectCornerNameByIndex(index)]; + handle.bringToFront(); + }); + + jQuery.each(boundsRotHandles, function(index, handle) { + if(handle) { + handle.position = itemGroup.bounds[getRectCornerNameByIndex(index)]+handle.data.offset; + handle.bringToFront(); + } + }); + + } else if(mode == 'rotate') { + var rotAngle = (event.point - rotGroupPivot).angle; + + jQuery.each(rotItems, function(i, item) { + + if(!item.data.origRot) { + item.data.origRot = item.rotation; + } + + if(event.modifiers.shift) { + rotAngle = Math.round(rotAngle / 45) *45; + item.applyMatrix = false; + item.pivot = rotGroupPivot; + item.rotation = rotAngle; + + } else { + item.rotate(rotAngle - prevRot[i], rotGroupPivot); + } + prevRot[i] = rotAngle; + }); + + } else if(mode == 'move' || mode == 'cloneMove') { + + var dragVector = (event.point - event.downPoint); + var selectedItems = pg.selection.getSelectedItems(); + + for(var i=0; i 0) return; // only first mouse button + + if(mode == 'rectSelection' && selectionRect) { + pg.selection.processRectangularSelection(event, selectionRect); + selectionRect.remove(); + + } else if(mode == 'move' || mode == 'cloneMove') { + + // resetting the items origin point for the next usage + var selectedItems = pg.selection.getSelectedItems(); + + jQuery.each(selectedItems, function(index, item) { + // remove the orig pos again + item.data.origPos = null; + }); + pg.undo.snapshot('moveSelection'); + + } else if(mode == 'scale') { + if (itemGroup) { + itemGroup.applyMatrix = true; + + // mark text items as scaled (for later use on font size calc) + for(var i=0; i 0) { + var group = new paper.Group(items); + pg.selection.clearSelection(); + pg.selection.setItemSelection(group, true); + for (var i = 0; i < group.children.length; i++) { + group.children[i].selected = true; + } + pg.undo.snapshot('groupSelection'); + jQuery(document).trigger('Grouped'); + return group; + } else { + return false; + } + }; + + + var ungroupSelection = function() { + var items = pg.selection.getSelectedItems(); + ungroupItems(items); + pg.statusbar.update(); + }; + + + var groupItems = function(items) { + if(items.length > 0) { + var group = new paper.Group(items); + jQuery(document).trigger('Grouped'); + pg.undo.snapshot('groupItems'); + return group; + } else { + return false; + } + }; + + + // ungroup items (only top hierarchy) + var ungroupItems = function(items) { + pg.selection.clearSelection(); + var emptyGroups = []; + for(var i=0; i 1; + }; + + var shouldShowUngroup = function() { + var items = pg.selection.getSelectedItems(); + for(var i=0; i 0) { + return true; + } + } + return false; + }; + + return { + groupSelection: groupSelection, + ungroupSelection: ungroupSelection, + groupItems: groupItems, + ungroupItems: ungroupItems, + getItemsGroup: getItemsGroup, + isGroup: isGroup, + isGroupChild:isGroupChild, + shouldShowGroup:shouldShowGroup, + shouldShowUngroup:shouldShowUngroup + }; + +}(); \ No newline at end of file diff --git a/src/helper/guides.js b/src/helper/guides.js new file mode 100644 index 00000000..58d24fd4 --- /dev/null +++ b/src/helper/guides.js @@ -0,0 +1,184 @@ +// functions related to guide items + +pg.guides = function() { + + var guideBlue = '#009dec'; + var guideGrey = '#aaaaaa'; + + var hoverItem = function(hitResult) { + var segments = hitResult.item.segments; + var clone = new paper.Path(segments); + setDefaultGuideStyle(clone); + if(hitResult.item.closed) { + clone.closed = true; + } + clone.parent = pg.layer.getGuideLayer(); + clone.strokeColor = guideBlue; + clone.fillColor = null; + clone.data.isHelperItem = true; + clone.bringToFront(); + + return clone; + }; + + + var hoverBounds = function(item) { + var rect = new paper.Path.Rectangle(item.internalBounds); + rect.matrix = item.matrix; + setDefaultGuideStyle(rect); + rect.parent = pg.layer.getGuideLayer(); + rect.strokeColor = guideBlue; + rect.fillColor = null; + rect.data.isHelperItem = true; + rect.bringToFront(); + + return rect; + }; + + + var rectSelect = function(event, color) { + var half = new paper.Point(0.5 / paper.view.zoom, 0.5 / paper.view.zoom); + var start = event.downPoint.add(half); + var end = event.point.add(half); + var rect = new paper.Path.Rectangle(start, end); + var zoom = 1.0/paper.view.zoom; + setDefaultGuideStyle(rect); + if(!color) color = guideGrey; + rect.parent = pg.layer.getGuideLayer(); + rect.strokeColor = color; + rect.data.isRectSelect = true; + rect.data.isHelperItem = true; + rect.dashArray = [3.0*zoom, 3.0*zoom]; + return rect; + }; + + + var line = function(from, to, color) { + var line = new paper.Path.Line(from, to); + var zoom = 1/paper.view.zoom; + setDefaultGuideStyle(line); + if (!color) color = guideGrey; + line.parent = pg.layer.getGuideLayer(); + line.strokeColor = color; + line.strokeColor = color; + line.dashArray = [5*zoom, 5*zoom]; + line.data.isHelperItem = true; + return line; + }; + + + var crossPivot = function(center, color) { + var zoom = 1/paper.view.zoom; + var star = new paper.Path.Star(center, 4, 4*zoom, 0.5*zoom); + setDefaultGuideStyle(star); + if(!color) color = guideBlue; + star.parent = pg.layer.getGuideLayer(); + star.fillColor = color; + star.strokeColor = color; + star.strokeWidth = 0.5*zoom; + star.data.isHelperItem = true; + star.rotate(45); + + return star; + }; + + + var rotPivot = function(center, color) { + var zoom = 1/paper.view.zoom; + var path = new paper.Path.Circle(center, 3*zoom); + setDefaultGuideStyle(path); + if(!color) color = guideBlue; + path.parent = pg.layer.getGuideLayer(); + path.fillColor = color; + path.data.isHelperItem = true; + + return path; + }; + + + var label = function(pos, content, color) { + var text = new paper.PointText(pos); + if(!color) color = guideGrey; + text.parent = pg.layer.getGuideLayer(); + text.fillColor = color; + text.content = content; + }; + + + var setDefaultGuideStyle = function(item) { + item.strokeWidth = 1/paper.view.zoom; + item.opacity = 1; + item.blendMode = 'normal'; + item.guide = true; + }; + + + var getGuideColor = function(colorName) { + if(colorName == 'blue') { + return guideBlue; + } else if(colorName == 'grey') { + return guideGrey; + } + }; + + + var getAllGuides = function() { + var allItems = []; + for(var i=0; i Date: Mon, 11 Sep 2017 14:23:30 -0400 Subject: [PATCH 02/17] Add more selection files --- src/components/paint-editor.jsx | 4 + src/components/select-mode.jsx | 19 + src/components/stroke-width-indicator.jsx | 2 +- src/containers/select-mode.jsx | 73 ++- src/containers/selection-hov.jsx | 45 ++ src/helper/bounding-box-tool.js | 326 ------------ src/helper/bounding-box/bounding-box-tool.js | 181 +++++++ src/helper/bounding-box/move-tool.js | 69 +++ src/helper/bounding-box/rotate-tool.js | 52 ++ src/helper/bounding-box/scale-tool.js | 183 +++++++ src/helper/compound-path.js | 78 +++ src/helper/compoundPath.js | 86 ---- src/helper/group.js | 251 +++++---- src/helper/guides.js | 340 ++++++------- src/helper/helper.js | 61 +++ src/helper/hover.js | 38 +- src/helper/item.js | 78 +++ src/helper/layer.js | 18 + src/helper/math.js | 36 ++ src/helper/selection.js | 505 +++++++++++++++++++ src/index.js | 5 +- src/reducers/hover.js | 33 ++ src/reducers/scratch-paint-reducer.js | 4 +- 23 files changed, 1718 insertions(+), 769 deletions(-) create mode 100644 src/components/select-mode.jsx create mode 100644 src/containers/selection-hov.jsx delete mode 100644 src/helper/bounding-box-tool.js create mode 100644 src/helper/bounding-box/bounding-box-tool.js create mode 100644 src/helper/bounding-box/move-tool.js create mode 100644 src/helper/bounding-box/rotate-tool.js create mode 100644 src/helper/bounding-box/scale-tool.js create mode 100644 src/helper/compound-path.js delete mode 100644 src/helper/compoundPath.js create mode 100644 src/helper/helper.js create mode 100644 src/helper/item.js create mode 100644 src/helper/layer.js create mode 100644 src/helper/math.js create mode 100644 src/helper/selection.js create mode 100644 src/reducers/hover.js diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index ec653731..b565eff8 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 SelectMode from '../containers/select-mode.jsx'; import PropTypes from 'prop-types'; import LineMode from '../containers/line-mode.jsx'; import FillColorIndicatorComponent from '../containers/fill-color-indicator.jsx'; @@ -126,6 +127,9 @@ class PaintEditorComponent extends React.Component { canvas={this.state.canvas} onUpdateSvg={this.props.onUpdateSvg} /> + ) : null} diff --git a/src/components/select-mode.jsx b/src/components/select-mode.jsx new file mode 100644 index 00000000..78e976f7 --- /dev/null +++ b/src/components/select-mode.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +const SelectModeComponent = props => ( + +); + +SelectModeComponent.propTypes = { + onMouseDown: PropTypes.func.isRequired +}; + +export default SelectModeComponent; diff --git a/src/components/stroke-width-indicator.jsx b/src/components/stroke-width-indicator.jsx index 3b249830..6b5774ed 100644 --- a/src/components/stroke-width-indicator.jsx +++ b/src/components/stroke-width-indicator.jsx @@ -23,7 +23,7 @@ const StrokeWidthIndicatorComponent = props => ( StrokeWidthIndicatorComponent.propTypes = { onChangeStrokeWidth: PropTypes.func.isRequired, - strokeWidth: PropTypes.string.isRequired + strokeWidth: PropTypes.number.isRequired }; export default StrokeWidthIndicatorComponent; diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 9cabc75e..cf652b5e 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -3,11 +3,16 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; -import {clearSelection} from '../reducers/selection'; -import {setHoveredItem} from '../reducers/hover'; -import {getHoveredItem} from '../helper/hover'; + import {changeMode} from '../reducers/modes'; +import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; + +import {getHoveredItem} from '../helper/hover'; +import {rectSelect} from '../helper/guides'; +import {clearSelection, selectRootItem, processRectangularSelection} from '../helper/selection'; + 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 { @@ -33,7 +38,9 @@ class SelectMode extends React.Component { fill: true, guide: false }; - + this.boundingBoxTool = new BoundingBoxTool(); + this.selectionBoxMode = false; + this.selectionRect = null; } componentDidMount () { if (this.props.isSelectModeActive) { @@ -50,47 +57,60 @@ class SelectMode extends React.Component { shouldComponentUpdate () { return false; // Static component, for now } - getHitOptions () { + 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 () { clearSelection(); - this.preProcessSelection(); + selectRootItem(); this.tool = new paper.Tool(); this.tool.onMouseDown = function (event) { - this.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, true /* preselectedOnly */)) { + this.selectionBoxMode = true; + } }; this.tool.onMouseMove = function (event) { - this.props.setHoveredItem(getHoveredItem(this.getHitOptions())); + this.props.setHoveredItem(getHoveredItem(event, this.getHitOptions())); }; this.tool.onMouseDrag = function (event) { - this.onMouseDrag(event); + if (event.event.button > 0) return; // only first mouse button + if (this.selectionBoxMode) { + this.selectionRect = rectSelect(event); + // Remove this rect on the next drag and up event + this.selectionRect.removeOnDrag(); + } else { + this.boundingBoxTool.onMouseDrag(event); + } }; this.tool.onMouseUp = function (event) { - this.onMouseUp(event); + if (event.event.button > 0) return; // only first mouse button + if (this.selectionBoxMode) { + processRectangularSelection(event, this.selectionRect); + this.selectionRect.remove(); + } else { + this.boundingBoxTool.onMouseUp(event); + this.props.onUpdateSvg(); + } + this.selectionBoxMode = false; + this.selectionRect = null; }; this.tool.activate(); } - preProcessSelection () { - // 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 = this.props.selectedItems; - for (let item of items) { - if(isCompoundPathChild(item)) { - var cp = getItemsCompoundPath(item); - setItemSelection(item, false); - setItemSelection(cp, true); - } - }; - }; deactivateTool () { this.props.setHoveredItem(); this.tool.remove(); @@ -105,9 +125,11 @@ class SelectMode extends React.Component { } SelectMode.propTypes = { + clearHoveredItem: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, isSelectModeActive: PropTypes.bool.isRequired, - onUpdateSvg: PropTypes.func.isRequired + onUpdateSvg: PropTypes.func.isRequired, + setHoveredItem: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -117,6 +139,9 @@ const mapDispatchToProps = dispatch => ({ setHoveredItem: hoveredItem => { dispatch(setHoveredItem(hoveredItem)); }, + clearHoveredItem: () => { + dispatch(clearHoveredItem()); + }, handleMouseDown: () => { dispatch(changeMode(Modes.SELECT)); } diff --git a/src/containers/selection-hov.jsx b/src/containers/selection-hov.jsx new file mode 100644 index 00000000..23f38030 --- /dev/null +++ b/src/containers/selection-hov.jsx @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; +import paper from 'paper'; + +const SelectionHOV = function (WrappedComponent) { + class SelectionComponent extends React.Component { + componentDidMount () { + if (this.props.hoveredItem) { + paper.view.update(); + } + } + componentDidUpdate (prevProps) { + if (this.props.hoveredItem && this.props.hoveredItem !== prevProps.hoveredItem) { + // A hover item has been added. Update the view + paper.view.update(); + } else if (!this.props.hoveredItem && prevProps.hoveredItem) { + // Remove the hover item + prevProps.hoveredItem.remove(); + paper.view.update(); + } + } + render () { + const { + hoveredItem, // eslint-disable-line no-unused-vars + ...props + } = this.props; + return ( + + ); + } + } + SelectionComponent.propTypes = { + hoveredItem: PropTypes.instanceOf(paper.Item) + }; + + const mapStateToProps = state => ({ + hoveredItem: state.scratchPaint.hoveredItem + }); + return connect( + mapStateToProps + )(SelectionComponent); +}; + +export default SelectionHOV; diff --git a/src/helper/bounding-box-tool.js b/src/helper/bounding-box-tool.js deleted file mode 100644 index 4479b4a0..00000000 --- a/src/helper/bounding-box-tool.js +++ /dev/null @@ -1,326 +0,0 @@ -import paper from 'paper'; - -var mode = 'none'; -var selectionRect; - -var itemGroup; -var pivot; -var corner; -var origPivot; -var origSize; -var origCenter; -var scaleItems; -var scaleItemsInsertBelow; - -var rotItems = []; -var rotGroupPivot; -var prevRot = []; - -class BoundingBoxTool extends paper.Tool { - onMouseDown: If BoundingBoxTool got a hit result, switch to bounding box tool as the primary tool. - Else switch to the default tool. - - Where should the move tool be handled? Might make sense on bounding box tool since whenever the bounding - box is active, move is possible - - Shift button handling? If you shift click, bounding box tool wants to add it to the selection. But shape tools - probably don't. - - If shift is held down during mouse click, don't switch to the bounding box tool even if it gets a hit? - Then we can decide how to deal with it differently for different modes. - - Alt button handling? - - Same as shift? - - - - - onMouseDown (event) { - if(event.event.button > 0) return; // only first mouse button - clearHoveredItem(); - - const hitResults = paper.project.hitTestAll(event.point, this.getHitOptions()); - // Prefer scale to trigger over rotate, since their regions overlap - if (hitResults && hitResults.length > 0) { - let hitResult = hitResults[0]; - for (let i = 0; i < hitResults.length; i++) { - if (hitResults[i].item.data && hitResults[i].item.data.isScaleHandle) { - hitResult = hitResults[i]; - this.mode = 'scale'; - break; - } else if (hitResults[i].item.data && hitResults[i].item.data.isRotHandle) { - hitResult = hitResults[i]; - this.mode = 'rotate'; - } - } - if (mode === 'rotate') { - rotGroupPivot = boundsPath.bounds.center; - rotItems = pg.selection.getSelectedItems(); - - jQuery.each(rotItems, function(i, item) { - prevRot[i] = (event.point - rotGroupPivot).angle; - }); - } else if (mode === 'scale') { - var index = hitResult.item.data.index; - pivot = boundsPath.bounds[getOpposingRectCornerNameByIndex(index)].clone(); - origPivot = boundsPath.bounds[getOpposingRectCornerNameByIndex(index)].clone(); - corner = boundsPath.bounds[getRectCornerNameByIndex(index)].clone(); - origSize = corner.subtract(pivot); - origCenter = boundsPath.bounds.center; - scaleItems = pg.selection.getSelectedItems(); - } - else { // Move mode - // deselect all by default if the shift key isn't pressed - // also needs some special love for compound paths and groups, - // as their children are not marked as "selected" - // deselect a currently selected item if shift is pressed - var root = pg.item.getRootItem(hitResult.item); - if(pg.item.isCompoundPathItem(root) || pg.group.isGroup(root)) { - if(!root.selected) { - if (!event.modifiers.shift) { - pg.selection.clearSelection() - } - root.selected = true; - for (var i = 0; i < root.children.length; i++) { - root.children[i].selected = true; - } - jQuery(document).trigger('SelectionChanged'); - if(event.modifiers.alt) { - mode = 'cloneMove'; - pg.selection.cloneSelection(); - - } else { - mode = 'move'; - } - } else { - if (event.modifiers.shift) { - root.selected = false; - for (var i = 0; i < root.children.length; i++) { - root.children[i].selected = false; - } - } else { - if(event.modifiers.alt) { - mode = 'cloneMove'; - pg.selection.cloneSelection(); - - } else { - mode = 'move'; - } - } - } - } else if(hitResult.item.selected) { - if (event.modifiers.shift) { - pg.selection.setItemSelection(hitResult.item, false); - } else { - if(event.modifiers.alt) { - mode = 'cloneMove'; - pg.selection.cloneSelection(); - - } else { - mode = 'move'; - } - } - } else { - if (!event.modifiers.shift) { - pg.selection.clearSelection() - } - pg.selection.setItemSelection(hitResult.item, true); - - if(event.modifiers.alt) { - mode = 'cloneMove'; - pg.selection.cloneSelection(); - - } else { - mode = 'move'; - } - } - } - // while transforming object, never show the bounds stuff - removeBoundsPath(); - } else { - if (!event.modifiers.shift) { - removeBoundsPath(); - pg.selection.clearSelection(); - } - mode = 'rectSelection'; - } - } - onMouseDrag (event) { - if(event.event.button > 0) return; // only first mouse button - - var modOrigSize = origSize; - - if(mode == 'rectSelection') { - selectionRect = pg.guides.rectSelect(event); - // Remove this rect on the next drag and up event - selectionRect.removeOnDrag(); - - } else if(mode == 'scale') { - // get index of scale items - var items = paper.project.getItems({ - 'match': function(item) { - if (item instanceof Layer) { - return false; - } - for (var i = 0; i < scaleItems.length; i++) { - if (!scaleItems[i].isBelow(item)) { - return false; - } - } - return true; - } - }); - if (items.length > 0) { - // Lowest item above all scale items in z index - scaleItemsInsertBelow = items[0]; - } - - itemGroup = new paper.Group(scaleItems); - itemGroup.insertBelow(scaleItemsInsertBelow); - itemGroup.addChild(boundsPath); - itemGroup.data.isHelperItem = true; - itemGroup.strokeScaling = false; - itemGroup.applyMatrix = false; - - if (event.modifiers.alt) { - pivot = origCenter; - modOrigSize = origSize*0.5; - } else { - pivot = origPivot; - } - - corner = corner.add(event.delta); - var size = corner.subtract(pivot); - var sx = 1.0, sy = 1.0; - if (Math.abs(modOrigSize.x) > 0.0000001) { - sx = size.x / modOrigSize.x; - } - if (Math.abs(modOrigSize.y) > 0.0000001) { - sy = size.y / modOrigSize.y; - } - - if (event.modifiers.shift) { - var signx = sx > 0 ? 1 : -1; - var signy = sy > 0 ? 1 : -1; - sx = sy = Math.max(Math.abs(sx), Math.abs(sy)); - sx *= signx; - sy *= signy; - } - - itemGroup.scale(sx, sy, pivot); - - jQuery.each(boundsScaleHandles, function(index, handle) { - handle.position = itemGroup.bounds[getRectCornerNameByIndex(index)]; - handle.bringToFront(); - }); - - jQuery.each(boundsRotHandles, function(index, handle) { - if(handle) { - handle.position = itemGroup.bounds[getRectCornerNameByIndex(index)]+handle.data.offset; - handle.bringToFront(); - } - }); - - } else if(mode == 'rotate') { - var rotAngle = (event.point - rotGroupPivot).angle; - - jQuery.each(rotItems, function(i, item) { - - if(!item.data.origRot) { - item.data.origRot = item.rotation; - } - - if(event.modifiers.shift) { - rotAngle = Math.round(rotAngle / 45) *45; - item.applyMatrix = false; - item.pivot = rotGroupPivot; - item.rotation = rotAngle; - - } else { - item.rotate(rotAngle - prevRot[i], rotGroupPivot); - } - prevRot[i] = rotAngle; - }); - - } else if(mode == 'move' || mode == 'cloneMove') { - - var dragVector = (event.point - event.downPoint); - var selectedItems = pg.selection.getSelectedItems(); - - for(var i=0; i 0) return; // only first mouse button - - if(mode == 'rectSelection' && selectionRect) { - pg.selection.processRectangularSelection(event, selectionRect); - selectionRect.remove(); - - } else if(mode == 'move' || mode == 'cloneMove') { - - // resetting the items origin point for the next usage - var selectedItems = pg.selection.getSelectedItems(); - - jQuery.each(selectedItems, function(index, item) { - // remove the orig pos again - item.data.origPos = null; - }); - pg.undo.snapshot('moveSelection'); - - } else if(mode == 'scale') { - if (itemGroup) { - itemGroup.applyMatrix = true; - - // mark text items as scaled (for later use on font size calc) - for(var i=0; i 0) return; // only first mouse button + this._modeMap[this.mode].onMouseDrag(event); + } + onMouseUp (event) { + if (event.event.button > 0) return; // only first mouse button + this._modeMap[this.mode].onMouseUp(event); + + this.mode = null; + + if (getSelectedItems().length <= 0) { + this.removeBoundsPath(); + } else { + this.setSelectionBounds(); + } + } + setSelectionBounds () { + this.removeBoundsPath(); + + const items = getSelectedItems(); + if (items.length <= 0) return; + + let rect = null; + for (const item of items) { + if (rect) { + rect = rect.unite(item.bounds); + } else { + rect = item.bounds; + } + } + + if (!this.boundsPath) { + this.boundsPath = new paper.Path.Rectangle(rect); + this.boundsPath.curves[0].divideAtTime(0.5); + this.boundsPath.curves[2].divideAtTime(0.5); + this.boundsPath.curves[4].divideAtTime(0.5); + this.boundsPath.curves[6].divideAtTime(0.5); + } + this.boundsPath.guide = true; + this.boundsPath.data.isSelectionBound = true; + this.boundsPath.data.isHelperItem = true; + this.boundsPath.fillColor = null; + this.boundsPath.strokeScaling = false; + this.boundsPath.fullySelected = true; + this.boundsPath.parent = getGuideLayer(); + + for (let index = 0; index < this.boundsPath.segments; index++) { + const segment = this.boundsPath.segments[index]; + let size = 4; + + if (index % 2 === 0) { + size = 6; + } + + if (index === 7) { + const offset = new paper.Point(0, 20); + + const arrows = new paper.Path(ARROW_PATH); + arrows.translate(segment.point + offset + [-10.5, -5]); + + const line = new paper.Path.Rectangle( + segment.point + offset - [1, 0], + segment.point + [1, 0]); + + const rotHandle = arrows.unite(line); + line.remove(); + arrows.remove(); + rotHandle.scale(1 / paper.view.zoom, segment.point); + rotHandle.data = { + offset: offset, + isRotHandle: true, + isHelperItem: true, + noSelect: true, + noHover: true + }; + rotHandle.fillColor = getGuideColor('blue'); + rotHandle.parent = getGuideLayer(); + this.boundsRotHandles[index] = rotHandle; + } + + this.boundsScaleHandles[index] = + new paper.Path.Rectangle({ + center: segment.point, + data: { + index: index, + isScaleHandle: true, + isHelperItem: true, + noSelect: true, + noHover: true + }, + size: [size / paper.view.zoom, size / paper.view.zoom], + fillColor: getGuideColor('blue'), + parent: getGuideLayer() + }); + } + } + removeBoundsPath () { + removeHelperItems(); + this.boundsPath = null; + this.boundsScaleHandles.length = 0; + this.boundsRotHandles.length = 0; + } +} + +export default BoundingBoxTool; diff --git a/src/helper/bounding-box/move-tool.js b/src/helper/bounding-box/move-tool.js new file mode 100644 index 00000000..c21c46ca --- /dev/null +++ b/src/helper/bounding-box/move-tool.js @@ -0,0 +1,69 @@ +import {isGroup} from '../group'; +import {isCompoundPathItem, getRootItem} from '../item'; +import {snapDeltaToAngle} from '../math'; +import {clearSelection, cloneSelection, getSelectedItems, setItemSelection, setGroupSelection} 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) { + // deselect all by default if multiselect isn't on + if (!multiselect) { + clearSelection(); + } + // also needs some special love for compound paths and groups, + // as their children are not marked as "selected" + // deselect a currently selected item if multiselect is on + const root = getRootItem(hitResult.item); + if (isCompoundPathItem(root) || isGroup(root)) { + if (!root.selected) { + setGroupSelection(root, true); + } else if (multiselect) { + setGroupSelection(root, false); + } + } else if (multiselect && hitResult.item.selected) { + setItemSelection(hitResult.item, false); + } else { + setItemSelection(hitResult.item, true); + } + if (clone) cloneSelection(); + this.selectedItems = getSelectedItems(); + } + onMouseDrag (event) { + const dragVector = (event.point - 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 + + snapDeltaToAngle(dragVector, Math.PI / 4); + } else { + item.position += event.delta; + } + } + } + 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/rotate-tool.js b/src/helper/bounding-box/rotate-tool.js new file mode 100644 index 00000000..2577ad45 --- /dev/null +++ b/src/helper/bounding-box/rotate-tool.js @@ -0,0 +1,52 @@ +class RotateTool { + constructor () { + this.rotItems = []; + this.rotGroupPivot = null; + this.prevRot = []; + } + + /** + * @param {!paper.HitResult} hitResult Data about the location of the mouse click + * @param {!object} boundsPath Where the boundaries of the hit item are + * @param {!Array.} selectedItems Set of selected paper.Items + */ + onMouseDown (boundsPath, selectedItems) { + this.rotGroupPivot = boundsPath.bounds.center; + this.rotItems = selectedItems; + + for (let i = 0; i < this.rotItems.length; i++) { + this.prevRot[i] = (event.point - this.rotGroupPivot).angle; + } + } + onMouseDrag (event) { + let rotAngle = (event.point - this.rotGroupPivot).angle; + + for (let i = 0; i < this.rotItems.length; i++) { + const item = this.rotItems[i]; + + if (!item.data.origRot) { + item.data.origRot = item.rotation; + } + + if (event.modifiers.shift) { + rotAngle = Math.round(rotAngle / 45) * 45; + item.applyMatrix = false; + item.pivot = this.rotGroupPivot; + item.rotation = rotAngle; + } else { + item.rotate(rotAngle - this.prevRot[i], this.rotGroupPivot); + } + this.prevRot[i] = rotAngle; + } + } + onMouseUp (event) { + if (event.event.button > 0) return; // only first mouse button + for (const item of this.rotItems) { + item.applyMatrix = true; + } + // @todo add back undo + // pg.undo.snapshot('rotateSelection'); + } +} + +export default RotateTool; diff --git a/src/helper/bounding-box/scale-tool.js b/src/helper/bounding-box/scale-tool.js new file mode 100644 index 00000000..3358d884 --- /dev/null +++ b/src/helper/bounding-box/scale-tool.js @@ -0,0 +1,183 @@ +import paper from 'paper'; + +class ScaleTool { + constructor () { + this.pivot = null; + this.origPivot = null; + this.corner = null; + this.origSize = null; + this.origCenter = null; + this.scaleItems = null; + this.itemGroup = null; + this.boundsPath = null; + // Lowest item above all scale items in z index + this.itemToInsertBelow = null; + } + + /** + * @param {!paper.HitResult} hitResult Data about the location of the mouse click + * @param {!object} boundsPath Where the boundaries of the hit item are + * @param {!Array.} selectedItems Set of selected paper.Items + * @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, boundsPath, selectedItems) { + const index = hitResult.item.data.index; + 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; + this.boundsPath = boundsPath; + this.scaleItems = selectedItems; + } + onMouseDrag (event) { + const modOrigSize = this.origSize; + + // get item to insert below so that scaled items stay in same z position + const items = paper.project.getItems({ + match: function (item) { + if (item instanceof paper.Layer) { + return false; + } + for (const scaleItem of this.scaleItems) { + if (!scaleItem.isBelow(item)) { + return false; + } + } + return true; + } + }); + if (items.length > 0) { + this.itemToInsertBelow = items[0]; + } + + this.itemGroup = new paper.Group(this.scaleItems); + this.itemGroup.insertBelow(this.itemToInsertBelow); + this.itemGroup.addChild(this.boundsPath); + this.itemGroup.data.isHelperItem = true; + this.itemGroup.strokeScaling = false; + this.itemGroup.applyMatrix = false; + + if (event.modifiers.alt) { + this.pivot = this.origCenter; + this.modOrigSize = this.origSize * 0.5; + } else { + this.pivot = this.origPivot; + } + + this.corner = this.corner.add(event.delta); + const size = this.corner.subtract(this.pivot); + let sx = 1.0; + let sy = 1.0; + if (Math.abs(modOrigSize.x) > 0.0000001) { + sx = size.x / modOrigSize.x; + } + if (Math.abs(modOrigSize.y) > 0.0000001) { + sy = size.y / modOrigSize.y; + } + + if (event.modifiers.shift) { + const signx = sx > 0 ? 1 : -1; + const signy = sy > 0 ? 1 : -1; + sx = sy = Math.max(Math.abs(sx), Math.abs(sy)); + sx *= signx; + sy *= signy; + } + + this.itemGroup.scale(sx, sy, this.pivot); + + for (let i = 0; i < this.boundsScaleHandles.length; i++) { + const handle = this.boundsScaleHandles[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.bringToFront(); + } + } + } + onMouseUp () { + this.pivot = null; + this.origPivot = null; + this.corner = null; + this.origSize = null; + this.origCenter = null; + this.scaleItems = null; + this.boundsPath = null; + + if (!this.itemGroup) { + return; + } + + this.itemGroup.applyMatrix = true; + + // mark text items as scaled (for later use on font size calc) + for (let i = 0; i < this.itemGroup.children.length; i++) { + const child = this.itemGroup.children[i]; + if (child.data.isPGTextItem) { + child.data.wasScaled = true; + } + } + + if (this.itemToInsertBelow) { + // No increment step because itemGroup.children is getting depleted + for (const i = 0; i < this.itemGroup.children.length;) { + this.itemGroup.children[i].insertBelow(this.itemToInsertBelow); + } + this.itemToInsertBelow = null; + } else if (this.itemGroup.layer) { + this.itemGroup.layer.addChildren(this.itemGroup.children); + } + this.itemGroup.remove(); + + // @todo add back undo + // pg.undo.snapshot('scaleSelection'); + } + getRectCornerNameByIndex (index) { + switch (index) { + case 0: + return 'bottomLeft'; + case 1: + return 'leftCenter'; + case 2: + return 'topLeft'; + case 3: + return 'topCenter'; + case 4: + return 'topRight'; + case 5: + return 'rightCenter'; + case 6: + return 'bottomRight'; + case 7: + return 'bottomCenter'; + } + } + getOpposingRectCornerNameByIndex (index) { + switch (index) { + case 0: + return 'topRight'; + case 1: + return 'rightCenter'; + case 2: + return 'bottomRight'; + case 3: + return 'bottomCenter'; + case 4: + return 'bottomLeft'; + case 5: + return 'leftCenter'; + case 6: + return 'topLeft'; + case 7: + return 'topCenter'; + } + } +} + +export default ScaleTool; diff --git a/src/helper/compound-path.js b/src/helper/compound-path.js new file mode 100644 index 00000000..89e51b8e --- /dev/null +++ b/src/helper/compound-path.js @@ -0,0 +1,78 @@ +const isCompoundPath = function (item) { + return item && item.className === 'CompoundPath'; +}; + +const isCompoundPathChild = function (item) { + if (item.parent) { + return item.parent.className === 'CompoundPath'; + } + return false; +}; + + +const getItemsCompoundPath = function (item) { + const itemParent = item.parent; + + if (isCompoundPath(itemParent)) { + return itemParent; + } + return null; + +}; + + +// const createFromSelection = function () { +// const items = getSelectedPaths(); +// if (items.length < 2) return; + +// const path = new paper.CompoundPath({fillRule: 'evenodd'}); + +// for (let i = 0; i < items.length; i++) { +// path.addChild(items[i]); +// items[i].selected = false; +// } + +// path = pg.stylebar.applyActiveToolbarStyle(path); + +// pg.selection.setItemSelection(path, true); +// pg.undo.snapshot('createCompoundPathFromSelection'); +// }; + + +// const releaseSelection = function () { +// const items = pg.selection.getSelectedItems(); + +// const cPathsToDelete = []; +// for (const i=0; i 0) { - var group = new paper.Group(items); - pg.selection.clearSelection(); - pg.selection.setItemSelection(group, true); - for (var i = 0; i < group.children.length; i++) { - group.children[i].selected = true; - } - pg.undo.snapshot('groupSelection'); - jQuery(document).trigger('Grouped'); - return group; - } else { - return false; - } - }; - - - var ungroupSelection = function() { - var items = pg.selection.getSelectedItems(); - ungroupItems(items); - pg.statusbar.update(); - }; - - - var groupItems = function(items) { - if(items.length > 0) { - var group = new paper.Group(items); - jQuery(document).trigger('Grouped'); - pg.undo.snapshot('groupItems'); - return group; - } else { - return false; - } - }; +const groupSelection = function () { + const items = getSelectedItems(); + if (items.length > 0) { + const group = new paper.Group(items); + clearSelection(); + setItemSelection(group, true); + for (let i = 0; i < group.children.length; i++) { + group.children[i].selected = true; + } + // jQuery(document).trigger('Grouped'); + // @todo add back undo + // pg.undo.snapshot('groupSelection'); + return group; + } + return false; +}; + +const ungroupLoop = function (group, recursive) { + // don't ungroup items that are not groups + if (!group || !group.children || !isGroup(group)) return; + + group.applyMatrix = true; + // iterate over group children recursively + for (let i = 0; i < group.children.length; i++) { + const groupChild = group.children[i]; + if (groupChild.hasChildren()) { + // recursion (groups can contain groups, ie. from SVG import) + if (recursive) { + ungroupLoop(groupChild, true /* recursive */); + continue; + } + } + groupChild.applyMatrix = true; + // move items from the group to the activeLayer (ungrouping) + groupChild.insertBelow(group); + groupChild.selected = true; + i--; + } +}; + +// ungroup items (only top hierarchy) +const ungroupItems = function (items) { + clearSelection(); + const emptyGroups = []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (isGroup(item) && !item.data.isPGTextItem) { + ungroupLoop(item, false /* recursive */); + + if (!item.hasChildren()) { + emptyGroups.push(item); + } + } + } + + // remove all empty groups after ungrouping + for (let j = 0; j < emptyGroups.length; j++) { + emptyGroups[j].remove(); + } + // jQuery(document).trigger('Ungrouped'); + // @todo add back undo + // pg.undo.snapshot('ungroupItems'); +}; + +const ungroupSelection = function () { + const items = getSelectedItems(); + ungroupItems(items); + + // pg.statusbar.update(); +}; - // ungroup items (only top hierarchy) - var ungroupItems = function(items) { - pg.selection.clearSelection(); - var emptyGroups = []; - for(var i=0; i 0) { + const group = new paper.Group(items); + // jQuery(document).trigger('Grouped'); + // @todo add back undo + // pg.undo.snapshot('groupItems'); + return group; + } + return false; +}; - if(!item.hasChildren()) { - emptyGroups.push(item); - } - } - } +const getItemsGroup = function (item) { + const itemParent = item.parent; - // remove all empty groups after ungrouping - for(var j=0; j 1; +}; +const shouldShowUngroup = function () { + const items = getSelectedItems(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (isGroup(item) && !item.data.isPGTextItem && item.children && item.children.length > 0) { + return true; + } + } + return false; +}; - var getItemsGroup = function(item) { - var itemParent = item.parent; - - if(isGroup(itemParent)) { - return itemParent; - } else { - return null; - } - }; - - - var isGroup = function(item) { - return pg.item.isGroupItem(item); - }; - - - var isGroupChild = function(item) { - var rootItem = pg.item.getRootItem(item); - return isGroup(rootItem); - }; - - var shouldShowGroup = function() { - var items = pg.selection.getSelectedItems(); - return items.length > 1; - }; - - var shouldShowUngroup = function() { - var items = pg.selection.getSelectedItems(); - for(var i=0; i 0) { - return true; - } - } - return false; - }; - - return { - groupSelection: groupSelection, - ungroupSelection: ungroupSelection, - groupItems: groupItems, - ungroupItems: ungroupItems, - getItemsGroup: getItemsGroup, - isGroup: isGroup, - isGroupChild:isGroupChild, - shouldShowGroup:shouldShowGroup, - shouldShowUngroup:shouldShowUngroup - }; - -}(); \ No newline at end of file +export { + groupSelection, + ungroupSelection, + groupItems, + ungroupItems, + getItemsGroup, + isGroup, + isGroupChild, + shouldShowGroup, + shouldShowUngroup +}; diff --git a/src/helper/guides.js b/src/helper/guides.js index 58d24fd4..ec4be677 100644 --- a/src/helper/guides.js +++ b/src/helper/guides.js @@ -1,184 +1,172 @@ -// functions related to guide items +import paper from 'paper'; +import {getGuideLayer} from './layer'; +import {removePaperItemsByTags, removePaperItemsByDataTags} from './helper'; -pg.guides = function() { - - var guideBlue = '#009dec'; - var guideGrey = '#aaaaaa'; - - var hoverItem = function(hitResult) { - var segments = hitResult.item.segments; - var clone = new paper.Path(segments); - setDefaultGuideStyle(clone); - if(hitResult.item.closed) { - clone.closed = true; - } - clone.parent = pg.layer.getGuideLayer(); - clone.strokeColor = guideBlue; - clone.fillColor = null; - clone.data.isHelperItem = true; - clone.bringToFront(); +const GUIDE_BLUE = '#009dec'; +const GUIDE_GREY = '#aaaaaa'; - return clone; - }; - - - var hoverBounds = function(item) { - var rect = new paper.Path.Rectangle(item.internalBounds); - rect.matrix = item.matrix; - setDefaultGuideStyle(rect); - rect.parent = pg.layer.getGuideLayer(); - rect.strokeColor = guideBlue; - rect.fillColor = null; - rect.data.isHelperItem = true; - rect.bringToFront(); +const setDefaultGuideStyle = function (item) { + item.strokeWidth = 1 / paper.view.zoom; + item.opacity = 1; + item.blendMode = 'normal'; + item.guide = true; +}; - return rect; - }; - - - var rectSelect = function(event, color) { - var half = new paper.Point(0.5 / paper.view.zoom, 0.5 / paper.view.zoom); - var start = event.downPoint.add(half); - var end = event.point.add(half); - var rect = new paper.Path.Rectangle(start, end); - var zoom = 1.0/paper.view.zoom; - setDefaultGuideStyle(rect); - if(!color) color = guideGrey; - rect.parent = pg.layer.getGuideLayer(); - rect.strokeColor = color; - rect.data.isRectSelect = true; - rect.data.isHelperItem = true; - rect.dashArray = [3.0*zoom, 3.0*zoom]; - return rect; - }; - - - var line = function(from, to, color) { - var line = new paper.Path.Line(from, to); - var zoom = 1/paper.view.zoom; - setDefaultGuideStyle(line); - if (!color) color = guideGrey; - line.parent = pg.layer.getGuideLayer(); - line.strokeColor = color; - line.strokeColor = color; - line.dashArray = [5*zoom, 5*zoom]; - line.data.isHelperItem = true; - return line; - }; +const hoverItem = function (hitResult) { + const segments = hitResult.item.segments; + const clone = new paper.Path(segments); + setDefaultGuideStyle(clone); + if (hitResult.item.closed) { + clone.closed = true; + } + clone.parent = getGuideLayer(); + clone.strokeColor = GUIDE_BLUE; + clone.fillColor = null; + clone.data.isHelperItem = true; + clone.bringToFront(); + + return clone; +}; + +const hoverBounds = function (item) { + const rect = new paper.Path.Rectangle(item.internalBounds); + rect.matrix = item.matrix; + setDefaultGuideStyle(rect); + rect.parent = getGuideLayer(); + rect.strokeColor = GUIDE_BLUE; + rect.fillColor = null; + rect.data.isHelperItem = true; + rect.bringToFront(); + + return rect; +}; + +const rectSelect = function (event, color) { + const half = new paper.Point(0.5 / paper.view.zoom, 0.5 / paper.view.zoom); + const start = event.downPoint.add(half); + const end = event.point.add(half); + const rect = new paper.Path.Rectangle(start, end); + const zoom = 1.0 / paper.view.zoom; + setDefaultGuideStyle(rect); + if (!color) color = GUIDE_GREY; + rect.parent = getGuideLayer(); + rect.strokeColor = color; + rect.data.isRectSelect = true; + rect.data.isHelperItem = true; + rect.dashArray = [3.0 * zoom, 3.0 * zoom]; + return rect; +}; + +const line = function (from, to, color) { + const theLine = new paper.Path.Line(from, to); + const zoom = 1 / paper.view.zoom; + setDefaultGuideStyle(theLine); + if (!color) color = GUIDE_GREY; + theLine.parent = getGuideLayer(); + theLine.strokeColor = color; + theLine.strokeColor = color; + theLine.dashArray = [5 * zoom, 5 * zoom]; + theLine.data.isHelperItem = true; + return theLine; +}; + +const crossPivot = function (center, color) { + const zoom = 1 / paper.view.zoom; + const star = new paper.Path.Star(center, 4, 4 * zoom, 0.5 * zoom); + setDefaultGuideStyle(star); + if (!color) color = GUIDE_BLUE; + star.parent = getGuideLayer(); + star.fillColor = color; + star.strokeColor = color; + star.strokeWidth = 0.5 * zoom; + star.data.isHelperItem = true; + star.rotate(45); + + return star; +}; + +const rotPivot = function (center, color) { + const zoom = 1 / paper.view.zoom; + const path = new paper.Path.Circle(center, 3 * zoom); + setDefaultGuideStyle(path); + if (!color) color = GUIDE_BLUE; + path.parent = getGuideLayer(); + path.fillColor = color; + path.data.isHelperItem = true; + + return path; +}; + +const label = function (pos, content, color) { + const text = new paper.PointText(pos); + if (!color) color = GUIDE_GREY; + text.parent = getGuideLayer(); + text.fillColor = color; + text.content = content; +}; + +const getGuideColor = function (colorName) { + if (colorName === 'blue') { + return GUIDE_BLUE; + } else if (colorName === 'grey') { + return GUIDE_GREY; + } +}; + +const getAllGuides = function () { + const allItems = []; + for (let i = 0; i < paper.project.layers.length; i++) { + const layer = paper.project.layers[i]; + for (let j = 0; j < layer.children.length; j++) { + const child = layer.children[j]; + // only give guides + if (!child.guide) { + continue; + } + allItems.push(child); + } + } + return allItems; +}; + +const getExportRectGuide = function () { + const guides = getAllGuides(); + for (let i = 0; i < guides.length; i++){ + if (guides[i].data && guides[i].data.isExportRect) { + return guides[i]; + } + } +}; - var crossPivot = function(center, color) { - var zoom = 1/paper.view.zoom; - var star = new paper.Path.Star(center, 4, 4*zoom, 0.5*zoom); - setDefaultGuideStyle(star); - if(!color) color = guideBlue; - star.parent = pg.layer.getGuideLayer(); - star.fillColor = color; - star.strokeColor = color; - star.strokeWidth = 0.5*zoom; - star.data.isHelperItem = true; - star.rotate(45); +const removeHelperItems = function () { + removePaperItemsByDataTags(['isHelperItem']); +}; - return star; - }; - - - var rotPivot = function(center, color) { - var zoom = 1/paper.view.zoom; - var path = new paper.Path.Circle(center, 3*zoom); - setDefaultGuideStyle(path); - if(!color) color = guideBlue; - path.parent = pg.layer.getGuideLayer(); - path.fillColor = color; - path.data.isHelperItem = true; - return path; - }; - - - var label = function(pos, content, color) { - var text = new paper.PointText(pos); - if(!color) color = guideGrey; - text.parent = pg.layer.getGuideLayer(); - text.fillColor = color; - text.content = content; - }; - - - var setDefaultGuideStyle = function(item) { - item.strokeWidth = 1/paper.view.zoom; - item.opacity = 1; - item.blendMode = 'normal'; - item.guide = true; - }; - - - var getGuideColor = function(colorName) { - if(colorName == 'blue') { - return guideBlue; - } else if(colorName == 'grey') { - return guideGrey; - } - }; - - - var getAllGuides = function() { - var allItems = []; - for(var i=0; i 0) { + selectItemSegments(child, state); + } else { + child.fullySelected = state; + } + } + } else { + for (let i = 0; i < item.segments.length; i++) { + item.segments[i].selected = state; + } + } +}; + +const setGroupSelection = function (root, selected) { + // fully selected segments need to be unselected first + root.fullySelected = false; + // then the item can be normally selected + 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; + } + } + } +}; + +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 (parentGroup) { + // do it recursive + setItemSelection(parentGroup, state); + + } else if (itemsCompoundPath) { + setItemSelection(itemsCompoundPath, state); + + } else { + if (item.data && item.data.noSelect) { + return; + } + setGroupSelection(item, state); + } + // pg.statusbar.update(); + // pg.stylebar.updateFromSelection(); + // pg.stylebar.blurInputs(); + + // jQuery(document).trigger('SelectionChanged'); + +}; + +const selectAllItems = function () { + const items = getAllSelectableItems(); + + for (let i = 0; i < items.length; i++) { + setItemSelection(items[i], true); + } +}; + +const selectAllSegments = function () { + const items = getAllSelectableItems(); + + for (let i = 0; i < items.length; i++) { + selectItemSegments(items[i], true); + } +}; + +const clearSelection = function () { + paper.project.deselectAll(); + + // pg.statusbar.update(); + // pg.stylebar.blurInputs(); + // jQuery(document).trigger('SelectionChanged'); +}; + +// this gets all selected non-grouped items and groups +// (alternative to paper.project.selectedItems, which includes +// group children in addition to the group) +// Returns in increasing Z order +const getSelectedItems = function (recursive) { + const allItems = paper.project.selectedItems; + const itemsAndGroups = []; + + if (recursive) { + for (let i = 0; i < allItems.length; i++) { + const item = allItems[i]; + if (item.data && !item.data.isSelectionBound) { + itemsAndGroups.push(item); + } + } + } else { + for (let i = 0; i < allItems.length; i++) { + const item = allItems[i]; + if ((isGroup(item) && !isGroup(item.parent)) || + !isGroup(item.parent)) { + if (item.data && !item.data.isSelectionBound) { + itemsAndGroups.push(item); + } + } + } + } + // sort items by index (0 at bottom) + itemsAndGroups.sort((a, b) => parseFloat(a.index) - parseFloat(b.index)); + return itemsAndGroups; +}; + +const deleteItemSelection = function () { + const items = getSelectedItems(); + for (let i = 0; i < items.length; i++) { + items[i].remove(); + } + + // jQuery(document).trigger('DeleteItems'); + // jQuery(document).trigger('SelectionChanged'); + paper.project.view.update(); + // @todo add back undo + // pg.undo.snapshot('deleteItemSelection'); +}; + +const removeSelectedSegments = function () { + // @todo add back undo + // pg.undo.snapshot('removeSelectedSegments'); + + const items = getSelectedItems(); + const segmentsToRemove = []; + + for (let i = 0; i < items.length; i++) { + const segments = items[i].segments; + for (let j = 0; j < segments.length; j++) { + const seg = segments[j]; + if (seg.selected) { + segmentsToRemove.push(seg); + } + } + } + + let removedSegments = false; + for (let i = 0; i < segmentsToRemove.length; i++) { + const seg = segmentsToRemove[i]; + seg.remove(); + removedSegments = true; + } + return removedSegments; +}; + +const deleteSelection = function (mode) { + if (mode === Modes.RESHAPE) { + // If there are points selected remove them. If not delete the item selected. + if (!removeSelectedSegments()) { + deleteItemSelection(); + } + } else { + deleteItemSelection(); + } +}; + +const splitPathRetainSelection = function (path, index, deselectSplitSegments) { + const selectedPoints = []; + + // collect points of selected segments, so we can reselect them + // once the path is split. + for (let i = 0; i < path.segments.length; i++) { + const seg = path.segments[i]; + if (seg.selected) { + if (deselectSplitSegments && i === index) { + continue; + } + selectedPoints.push(seg.point); + } + } + + const newPath = path.split(index, 0); + if (!newPath) return; + + // reselect all of the newPaths segments that are in the exact same location + // as the ones that are stored in selectedPoints + for (let i = 0; i < newPath.segments.length; i++) { + const seg = newPath.segments[i]; + for (let j = 0; j < selectedPoints.length; j++) { + const point = selectedPoints[j]; + if (point.x === seg.point.x && point.y === seg.point.y) { + seg.selected = true; + } + } + } + + // only do this if path and newPath are different + // (split at more than one point) + if (path !== newPath) { + for (let i = 0; i < path.segments.length; i++) { + const seg = path.segments[i]; + for (let j = 0; j < selectedPoints.length; j++) { + const point = selectedPoints[j]; + if (point.x === seg.point.x && point.y === seg.point.y) { + seg.selected = true; + } + } + } + } +}; + +const splitPathAtSelectedSegments = function () { + const items = getSelectedItems(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const segments = item.segments; + for (let j = 0; j < segments.length; j++) { + const segment = segments[j]; + if (segment.selected) { + if (item.closed || + (segment.next && + !segment.next.selected && + segment.previous && + !segment.previous.selected)) { + splitPathRetainSelection(item, j, true); + splitPathAtSelectedSegments(); + return; + } + } + } + } +}; + +const deleteSegments = function (item) { + if (item.children) { + for (let i = 0; i < item.children.length; i++) { + const child = item.children[i]; + deleteSegments(child); + } + } else { + const segments = item.segments; + for (let j = 0; j < segments.length; j++) { + const segment = segments[j]; + if (segment.selected) { + if (item.closed || + (segment.next && + !segment.next.selected && + segment.previous && + !segment.previous.selected)) { + + splitPathRetainSelection(item, j); + deleteSelection(); + return; + + } else if (!item.closed) { + segment.remove(); + j--; // decrease counter if we removed one from the loop + } + + } + } + } + // remove items with no segments left + if (item.segments.length <= 0) { + item.remove(); + } +}; + +const deleteSegmentSelection = function () { + + const items = getSelectedItems(); + for (let i = 0; i < items.length; i++) { + deleteSegments(items[i]); + } + + // jQuery(document).trigger('DeleteSegments'); + // jQuery(document).trigger('SelectionChanged'); + paper.project.view.update(); + // @todo add back undo + // pg.undo.snapshot('deleteSegmentSelection'); +}; + +const cloneSelection = function () { + const selectedItems = getSelectedItems(); + for (let i = 0; i < selectedItems.length; i++) { + const item = selectedItems[i]; + item.clone(); + item.selected = false; + } + // @todo add back undo + // pg.undo.snapshot('cloneSelection'); +}; + +// only returns paths, no compound paths, groups or any other stuff +const getSelectedPaths = function () { + const allPaths = getSelectedItems(); + const paths = []; + + for (let i = 0; i < allPaths.length; i++) { + const path = allPaths[i]; + if (path.className === 'Path') { + paths.push(path); + } + } + 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; + + 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; + + } + } + + itemBounds.remove(); +}; + +const handleRectangularSelectionItems = function (item, event, rect, mode) { + if (isPathItem(item)) { + let segmentMode = false; + + // first round checks for segments inside the selectionRect + for (let j = 0; j < item.segments.length; j++) { + const seg = item.segments[j]; + if (rect.contains(seg.point)) { + if (mode === 'detail') { + if (event.modifiers.shift && seg.selected) { + seg.selected = false; + } else { + seg.selected = true; + } + segmentMode = true; + + } else { + if (event.modifiers.shift && item.selected) { + setItemSelection(item, false); + + } else { + setItemSelection(item, true); + } + return false; + } + } + } + + // 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 + // with the selectionRect + if (mode === 'detail') { + for (let k = 0; k < intersections.length; k++) { + const curve = intersections[k].curve; + // intersections contains every curve twice because + // the selectionRect intersects a circle always at + // two points. so we skip every other curve + if (k % 2 === 1) { + continue; + } + + if (event.modifiers.shift) { + curve.selected = !curve.selected; + } else { + curve.selected = true; + } + } + + } else { + if (event.modifiers.shift && item.selected) { + setItemSelection(item, false); + + } else { + setItemSelection(item, true); + } + return false; + } + } + // pg.statusbar.update(); + + } else if (isBoundsItem(item)) { + if (checkBoundsItem(rect, item, event)) { + return false; + } + } + return true; +}; + +// if the rectangular selection found a group, drill into it recursively +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); + + } else if (!handleRectangularSelectionItems(child, event, rect, mode)) { + return false; + } + } + return true; +}; + +const processRectangularSelection = function (event, rect, mode) { + const allItems = getAllSelectableItems(); + + itemLoop: + for (let i = 0; i < allItems.length; i++) { + const item = allItems[i]; + if (mode === 'detail' && isPGTextItem(getRootItem(item))) { + continue itemLoop; + } + // 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; + } + } +}; + +const selectRootItem = function () { + // when switching to the select tool while having a child object of a + // compound path selected, deselect the child and select the compound path + // instead. (otherwise the compound path breaks because of scale-grouping) + const items = getSelectedItems(); + for (const item of items) { + if (isCompoundPathChild(item)) { + const cp = getItemsCompoundPath(item); + setItemSelection(item, false); + setItemSelection(cp, true); + } + } +}; + +const shouldShowIfSelection = function () { + return getSelectedItems().length > 0; +}; + +const shouldShowIfSelectionRecursive = function () { + return getSelectedItems(true /* recursive */).length > 0; +}; + +const shouldShowSelectAll = function () { + return paper.project.getItems({class: paper.PathItem}).length > 0; +}; + +export { + selectAllItems, + selectAllSegments, + clearSelection, + deleteSelection, + deleteItemSelection, + deleteSegmentSelection, + splitPathAtSelectedSegments, + cloneSelection, + setItemSelection, + setGroupSelection, + getSelectedItems, + getSelectedPaths, + removeSelectedSegments, + processRectangularSelection, + selectRootItem, + shouldShowIfSelection, + shouldShowIfSelectionRecursive, + shouldShowSelectAll +}; diff --git a/src/index.js b/src/index.js index b7d91a70..2c655e14 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,10 @@ import PaintEditor from './containers/paint-editor.jsx'; +import SelectionHOV from './containers/selection-hov.jsx'; import ScratchPaintReducer from './reducers/scratch-paint-reducer'; +const Wrapped = SelectionHOV(PaintEditor); + export { - PaintEditor as default, + Wrapped as default, ScratchPaintReducer }; diff --git a/src/reducers/hover.js b/src/reducers/hover.js new file mode 100644 index 00000000..552ccece --- /dev/null +++ b/src/reducers/hover.js @@ -0,0 +1,33 @@ +const CHANGE_HOVERED = 'scratch-paint/hover/CHANGE_HOVERED'; +const initialState = null; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case CHANGE_HOVERED: + return action.hoveredItem; + default: + return state; + } +}; + +// Action creators ================================== +const setHoveredItem = function (hoveredItem) { + return { + type: CHANGE_HOVERED, + hoveredItem: hoveredItem + }; +}; + +const clearHoveredItem = function () { + return { + type: CHANGE_HOVERED, + hoveredItem: null + }; +}; + +export { + reducer as default, + setHoveredItem, + clearHoveredItem +}; diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 6e637526..3dfabe6b 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -3,10 +3,12 @@ import modeReducer from './modes'; import brushModeReducer from './brush-mode'; import eraserModeReducer from './eraser-mode'; import colorReducer from './color'; +import hoverReducer from './hover'; export default combineReducers({ mode: modeReducer, brushMode: brushModeReducer, eraserMode: eraserModeReducer, - color: colorReducer + color: colorReducer, + hoveredItem: hoverReducer }); From 3126c2ca7ff41b5a8f68fd087bd697e983a0e282 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 12 Sep 2017 17:43:26 -0400 Subject: [PATCH 03/17] correct stroke width type" --- src/containers/brush-mode.jsx | 2 +- src/containers/line-mode.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index 3142529b..bf1b372a 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -80,7 +80,7 @@ BrushMode.propTypes = { colorState: PropTypes.shape({ fillColor: PropTypes.string.isRequired, strokeColor: PropTypes.string.isRequired, - strokeWidth: PropTypes.string.isRequired + strokeWidth: PropTypes.number.isRequired }).isRequired, handleMouseDown: PropTypes.func.isRequired, isBrushModeActive: PropTypes.bool.isRequired, diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index d1f00b0c..35e47b53 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -272,7 +272,7 @@ LineMode.propTypes = { colorState: PropTypes.shape({ fillColor: PropTypes.string.isRequired, strokeColor: PropTypes.string.isRequired, - strokeWidth: PropTypes.string.isRequired + strokeWidth: PropTypes.number.isRequired }).isRequired, handleMouseDown: PropTypes.func.isRequired, isLineModeActive: PropTypes.bool.isRequired, From 0e91439eddb013980c57280a5f644f12366dd82f Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 13 Sep 2017 15:17:59 -0400 Subject: [PATCH 04/17] Get move working --- src/containers/paper-canvas.css | 1 + src/containers/select-mode.jsx | 65 ++++++++++++-------- src/helper/bounding-box/bounding-box-tool.js | 25 ++++---- src/helper/bounding-box/move-tool.js | 34 ++++------ src/helper/hover.js | 2 +- src/helper/layer.js | 2 +- src/modes/modes.js | 3 +- src/reducers/hover.js | 6 ++ 8 files changed, 75 insertions(+), 63 deletions(-) diff --git a/src/containers/paper-canvas.css b/src/containers/paper-canvas.css index d2f149fa..16edbb7a 100644 --- a/src/containers/paper-canvas.css +++ b/src/containers/paper-canvas.css @@ -4,4 +4,5 @@ margin: auto; position: relative; background-color: #fff; + image-rendering: pixelated; } diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index cf652b5e..79dbe330 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -24,12 +24,7 @@ class SelectMode extends React.Component { bindAll(this, [ 'activateTool', 'deactivateTool', - 'getHitOptions', - 'preProcessSelection', - 'onMouseDown', - 'onMouseMove', - 'onMouseDrag', - 'onMouseUp' + 'getHitOptions' ]); this._hitOptions = { segments: true, @@ -71,48 +66,66 @@ class SelectMode extends React.Component { selectRootItem(); this.tool = new paper.Tool(); - + const selectMode = this; this.tool.onMouseDown = function (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, true /* preselectedOnly */)) { - this.selectionBoxMode = true; + + 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) { - this.props.setHoveredItem(getHoveredItem(event, this.getHitOptions())); + 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 + if (oldHoveredItem) { + oldHoveredItem.remove(); + } + selectMode.props.setHoveredItem(hoveredItem); + } }; this.tool.onMouseDrag = function (event) { if (event.event.button > 0) return; // only first mouse button - if (this.selectionBoxMode) { - this.selectionRect = rectSelect(event); + + if (selectMode.selectionBoxMode) { + selectMode.selectionRect = rectSelect(event); // Remove this rect on the next drag and up event - this.selectionRect.removeOnDrag(); + selectMode.selectionRect.removeOnDrag(); } else { - this.boundingBoxTool.onMouseDrag(event); + selectMode.boundingBoxTool.onMouseDrag(event); } }; this.tool.onMouseUp = function (event) { if (event.event.button > 0) return; // only first mouse button - if (this.selectionBoxMode) { - processRectangularSelection(event, this.selectionRect); - this.selectionRect.remove(); + + if (selectMode.selectionBoxMode) { + if (selectMode.selectionRect) { + processRectangularSelection(event, selectMode.selectionRect); + selectMode.selectionRect.remove(); + } + selectMode.boundingBoxTool.setSelectionBounds(); } else { - this.boundingBoxTool.onMouseUp(event); - this.props.onUpdateSvg(); + selectMode.boundingBoxTool.onMouseUp(event); + selectMode.props.onUpdateSvg(); } - this.selectionBoxMode = false; - this.selectionRect = null; + selectMode.selectionBoxMode = false; + selectMode.selectionRect = null; }; this.tool.activate(); } deactivateTool () { - this.props.setHoveredItem(); + this.props.clearHoveredItem(); this.tool.remove(); this.tool = null; this.hitResult = null; @@ -127,13 +140,15 @@ class SelectMode extends React.Component { SelectMode.propTypes = { clearHoveredItem: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, + hoveredItem: PropTypes.instanceOf(paper.Item), isSelectModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, setHoveredItem: PropTypes.func.isRequired }; const mapStateToProps = state => ({ - isSelectModeActive: state.scratchPaint.mode === Modes.SELECT + isSelectModeActive: state.scratchPaint.mode === Modes.SELECT, + hoveredItem: state.scratchPaint.hoveredItem }); const mapDispatchToProps = dispatch => ({ setHoveredItem: hoveredItem => { diff --git a/src/helper/bounding-box/bounding-box-tool.js b/src/helper/bounding-box/bounding-box-tool.js index 457e83f0..cf9aceae 100644 --- a/src/helper/bounding-box/bounding-box-tool.js +++ b/src/helper/bounding-box/bounding-box-tool.js @@ -40,17 +40,18 @@ class BoundingBoxTool { * @param {!MouseEvent} event The mouse event * @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) - * @param {boolean} preselectedOnly If true, only get hit results on items that are already selected + * @param {paper.hitOptions} hitOptions The options with which to detect whether mouse down has hit + * anything editable * @return {boolean} True if there was a hit, false otherwise */ - onMouseDown (event, clone, multiselect, preselectedOnly) { - const hitResults = paper.project.hitTestAll(event.point, this.getHitOptions(preselectedOnly)); + onMouseDown (event, clone, multiselect, hitOptions) { + const hitResults = paper.project.hitTestAll(event.point, hitOptions); if (!hitResults || hitResults.length === 0) { if (!multiselect) { this.removeBoundsPath(); clearSelection(); } - return null; + return false; } // Prefer scale to trigger over rotate, and scale and rotate to trigger over other hits @@ -65,14 +66,17 @@ class BoundingBoxTool { hitResult = hitResults[i]; this.mode = Modes.ROTATE; this._modeMap[this.mode].onMouseDown(hitResult, this.boundsPath, getSelectedItems()); - } else { - this.mode = Modes.MOVE; - this._modeMap[this.mode].onMouseDown(hitResult, clone, multiselect); } } + if (!this.mode) { + this.mode = Modes.MOVE; + this._modeMap[this.mode].onMouseDown(hitResult, clone, multiselect); + } + // while transforming object, never show the bounds stuff this.removeBoundsPath(); + return true; } onMouseDrag (event) { if (event.event.button > 0) return; // only first mouse button @@ -83,12 +87,7 @@ class BoundingBoxTool { this._modeMap[this.mode].onMouseUp(event); this.mode = null; - - if (getSelectedItems().length <= 0) { - this.removeBoundsPath(); - } else { - this.setSelectionBounds(); - } + this.setSelectionBounds(); } setSelectionBounds () { this.removeBoundsPath(); diff --git a/src/helper/bounding-box/move-tool.js b/src/helper/bounding-box/move-tool.js index c21c46ca..6303d6cd 100644 --- a/src/helper/bounding-box/move-tool.js +++ b/src/helper/bounding-box/move-tool.js @@ -1,7 +1,7 @@ import {isGroup} from '../group'; import {isCompoundPathItem, getRootItem} from '../item'; import {snapDeltaToAngle} from '../math'; -import {clearSelection, cloneSelection, getSelectedItems, setItemSelection, setGroupSelection} from '../selection'; +import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; class MoveTool { constructor () { @@ -14,31 +14,22 @@ class MoveTool { * @param {boolean} multiselect Whether to multiselect on mouse down (e.g. shift key held) */ onMouseDown (hitResult, clone, multiselect) { - // deselect all by default if multiselect isn't on - if (!multiselect) { - clearSelection(); - } - // also needs some special love for compound paths and groups, - // as their children are not marked as "selected" - // deselect a currently selected item if multiselect is on const root = getRootItem(hitResult.item); - if (isCompoundPathItem(root) || isGroup(root)) { - if (!root.selected) { - setGroupSelection(root, true); - } else if (multiselect) { - setGroupSelection(root, false); + const item = isCompoundPathItem(root) || isGroup(root) ? root : hitResult.item; + if (!item.selected) { + // deselect all by default if multiselect isn't on + if (!multiselect) { + clearSelection(); } - } else if (multiselect && hitResult.item.selected) { - setItemSelection(hitResult.item, false); - } else { - setItemSelection(hitResult.item, true); + setItemSelection(item, true); + } else if (multiselect) { + setItemSelection(item, false); } if (clone) cloneSelection(); this.selectedItems = getSelectedItems(); } onMouseDrag (event) { - const dragVector = (event.point - event.downPoint); - + 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 @@ -47,10 +38,9 @@ class MoveTool { } if (event.modifiers.shift) { - item.position = item.data.origPos + - snapDeltaToAngle(dragVector, Math.PI / 4); + item.position = item.data.origPos.add(snapDeltaToAngle(dragVector, Math.PI / 4)); } else { - item.position += event.delta; + item.position = item.data.origPos.add(dragVector); } } } diff --git a/src/helper/hover.js b/src/helper/hover.js index ee326b8e..b4c0bc1f 100644 --- a/src/helper/hover.js +++ b/src/helper/hover.js @@ -16,7 +16,7 @@ const getHoveredItem = function (event, hitOptions) { let hitResult; for (const result of hitResults) { - if (!(result.item.data && result.item.data.noHover) && !hitResult.item.selected) { + if (!(result.item.data && result.item.data.noHover) && !result.item.selected) { hitResult = result; break; } diff --git a/src/helper/layer.js b/src/helper/layer.js index 404196f3..0e33649a 100644 --- a/src/helper/layer.js +++ b/src/helper/layer.js @@ -15,4 +15,4 @@ const getGuideLayer = function () { return guideLayer; }; -export default getGuideLayer; +export {getGuideLayer}; diff --git a/src/modes/modes.js b/src/modes/modes.js index c1b64738..82394802 100644 --- a/src/modes/modes.js +++ b/src/modes/modes.js @@ -3,7 +3,8 @@ import keyMirror from 'keymirror'; const Modes = keyMirror({ BRUSH: null, ERASER: null, - LINE: null + LINE: null, + SELECT: null }); export default Modes; diff --git a/src/reducers/hover.js b/src/reducers/hover.js index 552ccece..0c4353b0 100644 --- a/src/reducers/hover.js +++ b/src/reducers/hover.js @@ -1,3 +1,5 @@ +import log from '../log/log'; + const CHANGE_HOVERED = 'scratch-paint/hover/CHANGE_HOVERED'; const initialState = null; @@ -5,6 +7,10 @@ const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { case CHANGE_HOVERED: + if (typeof action.hoveredItem === 'undefined') { + log.warn(`Hovered item should not be set to undefined. Use null.`); + return state; + } return action.hoveredItem; default: return state; From 9216098a3f690ca7b3c985472296576b734a13b2 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 13 Sep 2017 16:59:37 -0400 Subject: [PATCH 05/17] get rotate working --- src/containers/paint-editor.jsx | 4 ++++ src/helper/bounding-box/bounding-box-tool.js | 8 ++++---- src/helper/bounding-box/rotate-tool.js | 20 ++++++++++++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index ba260313..1db285bf 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import PaintEditorComponent from '../components/paint-editor.jsx'; import {changeMode} from '../reducers/modes'; +import {getGuideLayer} from '../helper/layer'; import Modes from '../modes/modes'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; @@ -21,6 +22,8 @@ class PaintEditor extends React.Component { document.removeEventListener('keydown', this.props.onKeyPress); } handleUpdateSvg () { + // Hide bounding box + getGuideLayer().visible = false; const bounds = paper.project.activeLayer.bounds; this.props.onUpdateSvg( paper.project.exportSVG({ @@ -29,6 +32,7 @@ class PaintEditor extends React.Component { }), paper.project.view.center.x - bounds.x, paper.project.view.center.y - bounds.y); + getGuideLayer().visible = true; } render () { return ( diff --git a/src/helper/bounding-box/bounding-box-tool.js b/src/helper/bounding-box/bounding-box-tool.js index cf9aceae..516c2c1b 100644 --- a/src/helper/bounding-box/bounding-box-tool.js +++ b/src/helper/bounding-box/bounding-box-tool.js @@ -119,7 +119,7 @@ class BoundingBoxTool { this.boundsPath.fullySelected = true; this.boundsPath.parent = getGuideLayer(); - for (let index = 0; index < this.boundsPath.segments; index++) { + for (let index = 0; index < this.boundsPath.segments.length; index++) { const segment = this.boundsPath.segments[index]; let size = 4; @@ -131,11 +131,11 @@ class BoundingBoxTool { const offset = new paper.Point(0, 20); const arrows = new paper.Path(ARROW_PATH); - arrows.translate(segment.point + offset + [-10.5, -5]); + arrows.translate(segment.point.add(offset).add(-10.5, -5)); const line = new paper.Path.Rectangle( - segment.point + offset - [1, 0], - segment.point + [1, 0]); + segment.point.add(offset).subtract(1, 0), + segment.point); const rotHandle = arrows.unite(line); line.remove(); diff --git a/src/helper/bounding-box/rotate-tool.js b/src/helper/bounding-box/rotate-tool.js index 2577ad45..f947befc 100644 --- a/src/helper/bounding-box/rotate-tool.js +++ b/src/helper/bounding-box/rotate-tool.js @@ -1,3 +1,5 @@ +import paper from 'paper'; + class RotateTool { constructor () { this.rotItems = []; @@ -10,16 +12,21 @@ class RotateTool { * @param {!object} boundsPath Where the boundaries of the hit item are * @param {!Array.} selectedItems Set of selected paper.Items */ - onMouseDown (boundsPath, selectedItems) { + onMouseDown (hitResult, boundsPath, selectedItems) { this.rotGroupPivot = boundsPath.bounds.center; - this.rotItems = selectedItems; + for (const item of selectedItems) { + // Rotate only root items; all nested items shouldn't get rotated again. + if (item.parent instanceof paper.Layer) { + this.rotItems.push(item); + } + } for (let i = 0; i < this.rotItems.length; i++) { - this.prevRot[i] = (event.point - this.rotGroupPivot).angle; + this.prevRot[i] = 90; } } onMouseDrag (event) { - let rotAngle = (event.point - this.rotGroupPivot).angle; + let rotAngle = (event.point.subtract(this.rotGroupPivot)).angle; for (let i = 0; i < this.rotItems.length; i++) { const item = this.rotItems[i]; @@ -44,6 +51,11 @@ class RotateTool { for (const item of this.rotItems) { item.applyMatrix = true; } + + this.rotItems.length = 0; + this.rotGroupPivot = null; + this.prevRot = []; + // @todo add back undo // pg.undo.snapshot('rotateSelection'); } From 3fa8073bd1aaf50795d0135ec9f99b57e728c70f Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 13 Sep 2017 17:45:06 -0400 Subject: [PATCH 06/17] get scale working --- src/helper/bounding-box/bounding-box-tool.js | 11 ++++++--- src/helper/bounding-box/rotate-tool.js | 2 +- src/helper/bounding-box/scale-tool.js | 26 ++++++++++++++------ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/helper/bounding-box/bounding-box-tool.js b/src/helper/bounding-box/bounding-box-tool.js index 516c2c1b..46bd2501 100644 --- a/src/helper/bounding-box/bounding-box-tool.js +++ b/src/helper/bounding-box/bounding-box-tool.js @@ -60,18 +60,23 @@ class BoundingBoxTool { if (hitResults[i].item.data && hitResults[i].item.data.isScaleHandle) { hitResult = hitResults[i]; this.mode = Modes.SCALE; - this._modeMap[this.mode].onMouseDown(hitResult, this.boundsPath, getSelectedItems()); break; } else if (hitResults[i].item.data && hitResults[i].item.data.isRotHandle) { hitResult = hitResults[i]; this.mode = Modes.ROTATE; - this._modeMap[this.mode].onMouseDown(hitResult, this.boundsPath, getSelectedItems()); } } - if (!this.mode) { this.mode = Modes.MOVE; + } + + if (this.mode === Modes.MOVE) { this._modeMap[this.mode].onMouseDown(hitResult, clone, multiselect); + } else if (this.mode === Modes.SCALE) { + this._modeMap[this.mode].onMouseDown( + hitResult, this.boundsPath, this.boundsScaleHandles, this.boundsRotHandles, getSelectedItems()); + } else if (this.mode === Modes.ROTATE) { + this._modeMap[this.mode].onMouseDown(hitResult, this.boundsPath, getSelectedItems()); } // while transforming object, never show the bounds stuff diff --git a/src/helper/bounding-box/rotate-tool.js b/src/helper/bounding-box/rotate-tool.js index f947befc..64bf7b50 100644 --- a/src/helper/bounding-box/rotate-tool.js +++ b/src/helper/bounding-box/rotate-tool.js @@ -15,7 +15,7 @@ class RotateTool { onMouseDown (hitResult, boundsPath, selectedItems) { this.rotGroupPivot = boundsPath.bounds.center; for (const item of selectedItems) { - // Rotate only root items; all nested items shouldn't get rotated again. + // Rotate only root items if (item.parent instanceof paper.Layer) { this.rotItems.push(item); } diff --git a/src/helper/bounding-box/scale-tool.js b/src/helper/bounding-box/scale-tool.js index 3358d884..2a20ba68 100644 --- a/src/helper/bounding-box/scale-tool.js +++ b/src/helper/bounding-box/scale-tool.js @@ -7,11 +7,13 @@ class ScaleTool { this.corner = null; this.origSize = null; this.origCenter = null; - this.scaleItems = null; this.itemGroup = null; this.boundsPath = null; // Lowest item above all scale items in z index this.itemToInsertBelow = null; + this.scaleItems = []; + this.boundsScaleHandles = []; + this.boundsRotHandles = []; } /** @@ -21,26 +23,34 @@ class ScaleTool { * @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, boundsPath, selectedItems) { + onMouseDown (hitResult, boundsPath, boundsScaleHandles, boundsRotHandles, selectedItems) { const index = hitResult.item.data.index; + 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.origSize = this.corner.subtract(this.pivot); this.origCenter = this.boundsPath.bounds.center; - this.boundsPath = boundsPath; - this.scaleItems = selectedItems; + for (const item of selectedItems) { + // Scale only root items + if (item.parent instanceof paper.Layer) { + this.scaleItems.push(item); + } + } } onMouseDrag (event) { + const scaleTool = this; const modOrigSize = this.origSize; // get item to insert below so that scaled items stay in same z position const items = paper.project.getItems({ match: function (item) { - if (item instanceof paper.Layer) { + if (item instanceof paper.Layer || item.data.isHelperItem) { return false; } - for (const scaleItem of this.scaleItems) { + for (const scaleItem of scaleTool.scaleItems) { if (!scaleItem.isBelow(item)) { return false; } @@ -107,8 +117,10 @@ class ScaleTool { this.corner = null; this.origSize = null; this.origCenter = null; - this.scaleItems = null; + this.scaleItems.length = 0; this.boundsPath = null; + this.boundsScaleHandles = []; + this.boundsRotHandles = []; if (!this.itemGroup) { return; From 6048c9f8662c934a4287a259af8d7a11f6e31b54 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 13 Sep 2017 17:58:27 -0400 Subject: [PATCH 07/17] hook into other tools --- src/containers/blob/blob.js | 4 ++-- src/containers/blob/style-path.js | 4 ---- src/containers/brush-mode.jsx | 3 ++- src/containers/line-mode.jsx | 4 ++-- src/containers/select-mode.jsx | 3 ++- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index 30bbdc95..a10565c2 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -3,6 +3,7 @@ import log from '../../log/log'; import BroadBrushHelper from './broad-brush-helper'; import SegmentBrushHelper from './segment-brush-helper'; import {styleCursorPreview} from './style-path'; +import {clearSelection} from '../../helper/selection'; /** * Shared code for the brush and eraser mode. Adds functions on the paper tool object @@ -232,8 +233,7 @@ class Blobbiness { // Eraser didn't hit anything selected, so assume they meant to erase from all instead of from subset // and deselect the selection if (items.length === 0) { - // TODO: Add back selection handling - // pg.selection.clearSelection(); + clearSelection(); items = paper.project.getItems({ match: function (item) { return blob.isMergeable(lastPath, item) && blob.touches(lastPath, item); diff --git a/src/containers/blob/style-path.js b/src/containers/blob/style-path.js index 658d2f77..26a2527b 100644 --- a/src/containers/blob/style-path.js +++ b/src/containers/blob/style-path.js @@ -2,8 +2,6 @@ const stylePath = function (path, options) { if (options.isEraser) { path.fillColor = 'white'; } else { - // TODO: Add back brush styling. Keep a separate active toolbar style for brush vs pen. - // path = pg.stylebar.applyActiveToolbarStyle(path); path.fillColor = options.fillColor; } }; @@ -14,8 +12,6 @@ const styleCursorPreview = function (path, options) { path.strokeColor = 'cornflowerblue'; path.strokeWidth = 1; } else { - // TODO: Add back brush styling. Keep a separate active toolbar style for brush vs pen. - // path = pg.stylebar.applyActiveToolbarStyle(path); path.fillColor = options.fillColor; } }; diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index bf1b372a..bbf36f11 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -6,6 +6,7 @@ import Modes from '../modes/modes'; import Blobbiness from './blob/blob'; import {changeBrushSize} from '../reducers/brush-mode'; import {changeMode} from '../reducers/modes'; +import {clearSelection} from '../helper/selection'; import BrushModeComponent from '../components/brush-mode.jsx'; class BrushMode extends React.Component { @@ -42,7 +43,7 @@ class BrushMode extends React.Component { activateTool () { // TODO: Instead of clearing selection, consider a kind of "draw inside" // analogous to how selection works with eraser - // pg.selection.clearSelection(); + clearSelection(); // TODO: This is temporary until a component that provides the brush size is hooked up this.props.canvas.addEventListener('mousewheel', this.onScroll); diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 35e47b53..0af6f939 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -4,6 +4,7 @@ import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; import {changeStrokeWidth} from '../reducers/stroke-width'; +import {clearSelection} from '../helper/selection'; import LineModeComponent from '../components/line-mode.jsx'; import {changeMode} from '../reducers/modes'; import paper from 'paper'; @@ -42,8 +43,7 @@ class LineMode extends React.Component { return false; // Static component, for now } activateTool () { - // TODO add back selection - // pg.selection.clearSelection(); + clearSelection(); this.props.canvas.addEventListener('mousewheel', this.onScroll); this.tool = new paper.Tool(); diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 79dbe330..104e3e98 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -62,8 +62,8 @@ class SelectMode extends React.Component { return this._hitOptions; } activateTool () { - clearSelection(); selectRootItem(); + this.boundingBoxTool.setSelectionBounds(); this.tool = new paper.Tool(); const selectMode = this; @@ -126,6 +126,7 @@ class SelectMode extends React.Component { } deactivateTool () { this.props.clearHoveredItem(); + this.boundingBoxTool.removeBoundsPath(); this.tool.remove(); this.tool = null; this.hitResult = null; From 25478a87fb81811aa3f50433dc456c68e941cc49 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 14 Sep 2017 11:56:12 -0400 Subject: [PATCH 08/17] add tests --- src/reducers/hover.js | 4 ++- test/unit/components/select-mode.test.jsx | 15 +++++++++ test/unit/hover-reducer.test.js | 40 +++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 test/unit/components/select-mode.test.jsx create mode 100644 test/unit/hover-reducer.test.js diff --git a/src/reducers/hover.js b/src/reducers/hover.js index 0c4353b0..2a3e31ee 100644 --- a/src/reducers/hover.js +++ b/src/reducers/hover.js @@ -1,3 +1,4 @@ +import paper from 'paper'; import log from '../log/log'; const CHANGE_HOVERED = 'scratch-paint/hover/CHANGE_HOVERED'; @@ -7,7 +8,8 @@ const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { case CHANGE_HOVERED: - if (typeof action.hoveredItem === 'undefined') { + if (typeof action.hoveredItem === 'undefined' || + (action.hoveredItem !== null && !(action.hoveredItem instanceof paper.Item))) { log.warn(`Hovered item should not be set to undefined. Use null.`); return state; } diff --git a/test/unit/components/select-mode.test.jsx b/test/unit/components/select-mode.test.jsx new file mode 100644 index 00000000..3ec1ba29 --- /dev/null +++ b/test/unit/components/select-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 SelectModeComponent from '../../../src/components/select-mode.jsx'; // eslint-disable-line no-unused-vars + +describe('SelectModeComponent', () => { + test('triggers callback when clicked', () => { + const onClick = jest.fn(); + const componentShallowWrapper = shallow( + + ); + componentShallowWrapper.simulate('click'); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/test/unit/hover-reducer.test.js b/test/unit/hover-reducer.test.js new file mode 100644 index 00000000..b8c63cd0 --- /dev/null +++ b/test/unit/hover-reducer.test.js @@ -0,0 +1,40 @@ +/* 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(); +}); + +test('setHoveredItem', () => { + let defaultState; + const item1 = new paper.Path(); + const item2 = new paper.Path(); + 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(); + expect(reducer(defaultState /* state */, clearHoveredItem() /* action */)).toBeNull(); + expect(reducer(item /* state */, clearHoveredItem() /* action */)).toBeNull(); +}); + +test('invalidSetHoveredItem', () => { + let defaultState; + const item = new paper.Path(); + const nonItem = {random: 'object'}; + let undef; + expect(reducer(defaultState /* state */, setHoveredItem(nonItem) /* action */)).toBeNull(); + expect(reducer(item /* state */, setHoveredItem(nonItem) /* action */)) + .toBe(item); + expect(reducer(item /* state */, setHoveredItem(undef) /* action */)) + .toBe(item); +}); From 67ac013edfce82c92b53875db7da061c3d440811 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 14 Sep 2017 14:34:45 -0400 Subject: [PATCH 09/17] prune unused function and lint --- src/containers/select-mode.jsx | 33 ++++---- src/helper/bounding-box/bounding-box-tool.js | 7 +- src/helper/bounding-box/scale-tool.js | 2 + src/helper/compound-path.js | 51 ------------ src/helper/group.js | 8 +- src/helper/guides.js | 88 -------------------- src/helper/helper.js | 3 - src/helper/math.js | 1 - src/helper/selection.js | 33 +++----- src/modes/modes.js | 3 +- 10 files changed, 43 insertions(+), 186 deletions(-) diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 104e3e98..e9ecd631 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -9,7 +9,7 @@ import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; import {getHoveredItem} from '../helper/hover'; import {rectSelect} from '../helper/guides'; -import {clearSelection, selectRootItem, processRectangularSelection} from '../helper/selection'; +import {selectRootItem, processRectangularSelection} from '../helper/selection'; import SelectModeComponent from '../components/select-mode.jsx'; import BoundingBoxTool from '../helper/bounding-box/bounding-box-tool'; @@ -66,12 +66,18 @@ class SelectMode extends React.Component { this.boundingBoxTool.setSelectionBounds(); this.tool = new paper.Tool(); + // Define these to sate linter const selectMode = this; + const hoveredItemProp = this.props.hoveredItem; + const setHoveredItemProp = this.props.setHoveredItem; + const onUpdateSvgProp = this.props.onUpdateSvg; + this.tool.onMouseDown = function (event) { - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button selectMode.props.clearHoveredItem(); - if (!selectMode.boundingBoxTool.onMouseDown( + if (!selectMode.boundingBoxTool + .onMouseDown( event, event.modifiers.alt, event.modifiers.shift, @@ -82,20 +88,19 @@ class SelectMode extends React.Component { 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 - if (oldHoveredItem) { - oldHoveredItem.remove(); + if ((!hoveredItem && hoveredItemProp) || // There is no longer a hovered item + (hoveredItem && !hoveredItemProp) || // There is now a hovered item + (hoveredItem && hoveredItemProp && hoveredItem.id !== hoveredItemProp.id)) { // hovered item changed + if (hoveredItemProp) { + hoveredItemProp.remove(); } - selectMode.props.setHoveredItem(hoveredItem); + setHoveredItemProp(hoveredItem); } }; this.tool.onMouseDrag = function (event) { - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button if (selectMode.selectionBoxMode) { selectMode.selectionRect = rectSelect(event); @@ -107,17 +112,17 @@ class SelectMode extends React.Component { }; this.tool.onMouseUp = function (event) { - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button if (selectMode.selectionBoxMode) { if (selectMode.selectionRect) { - processRectangularSelection(event, selectMode.selectionRect); + processRectangularSelection(event, selectMode.selectionRect, Modes.SELECT); selectMode.selectionRect.remove(); } selectMode.boundingBoxTool.setSelectionBounds(); } else { selectMode.boundingBoxTool.onMouseUp(event); - selectMode.props.onUpdateSvg(); + onUpdateSvgProp(); } selectMode.selectionBoxMode = false; selectMode.selectionRect = null; diff --git a/src/helper/bounding-box/bounding-box-tool.js b/src/helper/bounding-box/bounding-box-tool.js index 46bd2501..32d96918 100644 --- a/src/helper/bounding-box/bounding-box-tool.js +++ b/src/helper/bounding-box/bounding-box-tool.js @@ -10,7 +10,12 @@ import RotateTool from './rotate-tool'; import MoveTool from './move-tool'; /** SVG for the rotation icon on the bounding box */ -const ARROW_PATH = 'M19.28,1.09C19.28.28,19,0,18.2,0c-1.67,0-3.34,0-5,0-.34,0-.88.24-1,.47a1.4,1.4,0,0,0,.36,1.08,15.27,15.27,0,0,0,1.46,1.36A6.4,6.4,0,0,1,6.52,4,5.85,5.85,0,0,1,5.24,3,15.27,15.27,0,0,0,6.7,1.61,1.4,1.4,0,0,0,7.06.54C7,.3,6.44.07,6.1.06c-1.67,0-3.34,0-5,0C.28,0,0,.31,0,1.12c0,1.67,0,3.34,0,5a1.23,1.23,0,0,0,.49,1,1.22,1.22,0,0,0,1-.31A14.38,14.38,0,0,0,2.84,5.26l.73.62a9.45,9.45,0,0,0,7.34,2,9.45,9.45,0,0,0,4.82-2.05l.73-.62a14.38,14.38,0,0,0,1.29,1.51,1.22,1.22,0,0,0,1,.31,1.23,1.23,0,0,0,.49-1C19.31,4.43,19.29,2.76,19.28,1.09Z'; +const ARROW_PATH = 'M19.28,1.09C19.28.28,19,0,18.2,0c-1.67,0-3.34,0-5,0-.34,0-.88.24-1,.47a1.4,1.4,' + + '0,0,0,.36,1.08,15.27,15.27,0,0,0,1.46,1.36A6.4,6.4,0,0,1,6.52,4,5.85,5.85,0,0,1,5.24,3,15.27,15.27,' + + '0,0,0,6.7,1.61,1.4,1.4,0,0,0,7.06.54C7,.3,6.44.07,6.1.06c-1.67,0-3.34,0-5,0C.28,0,0,.31,0,1.12c0,1.67,' + + '0,3.34,0,5a1.23,1.23,0,0,0,.49,1,1.22,1.22,0,0,0,1-.31A14.38,14.38,0,0,0,2.84,5.26l.73.62a9.45,9.45,' + + '0,0,0,7.34,2,9.45,9.45,0,0,0,4.82-2.05l.73-.62a14.38,14.38,0,0,0,1.29,1.51,1.22,1.22,' + + '0,0,0,1,.31,1.23,1.23,0,0,0,.49-1C19.31,4.43,19.29,2.76,19.28,1.09Z'; /** Modes of the bounding box tool, which can do many things depending on how it's used. */ const Modes = keyMirror({ SCALE: null, diff --git a/src/helper/bounding-box/scale-tool.js b/src/helper/bounding-box/scale-tool.js index 2a20ba68..23cb96eb 100644 --- a/src/helper/bounding-box/scale-tool.js +++ b/src/helper/bounding-box/scale-tool.js @@ -19,6 +19,8 @@ class ScaleTool { /** * @param {!paper.HitResult} hitResult Data about the location of the mouse click * @param {!object} boundsPath Where the boundaries of the hit item are + * @param {!object} boundsScaleHandles Bounding box scale handles + * @param {!object} boundsRotHandles Bounding box rotation handle * @param {!Array.} selectedItems Set of selected paper.Items * @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) diff --git a/src/helper/compound-path.js b/src/helper/compound-path.js index 89e51b8e..24c808b2 100644 --- a/src/helper/compound-path.js +++ b/src/helper/compound-path.js @@ -20,59 +20,8 @@ const getItemsCompoundPath = function (item) { }; - -// const createFromSelection = function () { -// const items = getSelectedPaths(); -// if (items.length < 2) return; - -// const path = new paper.CompoundPath({fillRule: 'evenodd'}); - -// for (let i = 0; i < items.length; i++) { -// path.addChild(items[i]); -// items[i].selected = false; -// } - -// path = pg.stylebar.applyActiveToolbarStyle(path); - -// pg.selection.setItemSelection(path, true); -// pg.undo.snapshot('createCompoundPathFromSelection'); -// }; - - -// const releaseSelection = function () { -// const items = pg.selection.getSelectedItems(); - -// const cPathsToDelete = []; -// for (const i=0; i 0) { const group = new paper.Group(items); - // jQuery(document).trigger('Grouped'); + // @todo: Set selection bounds; enable/disable grouping icons // @todo add back undo // pg.undo.snapshot('groupItems'); return group; diff --git a/src/helper/guides.js b/src/helper/guides.js index ec4be677..ffd65d76 100644 --- a/src/helper/guides.js +++ b/src/helper/guides.js @@ -57,54 +57,6 @@ const rectSelect = function (event, color) { return rect; }; -const line = function (from, to, color) { - const theLine = new paper.Path.Line(from, to); - const zoom = 1 / paper.view.zoom; - setDefaultGuideStyle(theLine); - if (!color) color = GUIDE_GREY; - theLine.parent = getGuideLayer(); - theLine.strokeColor = color; - theLine.strokeColor = color; - theLine.dashArray = [5 * zoom, 5 * zoom]; - theLine.data.isHelperItem = true; - return theLine; -}; - -const crossPivot = function (center, color) { - const zoom = 1 / paper.view.zoom; - const star = new paper.Path.Star(center, 4, 4 * zoom, 0.5 * zoom); - setDefaultGuideStyle(star); - if (!color) color = GUIDE_BLUE; - star.parent = getGuideLayer(); - star.fillColor = color; - star.strokeColor = color; - star.strokeWidth = 0.5 * zoom; - star.data.isHelperItem = true; - star.rotate(45); - - return star; -}; - -const rotPivot = function (center, color) { - const zoom = 1 / paper.view.zoom; - const path = new paper.Path.Circle(center, 3 * zoom); - setDefaultGuideStyle(path); - if (!color) color = GUIDE_BLUE; - path.parent = getGuideLayer(); - path.fillColor = color; - path.data.isHelperItem = true; - - return path; -}; - -const label = function (pos, content, color) { - const text = new paper.PointText(pos); - if (!color) color = GUIDE_GREY; - text.parent = getGuideLayer(); - text.fillColor = color; - text.content = content; -}; - const getGuideColor = function (colorName) { if (colorName === 'blue') { return GUIDE_BLUE; @@ -113,60 +65,20 @@ const getGuideColor = function (colorName) { } }; -const getAllGuides = function () { - const allItems = []; - for (let i = 0; i < paper.project.layers.length; i++) { - const layer = paper.project.layers[i]; - for (let j = 0; j < layer.children.length; j++) { - const child = layer.children[j]; - // only give guides - if (!child.guide) { - continue; - } - allItems.push(child); - } - } - return allItems; -}; - -const getExportRectGuide = function () { - const guides = getAllGuides(); - for (let i = 0; i < guides.length; i++){ - if (guides[i].data && guides[i].data.isExportRect) { - return guides[i]; - } - } -}; - - const removeHelperItems = function () { removePaperItemsByDataTags(['isHelperItem']); }; - const removeAllGuides = function () { removePaperItemsByTags(['guide']); }; - -const removeExportRectGuide = function () { - removePaperItemsByDataTags(['isExportRect']); -}; - - export { hoverItem, hoverBounds, rectSelect, - line, - crossPivot, - rotPivot, - label, removeAllGuides, removeHelperItems, - removeExportRectGuide, - getAllGuides, - getExportRectGuide, getGuideColor, setDefaultGuideStyle }; diff --git a/src/helper/helper.js b/src/helper/helper.js index accadea6..7a041959 100644 --- a/src/helper/helper.js +++ b/src/helper/helper.js @@ -28,7 +28,6 @@ const getPaperItemsByTags = function (tags) { return foundItems; }; - const removePaperItemsByDataTags = function (tags) { const allItems = getAllPaperItems(true); for (const item of allItems) { @@ -40,7 +39,6 @@ const removePaperItemsByDataTags = function (tags) { } }; - const removePaperItemsByTags = function (tags) { const allItems = getAllPaperItems(true); for (const item of allItems) { @@ -52,7 +50,6 @@ const removePaperItemsByTags = function (tags) { } }; - export { getAllPaperItems, getPaperItemsByTags, diff --git a/src/helper/math.js b/src/helper/math.js index a1573087..a769aa67 100644 --- a/src/helper/math.js +++ b/src/helper/math.js @@ -27,7 +27,6 @@ const snapDeltaToAngle = function (delta, snapAngle) { return new paper.Point(dirx * d, diry * d); }; - export { checkPointsClose, getRandomInt, diff --git a/src/helper/selection.js b/src/helper/selection.js index 86b7b8af..6ecba05c 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -58,21 +58,15 @@ const setItemSelection = function (item, state) { if (parentGroup) { // do it recursive setItemSelection(parentGroup, state); - } else if (itemsCompoundPath) { setItemSelection(itemsCompoundPath, state); - } else { if (item.data && item.data.noSelect) { return; } setGroupSelection(item, state); } - // pg.statusbar.update(); - // pg.stylebar.updateFromSelection(); - // pg.stylebar.blurInputs(); - - // jQuery(document).trigger('SelectionChanged'); + // @todo: Update toolbar state on change }; @@ -94,13 +88,10 @@ const selectAllSegments = function () { const clearSelection = function () { paper.project.deselectAll(); - - // pg.statusbar.update(); - // pg.stylebar.blurInputs(); - // jQuery(document).trigger('SelectionChanged'); + // @todo: Update toolbar state on change }; -// this gets all selected non-grouped items and groups +// This gets all selected non-grouped items and groups // (alternative to paper.project.selectedItems, which includes // group children in addition to the group) // Returns in increasing Z order @@ -137,8 +128,7 @@ const deleteItemSelection = function () { items[i].remove(); } - // jQuery(document).trigger('DeleteItems'); - // jQuery(document).trigger('SelectionChanged'); + // @todo: Update toolbar state on change paper.project.view.update(); // @todo add back undo // pg.undo.snapshot('deleteItemSelection'); @@ -289,9 +279,8 @@ const deleteSegmentSelection = function () { for (let i = 0; i < items.length; i++) { deleteSegments(items[i]); } - - // jQuery(document).trigger('DeleteSegments'); - // jQuery(document).trigger('SelectionChanged'); + + // @todo: Update toolbar state on change paper.project.view.update(); // @todo add back undo // pg.undo.snapshot('deleteSegmentSelection'); @@ -308,7 +297,7 @@ const cloneSelection = function () { // pg.undo.snapshot('cloneSelection'); }; -// only returns paths, no compound paths, groups or any other stuff +// Only returns paths, no compound paths, groups or any other stuff const getSelectedPaths = function () { const allPaths = getSelectedItems(); const paths = []; @@ -359,7 +348,7 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { for (let j = 0; j < item.segments.length; j++) { const seg = item.segments[j]; if (rect.contains(seg.point)) { - if (mode === 'detail') { + if (mode === Modes.RESHAPE) { if (event.modifiers.shift && seg.selected) { seg.selected = false; } else { @@ -384,7 +373,7 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { if (intersections.length > 0 && !segmentMode) { // if in detail select mode, select the curves that intersect // with the selectionRect - if (mode === 'detail') { + if (mode === Modes.RESHAPE) { for (let k = 0; k < intersections.length; k++) { const curve = intersections[k].curve; // intersections contains every curve twice because @@ -411,7 +400,7 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { return false; } } - // pg.statusbar.update(); + // @todo: Update toolbar state on change } else if (isBoundsItem(item)) { if (checkBoundsItem(rect, item, event)) { @@ -442,7 +431,7 @@ const processRectangularSelection = function (event, rect, mode) { itemLoop: for (let i = 0; i < allItems.length; i++) { const item = allItems[i]; - if (mode === 'detail' && isPGTextItem(getRootItem(item))) { + if (mode === Modes.RESHAPE && isPGTextItem(getRootItem(item))) { continue itemLoop; } // check for item segment points inside selectionRect diff --git a/src/modes/modes.js b/src/modes/modes.js index 82394802..a12446e3 100644 --- a/src/modes/modes.js +++ b/src/modes/modes.js @@ -4,7 +4,8 @@ const Modes = keyMirror({ BRUSH: null, ERASER: null, LINE: null, - SELECT: null + SELECT: null, + RESHAPE: null }); export default Modes; From f1fdd436fa0e1c557bc462c7f3eb11281c6542d7 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 14 Sep 2017 15:24:04 -0400 Subject: [PATCH 10/17] add canvas prebuilt dep --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3e56fbb8..7fabae78 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "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", From 9a09c4324d231d470659556023904bbeb46a414d Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 14 Sep 2017 17:13:56 -0400 Subject: [PATCH 11/17] the declarations done to avoid lint errors actually broke hover, so put them back --- src/containers/paint-editor.jsx | 2 ++ src/containers/select-mode.jsx | 23 +++++++++-------------- src/containers/selection-hov.jsx | 5 +++-- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 1db285bf..9d48fa2d 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -62,6 +62,8 @@ const mapDispatchToProps = dispatch => ({ dispatch(changeMode(Modes.BRUSH)); } else if (event.key === 'l') { dispatch(changeMode(Modes.LINE)); + } else if (event.key === 's') { + dispatch(changeMode(Modes.SELECT)); } } }); diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index e9ecd631..572f6885 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -68,9 +68,6 @@ class SelectMode extends React.Component { // Define these to sate linter const selectMode = this; - const hoveredItemProp = this.props.hoveredItem; - const setHoveredItemProp = this.props.setHoveredItem; - const onUpdateSvgProp = this.props.onUpdateSvg; this.tool.onMouseDown = function (event) { if (event.event.button > 0) return; // only first mouse button @@ -88,13 +85,11 @@ class SelectMode extends React.Component { this.tool.onMouseMove = function (event) { const hoveredItem = getHoveredItem(event, selectMode.getHitOptions()); - if ((!hoveredItem && hoveredItemProp) || // There is no longer a hovered item - (hoveredItem && !hoveredItemProp) || // There is now a hovered item - (hoveredItem && hoveredItemProp && hoveredItem.id !== hoveredItemProp.id)) { // hovered item changed - if (hoveredItemProp) { - hoveredItemProp.remove(); - } - setHoveredItemProp(hoveredItem); + 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); } }; @@ -122,7 +117,7 @@ class SelectMode extends React.Component { selectMode.boundingBoxTool.setSelectionBounds(); } else { selectMode.boundingBoxTool.onMouseUp(event); - onUpdateSvgProp(); + selectMode.props.onUpdateSvg(); } selectMode.selectionBoxMode = false; selectMode.selectionRect = null; @@ -146,10 +141,10 @@ class SelectMode extends React.Component { SelectMode.propTypes = { clearHoveredItem: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, - hoveredItem: PropTypes.instanceOf(paper.Item), + hoveredItem: PropTypes.instanceOf(paper.Item), // eslint-disable-line react/no-unused-prop-types isSelectModeActive: PropTypes.bool.isRequired, - onUpdateSvg: PropTypes.func.isRequired, - setHoveredItem: PropTypes.func.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 => ({ diff --git a/src/containers/selection-hov.jsx b/src/containers/selection-hov.jsx index 23f38030..81a4f7df 100644 --- a/src/containers/selection-hov.jsx +++ b/src/containers/selection-hov.jsx @@ -13,11 +13,12 @@ const SelectionHOV = function (WrappedComponent) { componentDidUpdate (prevProps) { if (this.props.hoveredItem && this.props.hoveredItem !== prevProps.hoveredItem) { // A hover item has been added. Update the view - paper.view.update(); + if (prevProps.hoveredItem) { + prevProps.hoveredItem.remove(); + } } else if (!this.props.hoveredItem && prevProps.hoveredItem) { // Remove the hover item prevProps.hoveredItem.remove(); - paper.view.update(); } } render () { From 99e023eea615732e4af1b8907b653370a30de761 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 21 Sep 2017 18:39:18 -0400 Subject: [PATCH 12/17] add select tool file --- src/helper/selection-tools/select-tool.js | 108 ++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/helper/selection-tools/select-tool.js diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js new file mode 100644 index 00000000..3cb574f5 --- /dev/null +++ b/src/helper/selection-tools/select-tool.js @@ -0,0 +1,108 @@ +import Modes from '../../modes/modes'; + +import {getHoveredItem} from '../hover'; +import {deleteSelection, selectRootItem} from '../selection'; +import BoundingBoxTool from './bounding-box-tool'; +import SelectionBoxTool from './selection-box-tool'; +import paper from 'paper'; + +class SelectTool extends paper.Tool { + static get TOLERANCE () { + return 6; + } + constructor (setHoveredItem, clearHoveredItem, onUpdateSvg) { + super(); + this.setHoveredItem = setHoveredItem; + this.clearHoveredItem = clearHoveredItem; + this.onUpdateSvg = onUpdateSvg; + this.boundingBoxTool = new BoundingBoxTool(onUpdateSvg); + this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT); + this.selectionBoxMode = false; + this._hitOptions = { + segments: true, + stroke: true, + curves: true, + fill: true, + guide: false + }; + + // We have to set these functions instead of just declaring them because + // paper.js tools hook up the listeners in the setter functions. + this.onMouseDown = this.handleMouseDown; + this.onMouseMove = this.handleMouseMove; + this.onMouseDrag = this.handleMouseDrag; + this.onMouseUp = this.handleMouseUp; + this.onKeyUp = this.handleKeyUp; + + selectRootItem(); + this.boundingBoxTool.setSelectionBounds(); + } + setPrevHoveredItem (prevHoveredItem) { + this.prevHoveredItem = prevHoveredItem; + } + getHitOptions (preselectedOnly) { + this._hitOptions.tolerance = SelectTool.TOLERANCE / paper.view.zoom; + if (preselectedOnly) { + this._hitOptions.selected = true; + } else { + delete this._hitOptions.selected; + } + return this._hitOptions; + } + handleMouseDown (event) { + if (event.event.button > 0) return; // only first mouse button + + this.clearHoveredItem(); + if (!this.boundingBoxTool + .onMouseDown( + event, + event.modifiers.alt, + event.modifiers.shift, + this.getHitOptions(false /* preseelectedOnly */))) { + this.selectionBoxMode = true; + this.selectionBoxTool.onMouseDown(event.modifiers.shift); + } + } + handleMouseMove (event) { + const hoveredItem = getHoveredItem(event, this.getHitOptions()); + if ((!hoveredItem && this.prevHoveredItem) || // There is no longer a hovered item + (hoveredItem && !this.prevHoveredItem) || // There is now a hovered item + (hoveredItem && this.prevHoveredItem && + hoveredItem.id !== this.prevHoveredItem.id)) { // hovered item changed + this.setHoveredItem(hoveredItem); + } + } + handleMouseDrag (event) { + if (event.event.button > 0) return; // only first mouse button + + if (this.selectionBoxMode) { + this.selectionBoxTool.onMouseDrag(event); + } else { + this.boundingBoxTool.onMouseDrag(event); + } + } + handleMouseUp (event) { + if (event.event.button > 0) return; // only first mouse button + + if (this.selectionBoxMode) { + this.selectionBoxTool.onMouseUp(event); + this.boundingBoxTool.setSelectionBounds(); + } else { + this.boundingBoxTool.onMouseUp(event); + } + this.selectionBoxMode = false; + } + handleKeyUp (event) { + // Backspace, delete + if (event.key === 'delete' || event.key === 'backspace') { + deleteSelection(Modes.SELECT); + this.onUpdateSvg(); + } + } + deactivateTool () { + this.clearHoveredItem(); + this.boundingBoxTool.removeBoundsPath(); + } +} + +export default SelectTool; From 2c374444b3d2aa428bd1bfc55d5a123a31e99aa7 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 21 Sep 2017 18:20:44 -0400 Subject: [PATCH 13/17] Pull in changes from reshape branch --- src/containers/paper-canvas.jsx | 8 ++ src/containers/select-mode.jsx | 106 ++---------------- src/helper/bounding-box/move-tool.js | 59 ---------- .../bounding-box-tool.js | 19 +++- src/helper/selection-tools/move-tool.js | 101 +++++++++++++++++ .../rotate-tool.js | 8 +- .../scale-tool.js | 8 +- .../selection-tools/selection-box-tool.js | 32 ++++++ src/helper/selection.js | 52 +++++---- 9 files changed, 202 insertions(+), 191 deletions(-) delete mode 100644 src/helper/bounding-box/move-tool.js rename src/helper/{bounding-box => selection-tools}/bounding-box-tool.js (91%) create mode 100644 src/helper/selection-tools/move-tool.js rename src/helper/{bounding-box => selection-tools}/rotate-tool.js (90%) rename src/helper/{bounding-box => selection-tools}/scale-tool.js (97%) create mode 100644 src/helper/selection-tools/selection-box-tool.js diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 50e95b51..7539bb0d 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -33,7 +33,15 @@ class PaperCanvas extends React.Component { onLoad: function (item) { // Remove viewbox if (item.clipped) { + let mask; + for (const child of item.children) { + if (child.isClipMask()) { + mask = child; + break; + } + } item.clipped = false; + mask.remove(); // Consider removing clip mask here? } while (item.reduce() !== item) { diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 572f6885..731ba0e6 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -7,35 +7,17 @@ import Modes from '../modes/modes'; import {changeMode} from '../reducers/modes'; import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; -import {getHoveredItem} from '../helper/hover'; -import {rectSelect} from '../helper/guides'; -import {selectRootItem, processRectangularSelection} from '../helper/selection'; - +import SelectTool from '../helper/selection-tools/select-tool'; import SelectModeComponent from '../components/select-mode.jsx'; -import BoundingBoxTool from '../helper/bounding-box/bounding-box-tool'; import paper from 'paper'; class SelectMode extends React.Component { - static get TOLERANCE () { - return 6; - } constructor (props) { super(props); bindAll(this, [ 'activateTool', - 'deactivateTool', - 'getHitOptions' + 'deactivateTool' ]); - this._hitOptions = { - segments: true, - stroke: true, - curves: true, - fill: true, - guide: false - }; - this.boundingBoxTool = new BoundingBoxTool(); - this.selectionBoxMode = false; - this.selectionRect = null; } componentDidMount () { if (this.props.isSelectModeActive) { @@ -43,6 +25,10 @@ class SelectMode extends React.Component { } } componentWillReceiveProps (nextProps) { + if (this.tool && nextProps.hoveredItem !== this.props.hoveredItem) { + this.tool.setPrevHoveredItem(nextProps.hoveredItem); + } + if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) { this.activateTool(); } else if (!nextProps.isSelectModeActive && this.props.isSelectModeActive) { @@ -52,84 +38,14 @@ class SelectMode extends React.Component { shouldComponentUpdate () { return false; // Static component, for now } - getHitOptions (preselectedOnly) { - this._hitOptions.tolerance = SelectMode.TOLERANCE / paper.view.zoom; - if (preselectedOnly) { - this._hitOptions.selected = true; - } else { - delete this._hitOptions.selected; - } - return this._hitOptions; - } activateTool () { - selectRootItem(); - this.boundingBoxTool.setSelectionBounds(); - this.tool = new paper.Tool(); - - // Define these to sate linter - const selectMode = this; - - this.tool.onMouseDown = function (event) { - if (event.event.button > 0) return; // only first mouse button - - selectMode.props.clearHoveredItem(); - if (!selectMode.boundingBoxTool - .onMouseDown( - event, - event.modifiers.alt, - event.modifiers.shift, - selectMode.getHitOptions(false /* preseelectedOnly */))) { - selectMode.selectionBoxMode = true; - } - }; - - this.tool.onMouseMove = function (event) { - const hoveredItem = getHoveredItem(event, selectMode.getHitOptions()); - const oldHoveredItem = selectMode.props.hoveredItem; - if ((!hoveredItem && oldHoveredItem) || // There is no longer a hovered item - (hoveredItem && !oldHoveredItem) || // There is now a hovered item - (hoveredItem && oldHoveredItem && hoveredItem.id !== oldHoveredItem.id)) { // hovered item changed - selectMode.props.setHoveredItem(hoveredItem); - } - }; - - - this.tool.onMouseDrag = function (event) { - if (event.event.button > 0) return; // only first mouse button - - if (selectMode.selectionBoxMode) { - selectMode.selectionRect = rectSelect(event); - // Remove this rect on the next drag and up event - selectMode.selectionRect.removeOnDrag(); - } else { - selectMode.boundingBoxTool.onMouseDrag(event); - } - }; - - this.tool.onMouseUp = function (event) { - if (event.event.button > 0) return; // only first mouse button - - if (selectMode.selectionBoxMode) { - if (selectMode.selectionRect) { - processRectangularSelection(event, selectMode.selectionRect, Modes.SELECT); - selectMode.selectionRect.remove(); - } - selectMode.boundingBoxTool.setSelectionBounds(); - } else { - selectMode.boundingBoxTool.onMouseUp(event); - selectMode.props.onUpdateSvg(); - } - selectMode.selectionBoxMode = false; - selectMode.selectionRect = null; - }; + this.tool = new SelectTool(this.props.setHoveredItem, this.props.clearHoveredItem, this.props.onUpdateSvg); this.tool.activate(); } deactivateTool () { - this.props.clearHoveredItem(); - this.boundingBoxTool.removeBoundsPath(); + this.tool.deactivateTool(); this.tool.remove(); this.tool = null; - this.hitResult = null; } render () { return ( @@ -141,10 +57,10 @@ class SelectMode extends React.Component { SelectMode.propTypes = { clearHoveredItem: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, - hoveredItem: PropTypes.instanceOf(paper.Item), // eslint-disable-line react/no-unused-prop-types + hoveredItem: PropTypes.instanceOf(paper.Item), isSelectModeActive: PropTypes.bool.isRequired, - onUpdateSvg: PropTypes.func.isRequired, // eslint-disable-line react/no-unused-prop-types - setHoveredItem: PropTypes.func.isRequired // eslint-disable-line react/no-unused-prop-types + onUpdateSvg: PropTypes.func.isRequired, + setHoveredItem: PropTypes.func.isRequired }; const mapStateToProps = state => ({ diff --git a/src/helper/bounding-box/move-tool.js b/src/helper/bounding-box/move-tool.js deleted file mode 100644 index 6303d6cd..00000000 --- a/src/helper/bounding-box/move-tool.js +++ /dev/null @@ -1,59 +0,0 @@ -import {isGroup} from '../group'; -import {isCompoundPathItem, getRootItem} from '../item'; -import {snapDeltaToAngle} from '../math'; -import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; - -class MoveTool { - constructor () { - this.selectedItems = null; - } - - /** - * @param {!paper.HitResult} hitResult Data about the location of the mouse click - * @param {boolean} clone Whether to clone on mouse down (e.g. alt key held) - * @param {boolean} multiselect Whether to multiselect on mouse down (e.g. shift key held) - */ - onMouseDown (hitResult, clone, multiselect) { - const root = getRootItem(hitResult.item); - const item = isCompoundPathItem(root) || isGroup(root) ? root : hitResult.item; - if (!item.selected) { - // deselect all by default if multiselect isn't on - if (!multiselect) { - clearSelection(); - } - setItemSelection(item, true); - } else if (multiselect) { - setItemSelection(item, false); - } - if (clone) cloneSelection(); - this.selectedItems = getSelectedItems(); - } - onMouseDrag (event) { - const dragVector = event.point.subtract(event.downPoint); - for (const item of this.selectedItems) { - // add the position of the item before the drag started - // for later use in the snap calculation - if (!item.data.origPos) { - item.data.origPos = item.position; - } - - if (event.modifiers.shift) { - item.position = item.data.origPos.add(snapDeltaToAngle(dragVector, Math.PI / 4)); - } else { - item.position = item.data.origPos.add(dragVector); - } - } - } - onMouseUp () { - // resetting the items origin point for the next usage - for (const item of this.selectedItems) { - item.data.origPos = null; - } - this.selectedItems = null; - - // @todo add back undo - // pg.undo.snapshot('moveSelection'); - } -} - -export default MoveTool; diff --git a/src/helper/bounding-box/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js similarity index 91% rename from src/helper/bounding-box/bounding-box-tool.js rename to src/helper/selection-tools/bounding-box-tool.js index 32d96918..c77a5dcd 100644 --- a/src/helper/bounding-box/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -28,17 +28,19 @@ const Modes = keyMirror({ * On mouse down, the type of function (move, scale, rotate) is determined based on what is clicked * (scale handle, rotate handle, the object itself). This determines the mode of the tool, which then * delegates actions to the MoveTool, RotateTool or ScaleTool accordingly. + * @param {!function} onUpdateSvg A callback to call when the image visibly changes */ class BoundingBoxTool { - constructor () { + constructor (onUpdateSvg) { + this.onUpdateSvg = onUpdateSvg; this.mode = null; this.boundsPath = null; this.boundsScaleHandles = []; this.boundsRotHandles = []; this._modeMap = {}; - this._modeMap[Modes.SCALE] = new ScaleTool(); - this._modeMap[Modes.ROTATE] = new RotateTool(); - this._modeMap[Modes.MOVE] = new MoveTool(); + this._modeMap[Modes.SCALE] = new ScaleTool(onUpdateSvg); + this._modeMap[Modes.ROTATE] = new RotateTool(onUpdateSvg); + this._modeMap[Modes.MOVE] = new MoveTool(onUpdateSvg); } /** @@ -75,8 +77,13 @@ class BoundingBoxTool { this.mode = Modes.MOVE; } + const hitProperties = { + hitResult: hitResult, + clone: event.modifiers.alt, + multiselect: event.modifiers.shift + }; if (this.mode === Modes.MOVE) { - this._modeMap[this.mode].onMouseDown(hitResult, clone, multiselect); + this._modeMap[this.mode].onMouseDown(hitProperties); } else if (this.mode === Modes.SCALE) { this._modeMap[this.mode].onMouseDown( hitResult, this.boundsPath, this.boundsScaleHandles, this.boundsRotHandles, getSelectedItems()); @@ -102,7 +109,7 @@ class BoundingBoxTool { setSelectionBounds () { this.removeBoundsPath(); - const items = getSelectedItems(); + const items = getSelectedItems(true /* recursive */); if (items.length <= 0) return; let rect = null; diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js new file mode 100644 index 00000000..e6470a92 --- /dev/null +++ b/src/helper/selection-tools/move-tool.js @@ -0,0 +1,101 @@ +import {isGroup} from '../group'; +import {isCompoundPathItem, getRootItem} from '../item'; +import {snapDeltaToAngle} from '../math'; +import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; + +class MoveTool { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { + this.selectedItems = null; + this.onUpdateSvg = onUpdateSvg; + } + + /** + * @param {!object} hitProperties Describes the mouse event + * @param {!paper.HitResult} hitProperties.hitResult Data about the location of the mouse click + * @param {?boolean} hitProperties.clone Whether to clone on mouse down (e.g. alt key held) + * @param {?boolean} hitProperties.multiselect Whether to multiselect on mouse down (e.g. shift key held) + * @param {?boolean} hitProperties.doubleClicked True if this is the second click in a short amout of time + * @param {?boolean} hitProperties.subselect True if we allow selection of subgroups, false if we should + * select the whole group. + */ + onMouseDown (hitProperties) { + let item = hitProperties.hitResult.item; + if (!hitProperties.subselect) { + const root = getRootItem(hitProperties.hitResult.item); + item = isCompoundPathItem(root) || isGroup(root) ? root : hitProperties.hitResult.item; + } + if (item.selected) { + // Double click causes all points to be selected in subselect mode. + if (hitProperties.doubleClicked) { + if (!hitProperties.multiselect) { + clearSelection(); + } + this._select(item, true /* state */, hitProperties.subselect, true /* fullySelect */); + } else if (hitProperties.multiselect) { + this._select(item, false /* state */, hitProperties.subselect); + } + } else { + // deselect all by default if multiselect isn't on + if (!hitProperties.multiselect) { + clearSelection(); + } + this._select(item, true, hitProperties.subselect); + } + if (hitProperties.clone) cloneSelection(hitProperties.subselect); + this.selectedItems = getSelectedItems(true /* subselect */); + } + /** + * Sets the selection state of an item. + * @param {!paper.Item} item Item to select or deselect + * @param {?boolean} state True if item should be selected, false if deselected + * @param {?boolean} subselect True if a subset of all points in an item are allowed to be + * selected, false if items must be selected all or nothing. + * @param {?boolean} fullySelect True if in addition to the item being selected, all of its + * control points should be selected. False if the item should be selected but not its + * points. Only relevant when subselect is true. + */ + _select (item, state, subselect, fullySelect) { + if (subselect) { + item.selected = false; + if (fullySelect) { + item.fullySelected = state; + } else { + item.selected = state; + } + } else { + setItemSelection(item, state); + } + } + onMouseDrag (event) { + const dragVector = event.point.subtract(event.downPoint); + for (const item of this.selectedItems) { + // add the position of the item before the drag started + // for later use in the snap calculation + if (!item.data.origPos) { + item.data.origPos = item.position; + } + + if (event.modifiers.shift) { + item.position = item.data.origPos.add(snapDeltaToAngle(dragVector, Math.PI / 4)); + } else { + item.position = item.data.origPos.add(dragVector); + } + } + } + onMouseUp () { + // resetting the items origin point for the next usage + for (const item of this.selectedItems) { + item.data.origPos = null; + } + this.selectedItems = null; + + // @todo add back undo + // pg.undo.snapshot('moveSelection'); + this.onUpdateSvg(); + } +} + +export default MoveTool; diff --git a/src/helper/bounding-box/rotate-tool.js b/src/helper/selection-tools/rotate-tool.js similarity index 90% rename from src/helper/bounding-box/rotate-tool.js rename to src/helper/selection-tools/rotate-tool.js index 64bf7b50..4893ede1 100644 --- a/src/helper/bounding-box/rotate-tool.js +++ b/src/helper/selection-tools/rotate-tool.js @@ -1,10 +1,14 @@ import paper from 'paper'; class RotateTool { - constructor () { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { this.rotItems = []; this.rotGroupPivot = null; this.prevRot = []; + this.onUpdateSvg = onUpdateSvg; } /** @@ -57,7 +61,7 @@ class RotateTool { this.prevRot = []; // @todo add back undo - // pg.undo.snapshot('rotateSelection'); + this.onUpdateSvg(); } } diff --git a/src/helper/bounding-box/scale-tool.js b/src/helper/selection-tools/scale-tool.js similarity index 97% rename from src/helper/bounding-box/scale-tool.js rename to src/helper/selection-tools/scale-tool.js index 23cb96eb..085395d8 100644 --- a/src/helper/bounding-box/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -1,7 +1,10 @@ import paper from 'paper'; class ScaleTool { - constructor () { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { this.pivot = null; this.origPivot = null; this.corner = null; @@ -14,6 +17,7 @@ class ScaleTool { this.scaleItems = []; this.boundsScaleHandles = []; this.boundsRotHandles = []; + this.onUpdateSvg = onUpdateSvg; } /** @@ -150,7 +154,7 @@ class ScaleTool { this.itemGroup.remove(); // @todo add back undo - // pg.undo.snapshot('scaleSelection'); + this.onUpdateSvg(); } getRectCornerNameByIndex (index) { switch (index) { diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js new file mode 100644 index 00000000..68f014a9 --- /dev/null +++ b/src/helper/selection-tools/selection-box-tool.js @@ -0,0 +1,32 @@ +import Modes from '../../modes/modes'; +import {rectSelect} from '../guides'; +import {clearSelection, processRectangularSelection} from '../selection'; + +class SelectionBoxTool { + constructor (mode) { + this.selectionRect = null; + this.mode = mode; + } + /** + * @param {boolean} multiselect Whether to multiselect on mouse down (e.g. shift key held) + */ + onMouseDown (multiselect) { + if (!multiselect) { + clearSelection(); + } + } + onMouseDrag (event) { + this.selectionRect = rectSelect(event); + // Remove this rect on the next drag and up event + this.selectionRect.removeOnDrag(); + } + onMouseUp (event) { + if (this.selectionRect) { + processRectangularSelection(event, this.selectionRect, Modes.RESHAPE); + this.selectionRect.remove(); + this.selectionRect = null; + } + } +} + +export default SelectionBoxTool; diff --git a/src/helper/selection.js b/src/helper/selection.js index 6ecba05c..226ab205 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -34,37 +34,40 @@ const selectItemSegments = function (item, state) { } }; -const setGroupSelection = function (root, selected) { - // fully selected segments need to be unselected first - root.fullySelected = false; - // then the item can be normally selected +const setGroupSelection = function (root, selected, fullySelected) { + root.fullySelected = fullySelected; root.selected = selected; // select children of compound-path or group if (isCompoundPath(root) || isGroup(root)) { const children = root.children; if (children) { - for (let i = 0; i < children.length; i++) { - children[i].selected = selected; + for (const child of children) { + if (isGroup(child)) { + setGroupSelection(child, selected, fullySelected); + } else { + child.fullySelected = fullySelected; + child.selected = selected; + } } } } }; -const setItemSelection = function (item, state) { +const setItemSelection = function (item, state, fullySelected) { const parentGroup = getItemsGroup(item); const itemsCompoundPath = getItemsCompoundPath(item); // if selection is in a group, select group not individual items if (parentGroup) { // do it recursive - setItemSelection(parentGroup, state); + setItemSelection(parentGroup, state, fullySelected); } else if (itemsCompoundPath) { - setItemSelection(itemsCompoundPath, state); + setGroupSelection(itemsCompoundPath, state, fullySelected); } else { if (item.data && item.data.noSelect) { return; } - setGroupSelection(item, state); + setGroupSelection(item, state, fullySelected); } // @todo: Update toolbar state on change @@ -355,11 +358,9 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { seg.selected = true; } segmentMode = true; - } else { if (event.modifiers.shift && item.selected) { setItemSelection(item, false); - } else { setItemSelection(item, true); } @@ -371,7 +372,7 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { // second round checks for path intersections const intersections = item.getIntersections(rect); if (intersections.length > 0 && !segmentMode) { - // if in detail select mode, select the curves that intersect + // if in reshape mode, select the curves that intersect // with the selectionRect if (mode === Modes.RESHAPE) { for (let k = 0; k < intersections.length; k++) { @@ -389,7 +390,6 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { curve.selected = true; } } - } else { if (event.modifiers.shift && item.selected) { setItemSelection(item, false); @@ -417,9 +417,8 @@ const rectangularSelectionGroupLoop = function (group, rect, root, event, mode) if (isGroup(child) || isCompoundPathItem(child)) { rectangularSelectionGroupLoop(child, rect, root, event, mode); - - } else if (!handleRectangularSelectionItems(child, event, rect, mode)) { - return false; + } else { + handleRectangularSelectionItems(child, event, rect, mode); } } return true; @@ -428,20 +427,16 @@ const rectangularSelectionGroupLoop = function (group, rect, root, event, mode) const processRectangularSelection = function (event, rect, mode) { const allItems = getAllSelectableItems(); - itemLoop: for (let i = 0; i < allItems.length; i++) { const item = allItems[i]; if (mode === Modes.RESHAPE && isPGTextItem(getRootItem(item))) { - continue itemLoop; + continue; } // check for item segment points inside selectionRect if (isGroup(item) || isCompoundPathItem(item)) { - if (!rectangularSelectionGroupLoop(item, rect, item, event, mode)) { - continue itemLoop; - } - - } else if (!handleRectangularSelectionItems(item, event, rect, mode)) { - continue itemLoop; + rectangularSelectionGroupLoop(item, rect, item, event, mode); + } else { + handleRectangularSelectionItems(item, event, rect, mode); } } }; @@ -454,8 +449,11 @@ const selectRootItem = function () { for (const item of items) { if (isCompoundPathChild(item)) { const cp = getItemsCompoundPath(item); - setItemSelection(item, false); - setItemSelection(cp, true); + setItemSelection(cp, true, true /* fullySelected */); + } + const rootItem = getRootItem(item); + if (item !== rootItem) { + setItemSelection(rootItem, true, true /* fullySelected */); } } }; From 060ff0ab15d73e36d5a58671ae18438ffe123172 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 11:10:17 -0400 Subject: [PATCH 14/17] Fix select tool being able to select sub pieces of groups, fix bounding box showing after delete --- src/helper/helper.js | 5 +++ src/helper/selection-tools/select-tool.js | 1 + .../selection-tools/selection-box-tool.js | 3 +- src/helper/selection.js | 32 +++++++++++++++---- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/helper/helper.js b/src/helper/helper.js index 7a041959..c7428106 100644 --- a/src/helper/helper.js +++ b/src/helper/helper.js @@ -1,5 +1,10 @@ import paper from 'paper'; +/** + * @param {boolean} includeGuides True if guide layer items like the bounding box should + * be included in the returned items. + * @return {Array} all top-level (direct descendants of a paper.Layer) items + */ const getAllPaperItems = function (includeGuides) { includeGuides = includeGuides || false; const allItems = []; diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index 3cb574f5..68a4adf2 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -96,6 +96,7 @@ class SelectTool extends paper.Tool { // Backspace, delete if (event.key === 'delete' || event.key === 'backspace') { deleteSelection(Modes.SELECT); + this.boundingBoxTool.removeBoundsPath(); this.onUpdateSvg(); } } diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js index 68f014a9..f057200f 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -1,4 +1,3 @@ -import Modes from '../../modes/modes'; import {rectSelect} from '../guides'; import {clearSelection, processRectangularSelection} from '../selection'; @@ -22,7 +21,7 @@ class SelectionBoxTool { } onMouseUp (event) { if (this.selectionRect) { - processRectangularSelection(event, this.selectionRect, Modes.RESHAPE); + processRectangularSelection(event, this.selectionRect, this.mode); this.selectionRect.remove(); this.selectionRect = null; } diff --git a/src/helper/selection.js b/src/helper/selection.js index 226ab205..db076952 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -6,6 +6,10 @@ import {getItemsGroup, isGroup} from './group'; import {getRootItem, isBoundsItem, isCompoundPathItem, isPathItem, isPGTextItem} from './item'; import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compound-path'; +/** + * @return {Array} all top-level (direct descendants of a paper.Layer) items + * that aren't guide items or helper items. + */ const getAllSelectableItems = function () { const allItems = getAllPaperItems(); const selectables = []; @@ -411,12 +415,12 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { }; // if the rectangular selection found a group, drill into it recursively -const rectangularSelectionGroupLoop = function (group, rect, root, event, mode) { +const _rectangularSelectionGroupLoop = function (group, rect, root, event, mode) { for (let i = 0; i < group.children.length; i++) { const child = group.children[i]; if (isGroup(child) || isCompoundPathItem(child)) { - rectangularSelectionGroupLoop(child, rect, root, event, mode); + _rectangularSelectionGroupLoop(child, rect, root, event, mode); } else { handleRectangularSelectionItems(child, event, rect, mode); } @@ -424,6 +428,14 @@ const rectangularSelectionGroupLoop = function (group, rect, root, event, mode) return true; }; +/** + * Called after drawing a selection rectangle in a select mode. In reshape mode, this + * selects all control points and curves within the rectangle. In select mode, this + * selects all items and groups that intersect the rectangle + * @param {!MouseEvent} event The mouse event to draw the rectangle + * @param {!paper.Rect} rect The selection rectangle + * @param {Modes} mode The mode of the paint editor when drawing the rectangle + */ const processRectangularSelection = function (event, rect, mode) { const allItems = getAllSelectableItems(); @@ -432,19 +444,25 @@ const processRectangularSelection = function (event, rect, mode) { if (mode === Modes.RESHAPE && isPGTextItem(getRootItem(item))) { continue; } - // check for item segment points inside selectionRect if (isGroup(item) || isCompoundPathItem(item)) { - rectangularSelectionGroupLoop(item, rect, item, event, mode); + // Drill into the group in reshape mode; check for item segment points inside + if (mode === Modes.RESHAPE) { + _rectangularSelectionGroupLoop(item, rect, item, event, mode); + } else { + setGroupSelection(item, true, true /* fullySelected */); + } } else { handleRectangularSelectionItems(item, event, rect, mode); } } }; +/** + * When switching to the select tool while having a child object of a + * compound path selected, deselect the child and select the compound path + * instead. (otherwise the compound path breaks because of scale-grouping) + */ const selectRootItem = function () { - // when switching to the select tool while having a child object of a - // compound path selected, deselect the child and select the compound path - // instead. (otherwise the compound path breaks because of scale-grouping) const items = getSelectedItems(); for (const item of items) { if (isCompoundPathChild(item)) { From fd9a4af83f859565ed4c1e37993592c4f9bf2bb8 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 12:12:07 -0400 Subject: [PATCH 15/17] add comments and clean up --- src/containers/paper-canvas.css | 2 + .../{selection-hov.jsx => selection-hoc.jsx} | 4 +- src/helper/guides.js | 28 ++++++++- src/helper/helper.js | 63 ------------------- .../selection-tools/bounding-box-tool.js | 2 +- src/helper/selection-tools/move-tool.js | 3 + src/helper/selection-tools/rotate-tool.js | 3 + src/helper/selection-tools/scale-tool.js | 18 +++--- src/helper/selection-tools/select-tool.js | 51 +++++++++++---- .../selection-tools/selection-box-tool.js | 1 + src/helper/selection.js | 22 ++++++- src/index.js | 2 +- 12 files changed, 109 insertions(+), 90 deletions(-) rename src/containers/{selection-hov.jsx => selection-hoc.jsx} (94%) delete mode 100644 src/helper/helper.js diff --git a/src/containers/paper-canvas.css b/src/containers/paper-canvas.css index 16edbb7a..82e8e028 100644 --- a/src/containers/paper-canvas.css +++ b/src/containers/paper-canvas.css @@ -4,5 +4,7 @@ margin: auto; position: relative; background-color: #fff; + /* Turn off anti-aliasing for the drawing canvas. Each time it's updated it switches + back and forth from aliased to unaliased and that looks bad */ image-rendering: pixelated; } diff --git a/src/containers/selection-hov.jsx b/src/containers/selection-hoc.jsx similarity index 94% rename from src/containers/selection-hov.jsx rename to src/containers/selection-hoc.jsx index 81a4f7df..ca1179ab 100644 --- a/src/containers/selection-hov.jsx +++ b/src/containers/selection-hoc.jsx @@ -3,7 +3,7 @@ import React from 'react'; import {connect} from 'react-redux'; import paper from 'paper'; -const SelectionHOV = function (WrappedComponent) { +const SelectionHOC = function (WrappedComponent) { class SelectionComponent extends React.Component { componentDidMount () { if (this.props.hoveredItem) { @@ -43,4 +43,4 @@ const SelectionHOV = function (WrappedComponent) { )(SelectionComponent); }; -export default SelectionHOV; +export default SelectionHOC; diff --git a/src/helper/guides.js b/src/helper/guides.js index ffd65d76..c6f917b8 100644 --- a/src/helper/guides.js +++ b/src/helper/guides.js @@ -1,6 +1,6 @@ import paper from 'paper'; import {getGuideLayer} from './layer'; -import {removePaperItemsByTags, removePaperItemsByDataTags} from './helper'; +import {getAllPaperItems} from './selection'; const GUIDE_BLUE = '#009dec'; const GUIDE_GREY = '#aaaaaa'; @@ -65,12 +65,34 @@ const getGuideColor = function (colorName) { } }; +const _removePaperItemsByDataTags = function (tags) { + const allItems = getAllPaperItems(true); + for (const item of allItems) { + for (const tag of tags) { + if (item.data && item.data[tag]) { + item.remove(); + } + } + } +}; + +const _removePaperItemsByTags = function (tags) { + const allItems = getAllPaperItems(true); + for (const item of allItems) { + for (const tag of tags) { + if (item[tag]) { + item.remove(); + } + } + } +}; + const removeHelperItems = function () { - removePaperItemsByDataTags(['isHelperItem']); + _removePaperItemsByDataTags(['isHelperItem']); }; const removeAllGuides = function () { - removePaperItemsByTags(['guide']); + _removePaperItemsByTags(['guide']); }; export { diff --git a/src/helper/helper.js b/src/helper/helper.js deleted file mode 100644 index c7428106..00000000 --- a/src/helper/helper.js +++ /dev/null @@ -1,63 +0,0 @@ -import paper from 'paper'; - -/** - * @param {boolean} includeGuides True if guide layer items like the bounding box should - * be included in the returned items. - * @return {Array} all top-level (direct descendants of a paper.Layer) items - */ -const getAllPaperItems = function (includeGuides) { - includeGuides = includeGuides || false; - const allItems = []; - for (const layer of paper.project.layers) { - for (const child of layer.children) { - // don't give guides back - if (!includeGuides && child.guide) { - continue; - } - allItems.push(child); - } - } - return allItems; -}; - -const getPaperItemsByTags = function (tags) { - const allItems = getAllPaperItems(true); - const foundItems = []; - for (const item of allItems) { - for (const tag of tags) { - if (item[tag] && foundItems.indexOf(item) === -1) { - foundItems.push(item); - } - } - } - return foundItems; -}; - -const removePaperItemsByDataTags = function (tags) { - const allItems = getAllPaperItems(true); - for (const item of allItems) { - for (const tag of tags) { - if (item.data && item.data[tag]) { - item.remove(); - } - } - } -}; - -const removePaperItemsByTags = function (tags) { - const allItems = getAllPaperItems(true); - for (const item of allItems) { - for (const tag of tags) { - if (item[tag]) { - item.remove(); - } - } - } -}; - -export { - getAllPaperItems, - getPaperItemsByTags, - removePaperItemsByDataTags, - removePaperItemsByTags -}; diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index c77a5dcd..dca535fd 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -24,7 +24,7 @@ const Modes = keyMirror({ }); /** - * A paper.Tool that handles transforming the selection and drawing a bounding box with handles. + * Tool that handles transforming the selection and drawing a bounding box with handles. * On mouse down, the type of function (move, scale, rotate) is determined based on what is clicked * (scale handle, rotate handle, the object itself). This determines the mode of the tool, which then * delegates actions to the MoveTool, RotateTool or ScaleTool accordingly. diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js index e6470a92..8853e631 100644 --- a/src/helper/selection-tools/move-tool.js +++ b/src/helper/selection-tools/move-tool.js @@ -3,6 +3,9 @@ import {isCompoundPathItem, getRootItem} from '../item'; import {snapDeltaToAngle} from '../math'; import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; +/** + * Tool to handle dragging an item to reposition it in a selection mode. + */ class MoveTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes diff --git a/src/helper/selection-tools/rotate-tool.js b/src/helper/selection-tools/rotate-tool.js index 4893ede1..2006cebf 100644 --- a/src/helper/selection-tools/rotate-tool.js +++ b/src/helper/selection-tools/rotate-tool.js @@ -1,5 +1,8 @@ import paper from 'paper'; +/** + * Tool to handle rotation when dragging the rotation handle in the bounding box tool. + */ class RotateTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes diff --git a/src/helper/selection-tools/scale-tool.js b/src/helper/selection-tools/scale-tool.js index 085395d8..8744ab72 100644 --- a/src/helper/selection-tools/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -1,5 +1,9 @@ import paper from 'paper'; +/** + * Tool to handle scaling items by pulling on the handles around the edges of the bounding + * box when in the bounding box tool. + */ class ScaleTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes @@ -34,9 +38,9 @@ class ScaleTool { this.boundsPath = boundsPath; this.boundsScaleHandles = boundsScaleHandles; this.boundsRotHandles = boundsRotHandles; - this.pivot = this.boundsPath.bounds[this.getOpposingRectCornerNameByIndex(index)].clone(); - this.origPivot = this.boundsPath.bounds[this.getOpposingRectCornerNameByIndex(index)].clone(); - this.corner = this.boundsPath.bounds[this.getRectCornerNameByIndex(index)].clone(); + this.pivot = this.boundsPath.bounds[this._getOpposingRectCornerNameByIndex(index)].clone(); + this.origPivot = this.boundsPath.bounds[this._getOpposingRectCornerNameByIndex(index)].clone(); + this.corner = this.boundsPath.bounds[this._getRectCornerNameByIndex(index)].clone(); this.origSize = this.corner.subtract(this.pivot); this.origCenter = this.boundsPath.bounds.center; for (const item of selectedItems) { @@ -105,14 +109,14 @@ class ScaleTool { for (let i = 0; i < this.boundsScaleHandles.length; i++) { const handle = this.boundsScaleHandles[i]; - handle.position = this.itemGroup.bounds[this.getRectCornerNameByIndex(i)]; + handle.position = this.itemGroup.bounds[this._getRectCornerNameByIndex(i)]; handle.bringToFront(); } for (let i = 0; i < this.boundsRotHandles.length; i++) { const handle = this.boundsRotHandles[i]; if (handle) { - handle.position = this.itemGroup.bounds[this.getRectCornerNameByIndex(i)] + handle.data.offset; + handle.position = this.itemGroup.bounds[this._getRectCornerNameByIndex(i)] + handle.data.offset; handle.bringToFront(); } } @@ -156,7 +160,7 @@ class ScaleTool { // @todo add back undo this.onUpdateSvg(); } - getRectCornerNameByIndex (index) { + _getRectCornerNameByIndex (index) { switch (index) { case 0: return 'bottomLeft'; @@ -176,7 +180,7 @@ class ScaleTool { return 'bottomCenter'; } } - getOpposingRectCornerNameByIndex (index) { + _getOpposingRectCornerNameByIndex (index) { switch (index) { case 0: return 'topRight'; diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index 68a4adf2..e2f561ed 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -6,10 +6,23 @@ import BoundingBoxTool from './bounding-box-tool'; import SelectionBoxTool from './selection-box-tool'; import paper from 'paper'; +/** + * paper.Tool that handles select mode. This is made up of 2 subtools. + * - The selection box tool is active when the user clicks an empty space and drags. + * It selects all items in the rectangle. + * - The bounding box tool is active if the user clicks on a non-empty space. It handles + * reshaping the item that was clicked. + */ class SelectTool extends paper.Tool { + /** The distance within which mouse events count as a hit against an item */ static get TOLERANCE () { return 6; } + /** + * @param {function} setHoveredItem Callback to set the hovered item + * @param {function} clearHoveredItem Callback to clear the hovered item + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ constructor (setHoveredItem, clearHoveredItem, onUpdateSvg) { super(); this.setHoveredItem = setHoveredItem; @@ -18,13 +31,6 @@ class SelectTool extends paper.Tool { this.boundingBoxTool = new BoundingBoxTool(onUpdateSvg); this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT); this.selectionBoxMode = false; - this._hitOptions = { - segments: true, - stroke: true, - curves: true, - fill: true, - guide: false - }; // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. @@ -37,21 +43,42 @@ class SelectTool extends paper.Tool { selectRootItem(); this.boundingBoxTool.setSelectionBounds(); } + /** + * To be called when the hovered item changes. When the select tool hovers over a + * new item, it compares against this to see if a hover item change event needs to + * be fired. + * @param {paper.Item} prevHoveredItem The highlight that indicates the mouse is over + * a given item currently + */ setPrevHoveredItem (prevHoveredItem) { this.prevHoveredItem = prevHoveredItem; } + /** + * Returns the hit options to use when conducting hit tests. + * @param {boolean} preselectedOnly True if we should only return results that are already + * selected. + * @return {object} See paper.Item.hitTest for definition of options + */ getHitOptions (preselectedOnly) { - this._hitOptions.tolerance = SelectTool.TOLERANCE / paper.view.zoom; + // Tolerance needs to be scaled when the view is zoomed in in order to represent the same + // distance for the user to move the mouse. + const hitOptions = { + segments: true, + stroke: true, + curves: true, + fill: true, + guide: false, + tolerance: SelectTool.TOLERANCE / paper.view.zoom + }; if (preselectedOnly) { - this._hitOptions.selected = true; - } else { - delete this._hitOptions.selected; + hitOptions.selected = true; } - return this._hitOptions; + return hitOptions; } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button + // If bounding box tool does not find an item that was hit, use selection box tool. this.clearHoveredItem(); if (!this.boundingBoxTool .onMouseDown( diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js index f057200f..bc787c51 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -1,6 +1,7 @@ import {rectSelect} from '../guides'; import {clearSelection, processRectangularSelection} from '../selection'; +/** Tool to handle drag selection. A dotted line box appears and everything enclosed is selected. */ class SelectionBoxTool { constructor (mode) { this.selectionRect = null; diff --git a/src/helper/selection.js b/src/helper/selection.js index db076952..db57ae92 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -1,11 +1,30 @@ import paper from 'paper'; import Modes from '../modes/modes'; -import {getAllPaperItems} from './helper'; import {getItemsGroup, isGroup} from './group'; import {getRootItem, isBoundsItem, isCompoundPathItem, isPathItem, isPGTextItem} from './item'; import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compound-path'; +/** + * @param {boolean} includeGuides True if guide layer items like the bounding box should + * be included in the returned items. + * @return {Array} all top-level (direct descendants of a paper.Layer) items + */ +const getAllPaperItems = function (includeGuides) { + includeGuides = includeGuides || false; + const allItems = []; + for (const layer of paper.project.layers) { + for (const child of layer.children) { + // don't give guides back + if (!includeGuides && child.guide) { + continue; + } + allItems.push(child); + } + } + return allItems; +}; + /** * @return {Array} all top-level (direct descendants of a paper.Layer) items * that aren't guide items or helper items. @@ -489,6 +508,7 @@ const shouldShowSelectAll = function () { }; export { + getAllPaperItems, selectAllItems, selectAllSegments, clearSelection, diff --git a/src/index.js b/src/index.js index 2c655e14..bbcbba68 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import PaintEditor from './containers/paint-editor.jsx'; -import SelectionHOV from './containers/selection-hov.jsx'; +import SelectionHOV from './containers/selection-hoc.jsx'; import ScratchPaintReducer from './reducers/scratch-paint-reducer'; const Wrapped = SelectionHOV(PaintEditor); From 05ad64fd015880785e2da17ccc9513b446ca42a0 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 12:22:09 -0400 Subject: [PATCH 16/17] fix being able to select subgroups again --- src/helper/selection.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/helper/selection.js b/src/helper/selection.js index db57ae92..4abd8b31 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -366,7 +366,7 @@ const checkBoundsItem = function (selectionRect, item, event) { itemBounds.remove(); }; -const handleRectangularSelectionItems = function (item, event, rect, mode) { +const _handleRectangularSelectionItems = function (item, event, rect, mode, root) { if (isPathItem(item)) { let segmentMode = false; @@ -383,9 +383,9 @@ const handleRectangularSelectionItems = function (item, event, rect, mode) { segmentMode = true; } else { if (event.modifiers.shift && item.selected) { - setItemSelection(item, false); + setItemSelection(root, false); } else { - setItemSelection(item, true); + setItemSelection(root, true, true /* fullySelected */); } return false; } @@ -441,7 +441,7 @@ const _rectangularSelectionGroupLoop = function (group, rect, root, event, mode) if (isGroup(child) || isCompoundPathItem(child)) { _rectangularSelectionGroupLoop(child, rect, root, event, mode); } else { - handleRectangularSelectionItems(child, event, rect, mode); + _handleRectangularSelectionItems(child, event, rect, mode, root); } } return true; @@ -464,14 +464,10 @@ const processRectangularSelection = function (event, rect, mode) { continue; } if (isGroup(item) || isCompoundPathItem(item)) { - // Drill into the group in reshape mode; check for item segment points inside - if (mode === Modes.RESHAPE) { - _rectangularSelectionGroupLoop(item, rect, item, event, mode); - } else { - setGroupSelection(item, true, true /* fullySelected */); - } + // check for item segment points inside + _rectangularSelectionGroupLoop(item, rect, item, event, mode); } else { - handleRectangularSelectionItems(item, event, rect, mode); + _handleRectangularSelectionItems(item, event, rect, mode, item); } } }; From e5303784a00ac4441f15c272b265376b9e18d893 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 22 Sep 2017 13:48:18 -0400 Subject: [PATCH 17/17] Switch state to track hover item ID instead of item itself --- package.json | 1 - src/containers/select-mode.jsx | 13 ++++---- src/containers/selection-hoc.jsx | 36 ++++++++++++++++------- src/helper/selection-tools/select-tool.js | 18 ++++++------ src/reducers/hover.js | 20 ++++++++----- src/reducers/scratch-paint-reducer.js | 2 +- test/unit/hover-reducer.test.js | 13 +++----- 7 files changed, 58 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 7fabae78..3e56fbb8 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "babel-plugin-transform-object-rest-spread": "^6.22.0", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.22.0", - "canvas-prebuilt": "^1.6.5-prerelease.1", "classnames": "2.2.5", "css-loader": "0.28.3", "enzyme": "^2.8.2", diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index 731ba0e6..3992d3f9 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -9,7 +9,6 @@ import {setHoveredItem, clearHoveredItem} from '../reducers/hover'; import SelectTool from '../helper/selection-tools/select-tool'; import SelectModeComponent from '../components/select-mode.jsx'; -import paper from 'paper'; class SelectMode extends React.Component { constructor (props) { @@ -25,8 +24,8 @@ class SelectMode extends React.Component { } } componentWillReceiveProps (nextProps) { - if (this.tool && nextProps.hoveredItem !== this.props.hoveredItem) { - this.tool.setPrevHoveredItem(nextProps.hoveredItem); + if (this.tool && nextProps.hoveredItemId !== this.props.hoveredItemId) { + this.tool.setPrevHoveredItemId(nextProps.hoveredItemId); } if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) { @@ -57,7 +56,7 @@ class SelectMode extends React.Component { SelectMode.propTypes = { clearHoveredItem: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, - hoveredItem: PropTypes.instanceOf(paper.Item), + hoveredItemId: PropTypes.number, isSelectModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, setHoveredItem: PropTypes.func.isRequired @@ -65,11 +64,11 @@ SelectMode.propTypes = { const mapStateToProps = state => ({ isSelectModeActive: state.scratchPaint.mode === Modes.SELECT, - hoveredItem: state.scratchPaint.hoveredItem + hoveredItemId: state.scratchPaint.hoveredItemId }); const mapDispatchToProps = dispatch => ({ - setHoveredItem: hoveredItem => { - dispatch(setHoveredItem(hoveredItem)); + setHoveredItem: hoveredItemId => { + dispatch(setHoveredItem(hoveredItemId)); }, clearHoveredItem: () => { dispatch(clearHoveredItem()); diff --git a/src/containers/selection-hoc.jsx b/src/containers/selection-hoc.jsx index ca1179ab..e468a179 100644 --- a/src/containers/selection-hoc.jsx +++ b/src/containers/selection-hoc.jsx @@ -1,29 +1,43 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; +import bindAll from 'lodash.bindall'; import paper from 'paper'; const SelectionHOC = function (WrappedComponent) { class SelectionComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'removeItemById' + ]); + } componentDidMount () { - if (this.props.hoveredItem) { + if (this.props.hoveredItemId) { paper.view.update(); } } componentDidUpdate (prevProps) { - if (this.props.hoveredItem && this.props.hoveredItem !== prevProps.hoveredItem) { - // A hover item has been added. Update the view - if (prevProps.hoveredItem) { - prevProps.hoveredItem.remove(); + // Hovered item has changed + if ((this.props.hoveredItemId && this.props.hoveredItemId !== prevProps.hoveredItemId) || + (!this.props.hoveredItemId && prevProps.hoveredItemId)) { + // Remove the old hover item if any + this.removeItemById(prevProps.hoveredItemId); + } + } + removeItemById (itemId) { + if (itemId) { + const match = paper.project.getItem({ + match: item => (item.id === itemId) + }); + if (match) { + match.remove(); } - } else if (!this.props.hoveredItem && prevProps.hoveredItem) { - // Remove the hover item - prevProps.hoveredItem.remove(); } } render () { const { - hoveredItem, // eslint-disable-line no-unused-vars + hoveredItemId, // eslint-disable-line no-unused-vars ...props } = this.props; return ( @@ -32,11 +46,11 @@ const SelectionHOC = function (WrappedComponent) { } } SelectionComponent.propTypes = { - hoveredItem: PropTypes.instanceOf(paper.Item) + hoveredItemId: PropTypes.number }; const mapStateToProps = state => ({ - hoveredItem: state.scratchPaint.hoveredItem + hoveredItemId: state.scratchPaint.hoveredItemId }); return connect( mapStateToProps diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index e2f561ed..22320a23 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -47,11 +47,11 @@ class SelectTool extends paper.Tool { * To be called when the hovered item changes. When the select tool hovers over a * new item, it compares against this to see if a hover item change event needs to * be fired. - * @param {paper.Item} prevHoveredItem The highlight that indicates the mouse is over - * a given item currently + * @param {paper.Item} prevHoveredItemId ID of the highlight item that indicates the mouse is + * over a given item currently */ - setPrevHoveredItem (prevHoveredItem) { - this.prevHoveredItem = prevHoveredItem; + setPrevHoveredItemId (prevHoveredItemId) { + this.prevHoveredItemId = prevHoveredItemId; } /** * Returns the hit options to use when conducting hit tests. @@ -92,11 +92,11 @@ class SelectTool extends paper.Tool { } handleMouseMove (event) { const hoveredItem = getHoveredItem(event, this.getHitOptions()); - if ((!hoveredItem && this.prevHoveredItem) || // There is no longer a hovered item - (hoveredItem && !this.prevHoveredItem) || // There is now a hovered item - (hoveredItem && this.prevHoveredItem && - hoveredItem.id !== this.prevHoveredItem.id)) { // hovered item changed - this.setHoveredItem(hoveredItem); + if ((!hoveredItem && this.prevHoveredItemId) || // There is no longer a hovered item + (hoveredItem && !this.prevHoveredItemId) || // There is now a hovered item + (hoveredItem && this.prevHoveredItemId && + hoveredItem.id !== this.prevHoveredItemId)) { // hovered item changed + this.setHoveredItem(hoveredItem ? hoveredItem.id : null); } } handleMouseDrag (event) { diff --git a/src/reducers/hover.js b/src/reducers/hover.js index 2a3e31ee..fe5d5cab 100644 --- a/src/reducers/hover.js +++ b/src/reducers/hover.js @@ -1,4 +1,3 @@ -import paper from 'paper'; import log from '../log/log'; const CHANGE_HOVERED = 'scratch-paint/hover/CHANGE_HOVERED'; @@ -8,29 +7,36 @@ const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { case CHANGE_HOVERED: - if (typeof action.hoveredItem === 'undefined' || - (action.hoveredItem !== null && !(action.hoveredItem instanceof paper.Item))) { + if (typeof action.hoveredItemId === 'undefined') { log.warn(`Hovered item should not be set to undefined. Use null.`); return state; + } else if (typeof action.hoveredItemId === 'undefined' || isNaN(action.hoveredItemId)) { + log.warn(`Hovered item should be an item ID number. Got: ${action.hoveredItemId}`); + return state; } - return action.hoveredItem; + return action.hoveredItemId; default: return state; } }; // Action creators ================================== -const setHoveredItem = function (hoveredItem) { +/** + * Set the hovered item state to the given item ID + * @param {number} hoveredItemId The paper.Item ID of the hover indicator item. + * @return {object} Redux action to change the hovered item. + */ +const setHoveredItem = function (hoveredItemId) { return { type: CHANGE_HOVERED, - hoveredItem: hoveredItem + hoveredItemId: hoveredItemId }; }; const clearHoveredItem = function () { return { type: CHANGE_HOVERED, - hoveredItem: null + hoveredItemId: null }; }; diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 3dfabe6b..0d44903b 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -10,5 +10,5 @@ export default combineReducers({ brushMode: brushModeReducer, eraserMode: eraserModeReducer, color: colorReducer, - hoveredItem: hoverReducer + hoveredItemId: hoverReducer }); diff --git a/test/unit/hover-reducer.test.js b/test/unit/hover-reducer.test.js index b8c63cd0..58f469b8 100644 --- a/test/unit/hover-reducer.test.js +++ b/test/unit/hover-reducer.test.js @@ -1,12 +1,7 @@ /* eslint-env jest */ -import paper from 'paper'; import reducer from '../../src/reducers/hover'; import {clearHoveredItem, setHoveredItem} from '../../src/reducers/hover'; -beforeEach(() => { - paper.setup(); -}); - test('initialState', () => { let defaultState; expect(reducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeNull(); @@ -14,22 +9,22 @@ test('initialState', () => { test('setHoveredItem', () => { let defaultState; - const item1 = new paper.Path(); - const item2 = new paper.Path(); + const item1 = 1; + const item2 = 2; expect(reducer(defaultState /* state */, setHoveredItem(item1) /* action */)).toBe(item1); expect(reducer(item1 /* state */, setHoveredItem(item2) /* action */)).toBe(item2); }); test('clearHoveredItem', () => { let defaultState; - const item = new paper.Path(); + const item = 1; expect(reducer(defaultState /* state */, clearHoveredItem() /* action */)).toBeNull(); expect(reducer(item /* state */, clearHoveredItem() /* action */)).toBeNull(); }); test('invalidSetHoveredItem', () => { let defaultState; - const item = new paper.Path(); + const item = 1; const nonItem = {random: 'object'}; let undef; expect(reducer(defaultState /* state */, setHoveredItem(nonItem) /* action */)).toBeNull();