Merge pull request #242 from fsih/toggleCurved

Toggle curved/pointed points in reshape tool
This commit is contained in:
DD Liu 2017-12-22 17:42:44 -05:00 committed by GitHub
commit 01d8ff736f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 168 additions and 29 deletions

View file

@ -12,7 +12,6 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl';
import Input from '../forms/input.jsx';
import InputGroup from '../input-group/input-group.jsx';
import LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx';
// import LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx';
import Modes from '../../lib/modes';
import styles from './mode-tools.css';
@ -20,11 +19,11 @@ import copyIcon from './icons/copy.svg';
import pasteIcon from './icons/paste.svg';
import brushIcon from '../brush-mode/brush.svg';
// import curvedPointIcon from './curved-point.svg';
import curvedPointIcon from './icons/curved-point.svg';
import eraserIcon from '../eraser-mode/eraser.svg';
// import flipHorizontalIcon from './icons/flip-horizontal.svg';
// import flipVerticalIcon from './icons/flip-vertical.svg';
// import straightPointIcon from './straight-point.svg';
import straightPointIcon from './icons/straight-point.svg';
import {MAX_STROKE_WIDTH} from '../../reducers/stroke-width';
@ -50,6 +49,16 @@ const ModeToolsComponent = props => {
defaultMessage: 'Paste',
description: 'Label for the paste button',
id: 'paint.modeTools.paste'
},
curved: {
defaultMessage: 'Curved',
description: 'Label for the button that converts selected points to curves',
id: 'paint.modeTools.curved'
},
pointed: {
defaultMessage: 'Pointed',
description: 'Label for the button that converts selected points to sharp points',
id: 'paint.modeTools.pointed'
}
});
@ -95,18 +104,18 @@ const ModeToolsComponent = props => {
case Modes.RESHAPE:
return (
<div className={classNames(props.className, styles.modeTools)}>
{/* <LabeledIconButton
imgAlt="Curved Point Icon"
<LabeledIconButton
disabled={!props.hasSelectedUncurvedPoints}
imgSrc={curvedPointIcon}
title="Curved"
onClick={function () {}}
title={props.intl.formatMessage(messages.curved)}
onClick={props.onCurvePoints}
/>
<LabeledIconButton
imgAlt="Straight Point Icon"
disabled={!props.hasSelectedUnpointedPoints}
imgSrc={straightPointIcon}
title="Pointed"
onClick={function () {}}
/> */}
title={props.intl.formatMessage(messages.pointed)}
onClick={props.onPointPoints}
/>
</div>
);
case Modes.SELECT:
@ -153,12 +162,16 @@ ModeToolsComponent.propTypes = {
className: PropTypes.string,
clipboardItems: PropTypes.arrayOf(PropTypes.array),
eraserValue: PropTypes.number,
hasSelectedUncurvedPoints: PropTypes.bool,
hasSelectedUnpointedPoints: PropTypes.bool,
intl: intlShape.isRequired,
mode: PropTypes.string.isRequired,
onBrushSliderChange: PropTypes.func,
onCopyToClipboard: PropTypes.func.isRequired,
onCurvePoints: PropTypes.func.isRequired,
onEraserSliderChange: PropTypes.func,
onPasteFromClipboard: PropTypes.func.isRequired,
onPointPoints: PropTypes.func.isRequired,
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item))
};

View file

