2017-12-20 14:19:13 -05:00
|
|
|
import paper from '@scratch/paper';
|
|
|
|
import React from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
|
|
|
import {connect} from 'react-redux';
|
|
|
|
import bindAll from 'lodash.bindall';
|
|
|
|
|
2018-08-30 17:51:11 -04:00
|
|
|
import CopyPasteHOC from '../hocs/copy-paste-hoc.jsx';
|
2017-12-20 14:19:13 -05:00
|
|
|
import ModeToolsComponent from '../components/mode-tools/mode-tools.jsx';
|
|
|
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
2018-07-05 14:16:43 -04:00
|
|
|
import {
|
|
|
|
deleteSelection,
|
|
|
|
getSelectedLeafItems,
|
|
|
|
getSelectedRootItems,
|
2018-09-05 15:34:00 -04:00
|
|
|
getAllRootItems,
|
|
|
|
selectAllItems,
|
|
|
|
selectAllSegments
|
2018-07-05 14:16:43 -04:00
|
|
|
} from '../helper/selection';
|
2017-12-22 11:48:42 -05:00
|
|
|
import {HANDLE_RATIO, ensureClockwise} from '../helper/math';
|
2018-06-28 00:21:01 -04:00
|
|
|
import {getRaster} from '../helper/layer';
|
2018-09-05 15:34:00 -04:00
|
|
|
import {flipBitmapHorizontal, flipBitmapVertical, selectAllBitmap} from '../helper/bitmap';
|
2018-06-28 00:21:01 -04:00
|
|
|
import {isBitmap} from '../lib/format';
|
|
|
|
import Formats from '../lib/format';
|
2018-07-05 14:16:43 -04:00
|
|
|
import Modes from '../lib/modes';
|
2017-12-20 14:19:13 -05:00
|
|
|
|
|
|
|
class ModeTools extends React.Component {
|
|
|
|
constructor (props) {
|
|
|
|
super(props);
|
|
|
|
bindAll(this, [
|
2017-12-21 14:49:05 -05:00
|
|
|
'_getSelectedUncurvedPoints',
|
|
|
|
'_getSelectedUnpointedPoints',
|
|
|
|
'hasSelectedUncurvedPoints',
|
|
|
|
'hasSelectedUnpointedPoints',
|
|
|
|
'handleCurvePoints',
|
2017-12-22 11:44:07 -05:00
|
|
|
'handleFlipHorizontal',
|
|
|
|
'handleFlipVertical',
|
2018-07-05 14:16:43 -04:00
|
|
|
'handleDelete',
|
2017-12-21 14:49:05 -05:00
|
|
|
'handlePasteFromClipboard',
|
|
|
|
'handlePointPoints'
|
2017-12-20 14:19:13 -05:00
|
|
|
]);
|
|
|
|
}
|
2017-12-21 14:49:05 -05:00
|
|
|
_getSelectedUncurvedPoints () {
|
|
|
|
const items = [];
|
2017-12-20 15:15:45 -05:00
|
|
|
const selectedItems = getSelectedLeafItems();
|
|
|
|
for (const item of selectedItems) {
|
2017-12-21 14:49:05 -05:00
|
|
|
if (!item.segments) continue;
|
2017-12-20 15:15:45 -05:00
|
|
|
for (const seg of item.segments) {
|
|
|
|
if (seg.selected) {
|
2017-12-21 14:49:05 -05:00
|
|
|
const prev = seg.getPrevious();
|
|
|
|
const next = seg.getNext();
|
|
|
|
const isCurved =
|
|
|
|
(!prev || seg.handleIn.length > 0) &&
|
|
|
|
(!next || seg.handleOut.length > 0) &&
|
|
|
|
(prev && next ? seg.handleOut.isColinear(seg.handleIn) : true);
|
|
|
|
if (!isCurved) items.push(seg);
|
2017-12-20 15:15:45 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-12-21 14:49:05 -05:00
|
|
|
return items;
|
|
|
|
}
|
|
|
|
_getSelectedUnpointedPoints () {
|
|
|
|
const points = [];
|
|
|
|
const selectedItems = getSelectedLeafItems();
|
|
|
|
for (const item of selectedItems) {
|
|
|
|
if (!item.segments) continue;
|
|
|
|
for (const seg of item.segments) {
|
|
|
|
if (seg.selected) {
|
|
|
|
if (seg.handleIn.length > 0 || seg.handleOut.length > 0) {
|
|
|
|
points.push(seg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return points;
|
|
|
|
}
|
|
|
|
hasSelectedUncurvedPoints () {
|
|
|
|
const points = this._getSelectedUncurvedPoints();
|
|
|
|
return points.length > 0;
|
|
|
|
}
|
|
|
|
hasSelectedUnpointedPoints () {
|
|
|
|
const points = this._getSelectedUnpointedPoints();
|
|
|
|
return points.length > 0;
|
|
|
|
}
|
|
|
|
handleCurvePoints () {
|
|
|
|
let changed;
|
|
|
|
const points = this._getSelectedUncurvedPoints();
|
|
|
|
for (const point of points) {
|
|
|
|
const prev = point.getPrevious();
|
|
|
|
const next = point.getNext();
|
|
|
|
const noHandles = point.handleIn.length === 0 && point.handleOut.length === 0;
|
|
|
|
if (!prev && !next) {
|
|
|
|
continue;
|
2017-12-21 15:30:22 -05:00
|
|
|
} else if (prev && next && noHandles) {
|
|
|
|
// Handles are parallel to the line from prev to next
|
|
|
|
point.handleIn = prev.point.subtract(next.point)
|
|
|
|
.normalize()
|
2017-12-22 11:32:06 -05:00
|
|
|
.multiply(prev.getCurve().length * HANDLE_RATIO);
|
2017-12-21 15:30:22 -05:00
|
|
|
} else if (prev && !next && point.handleIn.length === 0) {
|
|
|
|
// Point is end point
|
|
|
|
// Direction is average of normal at the point and direction to prev point, using the
|
|
|
|
// normal that points out from the convex side
|
2017-12-22 11:32:06 -05:00
|
|
|
// Lenth is curve length * HANDLE_RATIO
|
2017-12-22 17:30:05 -05:00
|
|
|
const convexity = prev.getCurve().getCurvatureAtTime(.5) < 0 ? -1 : 1;
|
2017-12-21 14:49:05 -05:00
|
|
|
point.handleIn = (prev.getCurve().getNormalAtTime(1)
|
2017-12-21 15:30:22 -05:00
|
|
|
.multiply(convexity)
|
2017-12-21 14:49:05 -05:00
|
|
|
.add(prev.point.subtract(point.point).normalize()))
|
|
|
|
.normalize()
|
2017-12-22 11:32:06 -05:00
|
|
|
.multiply(prev.getCurve().length * HANDLE_RATIO);
|
2017-12-21 15:30:22 -05:00
|
|
|
} else if (next && !prev && point.handleOut.length === 0) {
|
2017-12-21 14:49:05 -05:00
|
|
|
// Point is start point
|
2017-12-21 15:30:22 -05:00
|
|
|
// Direction is average of normal at the point and direction to prev point, using the
|
|
|
|
// normal that points out from the convex side
|
2017-12-22 11:32:06 -05:00
|
|
|
// Lenth is curve length * HANDLE_RATIO
|
2017-12-22 17:30:05 -05:00
|
|
|
const convexity = point.getCurve().getCurvatureAtTime(.5) < 0 ? -1 : 1;
|
2017-12-21 14:49:05 -05:00
|
|
|
point.handleOut = (point.getCurve().getNormalAtTime(0)
|
2017-12-21 15:30:22 -05:00
|
|
|
.multiply(convexity)
|
2017-12-21 14:49:05 -05:00
|
|
|
.add(next.point.subtract(point.point).normalize()))
|
|
|
|
.normalize()
|
2017-12-22 11:32:06 -05:00
|
|
|
.multiply(point.getCurve().length * HANDLE_RATIO);
|
2017-12-21 14:49:05 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Point guaranteed to have a handle now. Make the second handle match the length and direction of first.
|
|
|
|
// This defines a curved point.
|
|
|
|
if (point.handleIn.length > 0 && next) {
|
|
|
|
point.handleOut = point.handleIn.multiply(-1);
|
|
|
|
} else if (point.handleOut.length > 0 && prev) {
|
|
|
|
point.handleIn = point.handleOut.multiply(-1);
|
|
|
|
}
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
if (changed) {
|
2018-07-12 15:48:30 -04:00
|
|
|
this.props.setSelectedItems(this.props.format);
|
2018-04-26 18:45:50 -04:00
|
|
|
this.props.onUpdateImage();
|
2017-12-21 14:49:05 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
handlePointPoints () {
|
|
|
|
let changed;
|
|
|
|
const points = this._getSelectedUnpointedPoints();
|
|
|
|
for (const point of points) {
|
|
|
|
const noHandles = point.handleIn.length === 0 && point.handleOut.length === 0;
|
|
|
|
if (!noHandles) {
|
|
|
|
point.handleIn = null;
|
|
|
|
point.handleOut = null;
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (changed) {
|
2018-07-12 15:48:30 -04:00
|
|
|
this.props.setSelectedItems(this.props.format);
|
2018-04-26 18:45:50 -04:00
|
|
|
this.props.onUpdateImage();
|
2017-12-21 14:49:05 -05:00
|
|
|
}
|
2017-12-20 15:15:45 -05:00
|
|
|
}
|
2018-06-28 00:21:01 -04:00
|
|
|
_handleFlip (horizontalScale, verticalScale, selectedItems) {
|
2018-01-25 11:33:43 -05:00
|
|
|
if (selectedItems.length === 0) {
|
2018-01-29 15:03:05 -05:00
|
|
|
// If nothing is selected, select everything
|
2018-01-25 11:33:43 -05:00
|
|
|
selectedItems = getAllRootItems();
|
|
|
|
}
|
2017-12-22 11:44:07 -05:00
|
|
|
// Record old indices
|
|
|
|
for (const item of selectedItems) {
|
|
|
|
item.data.index = item.index;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Group items so that they flip as a unit
|
|
|
|
const itemGroup = new paper.Group(selectedItems);
|
|
|
|
// Flip
|
2018-01-29 15:03:05 -05:00
|
|
|
itemGroup.scale(horizontalScale, verticalScale);
|
2017-12-22 11:48:42 -05:00
|
|
|
ensureClockwise(itemGroup);
|
2017-12-22 11:44:07 -05:00
|
|
|
|
|
|
|
// Remove flipped item from group and insert at old index. Must insert from bottom index up.
|
|
|
|
for (let i = 0; i < selectedItems.length; i++) {
|
|
|
|
itemGroup.layer.insertChild(selectedItems[i].data.index, selectedItems[i]);
|
|
|
|
selectedItems[i].data.index = null;
|
|
|
|
}
|
|
|
|
itemGroup.remove();
|
|
|
|
|
2018-04-26 18:45:50 -04:00
|
|
|
this.props.onUpdateImage();
|
2017-12-22 11:44:07 -05:00
|
|
|
}
|
|
|
|
handleFlipHorizontal () {
|
2018-06-28 00:21:01 -04:00
|
|
|
const selectedItems = getSelectedRootItems();
|
|
|
|
if (isBitmap(this.props.format) && selectedItems.length === 0) {
|
2018-06-28 01:54:05 -04:00
|
|
|
getRaster().canvas = flipBitmapHorizontal(getRaster().canvas);
|
2018-06-28 00:21:01 -04:00
|
|
|
this.props.onUpdateImage();
|
|
|
|
} else {
|
|
|
|
this._handleFlip(-1, 1, selectedItems);
|
|
|
|
}
|
2017-12-22 11:44:07 -05:00
|
|
|
}
|
|
|
|
handleFlipVertical () {
|
2018-06-28 00:21:01 -04:00
|
|
|
const selectedItems = getSelectedRootItems();
|
|
|
|
if (isBitmap(this.props.format) && selectedItems.length === 0) {
|
2018-06-28 01:54:05 -04:00
|
|
|
getRaster().canvas = flipBitmapVertical(getRaster().canvas);
|
2018-06-28 00:21:01 -04:00
|
|
|
this.props.onUpdateImage();
|
|
|
|
} else {
|
|
|
|
this._handleFlip(1, -1, selectedItems);
|
|
|
|
}
|
2017-12-22 11:44:07 -05:00
|
|
|
}
|
2018-08-29 15:29:13 -04:00
|
|
|
handlePasteFromClipboard () {
|
|
|
|
if (this.props.onPasteFromClipboard()) {
|
|
|
|
this.props.onUpdateImage();
|
2017-12-20 14:19:13 -05:00
|
|
|
}
|
|
|
|
}
|
2018-08-29 15:29:13 -04:00
|
|
|
handleDelete () {
|
2018-09-05 15:34:00 -04:00
|
|
|
if (!this.props.selectedItems.length) {
|
|
|
|
if (isBitmap(this.props.format)) {
|
|
|
|
selectAllBitmap(this.props.clearSelectedItems);
|
|
|
|
} else if (this.props.mode === Modes.RESHAPE) {
|
|
|
|
selectAllSegments();
|
|
|
|
} else {
|
|
|
|
selectAllItems();
|
|
|
|
}
|
|
|
|
}
|
2018-08-29 15:29:13 -04:00
|
|
|
if (deleteSelection(this.props.mode, this.props.onUpdateImage)) {
|
2018-07-12 15:48:30 -04:00
|
|
|
this.props.setSelectedItems(this.props.format);
|
2017-12-20 14:19:13 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
render () {
|
|
|
|
return (
|
|
|
|
<ModeToolsComponent
|
2017-12-21 14:49:05 -05:00
|
|
|
hasSelectedUncurvedPoints={this.hasSelectedUncurvedPoints()}
|
|
|
|
hasSelectedUnpointedPoints={this.hasSelectedUnpointedPoints()}
|
2018-08-29 15:29:13 -04:00
|
|
|
onCopyToClipboard={this.props.onCopyToClipboard}
|
2017-12-21 14:49:05 -05:00
|
|
|
onCurvePoints={this.handleCurvePoints}
|
2018-07-05 14:16:43 -04:00
|
|
|
onDelete={this.handleDelete}
|
2017-12-22 11:44:07 -05:00
|
|
|
onFlipHorizontal={this.handleFlipHorizontal}
|
|
|
|
onFlipVertical={this.handleFlipVertical}
|
2017-12-20 14:19:13 -05:00
|
|
|
onPasteFromClipboard={this.handlePasteFromClipboard}
|
2017-12-21 14:49:05 -05:00
|
|
|
onPointPoints={this.handlePointPoints}
|
2018-05-17 10:37:02 -04:00
|
|
|
onUpdateImage={this.props.onUpdateImage}
|
2017-12-20 14:19:13 -05:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ModeTools.propTypes = {
|
2018-09-05 15:34:00 -04:00
|
|
|
clearSelectedItems: PropTypes.func.isRequired,
|
2018-08-29 15:29:13 -04:00
|
|
|
format: PropTypes.oneOf(Object.keys(Formats)),
|
2018-07-05 14:16:43 -04:00
|
|
|
mode: PropTypes.oneOf(Object.keys(Modes)),
|
2018-08-29 15:29:13 -04:00
|
|
|
onCopyToClipboard: PropTypes.func.isRequired,
|
|
|
|
onPasteFromClipboard: PropTypes.func.isRequired,
|
2018-04-26 18:45:50 -04:00
|
|
|
onUpdateImage: PropTypes.func.isRequired,
|
2017-12-20 15:15:45 -05:00
|
|
|
// Listen on selected items to update hasSelectedPoints
|
2017-12-21 17:03:28 -05:00
|
|
|
selectedItems:
|
|
|
|
PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), // eslint-disable-line react/no-unused-prop-types
|
2017-12-20 14:44:39 -05:00
|
|
|
setSelectedItems: PropTypes.func.isRequired
|
2017-12-20 14:19:13 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
const mapStateToProps = state => ({
|
2018-06-28 00:21:01 -04:00
|
|
|
format: state.scratchPaint.format,
|
2018-07-05 14:16:43 -04:00
|
|
|
mode: state.scratchPaint.mode,
|
2017-12-20 15:15:45 -05:00
|
|
|
selectedItems: state.scratchPaint.selectedItems
|
2017-12-20 14:19:13 -05:00
|
|
|
});
|
|
|
|
const mapDispatchToProps = dispatch => ({
|
|
|
|
clearSelectedItems: () => {
|
|
|
|
dispatch(clearSelectedItems());
|
|
|
|
},
|
2018-07-12 15:48:30 -04:00
|
|
|
setSelectedItems: format => {
|
|
|
|
dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
|
2017-12-20 14:19:13 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-08-29 15:29:13 -04:00
|
|
|
export default CopyPasteHOC(connect(
|
2017-12-20 14:19:13 -05:00
|
|
|
mapStateToProps,
|
|
|
|
mapDispatchToProps
|
2018-08-29 15:29:13 -04:00
|
|
|
)(ModeTools));
|