mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Add group and ungroup
This commit is contained in:
parent
091201756b
commit
083bf63869
12 changed files with 112 additions and 42 deletions
|
@ -113,7 +113,7 @@ class PaintEditorComponent extends React.Component {
|
|||
imgAlt="Ungroup Icon"
|
||||
imgSrc={ungroupIcon}
|
||||
title="Ungroup"
|
||||
onClick={function () {}}
|
||||
onClick={this.props.onUngroup}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import paper from '@scratch/paper';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
@ -28,6 +29,9 @@ class OvalMode extends React.Component {
|
|||
if (this.tool && nextProps.colorState !== this.props.colorState) {
|
||||
this.tool.setColorState(nextProps.colorState);
|
||||
}
|
||||
if (this.tool && nextProps.selectedItems !== this.props.selectedItems) {
|
||||
this.tool.onSelectionChanged(nextProps.selectedItems);
|
||||
}
|
||||
|
||||
if (nextProps.isOvalModeActive && !this.props.isOvalModeActive) {
|
||||
this.activateTool();
|
||||
|
@ -73,12 +77,14 @@ OvalMode.propTypes = {
|
|||
handleMouseDown: PropTypes.func.isRequired,
|
||||
isOvalModeActive: PropTypes.bool.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)),
|
||||
setSelectedItems: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
colorState: state.scratchPaint.color,
|
||||
isOvalModeActive: state.scratchPaint.mode === Modes.OVAL
|
||||
isOvalModeActive: state.scratchPaint.mode === Modes.OVAL,
|
||||
selectedItems: state.scratchPaint.selectedItems
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
clearSelectedItems: () => {
|
||||
|
@ -89,8 +95,6 @@ const mapDispatchToProps = dispatch => ({
|
|||
},
|
||||
handleMouseDown: () => {
|
||||
dispatch(changeMode(Modes.OVAL));
|
||||
},
|
||||
deactivateTool () {
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -4,10 +4,13 @@ import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx';
|
|||
|
||||
import {changeMode} from '../reducers/modes';
|
||||
import {undo, redo, undoSnapshot} from '../reducers/undo';
|
||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||
|
||||
import {getGuideLayer} from '../helper/layer';
|
||||
import {performUndo, performRedo, performSnapshot} from '../helper/undo';
|
||||
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
|
||||
import {groupSelection, ungroupSelection} from '../helper/group';
|
||||
import {getSelectedLeafItems} from '../helper/selection';
|
||||
|
||||
import Modes from '../modes/modes';
|
||||
import {connect} from 'react-redux';
|
||||
|
@ -24,7 +27,9 @@ class PaintEditor extends React.Component {
|
|||
'handleSendBackward',
|
||||
'handleSendForward',
|
||||
'handleSendToBack',
|
||||
'handleSendToFront'
|
||||
'handleSendToFront',
|
||||
'handleGroup',
|
||||
'handleUngroup'
|
||||
]);
|
||||
}
|
||||
componentDidMount () {
|
||||
|
@ -51,10 +56,16 @@ class PaintEditor extends React.Component {
|
|||
paper.project.addLayer(guideLayer);
|
||||
}
|
||||
handleUndo () {
|
||||
performUndo(this.props.undoState, this.props.onUndo, this.handleUpdateSvg);
|
||||
performUndo(this.props.undoState, this.props.onUndo, this.props.setSelectedItems, this.handleUpdateSvg);
|
||||
}
|
||||
handleRedo () {
|
||||
performRedo(this.props.undoState, this.props.onRedo, this.handleUpdateSvg);
|
||||
performRedo(this.props.undoState, this.props.onRedo, this.props.setSelectedItems, this.handleUpdateSvg);
|
||||
}
|
||||
handleGroup () {
|
||||
groupSelection(this.props.clearSelectedItems, this.props.setSelectedItems, this.handleUpdateSvg);
|
||||
}
|
||||
handleUngroup () {
|
||||
ungroupSelection(this.props.clearSelectedItems, this.props.setSelectedItems, this.handleUpdateSvg);
|
||||
}
|
||||
handleSendBackward () {
|
||||
sendBackward(this.handleUpdateSvg);
|
||||
|
@ -76,12 +87,14 @@ class PaintEditor extends React.Component {
|
|||
rotationCenterY={this.props.rotationCenterY}
|
||||
svg={this.props.svg}
|
||||
svgId={this.props.svgId}
|
||||
onGroup={this.handleGroup}
|
||||
onRedo={this.handleRedo}
|
||||
onSendBackward={this.handleSendBackward}
|
||||
onSendForward={this.handleSendForward}
|
||||
onSendToBack={this.handleSendToBack}
|
||||
onSendToFront={this.handleSendToFront}
|
||||
onUndo={this.handleUndo}
|
||||
onUngroup={this.handleUngroup}
|
||||
onUpdateName={this.props.onUpdateName}
|
||||
onUpdateSvg={this.handleUpdateSvg}
|
||||
/>
|
||||
|
@ -90,6 +103,7 @@ class PaintEditor extends React.Component {
|
|||
}
|
||||
|
||||
PaintEditor.propTypes = {
|
||||
clearSelectedItems: PropTypes.func.isRequired,
|
||||
name: PropTypes.string,
|
||||
onKeyPress: PropTypes.func.isRequired,
|
||||
onRedo: PropTypes.func.isRequired,
|
||||
|
@ -98,6 +112,7 @@ PaintEditor.propTypes = {
|
|||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
rotationCenterX: PropTypes.number,
|
||||
rotationCenterY: PropTypes.number,
|
||||
setSelectedItems: PropTypes.func.isRequired,
|
||||
svg: PropTypes.string,
|
||||
svgId: PropTypes.string,
|
||||
undoSnapshot: PropTypes.func.isRequired,
|
||||
|
@ -122,6 +137,12 @@ const mapDispatchToProps = dispatch => ({
|
|||
dispatch(changeMode(Modes.SELECT));
|
||||
}
|
||||
},
|
||||
clearSelectedItems: () => {
|
||||
dispatch(clearSelectedItems());
|
||||
},
|
||||
setSelectedItems: () => {
|
||||
dispatch(setSelectedItems(getSelectedLeafItems()));
|
||||
},
|
||||
onUndo: () => {
|
||||
dispatch(undo());
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import paper from '@scratch/paper';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
@ -28,6 +29,9 @@ class RectMode extends React.Component {
|
|||
if (this.tool && nextProps.colorState !== this.props.colorState) {
|
||||
this.tool.setColorState(nextProps.colorState);
|
||||
}
|
||||
if (this.tool && nextProps.selectedItems !== this.props.selectedItems) {
|
||||
this.tool.onSelectionChanged(nextProps.selectedItems);
|
||||
}
|
||||
|
||||
if (nextProps.isRectModeActive && !this.props.isRectModeActive) {
|
||||
this.activateTool();
|
||||
|
@ -73,12 +77,14 @@ RectMode.propTypes = {
|
|||
handleMouseDown: PropTypes.func.isRequired,
|
||||
isRectModeActive: PropTypes.bool.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)),
|
||||
setSelectedItems: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
colorState: state.scratchPaint.color,
|
||||
isRectModeActive: state.scratchPaint.mode === Modes.RECT
|
||||
isRectModeActive: state.scratchPaint.mode === Modes.RECT,
|
||||
selectedItems: state.scratchPaint.selectedItems
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
clearSelectedItems: () => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import paper from '@scratch/paper';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
@ -29,6 +30,9 @@ class SelectMode extends React.Component {
|
|||
if (this.tool && nextProps.hoveredItemId !== this.props.hoveredItemId) {
|
||||
this.tool.setPrevHoveredItemId(nextProps.hoveredItemId);
|
||||
}
|
||||
if (this.tool && nextProps.selectedItems !== this.props.selectedItems) {
|
||||
this.tool.onSelectionChanged(nextProps.selectedItems);
|
||||
}
|
||||
|
||||
if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) {
|
||||
this.activateTool();
|
||||
|
@ -71,13 +75,15 @@ SelectMode.propTypes = {
|
|||
hoveredItemId: PropTypes.number,
|
||||
isSelectModeActive: PropTypes.bool.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)),
|
||||
setHoveredItem: PropTypes.func.isRequired,
|
||||
setSelectedItems: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isSelectModeActive: state.scratchPaint.mode === Modes.SELECT,
|
||||
hoveredItemId: state.scratchPaint.hoveredItemId
|
||||
hoveredItemId: state.scratchPaint.hoveredItemId,
|
||||
selectedItems: state.scratchPaint.selectedItems
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
setHoveredItem: hoveredItemId => {
|
||||
|
|
|
@ -6,8 +6,7 @@ const isGroup = function (item) {
|
|||
return isGroupItem(item);
|
||||
};
|
||||
|
||||
const groupSelection = function (clearSelectedItems) {
|
||||
const items = getSelectedRootItems();
|
||||
const groupItems = function (items, clearSelectedItems, setSelectedItems, onUpdateSvg) {
|
||||
if (items.length > 0) {
|
||||
const group = new paper.Group(items);
|
||||
clearSelection(clearSelectedItems);
|
||||
|
@ -15,15 +14,20 @@ const groupSelection = function (clearSelectedItems) {
|
|||
for (let i = 0; i < group.children.length; i++) {
|
||||
group.children[i].selected = true;
|
||||
}
|
||||
setSelectedItems();
|
||||
// @todo: Set selection bounds; enable/disable grouping icons
|
||||
// @todo add back undo
|
||||
// pg.undo.snapshot('groupSelection');
|
||||
onUpdateSvg();
|
||||
return group;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const ungroupLoop = function (group, recursive, selectUngroupedItems) {
|
||||
const groupSelection = function (clearSelectedItems, setSelectedItems, onUpdateSvg) {
|
||||
const items = getSelectedRootItems();
|
||||
return groupItems(items, clearSelectedItems, setSelectedItems, onUpdateSvg);
|
||||
};
|
||||
|
||||
const ungroupLoop = function (group, recursive, setSelectedItems) {
|
||||
// Can't ungroup items that are not groups
|
||||
if (!group || !group.children || !isGroup(group)) return;
|
||||
|
||||
|
@ -34,7 +38,7 @@ const ungroupLoop = function (group, recursive, selectUngroupedItems) {
|
|||
if (groupChild.hasChildren()) {
|
||||
// recursion (groups can contain groups, ie. from SVG import)
|
||||
if (recursive) {
|
||||
ungroupLoop(groupChild, recursive, selectUngroupedItems);
|
||||
ungroupLoop(groupChild, recursive, setSelectedItems);
|
||||
continue;
|
||||
}
|
||||
if (groupChild.children.length === 1) {
|
||||
|
@ -44,7 +48,7 @@ const ungroupLoop = function (group, recursive, selectUngroupedItems) {
|
|||
groupChild.applyMatrix = true;
|
||||
// move items from the group to the activeLayer (ungrouping)
|
||||
groupChild.insertBelow(group);
|
||||
if (selectUngroupedItems) {
|
||||
if (setSelectedItems) {
|
||||
groupChild.selected = true;
|
||||
}
|
||||
i--;
|
||||
|
@ -52,44 +56,38 @@ const ungroupLoop = function (group, recursive, selectUngroupedItems) {
|
|||
};
|
||||
|
||||
// ungroup items (only top hierarchy)
|
||||
const ungroupItems = function (items, selectUngroupedItems) {
|
||||
const ungroupItems = function (items, setSelectedItems, onUpdateSvg) {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
const emptyGroups = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (isGroup(item) && !item.data.isPGTextItem) {
|
||||
ungroupLoop(item, false /* recursive */, selectUngroupedItems /* selectUngroupedItems */);
|
||||
ungroupLoop(item, false /* recursive */, setSelectedItems);
|
||||
|
||||
if (!item.hasChildren()) {
|
||||
emptyGroups.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (setSelectedItems) {
|
||||
setSelectedItems();
|
||||
}
|
||||
// remove all empty groups after ungrouping
|
||||
for (let j = 0; j < emptyGroups.length; j++) {
|
||||
emptyGroups[j].remove();
|
||||
}
|
||||
// @todo: Set selection bounds; enable/disable grouping icons
|
||||
// @todo add back undo
|
||||
// pg.undo.snapshot('ungroupItems');
|
||||
if (onUpdateSvg) {
|
||||
onUpdateSvg();
|
||||
}
|
||||
};
|
||||
|
||||
const ungroupSelection = function (clearSelectedItems) {
|
||||
const ungroupSelection = function (clearSelectedItems, setSelectedItems, onUpdateSvg) {
|
||||
const items = getSelectedRootItems();
|
||||
clearSelection(clearSelectedItems);
|
||||
ungroupItems(items, true /* selectUngroupedItems */);
|
||||
};
|
||||
|
||||
|
||||
const groupItems = function (items) {
|
||||
if (items.length > 0) {
|
||||
const group = new paper.Group(items);
|
||||
// @todo: Set selection bounds; enable/disable grouping icons
|
||||
// @todo add back undo
|
||||
// pg.undo.snapshot('groupItems');
|
||||
return group;
|
||||
}
|
||||
return false;
|
||||
ungroupItems(items, setSelectedItems, onUpdateSvg);
|
||||
};
|
||||
|
||||
const getItemsGroup = function (item) {
|
||||
|
|
|
@ -49,6 +49,18 @@ class BoundingBoxTool {
|
|||
this._modeMap[BoundingBoxModes.MOVE] = new MoveTool(mode, setSelectedItems, clearSelectedItems, onUpdateSvg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called if the selection changes to update the bounds of the bounding box.
|
||||
* @param {Array<paper.Item>} selectedItems Array of selected items.
|
||||
*/
|
||||
onSelectionChanged (selectedItems) {
|
||||
if (selectedItems) {
|
||||
this.setSelectionBounds();
|
||||
} else {
|
||||
this.removeBoundsPath();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!MouseEvent} event The mouse event
|
||||
* @param {boolean} clone Whether to clone on mouse down (e.g. alt key held)
|
||||
|
|
|
@ -57,6 +57,13 @@ class SelectTool extends paper.Tool {
|
|||
setPrevHoveredItemId (prevHoveredItemId) {
|
||||
this.prevHoveredItemId = prevHoveredItemId;
|
||||
}
|
||||
/**
|
||||
* Should be called if the selection changes to update the bounds of the bounding box.
|
||||
* @param {Array<paper.Item>} selectedItems Array of selected items.
|
||||
*/
|
||||
onSelectionChanged (selectedItems) {
|
||||
this.boundingBoxTool.onSelectionChanged(selectedItems);
|
||||
}
|
||||
/**
|
||||
* Returns the hit options to use when conducting hit tests.
|
||||
* @param {boolean} preselectedOnly True if we should only return results that are already
|
||||
|
|
|
@ -47,6 +47,13 @@ class OvalTool extends paper.Tool {
|
|||
tolerance: OvalTool.TOLERANCE / paper.view.zoom
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Should be called if the selection changes to update the bounds of the bounding box.
|
||||
* @param {Array<paper.Item>} selectedItems Array of selected items.
|
||||
*/
|
||||
onSelectionChanged (selectedItems) {
|
||||
this.boundingBoxTool.onSelectionChanged(selectedItems);
|
||||
}
|
||||
setColorState (colorState) {
|
||||
this.colorState = colorState;
|
||||
}
|
||||
|
|
|
@ -47,6 +47,13 @@ class RectTool extends paper.Tool {
|
|||
tolerance: RectTool.TOLERANCE / paper.view.zoom
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Should be called if the selection changes to update the bounds of the bounding box.
|
||||
* @param {Array<paper.Item>} selectedItems Array of selected items.
|
||||
*/
|
||||
onSelectionChanged (selectedItems) {
|
||||
this.boundingBoxTool.onSelectionChanged(selectedItems);
|
||||
}
|
||||
setColorState (colorState) {
|
||||
this.colorState = colorState;
|
||||
}
|
||||
|
|
|
@ -11,19 +11,21 @@ const performSnapshot = function (dispatchPerformSnapshot) {
|
|||
// updateButtonVisibility();
|
||||
};
|
||||
|
||||
const _restore = function (entry, onUpdateSvg) {
|
||||
const _restore = function (entry, setSelectedItems, onUpdateSvg) {
|
||||
for (const layer of paper.project.layers) {
|
||||
layer.removeChildren();
|
||||
}
|
||||
paper.project.clear();
|
||||
paper.project.importJSON(entry.json);
|
||||
paper.view.update();
|
||||
|
||||
setSelectedItems();
|
||||
onUpdateSvg(true /* skipSnapshot */);
|
||||
};
|
||||
|
||||
const performUndo = function (undoState, dispatchPerformUndo, onUpdateSvg) {
|
||||
const performUndo = function (undoState, dispatchPerformUndo, setSelectedItems, onUpdateSvg) {
|
||||
if (undoState.pointer > 0) {
|
||||
_restore(undoState.stack[undoState.pointer - 1], onUpdateSvg);
|
||||
_restore(undoState.stack[undoState.pointer - 1], setSelectedItems, onUpdateSvg);
|
||||
dispatchPerformUndo();
|
||||
|
||||
// @todo enable/disable buttons
|
||||
|
@ -32,9 +34,9 @@ const performUndo = function (undoState, dispatchPerformUndo, onUpdateSvg) {
|
|||
};
|
||||
|
||||
|
||||
const performRedo = function (undoState, dispatchPerformRedo, onUpdateSvg) {
|
||||
const performRedo = function (undoState, dispatchPerformRedo, setSelectedItems, onUpdateSvg) {
|
||||
if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) {
|
||||
_restore(undoState.stack[undoState.pointer + 1], onUpdateSvg);
|
||||
_restore(undoState.stack[undoState.pointer + 1], setSelectedItems, onUpdateSvg);
|
||||
dispatchPerformRedo();
|
||||
|
||||
// @todo enable/disable buttons
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import PaintEditor from './containers/paint-editor.jsx';
|
||||
import SelectionHOV from './containers/selection-hoc.jsx';
|
||||
import SelectionHOC from './containers/selection-hoc.jsx';
|
||||
import ScratchPaintReducer from './reducers/scratch-paint-reducer';
|
||||
|
||||
const Wrapped = SelectionHOV(PaintEditor);
|
||||
const Wrapped = SelectionHOC(PaintEditor);
|
||||
|
||||
export {
|
||||
Wrapped as default,
|
||||
|
|
Loading…
Reference in a new issue