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
});