scratch-paint/src/containers/mode-tools.jsx

262 lines
10 KiB
React
Raw Normal View History

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';
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';
import {
deleteSelection,
getSelectedLeafItems,
getSelectedRootItems,
getAllRootItems,
selectAllItems,
selectAllSegments
} from '../helper/selection';
import {HANDLE_RATIO, ensureClockwise} from '../helper/math';
2018-06-28 00:21:01 -04:00
import {getRaster} from '../helper/layer';
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';
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',
'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 = [];
const selectedItems = getSelectedLeafItems();
for (const item of selectedItems) {
2017-12-21 14:49:05 -05:00
if (!item.segments) continue;
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-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;
} else if (prev && next && noHandles) {
// Handles are parallel to the line from prev to next
point.handleIn = prev.point.subtract(next.point)
.normalize()
.multiply(prev.getCurve().length * HANDLE_RATIO);
} 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
// Lenth is curve length * HANDLE_RATIO
const convexity = prev.getCurve().getCurvatureAtTime(.5) < 0 ? -1 : 1;
2017-12-21 14:49:05 -05:00
point.handleIn = (prev.getCurve().getNormalAtTime(1)
.multiply(convexity)
2017-12-21 14:49:05 -05:00
.add(prev.point.subtract(point.point).normalize()))
.normalize()
.multiply(prev.getCurve().length * HANDLE_RATIO);
} else if (next && !prev && point.handleOut.length === 0) {
2017-12-21 14:49:05 -05:00
// Point is start point
// Direction is average of normal at the point and direction to prev point, using the
// normal that points out from the convex side
// Lenth is curve length * HANDLE_RATIO
const convexity = point.getCurve().getCurvatureAtTime(.5) < 0 ? -1 : 1;
2017-12-21 14:49:05 -05:00
point.handleOut = (point.getCurve().getNormalAtTime(0)
.multiply(convexity)
2017-12-21 14:49:05 -05:00
.add(next.point.subtract(point.point).normalize()))
.normalize()
.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) {
this.props.setSelectedItems(this.props.format);
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) {
this.props.setSelectedItems(this.props.format);
this.props.onUpdateImage();
2017-12-21 14:49:05 -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) {
// 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
itemGroup.scale(horizontalScale, verticalScale);
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();
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 () {
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)) {
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}
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 = {
clearSelectedItems: PropTypes.func.isRequired,
2018-08-29 15:29:13 -04:00
format: PropTypes.oneOf(Object.keys(Formats)),
mode: PropTypes.oneOf(Object.keys(Modes)),
2018-08-29 15:29:13 -04:00
onCopyToClipboard: PropTypes.func.isRequired,
onPasteFromClipboard: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired,
// 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,
mode: state.scratchPaint.mode,
selectedItems: state.scratchPaint.selectedItems
2017-12-20 14:19:13 -05:00
});
const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
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));