From 448ff9bfe43e9513e87467390cc111663d6b1631 Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 11 Sep 2017 14:23:30 -0400 Subject: [PATCH] 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 });