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