Add more selection files

This commit is contained in:
DD 2017-09-11 14:23:30 -04:00
parent ef367646fb
commit 448ff9bfe4
23 changed files with 1718 additions and 769 deletions

View file

@ -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}
/>
<SelectMode
onUpdateSvg={this.props.onUpdateSvg}
/>
</div>
) : null}

View file

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
const SelectModeComponent = props => (
<button onClick={props.onMouseDown}>
<FormattedMessage
defaultMessage="Select"
description="Label for the select tool, which allows selecting, moving, and resizing shapes"
id="paint.selectMode.select"
/>
</button>
);
SelectModeComponent.propTypes = {
onMouseDown: PropTypes.func.isRequired
};
export default SelectModeComponent;

View file

@ -23,7 +23,7 @@ const StrokeWidthIndicatorComponent = props => (
StrokeWidthIndicatorComponent.propTypes = {
onChangeStrokeWidth: PropTypes.func.isRequired,
strokeWidth: PropTypes.string.isRequired
strokeWidth: PropTypes.number.isRequired
};
export default StrokeWidthIndicatorComponent;

View file

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

View file

@ -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 (
<WrappedComponent {...props} />
);
}
}
SelectionComponent.propTypes = {
hoveredItem: PropTypes.instanceOf(paper.Item)
};
const mapStateToProps = state => ({
hoveredItem: state.scratchPaint.hoveredItem
});
return connect(
mapStateToProps
)(SelectionComponent);
};
export default SelectionHOV;

View file

@ -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<selectedItems.length; i++) {
var item = selectedItems[i];
// 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 +
pg.math.snapDeltaToAngle(dragVector, Math.PI*2/8);
} else {
item.position += event.delta;
}
}
}
}
onMouseUp (event) {
if(event.event.button > 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<itemGroup.children.length; i++) {
var child = itemGroup.children[i];
if(child.data.isPGTextItem) {
child.data.wasScaled = true;
}
}
if (scaleItemsInsertBelow) {
// No increment step because itemGroup.children is getting depleted
for (var i = 0; i < itemGroup.children.length;) {
itemGroup.children[i].insertBelow(scaleItemsInsertBelow);
}
scaleItemsInsertBelow = null;
} else if (itemGroup.layer) {
itemGroup.layer.addChildren(itemGroup.children);
}
itemGroup.remove();
pg.undo.snapshot('scaleSelection');
}
} else if(mode == 'rotate') {
jQuery.each(rotItems, function(i, item) {
item.applyMatrix = true;
});
pg.undo.snapshot('rotateSelection');
}
mode = 'none';
selectionRect = null;
if(pg.selection.getSelectedItems().length <= 0) {
removeBoundsPath();
} else {
setSelectionBounds();
}
}
}

View file

