From cf75703580bd72438cdb373382787cde3f3a4f23 Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 11 Sep 2017 10:52:00 -0400 Subject: [PATCH] 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