In-progress add select mode

This commit is contained in:
DD 2017-09-11 10:52:00 -04:00
parent a2cd53b159
commit cf75703580
6 changed files with 913 additions and 0 deletions

View file

@ -0,0 +1,128 @@
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../modes/modes';
import {clearSelection} from '../reducers/selection';
import {setHoveredItem} from '../reducers/hover';
import {getHoveredItem} from '../helper/hover';
import {changeMode} from '../reducers/modes';
import SelectModeComponent from '../components/select-mode.jsx';
import paper from 'paper';
class SelectMode extends React.Component {
static get TOLERANCE () {
return 6;
}
constructor (props) {
super(props);
bindAll(this, [
'activateTool',
'deactivateTool',
'getHitOptions',
'preProcessSelection',
'onMouseDown',
'onMouseMove',
'onMouseDrag',
'onMouseUp'
]);
this._hitOptions = {
segments: true,
stroke: true,
curves: true,
fill: true,
guide: false
};
}
componentDidMount () {
if (this.props.isSelectModeActive) {
this.activateTool(this.props);
}
}
componentWillReceiveProps (nextProps) {
if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) {
this.activateTool();
} else if (!nextProps.isSelectModeActive && this.props.isSelectModeActive) {
this.deactivateTool();
}
}
shouldComponentUpdate () {
return false; // Static component, for now
}
getHitOptions () {
this._hitOptions.tolerance = SelectMode.TOLERANCE / paper.view.zoom;
return this._hitOptions;
}
activateTool () {
clearSelection();
this.preProcessSelection();
this.tool = new paper.Tool();
this.tool.onMouseDown = function (event) {
this.onMouseDown(event);
};
this.tool.onMouseMove = function (event) {
this.props.setHoveredItem(getHoveredItem(this.getHitOptions()));
};
this.tool.onMouseDrag = function (event) {
this.onMouseDrag(event);
};
this.tool.onMouseUp = function (event) {
this.onMouseUp(event);
};
this.tool.activate();
}
preProcessSelection () {
// when switching to the select tool while having a child object of a
// compound path selected, deselect the child and select the compound path
// instead. (otherwise the compound path breaks because of scale-grouping)
const items = this.props.selectedItems;
for (let item of items) {
if(isCompoundPathChild(item)) {
var cp = getItemsCompoundPath(item);
setItemSelection(item, false);
setItemSelection(cp, true);
}
};
};
deactivateTool () {
this.props.setHoveredItem();
this.tool.remove();
this.tool = null;
this.hitResult = null;
}
render () {
return (
<SelectModeComponent onMouseDown={this.props.handleMouseDown} />
);
}
}
SelectMode.propTypes = {
handleMouseDown: PropTypes.func.isRequired,
isSelectModeActive: PropTypes.bool.isRequired,
onUpdateSvg: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
isSelectModeActive: state.scratchPaint.mode === Modes.SELECT
});
const mapDispatchToProps = dispatch => ({
setHoveredItem: hoveredItem => {
dispatch(setHoveredItem(hoveredItem));
},
handleMouseDown: () => {
dispatch(changeMode(Modes.SELECT));
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(SelectMode);

View file

@ -0,0 +1,326 @@
import paper from 'paper';
var mode = 'none';
var selectionRect;
var itemGroup;
var pivot;
var corner;
var origPivot;
var origSize;
var origCenter;
var scaleItems;
var scaleItemsInsertBelow;
var rotItems = [];
var rotGroupPivot;
var prevRot = [];
class BoundingBoxTool extends paper.Tool {
onMouseDown: If BoundingBoxTool got a hit result, switch to bounding box tool as the primary tool.
Else switch to the default tool.
Where should the move tool be handled? Might make sense on bounding box tool since whenever the bounding
box is active, move is possible
Shift button handling? If you shift click, bounding box tool wants to add it to the selection. But shape tools
probably don't.
- If shift is held down during mouse click, don't switch to the bounding box tool even if it gets a hit?
Then we can decide how to deal with it differently for different modes.
Alt button handling?
- Same as shift?
onMouseDown (event) {
if(event.event.button > 0) return; // only first mouse button
clearHoveredItem();
const hitResults = paper.project.hitTestAll(event.point, this.getHitOptions());
// Prefer scale to trigger over rotate, since their regions overlap
if (hitResults && hitResults.length > 0) {
let hitResult = hitResults[0];
for (let i = 0; i < hitResults.length; i++) {
if (hitResults[i].item.data && hitResults[i].item.data.isScaleHandle) {
hitResult = hitResults[i];
this.mode = 'scale';
break;
} else if (hitResults[i].item.data && hitResults[i].item.data.isRotHandle) {
hitResult = hitResults[i];
this.mode = 'rotate';
}
}
if (mode === 'rotate') {
rotGroupPivot = boundsPath.bounds.center;
rotItems = pg.selection.getSelectedItems();
jQuery.each(rotItems, function(i, item) {
prevRot[i] = (event.point - rotGroupPivot).angle;
});
} else if (mode === 'scale') {
var index = hitResult.item.data.index;
pivot = boundsPath.bounds[getOpposingRectCornerNameByIndex(index)].clone();
origPivot = boundsPath.bounds[getOpposingRectCornerNameByIndex(index)].clone();
corner = boundsPath.bounds[getRectCornerNameByIndex(index)].clone();
origSize = corner.subtract(pivot);
origCenter = boundsPath.bounds.center;
scaleItems = pg.selection.getSelectedItems();
}
else { // Move mode
// deselect all by default if the shift key isn't pressed
// also needs some special love for compound paths and groups,
// as their children are not marked as "selected"
// deselect a currently selected item if shift is pressed
var root = pg.item.getRootItem(hitResult.item);
if(pg.item.isCompoundPathItem(root) || pg.group.isGroup(root)) {
if(!root.selected) {
if (!event.modifiers.shift) {
pg.selection.clearSelection()
}
root.selected = true;
for (var i = 0; i < root.children.length; i++) {
root.children[i].selected = true;
}
jQuery(document).trigger('SelectionChanged');
if(event.modifiers.alt) {
mode = 'cloneMove';
pg.selection.cloneSelection();
} else {
mode = 'move';
}
} else {
if (event.modifiers.shift) {
root.selected = false;
for (var i = 0; i < root.children.length; i++) {
root.children[i].selected = false;
}
} else {
if(event.modifiers.alt) {
mode = 'cloneMove';
pg.selection.cloneSelection();
} else {
mode = 'move';
}
}
}
} else if(hitResult.item.selected) {
if (event.modifiers.shift) {
pg.selection.setItemSelection(hitResult.item, false);
} else {
if(event.modifiers.alt) {
mode = 'cloneMove';
pg.selection.cloneSelection();
} else {
mode = 'move';
}
}
} else {
if (!event.modifiers.shift) {
pg.selection.clearSelection()
}
pg.selection.setItemSelection(hitResult.item, true);
if(event.modifiers.alt) {
mode = 'cloneMove';
pg.selection.cloneSelection();
} else {
mode = 'move';
}
}
}
// while transforming object, never show the bounds stuff
removeBoundsPath();
} else {
if (!event.modifiers.shift) {
removeBoundsPath();
pg.selection.clearSelection();
}
mode = 'rectSelection';
}
}
onMouseDrag (event) {
if(event.event.button > 0) return; // only first mouse button
var modOrigSize = origSize;
if(mode == 'rectSelection') {
selectionRect = pg.guides.rectSelect(event);
// Remove this rect on the next drag and up event
selectionRect.removeOnDrag();
} else if(mode == 'scale') {
// get index of scale items
var items = paper.project.getItems({
'match': function(item) {
if (item instanceof Layer) {
return false;
}
for (var i = 0; i < scaleItems.length; i++) {
if (!scaleItems[i].isBelow(item)) {
return false;
}
}
return true;
}
});
if (items.length > 0) {
// Lowest item above all scale items in z index
scaleItemsInsertBelow = items[0];
}
itemGroup = new paper.Group(scaleItems);
itemGroup.insertBelow(scaleItemsInsertBelow);
itemGroup.addChild(boundsPath);
itemGroup.data.isHelperItem = true;
itemGroup.strokeScaling = false;
itemGroup.applyMatrix = false;
if (event.modifiers.alt) {
pivot = origCenter;
modOrigSize = origSize*0.5;
} else {
pivot = origPivot;
}
corner = corner.add(event.delta);
var size = corner.subtract(pivot);
var sx = 1.0, sy = 1.0;
if (Math.abs(modOrigSize.x) > 0.0000001) {
sx = size.x / modOrigSize.x;
}
if (Math.abs(modOrigSize.y) > 0.0000001) {
sy = size.y / modOrigSize.y;
}
if (event.modifiers.shift) {
var signx = sx > 0 ? 1 : -1;
var signy = sy > 0 ? 1 : -1;
sx = sy = Math.max(Math.abs(sx), Math.abs(sy));
sx *= signx;
sy *= signy;
}
itemGroup.scale(sx, sy, pivot);
jQuery.each(boundsScaleHandles, function(index, handle) {
handle.position = itemGroup.bounds[getRectCornerNameByIndex(index)];
handle.bringToFront();
});
jQuery.each(boundsRotHandles, function(index, handle) {
if(handle) {
handle.position = itemGroup.bounds[getRectCornerNameByIndex(index)]+handle.data.offset;
handle.bringToFront();
}
});
} else if(mode == 'rotate') {
var rotAngle = (event.point - rotGroupPivot).angle;
jQuery.each(rotItems, function(i, item) {
if(!item.data.origRot) {
item.data.origRot = item.rotation;
}
if(event.modifiers.shift) {
rotAngle = Math.round(rotAngle / 45) *45;
item.applyMatrix = false;
item.pivot = rotGroupPivot;
item.rotation = rotAngle;
} else {
item.rotate(rotAngle - prevRot[i], rotGroupPivot);
}
prevRot[i] = rotAngle;
});
} else if(mode == 'move' || mode == 'cloneMove') {
var dragVector = (event.point - event.downPoint);
var selectedItems = pg.selection.getSelectedItems();
for(var i=0; i<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,86 @@
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
};
}();

139
src/helper/group.js Normal file
View file

@ -0,0 +1,139 @@
// function related to groups and grouping
pg.group = function() {
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;
}
};
// 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 */);
if(!item.hasChildren()) {
emptyGroups.push(item);
}
}
}
// 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');
};
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--;
}
};
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
};
}();