@ -0,0 +1,181 @@
import paper from 'paper';
import keyMirror from 'keymirror';
import {clearSelection, getSelectedItems} from '../selection';
import {getGuideColor, removeHelperItems} from '../guides';
import {getGuideLayer} from '../layer';
import ScaleTool from './scale-tool';
import RotateTool from './rotate-tool';
import MoveTool from './move-tool';
/** SVG for the rotation icon on the bounding box */
const ARROW_PATH = 'M19.28,1.09C19.28.28,19,0,18.2,0c-1.67,0-3.34,0-5,0-.34,0-.88.24-1,.47a1.4,1.4,0,0,0,.36,1.08,15.27,15.27,0,0,0,1.46,1.36A6.4,6.4,0,0,1,6.52,4,5.85,5.85,0,0,1,5.24,3,15.27,15.27,0,0,0,6.7,1.61,1.4,1.4,0,0,0,7.06.54C7,.3,6.44.07,6.1.06c-1.67,0-3.34,0-5,0C.28,0,0,.31,0,1.12c0,1.67,0,3.34,0,5a1.23,1.23,0,0,0,.49,1,1.22,1.22,0,0,0,1-.31A14.38,14.38,0,0,0,2.84,5.26l.73.62a9.45,9.45,0,0,0,7.34,2,9.45,9.45,0,0,0,4.82-2.05l.73-.62a14.38,14.38,0,0,0,1.29,1.51,1.22,1.22,0,0,0,1,.31,1.23,1.23,0,0,0,.49-1C19.31,4.43,19.29,2.76,19.28,1.09Z';
/** Modes of the bounding box tool, which can do many things depending on how it's used. */
const Modes = keyMirror({
SCALE: null,
ROTATE: null,
MOVE: null
});
/**
* A paper.Tool that handles transforming the selection and drawing a bounding box with handles.
* On mouse down, the type of function (move, scale, rotate) is determined based on what is clicked
* (scale handle, rotate handle, the object itself). This determines the mode of the tool, which then
* delegates actions to the MoveTool, RotateTool or ScaleTool accordingly.
*/
class BoundingBoxTool {
constructor () {
this.mode = null;
this.boundsPath = null;
this.boundsScaleHandles = [];
this.boundsRotHandles = [];
this._modeMap = {};
this._modeMap[Modes.SCALE] = new ScaleTool();
this._modeMap[Modes.ROTATE] = new RotateTool();
this._modeMap[Modes.MOVE] = new MoveTool();
}
/**
* @param {!MouseEvent} event The mouse event
* @param {boolean} clone Whether to clone on mouse down (e.g. alt key held)
* @param {boolean} multiselect Whether to multiselect on mouse down (e.g. shift key held)
* @param {boolean} preselectedOnly If true, only get hit results on items that are already selected
* @return {boolean} True if there was a hit, false otherwise
*/
onMouseDown (event, clone, multiselect, preselectedOnly) {
const hitResults = paper.project.hitTestAll(event.point, this.getHitOptions(preselectedOnly));
if (!hitResults || hitResults.length === 0) {
if (!multiselect) {
this.removeBoundsPath();
clearSelection();
}
return null;
}
// Prefer scale to trigger over rotate, and scale and rotate to trigger over other hits
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 = Modes.SCALE;
this._modeMap[this.mode].onMouseDown(hitResult, this.boundsPath, getSelectedItems());
break;
} else if (hitResults[i].item.data && hitResults[i].item.data.isRotHandle) {
hitResult = hitResults[i];
this.mode = Modes.ROTATE;
this._modeMap[this.mode].onMouseDown(hitResult, this.boundsPath, getSelectedItems());
} else {
this.mode = Modes.MOVE;
this._modeMap[this.mode].onMouseDown(hitResult, clone, multiselect);
}
}
// while transforming object, never show the bounds stuff
this.removeBoundsPath();
}
onMouseDrag (event) {
if (event.event.button > 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;

View file

@ -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;

View file

@ -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.<paper.Item>} 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;

View file

@ -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.<paper.Item>} 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;

View file

