scratch-paint/src/helper/selection.js

540 lines
17 KiB
JavaScript
Raw Normal View History

2017-09-11 14:23:30 -04:00
import paper from 'paper';
import Modes from '../modes/modes';
import {getItemsGroup, isGroup} from './group';
2017-09-21 18:20:44 -04:00
import {getRootItem, isCompoundPathItem, isBoundsItem, isPathItem, isPGTextItem} from './item';
2017-09-11 14:23:30 -04:00
import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compound-path';
2017-10-05 15:41:22 -04:00
import {performSnapshot} from './undo';
2017-09-11 14:23:30 -04:00
2017-09-22 12:12:07 -04:00
/**
* @param {boolean} includeGuides True if guide layer items like the bounding box should
* be included in the returned items.
* @return {Array<paper.item>} all top-level (direct descendants of a paper.Layer) items
*/
const getAllRootItems = function (includeGuides) {
2017-09-22 12:12:07 -04:00
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;
};
/**
* @return {Array<paper.item>} all top-level (direct descendants of a paper.Layer) items
* that aren't guide items or helper items.
*/
const getAllSelectableRootItems = function () {
const allItems = getAllRootItems();
2017-09-11 14:23:30 -04:00
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;
}
}
};
2017-09-21 18:20:44 -04:00
const setGroupSelection = function (root, selected, fullySelected) {
root.fullySelected = fullySelected;
2017-09-11 14:23:30 -04:00
root.selected = selected;
// select children of compound-path or group
if (isCompoundPath(root) || isGroup(root)) {
const children = root.children;
if (children) {
2017-09-21 18:20:44 -04:00
for (const child of children) {
if (isGroup(child)) {
setGroupSelection(child, selected, fullySelected);
} else {
child.fullySelected = fullySelected;
child.selected = selected;
}
2017-09-11 14:23:30 -04:00
}
}
}
};
2017-09-21 18:20:44 -04:00
const setItemSelection = function (item, state, fullySelected) {
2017-09-11 14:23:30 -04:00
const parentGroup = getItemsGroup(item);
const itemsCompoundPath = getItemsCompoundPath(item);
2017-09-21 10:36:26 -04:00
// if selection is in a group, select group
2017-09-11 14:23:30 -04:00
if (parentGroup) {
// do it recursive
2017-09-21 18:20:44 -04:00
setItemSelection(parentGroup, state, fullySelected);
2017-09-11 14:23:30 -04:00
} else if (itemsCompoundPath) {
2017-09-21 18:20:44 -04:00
setGroupSelection(itemsCompoundPath, state, fullySelected);
2017-09-11 14:23:30 -04:00
} else {
if (item.data && item.data.noSelect) {
return;
}
2017-09-21 18:20:44 -04:00
setGroupSelection(item, state, fullySelected);
2017-09-11 14:23:30 -04:00
}
2017-09-14 14:34:45 -04:00
// @todo: Update toolbar state on change
2017-09-11 14:23:30 -04:00
};
const selectAllItems = function () {
const items = getAllSelectableRootItems();
2017-09-11 14:23:30 -04:00
for (let i = 0; i < items.length; i++) {
setItemSelection(items[i], true);
}
};
const selectAllSegments = function () {
const items = getAllSelectableRootItems();
2017-09-11 14:23:30 -04:00
for (let i = 0; i < items.length; i++) {
selectItemSegments(items[i], true);
}
};
/** @param {!function} dispatchClearSelect Function to update the Redux select state */
const clearSelection = function (dispatchClearSelect) {
2017-09-11 14:23:30 -04:00
paper.project.deselectAll();
2017-09-14 14:34:45 -04:00
// @todo: Update toolbar state on change
dispatchClearSelect();
2017-09-11 14:23:30 -04:00
};
/**
* This gets all selected non-grouped items and groups
* (alternative to paper.project.selectedItems, which includes
* group children in addition to the group)
* @return {Array<paper.Item>} in increasing Z order.
*/
const getSelectedRootItems = function () {
2017-09-11 14:23:30 -04:00
const allItems = paper.project.selectedItems;
const itemsAndGroups = [];
for (let i = 0; i < allItems.length; i++) {
const item = allItems[i];
if ((isGroup(item) && !isGroup(item.parent)) ||
!isGroup(item.parent)) {
2017-09-11 14:23:30 -04:00
if (item.data && !item.data.isSelectionBound) {
itemsAndGroups.push(item);
}
}
}
2017-09-11 14:23:30 -04:00
// sort items by index (0 at bottom)
itemsAndGroups.sort((a, b) => parseFloat(a.index) - parseFloat(b.index));
return itemsAndGroups;
};
/**
* This gets all selected items that are as deeply nested as possible. Does not
* return the parent groups.
* @return {Array<paper.Item>} in increasing Z order.
*/
const getSelectedLeafItems = function () {
const allItems = paper.project.selectedItems;
const items = [];
for (let i = 0; i < allItems.length; i++) {
const item = allItems[i];
if (!isGroup(item) && item.data && !item.data.isSelectionBound) {
items.push(item);
}
}
// sort items by index (0 at bottom)
items.sort((a, b) => parseFloat(a.index) - parseFloat(b.index));
return items;
};
2017-10-05 15:41:22 -04:00
const deleteItemSelection = function (items, undoSnapshot) {
2017-09-11 14:23:30 -04:00
for (let i = 0; i < items.length; i++) {
items[i].remove();
}
2017-09-14 14:34:45 -04:00
// @todo: Update toolbar state on change
2017-09-11 14:23:30 -04:00
paper.project.view.update();
2017-10-05 15:41:22 -04:00
performSnapshot(undoSnapshot);
2017-09-11 14:23:30 -04:00
};
2017-10-05 15:41:22 -04:00
const removeSelectedSegments = function (items, undoSnapshot) {
performSnapshot(undoSnapshot);
2017-09-11 14:23:30 -04:00
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;
};
2017-10-05 15:41:22 -04:00
const deleteSelection = function (mode, undoSnapshot) {
2017-09-11 14:23:30 -04:00
if (mode === Modes.RESHAPE) {
const selectedItems = getSelectedLeafItems();
2017-09-11 14:23:30 -04:00
// If there are points selected remove them. If not delete the item selected.
2017-10-05 15:41:22 -04:00
if (!removeSelectedSegments(selectedItems, undoSnapshot)) {
deleteItemSelection(selectedItems, undoSnapshot);
2017-09-11 14:23:30 -04:00
}
} else {
const selectedItems = getSelectedRootItems();
2017-10-05 15:41:22 -04:00
deleteItemSelection(selectedItems, undoSnapshot);
2017-09-11 14:23:30 -04:00
}
};
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 = getSelectedRootItems();
2017-09-11 14:23:30 -04:00
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;
}
}
}
}
};
2017-10-05 15:41:22 -04:00
const deleteSegments = function (item, undoSnapshot) {
2017-09-11 14:23:30 -04:00
if (item.children) {
for (let i = 0; i < item.children.length; i++) {
const child = item.children[i];
2017-10-05 15:41:22 -04:00
deleteSegments(child, undoSnapshot);
2017-09-11 14:23:30 -04:00
}
} 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);
2017-10-05 15:41:22 -04:00
deleteSelection(Modes.RESHAPE, undoSnapshot);
2017-09-11 14:23:30 -04:00
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();
}
};
2017-10-05 15:41:22 -04:00
const deleteSegmentSelection = function (items, undoSnapshot) {
2017-09-11 14:23:30 -04:00
for (let i = 0; i < items.length; i++) {
2017-10-05 15:41:22 -04:00
deleteSegments(items[i], undoSnapshot);
2017-09-11 14:23:30 -04:00
}
2017-09-14 14:34:45 -04:00
// @todo: Update toolbar state on change
2017-09-11 14:23:30 -04:00
paper.project.view.update();
2017-10-05 15:41:22 -04:00
performSnapshot(undoSnapshot);
2017-09-11 14:23:30 -04:00
};
2017-10-05 15:41:22 -04:00
const cloneSelection = function (recursive, undoSnapshot) {
const selectedItems = recursive ? getSelectedLeafItems() : getSelectedRootItems();
2017-09-11 14:23:30 -04:00
for (let i = 0; i < selectedItems.length; i++) {
const item = selectedItems[i];
item.clone();
item.selected = false;
}
2017-10-05 15:41:22 -04:00
performSnapshot(undoSnapshot);
2017-09-11 14:23:30 -04:00
};
2017-09-14 14:34:45 -04:00
// Only returns paths, no compound paths, groups or any other stuff
2017-09-11 14:23:30 -04:00
const getSelectedPaths = function () {
const allPaths = getSelectedRootItems();
2017-09-11 14:23:30 -04:00
const paths = [];
for (let i = 0; i < allPaths.length; i++) {
const path = allPaths[i];
if (path.className === 'Path') {
paths.push(path);
}
}
return paths;
};
2017-09-21 18:20:44 -04:00
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;
2017-09-11 14:23:30 -04:00
2017-09-21 18:20:44 -04:00
}
}
2017-09-11 14:23:30 -04:00
2017-09-21 18:20:44 -04:00
itemBounds.remove();
};
2017-09-11 14:23:30 -04:00
const _handleRectangularSelectionItems = function (item, event, rect, mode, root) {
2017-09-11 14:23:30 -04:00
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)) {
2017-09-14 14:34:45 -04:00
if (mode === Modes.RESHAPE) {
2017-09-11 14:23:30 -04:00
if (event.modifiers.shift && seg.selected) {
seg.selected = false;
} else {
seg.selected = true;
}
segmentMode = true;
} else {
if (event.modifiers.shift && item.selected) {
setItemSelection(root, false);
2017-09-11 14:23:30 -04:00
} else {
setItemSelection(root, true, true /* fullySelected */);
2017-09-11 14:23:30 -04:00
}
return false;
}
}
}
// second round checks for path intersections
const intersections = item.getIntersections(rect);
if (intersections.length > 0 && !segmentMode) {
2017-09-21 18:20:44 -04:00
// if in reshape mode, select the curves that intersect
2017-09-11 14:23:30 -04:00
// with the selectionRect
2017-09-14 14:34:45 -04:00
if (mode === Modes.RESHAPE) {
2017-09-11 14:23:30 -04:00
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;
}
}
2017-09-14 14:34:45 -04:00
// @todo: Update toolbar state on change
2017-09-11 14:23:30 -04:00
2017-09-21 18:20:44 -04:00
} else if (isBoundsItem(item)) {
if (checkBoundsItem(rect, item, event)) {
return false;
}
2017-09-11 14:23:30 -04:00
}
return true;
};
// if the rectangular selection found a group, drill into it recursively
const _rectangularSelectionGroupLoop = function (group, rect, root, event, mode) {
2017-09-11 14:23:30 -04:00
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);
2017-09-21 18:20:44 -04:00
} else {
_handleRectangularSelectionItems(child, event, rect, mode, root);
2017-09-11 14:23:30 -04:00
}
}
return true;
};
/**
* Called after drawing a selection rectangle in a select mode. In reshape mode, this
* selects all control points and curves within the rectangle. In select mode, this
* selects all items and groups that intersect the rectangle
* @param {!MouseEvent} event The mouse event to draw the rectangle
* @param {!paper.Rect} rect The selection rectangle
* @param {Modes} mode The mode of the paint editor when drawing the rectangle
*/
2017-09-11 14:23:30 -04:00
const processRectangularSelection = function (event, rect, mode) {
const allItems = getAllSelectableRootItems();
2017-09-11 14:23:30 -04:00
for (let i = 0; i < allItems.length; i++) {
const item = allItems[i];
2017-09-14 14:34:45 -04:00
if (mode === Modes.RESHAPE && isPGTextItem(getRootItem(item))) {
2017-09-21 18:20:44 -04:00
continue;
2017-09-11 14:23:30 -04:00
}
if (isGroup(item) || isCompoundPathItem(item)) {
// check for item segment points inside
_rectangularSelectionGroupLoop(item, rect, item, event, mode);
2017-09-21 18:20:44 -04:00
} else {
_handleRectangularSelectionItems(item, event, rect, mode, item);
2017-09-11 14:23:30 -04:00
}
}
};
/**
* 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)
*/
2017-09-11 14:23:30 -04:00
const selectRootItem = function () {
const items = getSelectedLeafItems();
2017-09-11 14:23:30 -04:00
for (const item of items) {
if (isCompoundPathChild(item)) {
const cp = getItemsCompoundPath(item);
2017-09-21 18:20:44 -04:00
setItemSelection(cp, true, true /* fullySelected */);
2017-09-11 14:23:30 -04:00
}
2017-09-21 10:36:26 -04:00
const rootItem = getRootItem(item);
if (item !== rootItem) {
2017-09-21 18:20:44 -04:00
setItemSelection(rootItem, true, true /* fullySelected */);
2017-09-21 10:36:26 -04:00
}
2017-09-11 14:23:30 -04:00
}
};
const shouldShowIfSelection = function () {
return getSelectedRootItems().length > 0;
2017-09-11 14:23:30 -04:00
};
const shouldShowIfSelectionRecursive = function () {
return getSelectedRootItems().length > 0;
2017-09-11 14:23:30 -04:00
};
const shouldShowSelectAll = function () {
return paper.project.getItems({class: paper.PathItem}).length > 0;
};
export {
getAllRootItems,
2017-09-11 14:23:30 -04:00
selectAllItems,
selectAllSegments,
clearSelection,
deleteSelection,
deleteItemSelection,
deleteSegmentSelection,
splitPathAtSelectedSegments,
cloneSelection,
setItemSelection,
setGroupSelection,
getSelectedLeafItems,
2017-09-11 14:23:30 -04:00
getSelectedPaths,
getSelectedRootItems,
2017-09-11 14:23:30 -04:00
removeSelectedSegments,
processRectangularSelection,
selectRootItem,
shouldShowIfSelection,
shouldShowIfSelectionRecursive,
shouldShowSelectAll
};