mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-10 14:42:13 -05:00
Bitmap select tool
This commit is contained in:
parent
d7298c0c43
commit
644655d25e
8 changed files with 301 additions and 26 deletions
|
@ -1,27 +1,25 @@
|
|||
import React from 'react';
|
||||
|
||||
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
|
||||
|
||||
import selectIcon from './marquee.svg';
|
||||
|
||||
const BitSelectComponent = () => (
|
||||
<ComingSoonTooltip
|
||||
place="right"
|
||||
tooltipId="bit-select-mode"
|
||||
>
|
||||
<ToolSelectComponent
|
||||
disabled
|
||||
imgDescriptor={{
|
||||
defaultMessage: 'Select',
|
||||
description: 'Label for the select tool, which allows selecting, moving, and resizing shapes',
|
||||
id: 'paint.selectMode.select'
|
||||
}}
|
||||
imgSrc={selectIcon}
|
||||
isSelected={false}
|
||||
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind
|
||||
/>
|
||||
</ComingSoonTooltip>
|
||||
const BitSelectComponent = props => (
|
||||
<ToolSelectComponent
|
||||
imgDescriptor={{
|
||||
defaultMessage: 'Select',
|
||||
description: 'Label for the select tool, which allows selecting, moving, and resizing shapes',
|
||||
id: 'paint.selectMode.select'
|
||||
}}
|
||||
imgSrc={selectIcon}
|
||||
isSelected={props.isSelected}
|
||||
onMouseDown={props.onMouseDown}
|
||||
/>
|
||||
);
|
||||
|
||||
BitSelectComponent.propTypes = {
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
onMouseDown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BitSelectComponent;
|
||||
|
|
|
@ -12,7 +12,7 @@ import BitOvalMode from '../../containers/bit-oval-mode.jsx';
|
|||
import BitRectMode from '../../containers/bit-rect-mode.jsx';
|
||||
import BitFillMode from '../../containers/bit-fill-mode.jsx';
|
||||
import BitEraserMode from '../../containers/bit-eraser-mode.jsx';
|
||||
import BitSelectMode from '../../components/bit-select-mode/bit-select-mode.jsx';
|
||||
import BitSelectMode from '../../containers/bit-select-mode.jsx';
|
||||
import Box from '../box/box.jsx';
|
||||
import Button from '../button/button.jsx';
|
||||
import ButtonGroup from '../button-group/button-group.jsx';
|
||||
|
@ -192,7 +192,9 @@ const PaintEditorComponent = props => (
|
|||
<BitEraserMode
|
||||
onUpdateImage={props.onUpdateImage}
|
||||
/>
|
||||
<BitSelectMode />
|
||||
<BitSelectMode
|
||||
onUpdateImage={props.onUpdateImage}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
92
src/containers/bit-select-mode.jsx
Normal file
92
src/containers/bit-select-mode.jsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
import paper from '@scratch/paper';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import Modes from '../lib/modes';
|
||||
|
||||
import {changeMode} from '../reducers/modes';
|
||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||
import {getSelectedLeafItems} from '../helper/selection';
|
||||
import BitSelectTool from '../helper/bit-tools/select-tool';
|
||||
import SelectModeComponent from '../components/bit-select-mode/bit-select-mode.jsx';
|
||||
|
||||
class BitSelectMode extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'activateTool',
|
||||
'deactivateTool'
|
||||
]);
|
||||
}
|
||||
componentDidMount () {
|
||||
if (this.props.isSelectModeActive) {
|
||||
this.activateTool(this.props);
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.tool && nextProps.selectedItems !== this.props.selectedItems) {
|
||||
this.tool.onSelectionChanged(nextProps.selectedItems);
|
||||
}
|
||||
|
||||
if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) {
|
||||
this.activateTool();
|
||||
} else if (!nextProps.isSelectModeActive && this.props.isSelectModeActive) {
|
||||
this.deactivateTool();
|
||||
}
|
||||
}
|
||||
shouldComponentUpdate (nextProps) {
|
||||
return nextProps.isSelectModeActive !== this.props.isSelectModeActive;
|
||||
}
|
||||
activateTool () {
|
||||
this.tool = new BitSelectTool(
|
||||
this.props.setSelectedItems,
|
||||
this.props.clearSelectedItems,
|
||||
this.props.onUpdateImage
|
||||
);
|
||||
this.tool.activate();
|
||||
}
|
||||
deactivateTool () {
|
||||
this.tool.deactivateTool();
|
||||
this.tool.remove();
|
||||
this.tool = null;
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<SelectModeComponent
|
||||
isSelected={this.props.isSelectModeActive}
|
||||
onMouseDown={this.props.handleMouseDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BitSelectMode.propTypes = {
|
||||
clearSelectedItems: PropTypes.func.isRequired,
|
||||
handleMouseDown: PropTypes.func.isRequired,
|
||||
isSelectModeActive: PropTypes.bool.isRequired,
|
||||
onUpdateImage: PropTypes.func.isRequired,
|
||||
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)),
|
||||
setSelectedItems: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isSelectModeActive: state.scratchPaint.mode === Modes.BIT_SELECT,
|
||||
selectedItems: state.scratchPaint.selectedItems
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
clearSelectedItems: () => {
|
||||
dispatch(clearSelectedItems());
|
||||
},
|
||||
setSelectedItems: () => {
|
||||
dispatch(setSelectedItems(getSelectedLeafItems()));
|
||||
},
|
||||
handleMouseDown: () => {
|
||||
dispatch(changeMode(Modes.BIT_SELECT));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BitSelectMode);
|
|
@ -1,5 +1,6 @@
|
|||
import paper from '@scratch/paper';
|
||||
import PropTypes from 'prop-types';
|
||||
import log from '../log/log';
|
||||
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
@ -136,7 +137,11 @@ class PaintEditor extends React.Component {
|
|||
case Modes.BIT_ERASER:
|
||||
this.props.changeMode(Modes.ERASER);
|
||||
break;
|
||||
case Modes.BIT_SELECT:
|
||||
this.props.changeMode(Modes.SELECT);
|
||||
break;
|
||||
default:
|
||||
log.error(`Mode not handled: ${this.props.mode}`);
|
||||
this.props.changeMode(Modes.BRUSH);
|
||||
}
|
||||
} else if (isBitmap(newFormat)) {
|
||||
|
@ -162,7 +167,13 @@ class PaintEditor extends React.Component {
|
|||
case Modes.ERASER:
|
||||
this.props.changeMode(Modes.BIT_ERASER);
|
||||
break;
|
||||
case Modes.RESHAPE:
|
||||
/* falls through */
|
||||
case Modes.SELECT:
|
||||
this.props.changeMode(Modes.BIT_SELECT);
|
||||
break;
|
||||
default:
|
||||
log.error(`Mode not handled: ${this.props.mode}`);
|
||||
this.props.changeMode(Modes.BIT_BRUSH);
|
||||
}
|
||||
}
|
||||
|
@ -298,7 +309,7 @@ class PaintEditor extends React.Component {
|
|||
this.eyeDropper.pickX = -1;
|
||||
this.eyeDropper.pickY = -1;
|
||||
this.eyeDropper.activate();
|
||||
|
||||
|
||||
this.intervalId = setInterval(() => {
|
||||
const colorInfo = this.eyeDropper.getColorInfo(
|
||||
this.eyeDropper.pickX,
|
||||
|
|
140
src/helper/bit-tools/select-tool.js
Normal file
140
src/helper/bit-tools/select-tool.js
Normal file
|
@ -0,0 +1,140 @@
|
|||
import Modes from '../../lib/modes';
|
||||
|
||||
import {getSelectedLeafItems} from '../selection';
|
||||
import {getRaster} from '../layer';
|
||||
|
||||
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
||||
import NudgeTool from '../selection-tools/nudge-tool';
|
||||
import SelectionBoxTool from '../selection-tools/selection-box-tool';
|
||||
import paper from '@scratch/paper';
|
||||
|
||||
/**
|
||||
* paper.Tool that handles select mode in bitmap. This is made up of 2 subtools.
|
||||
* - The selection box tool is active when the user clicks an empty space and drags.
|
||||
* It selects all items in the rectangle.
|
||||
* - The bounding box tool is active if the user clicks on a non-empty space. It handles
|
||||
* reshaping the selection.
|
||||
*/
|
||||
class SelectTool extends paper.Tool {
|
||||
/** The distance within which mouse events count as a hit against an item */
|
||||
static get TOLERANCE () {
|
||||
return 6;
|
||||
}
|
||||
/**
|
||||
* @param {function} setSelectedItems Callback to set the set of selected items in the Redux state
|
||||
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
||||
* @param {!function} onUpdateImage A callback to call when the image visibly changes
|
||||
*/
|
||||
constructor (setSelectedItems, clearSelectedItems, onUpdateImage) {
|
||||
super();
|
||||
this.onUpdateImage = onUpdateImage;
|
||||
this.boundingBoxTool = new BoundingBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems, onUpdateImage);
|
||||
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage);
|
||||
this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems);
|
||||
this.selectionBoxMode = false;
|
||||
this.selection = null;
|
||||
this.active = false;
|
||||
|
||||
// We have to set these functions instead of just declaring them because
|
||||
// paper.js tools hook up the listeners in the setter functions.
|
||||
this.onMouseDown = this.handleMouseDown;
|
||||
this.onMouseDrag = this.handleMouseDrag;
|
||||
this.onMouseUp = this.handleMouseUp;
|
||||
this.onKeyUp = nudgeTool.onKeyUp;
|
||||
this.onKeyDown = nudgeTool.onKeyDown;
|
||||
|
||||
this.boundingBoxTool.setSelectionBounds();
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
* selected.
|
||||
* @return {object} See paper.Item.hitTest for definition of options
|
||||
*/
|
||||
getHitOptions (preselectedOnly) {
|
||||
// Tolerance needs to be scaled when the view is zoomed in in order to represent the same
|
||||
// distance for the user to move the mouse.
|
||||
const hitOptions = {
|
||||
segments: true,
|
||||
stroke: true,
|
||||
curves: true,
|
||||
fill: true,
|
||||
guide: false,
|
||||
tolerance: SelectTool.TOLERANCE / paper.view.zoom
|
||||
};
|
||||
if (preselectedOnly) {
|
||||
hitOptions.selected = true;
|
||||
}
|
||||
return hitOptions;
|
||||
}
|
||||
handleMouseDown (event) {
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
this.active = true;
|
||||
|
||||
// If bounding box tool does not find an item that was hit, rasterize the old selection,
|
||||
// then use selection box tool.
|
||||
if (!this.boundingBoxTool
|
||||
.onMouseDown(
|
||||
event,
|
||||
event.modifiers.alt,
|
||||
event.modifiers.shift,
|
||||
this.getHitOptions(false /* preseelectedOnly */))) {
|
||||
this.commitSelection();
|
||||
this.selectionBoxMode = true;
|
||||
this.selectionBoxTool.onMouseDown(event.modifiers.shift);
|
||||
}
|
||||
}
|
||||
handleMouseDrag (event) {
|
||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||
|
||||
if (this.selectionBoxMode) {
|
||||
this.selectionBoxTool.onMouseDrag(event);
|
||||
} else {
|
||||
this.boundingBoxTool.onMouseDrag(event);
|
||||
}
|
||||
}
|
||||
handleMouseUp (event) {
|
||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||
|
||||
if (this.selectionBoxMode) {
|
||||
this.selectionBoxTool.onMouseUpBitmap(event);
|
||||
} else {
|
||||
this.boundingBoxTool.onMouseUp(event);
|
||||
}
|
||||
this.selectionBoxMode = false;
|
||||
this.active = false;
|
||||
}
|
||||
commitSelection () {
|
||||
const selection = getSelectedLeafItems();
|
||||
if (selection.length) {
|
||||
// @todo handle non-rasters?
|
||||
for (const item of selection) {
|
||||
if (item instanceof paper.Raster) {
|
||||
// TODO image smoothing?
|
||||
getRaster().canvas.drawImage(
|
||||
item.canvas,
|
||||
item.bounds.topLeft.x,
|
||||
item.bounds.topLeft.y,
|
||||
// Apply transform
|
||||
);
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
deactivateTool () {
|
||||
this.commitSelection();
|
||||
this.boundingBoxTool.removeBoundsPath();
|
||||
this.boundingBoxTool = null;
|
||||
this.selectionBoxTool = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectTool;
|
|
@ -37,7 +37,7 @@ class SelectTool extends paper.Tool {
|
|||
this.selectionBoxMode = false;
|
||||
this.prevHoveredItemId = null;
|
||||
this.active = false;
|
||||
|
||||
|
||||
// We have to set these functions instead of just declaring them because
|
||||
// paper.js tools hook up the listeners in the setter functions.
|
||||
this.onMouseDown = this.handleMouseDown;
|
||||
|
@ -128,7 +128,7 @@ class SelectTool extends paper.Tool {
|
|||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||
|
||||
if (this.selectionBoxMode) {
|
||||
this.selectionBoxTool.onMouseUp(event);
|
||||
this.selectionBoxTool.onMouseUpVector(event);
|
||||
} else {
|
||||
this.boundingBoxTool.onMouseUp(event);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import paper from '@scratch/paper';
|
||||
import {rectSelect} from '../guides';
|
||||
import {clearSelection, processRectangularSelection} from '../selection';
|
||||
import {getRaster} from '../layer';
|
||||
|
||||
/** Tool to handle drag selection. A dotted line box appears and everything enclosed is selected. */
|
||||
class SelectionBoxTool {
|
||||
|
@ -29,7 +31,7 @@ class SelectionBoxTool {
|
|||
// Remove this rect on the next drag and up event
|
||||
this.selectionRect.removeOnDrag();
|
||||
}
|
||||
onMouseUp (event) {
|
||||
onMouseUpVector (event) {
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
if (this.selectionRect) {
|
||||
processRectangularSelection(event, this.selectionRect, this.mode);
|
||||
|
@ -38,6 +40,34 @@ class SelectionBoxTool {
|
|||
this.setSelectedItems();
|
||||
}
|
||||
}
|
||||
onMouseUpBitmap (event) {
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
if (this.selectionRect) {
|
||||
const rect = new paper.Rectangle(
|
||||
~~this.selectionRect.bounds.x,
|
||||
~~this.selectionRect.bounds.y,
|
||||
~~this.selectionRect.bounds.width,
|
||||
~~this.selectionRect.bounds.height,
|
||||
);
|
||||
|
||||
if (rect.area) {
|
||||
// Pull selected raster to active layer
|
||||
const raster = getRaster().getSubRaster(rect);
|
||||
raster.parent = paper.project.activeLayer;
|
||||
raster.selected = true;
|
||||
this.setSelectedItems();
|
||||
|
||||
// Clear selection from raster layer
|
||||
const context = getRaster().canvas.getContext('2d');
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.clearRect(rect.x, rect.y, rect.width, rect.height);
|
||||
}
|
||||
|
||||
// Remove dotted rectangle
|
||||
this.selectionRect.remove();
|
||||
this.selectionRect = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectionBoxTool;
|
||||
|
|
|
@ -8,6 +8,7 @@ const Modes = keyMirror({
|
|||
BIT_TEXT: null,
|
||||
BIT_FILL: null,
|
||||
BIT_ERASER: null,
|
||||
BIT_SELECT: null,
|
||||
BRUSH: null,
|
||||
ERASER: null,
|
||||
LINE: null,
|
||||
|
@ -27,7 +28,8 @@ const BitmapModes = keyMirror({
|
|||
BIT_RECT: null,
|
||||
BIT_TEXT: null,
|
||||
BIT_FILL: null,
|
||||
BIT_ERASER: null
|
||||
BIT_ERASER: null,
|
||||
BIT_SELECT: null
|
||||
});
|
||||
|
||||
export {
|
||||
|
|
Loading…
Reference in a new issue