@ -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<items.length; i++) {
// const item = items[i];
// if (isCompoundPath(item)) {
// for (const j=0; j<item.children.length; j++) {
// const path = item.children[j];
// path.parent = item.layer;
// pg.selection.setItemSelection(path, true);
// j--;
// }
// cPathsToDelete.push(item);
// pg.selection.setItemSelection(item, false);
// } else {
// items[i].parent = item.layer;
// }
// }
// for (const j=0; j<cPathsToDelete.length; j++) {
// cPathsToDelete[j].remove();
// }
// pg.undo.snapshot('releaseCompoundPath');
// };
export {
isCompoundPath,
isCompoundPathChild,
getItemsCompoundPath
// createFromSelection,
// releaseSelection
};

View file

@ -1,86 +0,0 @@
pg.compoundPath = function() {
var isCompoundPath = function(item) {
return item && item.className === 'CompoundPath';
};
var isCompoundPathChild = function(item) {
if(item.parent) {
return item.parent.className === 'CompoundPath';
} else {
return false;
}
};
var getItemsCompoundPath = function(item) {
var itemParent = item.parent;
if(isCompoundPath(itemParent)) {
return itemParent;
} else {
return null;
}
};
var createFromSelection = function() {
var items = pg.selection.getSelectedPaths();
if(items.length < 2) return;
var path = new paper.CompoundPath({fillRule: 'evenodd'});
for(var 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');
};
var releaseSelection = function() {
var items = pg.selection.getSelectedItems();
var cPathsToDelete = [];
for(var i=0; i<items.length; i++) {
var item = items[i];
if(isCompoundPath(item)) {
for(var j=0; j<item.children.length; j++) {
var path = item.children[j];
path.parent = item.layer;
pg.selection.setItemSelection(path, true);
j--;
}
cPathsToDelete.push(item);
pg.selection.setItemSelection(item, false);
} else {
items[i].parent = item.layer;
}
}
for(var j=0; j<cPathsToDelete.length; j++) {
cPathsToDelete[j].remove();
}
pg.undo.snapshot('releaseCompoundPath');
};
return {
isCompoundPath: isCompoundPath,
isCompoundPathChild: isCompoundPathChild,
getItemsCompoundPath: getItemsCompoundPath,
createFromSelection: createFromSelection,
releaseSelection: releaseSelection
};
}();

View file

@ -1,139 +1,132 @@
// function related to groups and grouping
import paper from 'paper';
import {getRootItem, isGroupItem} from './item';
import {clearSelection, getSelectedItems, setItemSelection} from './selection';
pg.group = function() {
const isGroup = function (item) {
return isGroupItem(item);
};
var groupSelection = function() {
var items = pg.selection.getSelectedItems();
if(items.length > 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<items.length; i++) {
var item = items[i];
if(isGroup(item) && !item.data.isPGTextItem) {
ungroupLoop(item, false /* recursive */);
const groupItems = function (items) {
if (items.length > 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<emptyGroups.length; j++) {
emptyGroups[j].remove();
}
jQuery(document).trigger('Ungrouped');
pg.undo.snapshot('ungroupItems');
};
if (isGroup(itemParent)) {
return itemParent;
}
return null;
};
const isGroupChild = function (item) {
const rootItem = getRootItem(item);
return isGroup(rootItem);
};
var 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(var i=0; i<group.children.length; i++) {
var 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--;
}
};
const shouldShowGroup = function () {
const items = getSelectedItems();
return items.length > 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<items.length; i++) {
var item = items[i];
if(isGroup(item) && !item.data.isPGTextItem && item.children && item.children.length > 0) {
return true;
}
}
return false;
};
return {
groupSelection: groupSelection,
ungroupSelection: ungroupSelection,
groupItems: groupItems,
ungroupItems: ungroupItems,
getItemsGroup: getItemsGroup,
isGroup: isGroup,
isGroupChild:isGroupChild,
shouldShowGroup:shouldShowGroup,
shouldShowUngroup:shouldShowUngroup
};
}();
export {
groupSelection,
ungroupSelection,
groupItems,
ungroupItems,
getItemsGroup,
isGroup,
isGroupChild,
shouldShowGroup,
shouldShowUngroup
};

View file

@ -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<paper.project.layers.length; i++) {
var layer = paper.project.layers[i];
for(var j=0; j<layer.children.length; j++) {
var child = layer.children[j];
// only give guides
if(!child.guide) {
continue;
}
allItems.push(child);
}
}
return allItems;
};
var getExportRectGuide = function() {
var guides = getAllGuides();
for(var i=0; i<guides.length; i++){
if(guides[i].data && guides[i].data.isExportRect) {
return guides[i];
}
}
};
var removeHelperItems = function() {
pg.helper.removePaperItemsByDataTags(['isHelperItem']);
};
var removeAllGuides = function() {
pg.helper.removePaperItemsByTags(['guide']);
};
var removeExportRectGuide = function() {
pg.helper.removePaperItemsByDataTags(['isExportRect']);
};
return {
hoverItem: hoverItem,
hoverBounds: hoverBounds,
rectSelect: rectSelect,
line: line,
crossPivot: crossPivot,
rotPivot: rotPivot,
label: label,
removeAllGuides: removeAllGuides,
removeHelperItems: removeHelperItems,
removeExportRectGuide: removeExportRectGuide,
getAllGuides: getAllGuides,
getExportRectGuide: getExportRectGuide,
getGuideColor: getGuideColor,
setDefaultGuideStyle:setDefaultGuideStyle
};
const removeAllGuides = function () {
removePaperItemsByTags(['guide']);
};
}();
const removeExportRectGuide = function () {
removePaperItemsByDataTags(['isExportRect']);
};
export {
hoverItem,
hoverBounds,
rectSelect,
line,
crossPivot,
rotPivot,
label,
removeAllGuides,
removeHelperItems,
removeExportRectGuide,
getAllGuides,
getExportRectGuide,
getGuideColor,
setDefaultGuideStyle
};