184
src/helper/guides.js Normal file
View file

@ -0,0 +1,184 @@
// functions related to guide items
pg.guides = function() {
var guideBlue = '#009dec';
var guideGrey = '#aaaaaa';
var hoverItem = function(hitResult) {
var segments = hitResult.item.segments;
var clone = new paper.Path(segments);
setDefaultGuideStyle(clone);
if(hitResult.item.closed) {
clone.closed = true;
}
clone.parent = pg.layer.getGuideLayer();
clone.strokeColor = guideBlue;
clone.fillColor = null;
clone.data.isHelperItem = true;
clone.bringToFront();
return clone;
};
var hoverBounds = function(item) {
var rect = new paper.Path.Rectangle(item.internalBounds);
rect.matrix = item.matrix;
setDefaultGuideStyle(rect);
rect.parent = pg.layer.getGuideLayer();
rect.strokeColor = guideBlue;
rect.fillColor = null;
rect.data.isHelperItem = true;
rect.bringToFront();
return rect;
};
var rectSelect = function(event, color) {
var half = new paper.Point(0.5 / paper.view.zoom, 0.5 / paper.view.zoom);
var start = event.downPoint.add(half);
var end = event.point.add(half);
var rect = new paper.Path.Rectangle(start, end);
var zoom = 1.0/paper.view.zoom;
setDefaultGuideStyle(rect);
if(!color) color = guideGrey;
rect.parent = pg.layer.getGuideLayer();
rect.strokeColor = color;
rect.data.isRectSelect = true;
rect.data.isHelperItem = true;
rect.dashArray = [3.0*zoom, 3.0*zoom];
return rect;
};
var line = function(from, to, color) {
var line = new paper.Path.Line(from, to);
var zoom = 1/paper.view.zoom;
setDefaultGuideStyle(line);
if (!color) color = guideGrey;
line.parent = pg.layer.getGuideLayer();
line.strokeColor = color;
line.strokeColor = color;
line.dashArray = [5*zoom, 5*zoom];
line.data.isHelperItem = true;
return line;
};
var crossPivot = function(center, color) {
var zoom = 1/paper.view.zoom;
var star = new paper.Path.Star(center, 4, 4*zoom, 0.5*zoom);
setDefaultGuideStyle(star);
if(!color) color = guideBlue;
star.parent = pg.layer.getGuideLayer();
star.fillColor = color;
star.strokeColor = color;
star.strokeWidth = 0.5*zoom;
star.data.isHelperItem = true;
star.rotate(45);
return star;
};
var rotPivot = function(center, color) {
var zoom = 1/paper.view.zoom;
var path = new paper.Path.Circle(center, 3*zoom);
setDefaultGuideStyle(path);
if(!color) color = guideBlue;
path.parent = pg.layer.getGuideLayer();
path.fillColor = color;
path.data.isHelperItem = true;
return path;
};
var label = function(pos, content, color) {
var text = new paper.PointText(pos);
if(!color) color = guideGrey;
text.parent = pg.layer.getGuideLayer();
text.fillColor = color;
text.content = content;
};
var setDefaultGuideStyle = function(item) {
item.strokeWidth = 1/paper.view.zoom;
item.opacity = 1;
item.blendMode = 'normal';
item.guide = true;
};
var getGuideColor = function(colorName) {
if(colorName == 'blue') {
return guideBlue;
} else if(colorName == 'grey') {
return guideGrey;
}
};
var getAllGuides = function() {
var allItems = [];
for(var i=0; i<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
};
}();

50
src/helper/hover.js Normal file
View file

@ -0,0 +1,50 @@
import paper from 'paper';
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
*/
const getHoveredItem = function (hitOptions, event) {
const hitResults = paper.project.hitTestAll(event.point, hitOptions);
if (hitResults.length === 0) {
return null;
}
let hitResult;
for (const result of hitResults) {
if (!(result.item.data && result.item.data.noHover) && !hitResult.item.selected) {
hitResult = result;
break;
}
}
if (!hitResult) {
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);
}
};
// Action creators ==================================
const clearHoveredItem = function () {
return {
type: CLEAR_HOVERED_ITEM
};
// TODO: paper.view.update();
};
export {
getHoveredItem,
clearHoveredItem
};