@ -8,15 +8,132 @@ import ModeToolsComponent from '../components/mode-tools/mode-tools.jsx';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {incrementPasteOffset, setClipboardItems} from '../reducers/clipboard';
import {clearSelection, getSelectedLeafItems, getSelectedRootItems} from '../helper/selection';
import {HANDLE_RATIO} from '../helper/math';
class ModeTools extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'_getSelectedUncurvedPoints',
'_getSelectedUnpointedPoints',
'hasSelectedUncurvedPoints',
'hasSelectedUnpointedPoints',
'handleCopyToClipboard',
'handlePasteFromClipboard'
'handleCurvePoints',
'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.onUpdateSvg();
}
}
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.onUpdateSvg();
}
}
handleCopyToClipboard () {
const selectedItems = getSelectedRootItems();
if (selectedItems.length > 0) {
@ -50,8 +167,12 @@ class ModeTools extends React.Component {
render () {
return (
<ModeToolsComponent
hasSelectedUncurvedPoints={this.hasSelectedUncurvedPoints()}
hasSelectedUnpointedPoints={this.hasSelectedUnpointedPoints()}
onCopyToClipboard={this.handleCopyToClipboard}
onCurvePoints={this.handleCurvePoints}
onPasteFromClipboard={this.handlePasteFromClipboard}
onPointPoints={this.handlePointPoints}
/>
);
}
@ -63,13 +184,17 @@ ModeTools.propTypes = {
incrementPasteOffset: PropTypes.func.isRequired,
onUpdateSvg: PropTypes.func.isRequired,
pasteOffset: PropTypes.number,
// Listen on selected items to update hasSelectedPoints
selectedItems:
PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), // eslint-disable-line react/no-unused-prop-types
setClipboardItems: PropTypes.func.isRequired,
setSelectedItems: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
clipboardItems: state.scratchPaint.clipboard.items,
pasteOffset: state.scratchPaint.clipboard.pasteOffset
pasteOffset: state.scratchPaint.clipboard.pasteOffset,
selectedItems: state.scratchPaint.selectedItems
});
const mapDispatchToProps = dispatch => ({
setClipboardItems: items => {

View file

@ -1,5 +1,8 @@
import paper from '@scratch/paper';
/** The ratio of the curve length to use for the handle length to convert squares into approximately circles. */
const HANDLE_RATIO = 0.3902628565;
const checkPointsClose = function (startPos, eventPoint, threshold) {
const xOff = Math.abs(startPos.x - eventPoint.x);
const yOff = Math.abs(startPos.y - eventPoint.y);
@ -95,6 +98,7 @@ const expandByOne = function (path) {
};
export {
HANDLE_RATIO,
checkPointsClose,
expandByOne,
getRandomInt,

View file

@ -79,6 +79,7 @@ class HandleTool {
}
}
if (moved) {
this.setSelectedItems();
this.onUpdateSvg();
}
this.selectedItems = [];

View file

@ -1,6 +1,7 @@
import paper from '@scratch/paper';
import {snapDeltaToAngle} from '../math';
import {clearSelection, getSelectedLeafItems} from '../selection';
import {HANDLE_RATIO} from '../math';
/** Subtool of ReshapeTool for moving control points. */
class PointTool {
@ -72,8 +73,8 @@ class PointTool {
hitProperties.hitResult.location.curve.length - hitProperties.hitResult.location.curveOffset;
// Handle length based on curve length until next point
let handleIn = hitProperties.hitResult.location.tangent.multiply(-beforeCurveLength / 2);
let handleOut = hitProperties.hitResult.location.tangent.multiply(afterCurveLength / 2);
let handleIn = hitProperties.hitResult.location.tangent.multiply(-beforeCurveLength * HANDLE_RATIO);
let handleOut = hitProperties.hitResult.location.tangent.multiply(afterCurveLength * HANDLE_RATIO);
// Don't let one handle overwhelm the other (results in path doubling back on itself weirdly)
if (handleIn.length > 3 * handleOut.length) {
handleIn = handleIn.multiply(3 * handleOut.length / handleIn.length);
@ -98,7 +99,7 @@ class PointTool {
if (beforeSegment && beforeSegment.handleOut) {
if (afterSegment) {
beforeSegment.handleOut =
beforeSegment.handleOut.multiply(beforeCurveLength / 2 / beforeSegment.handleOut.length);
beforeSegment.handleOut.multiply(beforeCurveLength * HANDLE_RATIO / beforeSegment.handleOut.length);
} else {
beforeSegment.handleOut = null;
}
@ -106,7 +107,7 @@ class PointTool {
if (afterSegment && afterSegment.handleIn) {
if (beforeSegment) {
afterSegment.handleIn =
afterSegment.handleIn.multiply(afterCurveLength / 2 / afterSegment.handleIn.length);
afterSegment.handleIn.multiply(afterCurveLength * HANDLE_RATIO / afterSegment.handleIn.length);
} else {
afterSegment.handleIn = null;
}
@ -123,14 +124,15 @@ class PointTool {
if (beforeSegment && beforeSegment.handleOut) {
if (afterSegment) {
beforeSegment.handleOut =
beforeSegment.handleOut.multiply(curveLength / 2 / beforeSegment.handleOut.length);
beforeSegment.handleOut.multiply(curveLength * HANDLE_RATIO / beforeSegment.handleOut.length);
} else {
beforeSegment.handleOut = null;
}
}
if (afterSegment && afterSegment.handleIn) {
if (beforeSegment) {
afterSegment.handleIn = afterSegment.handleIn.multiply(curveLength / 2 / afterSegment.handleIn.length);
afterSegment.handleIn =
afterSegment.handleIn.multiply(curveLength * HANDLE_RATIO / afterSegment.handleIn.length);
} else {
afterSegment.handleIn = null;
}

View file

@ -10,17 +10,11 @@ const reducer = function (state, action) {
log.warn(`No selected items or wrong format provided: ${action.selectedItems}`);
return state;
}
// If they are not equal, return the new list of items. Else return old list
if (action.selectedItems.length !== state.length) {
return action.selectedItems;
// If they are both empty, no change
if (action.selectedItems.length === 0 && state.length === 0) {
return state;
}
// Shallow equality check (we may need to update this later for more granularity)
for (let i = 0; i < action.selectedItems.length; i++) {
if (action.selectedItems[i] !== state[i]) {
return action.selectedItems;
}
}
return state;
return action.selectedItems;
default:
return state;
}