2017-10-12 18:35:30 -04:00
|
|
|
import paper from '@scratch/paper';
|
2017-09-11 14:23:30 -04:00
|
|
|
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-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
|
|
|
|
*/
|
2017-10-11 11:32:51 -04:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2017-09-22 11:10:17 -04:00
|
|
|
/**
|
|
|
|
* @return {Array<paper.item>} all top-level (direct descendants of a paper.Layer) items
|
|
|
|
* that aren't guide items or helper items.
|
|
|
|
*/
|
2017-10-11 11:32:51 -04:00
|
|
|
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-10-05 16:27:47 -04:00
|
|
|
const _setGroupSelection = function (root, selected, fullySelected) {
|
2017-09-21 18:20:44 -04:00
|
|
|
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)) {
|
2017-10-05 16:27:47 -04:00
|
|
|
_setGroupSelection(child, selected, fullySelected);
|
2017-09-21 18:20:44 -04:00
|
|
|
} 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-10-05 16:27:47 -04:00
|
|
|
_setGroupSelection(itemsCompoundPath, state, fullySelected);
|
2017-09-11 14:23:30 -04:00
|
|
|
} else {
|
|
|
|
if (item.data && item.data.noSelect) {
|
|
|
|
return;
|
|
|
|
}
|
2017-10-05 16:27:47 -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 () {
|
2017-10-11 11:32:51 -04:00
|
|
|
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 () {
|
2017-10-11 11:32:51 -04:00
|
|
|
const items = getAllSelectableRootItems();
|
2017-09-11 14:23:30 -04:00
|
|
|
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
selectItemSegments(items[i], true);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-10-02 15:25:04 -04:00
|
|
|
/** @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
|
2017-10-02 15:25:04 -04:00
|
|
|
dispatchClearSelect();
|
2017-09-11 14:23:30 -04:00
|
|
|
};
|
|
|
|
|
2017-10-11 11:32:51 -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-10-20 11:05:55 -04:00
|
|
|
const allItems = getAllSelectableRootItems();
|
|
|
|
const items = [];
|
2017-09-11 14:23:30 -04:00
|
|
|
|
2017-10-20 11:05:55 -04:00
|
|
|
for (const item of allItems) {
|
|
|
|
if (item.selected) {
|
|
|
|
items.push(item);
|
2017-09-11 14:23:30 -04:00
|
|
|
}
|
|
|
|
}
|
2017-10-11 11:32:51 -04:00
|
|
|
|
2017-09-11 14:23:30 -04:00
|
|
|
// sort items by index (0 at bottom)
|
2017-10-20 11:05:55 -04:00
|
|
|
items.sort((a, b) => parseFloat(a.index) - parseFloat(b.index));
|
|
|
|
return items;
|
2017-09-11 14:23:30 -04:00
|
|
|
};
|
|
|
|
|
2017-10-11 11:32:51 -04:00
|
|
|
/**
|
|
|
|
* 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];
|
2017-10-20 14:54:21 -04:00
|
|
|
if (!(item instanceof paper.Layer) && !isGroup(item) && item.data && !item.data.isSelectionBound) {
|
2017-10-11 11:32:51 -04:00
|
|
|
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 18:12:22 -04:00
|
|
|
const _deleteItemSelection = function (items, onUpdateSvg) {
|
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-10-12 11:16:09 -04:00
|
|
|
if (items.length > 0) {
|
2017-10-05 16:27:47 -04:00
|
|
|
paper.project.view.update();
|
2017-10-05 18:12:22 -04:00
|
|
|
onUpdateSvg();
|
2017-10-05 16:27:47 -04:00
|
|
|
}
|
2017-09-11 14:23:30 -04:00
|
|
|
};
|
|
|
|
|
2017-10-05 18:12:22 -04:00
|
|
|
const _removeSelectedSegments = function (items, onUpdateSvg) {
|
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;
|
|
|
|
}
|
2017-10-05 16:27:47 -04:00
|
|
|
if (removedSegments) {
|
|
|
|
paper.project.view.update();
|
2017-10-05 18:12:22 -04:00
|
|
|
onUpdateSvg();
|
2017-10-05 16:27:47 -04:00
|
|
|
}
|
2017-09-11 14:23:30 -04:00
|
|
|
return removedSegments;
|
|
|
|
};
|
|
|
|
|
2017-10-05 18:12:22 -04:00
|
|
|
const deleteSelection = function (mode, onUpdateSvg) {
|
2017-09-11 14:23:30 -04:00
|
|
|
if (mode === Modes.RESHAPE) {
|
2017-10-11 11:32:51 -04:00
|
|
|
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 18:12:22 -04:00
|
|
|
if (!_removeSelectedSegments(selectedItems, onUpdateSvg)) {
|
|
|
|
_deleteItemSelection(selectedItems, onUpdateSvg);
|
2017-09-11 14:23:30 -04:00
|
|
|
}
|
|
|
|
} else {
|
2017-10-11 11:32:51 -04:00
|
|
|
const selectedItems = getSelectedRootItems();
|
2017-10-05 18:12:22 -04:00
|
|
|
_deleteItemSelection(selectedItems, onUpdateSvg);
|
2017-09-11 14:23:30 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-10-05 18:12:22 -04:00
|
|
|
const cloneSelection = function (recursive, onUpdateSvg) {
|
2017-10-11 11:32:51 -04:00
|
|
|
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 18:12:22 -04:00
|
|
|
onUpdateSvg();
|
2017-09-11 14:23:30 -04:00
|
|
|
};
|
|
|
|
|
2017-10-05 16:27:47 -04:00
|
|
|
const _checkBoundsItem = function (selectionRect, item, event) {
|
2017-09-21 18:20:44 -04:00
|
|
|
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
|
|
|
|
2017-09-22 12:22:09 -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) {
|
2017-09-22 12:22:09 -04:00
|
|
|
setItemSelection(root, false);
|
2017-09-11 14:23:30 -04:00
|
|
|
} else {
|
2017-09-22 12:22:09 -04:00
|
|
|
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)) {
|
2017-10-05 16:27:47 -04:00
|
|
|
if (_checkBoundsItem(rect, item, event)) {
|
2017-09-21 18:20:44 -04:00
|
|
|
return false;
|
|
|
|
}
|
2017-09-11 14:23:30 -04:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
// if the rectangular selection found a group, drill into it recursively
|
2017-09-22 11:10:17 -04:00
|
|
|
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)) {
|
2017-09-22 11:10:17 -04:00
|
|
|
_rectangularSelectionGroupLoop(child, rect, root, event, mode);
|
2017-09-21 18:20:44 -04:00
|
|
|
} else {
|
2017-09-22 12:22:09 -04:00
|
|
|
_handleRectangularSelectionItems(child, event, rect, mode, root);
|
2017-09-11 14:23:30 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2017-09-22 11:10:17 -04:00
|
|
|
/**
|
|
|
|
* 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) {
|
2017-10-11 11:32:51 -04:00
|
|
|
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)) {
|
2017-09-22 12:22:09 -04:00
|
|
|
// check for item segment points inside
|
|
|
|
_rectangularSelectionGroupLoop(item, rect, item, event, mode);
|
2017-09-21 18:20:44 -04:00
|
|
|
} else {
|
2017-09-22 12:22:09 -04:00
|
|
|
_handleRectangularSelectionItems(item, event, rect, mode, item);
|
2017-09-11 14:23:30 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-09-22 11:10:17 -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 () {
|
2017-10-11 11:32:51 -04:00
|
|
|
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 () {
|
2017-10-11 11:32:51 -04:00
|
|
|
return getSelectedRootItems().length > 0;
|
2017-09-11 14:23:30 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
const shouldShowIfSelectionRecursive = function () {
|
2017-10-11 11:32:51 -04:00
|
|
|
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 {
|
2017-10-11 11:32:51 -04:00
|
|
|
getAllRootItems,
|
2017-09-11 14:23:30 -04:00
|
|
|
selectAllItems,
|
|
|
|
selectAllSegments,
|
|
|
|
clearSelection,
|
|
|
|
deleteSelection,
|
|
|
|
cloneSelection,
|
|
|
|
setItemSelection,
|
2017-10-11 11:32:51 -04:00
|
|
|
getSelectedLeafItems,
|
|
|
|
getSelectedRootItems,
|
2017-09-11 14:23:30 -04:00
|
|
|
processRectangularSelection,
|
|
|
|
selectRootItem,
|
|
|
|
shouldShowIfSelection,
|
|
|
|
shouldShowIfSelectionRecursive,
|
|
|
|
shouldShowSelectAll
|
|
|
|
};
|