61
src/helper/helper.js Normal file
View file

@ -0,0 +1,61 @@
import paper from 'paper';
const getAllPaperItems = function (includeGuides) {
includeGuides = includeGuides || false;
const allItems = [];
for (const layer of paper.project.layers) {
for (const child of layer.children) {
// don't give guides back
if (!includeGuides && child.guide) {
continue;
}
allItems.push(child);
}
}
return allItems;
};
const getPaperItemsByTags = function (tags) {
const allItems = getAllPaperItems(true);
const foundItems = [];
for (const item of allItems) {
for (const tag of tags) {
if (item[tag] && foundItems.indexOf(item) === -1) {
foundItems.push(item);
}
}
}
return foundItems;
};
const removePaperItemsByDataTags = function (tags) {
const allItems = getAllPaperItems(true);
for (const item of allItems) {
for (const tag of tags) {
if (item.data && item.data[tag]) {
item.remove();
}
}
}
};
const removePaperItemsByTags = function (tags) {
const allItems = getAllPaperItems(true);
for (const item of allItems) {
for (const tag of tags) {
if (item[tag]) {
item.remove();
}
}
}
};
export {
getAllPaperItems,
getPaperItemsByTags,
removePaperItemsByDataTags,
removePaperItemsByTags
};

View file

@ -1,12 +1,14 @@
import paper from 'paper';
import {isBoundsItem, getRootItem} from './item';
import {hoverBounds, hoverItem} from './guides';
import {isGroupChild} from './group';
const CLEAR_HOVERED_ITEM = 'scratch-paint/hover/CLEAR_HOVERED_ITEM';
/**
* @param hitOptions hit options to use
* @param event mouse event
* @return the hovered item or null if there is none
* @param {!MouseEvent} event mouse event
* @param {?object} hitOptions hit options to use
* @return {paper.Item} the hovered item or null if there is none
*/
const getHoveredItem = function (hitOptions, event) {
const getHoveredItem = function (event, hitOptions) {
const hitResults = paper.project.hitTestAll(event.point, hitOptions);
if (hitResults.length === 0) {
return null;
@ -23,28 +25,14 @@ const getHoveredItem = function (hitOptions, event) {
return null;
}
if (pg.item.isBoundsItem(hitResult.item)) {
return pg.guides.hoverBounds(hitResult.item);
} else if(pg.group.isGroupChild(hitResult.item)) {
return pg.guides.hoverBounds(pg.item.getRootItem(hitResult.item));
} else {
return pg.guides.hoverItem(hitResult);
if (isBoundsItem(hitResult.item)) {
return hoverBounds(hitResult.item);
} else if (isGroupChild(hitResult.item)) {
return hoverBounds(getRootItem(hitResult.item));
}
return hoverItem(hitResult);
};
// Action creators ==================================
const clearHoveredItem = function () {
return {
type: CLEAR_HOVERED_ITEM
};
// TODO: paper.view.update();
};
export {
getHoveredItem,
clearHoveredItem
getHoveredItem
};

78
src/helper/item.js Normal file
View file

@ -0,0 +1,78 @@
import paper from 'paper';
const getRootItem = function (item) {
if (item.parent.className === 'Layer') {
return item;
}
return getRootItem(item.parent);
};
const isBoundsItem = function (item) {
if (item.className === 'PointText' ||
item.className === 'Shape' ||
item.className === 'PlacedSymbol' ||
item.className === 'Raster') {
return true;
}
return false;
};
const isPathItem = function (item) {
return item.className === 'Path';
};
const isCompoundPathItem = function (item) {
return item.className === 'CompoundPath';
};
const isGroupItem = function (item) {
return item && item.className && item.className === 'Group';
};
const isPointTextItem = function (item) {
return item.className === 'PointText';
};
const isPGTextItem = function (item) {
return getRootItem(item).data.isPGTextItem;
};
const setPivot = function (item, point) {
if (isBoundsItem(item)) {
item.pivot = item.globalToLocal(point);
} else {
item.pivot = point;
}
};
const getPositionInView = function (item) {
const itemPos = new paper.Point();
itemPos.x = item.position.x - paper.view.bounds.x;
itemPos.y = item.position.y - paper.view.bounds.y;
return itemPos;
};
const setPositionInView = function (item, pos) {
item.position.x = paper.view.bounds.x + pos.x;
item.position.y = paper.view.bounds.y + pos.y;
};
export {
isBoundsItem,
isPathItem,
isCompoundPathItem,
isGroupItem,
isPointTextItem,
isPGTextItem,
setPivot,
getPositionInView,
setPositionInView,
getRootItem
};

18
src/helper/layer.js Normal file
View file

@ -0,0 +1,18 @@
import paper from 'paper';
const getGuideLayer = function () {
for (let i = 0; i < paper.project.layers.length; i++) {
const layer = paper.project.layers[i];
if (layer.data && layer.data.isGuideLayer) {
return layer;
}
}
// Create if it doesn't exist
const guideLayer = new paper.Layer();
guideLayer.data.isGuideLayer = true;
guideLayer.bringToFront();
return guideLayer;
};
export default getGuideLayer;

36
src/helper/math.js Normal file
View file

@ -0,0 +1,36 @@
import paper from 'paper';
const checkPointsClose = function (startPos, eventPoint, threshold) {
const xOff = Math.abs(startPos.x - eventPoint.x);
const yOff = Math.abs(startPos.y - eventPoint.y);
if (xOff < threshold && yOff < threshold) {
return true;
}
return false;
};
const getRandomInt = function (min, max) {
return Math.floor(Math.random() * (max - min)) + min;
};
const getRandomBoolean = function () {
return getRandomInt(0, 2) === 1;
};
// Thanks Mikko Mononen! https://github.com/memononen/stylii
const snapDeltaToAngle = function (delta, snapAngle) {
let angle = Math.atan2(delta.y, delta.x);
angle = Math.round(angle / snapAngle) * snapAngle;
const dirx = Math.cos(angle);
const diry = Math.sin(angle);
const d = (dirx * delta.x) + (diry * delta.y);
return new paper.Point(dirx * d, diry * d);
};
export {
checkPointsClose,
getRandomInt,
getRandomBoolean,
snapDeltaToAngle
};

505
src/helper/selection.js Normal file
View file

@ -0,0 +1,505 @@
import paper from 'paper';
import Modes from '../modes/modes';
import {getAllPaperItems} from './helper';
import {getItemsGroup, isGroup} from './group';
import {getRootItem, isBoundsItem, isCompoundPathItem, isPathItem, isPGTextItem} from './item';
import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compound-path';
const getAllSelectableItems = function () {
const allItems = getAllPaperItems();
const selectables = [];
for (let i = 0; i < allItems.length; i++) {
if (allItems[i].data && !allItems[i].data.isHelperItem) {
selectables.push(allItems[i]);
}
}
return selectables;
};
const selectItemSegments = function (item, state) {
if (item.children) {
for (let i = 0; i < item.children.length; i++) {
const child = item.children[i];
if (child.children && child.children.length > 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
};

View file

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

33
src/reducers/hover.js Normal file
View file

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

View file

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