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'; 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'; import {getRaster} from '../helper/layer'; import {flipBitmapHorizontal, flipBitmapVertical, selectAllBitmap} from '../helper/bitmap'; import {isBitmap} from '../lib/format'; import Formats from '../lib/format'; import Modes from '../lib/modes'; class ModeTools extends React.Component { constructor (props) { super(props); bindAll(this, [ '_getSelectedUncurvedPoints', '_getSelectedUnpointedPoints', 'hasSelectedUncurvedPoints', 'hasSelectedUnpointedPoints', 'handleCurvePoints', 'handleFlipHorizontal', 'handleFlipVertical', 'handleDelete', 'handlePasteFromClipboard', 'handlePointPoints' ]); } _getSelectedUncurvedPoints () { const items = []; const selectedItems = getSelectedLeafItems(); for (const item of selectedItems) { if (!item.segments) continue; for (const seg of item.segments) { if (seg.selected) { 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); } } } 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; point.handleIn = (prev.getCurve().getNormalAtTime(1) .multiply(convexity) .add(prev.point.subtract(point.point).normalize())) .normalize() .multiply(prev.getCurve().length * HANDLE_RATIO); } else if (next && !prev && point.handleOut.length === 0) { // 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; point.handleOut = (point.getCurve().getNormalAtTime(0) .multiply(convexity) .add(next.point.subtract(point.point).normalize())) .normalize() .multiply(point.getCurve().length * HANDLE_RATIO); } // 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(); } } 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(); } } _handleFlip (horizontalScale, verticalScale, selectedItems) { if (selectedItems.length === 0) { // If nothing is selected, select everything selectedItems = getAllRootItems(); } // 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); // 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(); } handleFlipHorizontal () { const selectedItems = getSelectedRootItems(); if (isBitmap(this.props.format) && selectedItems.length === 0) { getRaster().canvas = flipBitmapHorizontal(getRaster().canvas); this.props.onUpdateImage(); } else { this._handleFlip(-1, 1, selectedItems); } } handleFlipVertical () { const selectedItems = getSelectedRootItems(); if (isBitmap(this.props.format) && selectedItems.length === 0) { getRaster().canvas = flipBitmapVertical(getRaster().canvas); this.props.onUpdateImage(); } else { this._handleFlip(1, -1, selectedItems); } } handlePasteFromClipboard () { if (this.props.onPasteFromClipboard()) { this.props.onUpdateImage(); } } 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(); } } if (deleteSelection(this.props.mode, this.props.onUpdateImage)) { this.props.setSelectedItems(this.props.format); } } render () { return ( ); } } ModeTools.propTypes = { clearSelectedItems: PropTypes.func.isRequired, format: PropTypes.oneOf(Object.keys(Formats)), mode: PropTypes.oneOf(Object.keys(Modes)), onCopyToClipboard: PropTypes.func.isRequired, onPasteFromClipboard: PropTypes.func.isRequired, onUpdateImage: PropTypes.func.isRequired, // Listen on selected items to update hasSelectedPoints selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), // eslint-disable-line react/no-unused-prop-types setSelectedItems: PropTypes.func.isRequired }; const mapStateToProps = state => ({ format: state.scratchPaint.format, mode: state.scratchPaint.mode, selectedItems: state.scratchPaint.selectedItems }); const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, setSelectedItems: format => { dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format))); } }); export default CopyPasteHOC(connect( mapStateToProps, mapDispatchToProps )(ModeTools));