Merge pull request 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
src
components/mode-tools
containers
helper
reducers

View file

@ -12,7 +12,6 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl';
import Input from '../forms/input.jsx'; import Input from '../forms/input.jsx';
import InputGroup from '../input-group/input-group.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 LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx';
import Modes from '../../lib/modes'; import Modes from '../../lib/modes';
import styles from './mode-tools.css'; import styles from './mode-tools.css';
@ -20,11 +19,11 @@ import copyIcon from './icons/copy.svg';
import pasteIcon from './icons/paste.svg'; import pasteIcon from './icons/paste.svg';
import brushIcon from '../brush-mode/brush.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 eraserIcon from '../eraser-mode/eraser.svg';
// import flipHorizontalIcon from './icons/flip-horizontal.svg'; // import flipHorizontalIcon from './icons/flip-horizontal.svg';
// import flipVerticalIcon from './icons/flip-vertical.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'; import {MAX_STROKE_WIDTH} from '../../reducers/stroke-width';
@ -50,6 +49,16 @@ const ModeToolsComponent = props => {
defaultMessage: 'Paste', defaultMessage: 'Paste',
description: 'Label for the paste button', description: 'Label for the paste button',
id: 'paint.modeTools.paste' 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: case Modes.RESHAPE:
return ( return (
<div className={classNames(props.className, styles.modeTools)}> <div className={classNames(props.className, styles.modeTools)}>
{/* <LabeledIconButton <LabeledIconButton
imgAlt="Curved Point Icon" disabled={!props.hasSelectedUncurvedPoints}
imgSrc={curvedPointIcon} imgSrc={curvedPointIcon}
title="Curved" title={props.intl.formatMessage(messages.curved)}
onClick={function () {}} onClick={props.onCurvePoints}
/> />
<LabeledIconButton <LabeledIconButton
imgAlt="Straight Point Icon" disabled={!props.hasSelectedUnpointedPoints}
imgSrc={straightPointIcon} imgSrc={straightPointIcon}
title="Pointed" title={props.intl.formatMessage(messages.pointed)}
onClick={function () {}} onClick={props.onPointPoints}
/> */} />
</div> </div>
); );
case Modes.SELECT: case Modes.SELECT:
@ -153,12 +162,16 @@ ModeToolsComponent.propTypes = {
className: PropTypes.string, className: PropTypes.string,
clipboardItems: PropTypes.arrayOf(PropTypes.array), clipboardItems: PropTypes.arrayOf(PropTypes.array),
eraserValue: PropTypes.number, eraserValue: PropTypes.number,
hasSelectedUncurvedPoints: PropTypes.bool,
hasSelectedUnpointedPoints: PropTypes.bool,
intl: intlShape.isRequired, intl: intlShape.isRequired,
mode: PropTypes.string.isRequired, mode: PropTypes.string.isRequired,
onBrushSliderChange: PropTypes.func, onBrushSliderChange: PropTypes.func,
onCopyToClipboard: PropTypes.func.isRequired, onCopyToClipboard: PropTypes.func.isRequired,
onCurvePoints: PropTypes.func.isRequired,
onEraserSliderChange: PropTypes.func, onEraserSliderChange: PropTypes.func,
onPasteFromClipboard: PropTypes.func.isRequired, onPasteFromClipboard: PropTypes.func.isRequired,
onPointPoints: PropTypes.func.isRequired,
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)) 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 {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {incrementPasteOffset, setClipboardItems} from '../reducers/clipboard'; import {incrementPasteOffset, setClipboardItems} from '../reducers/clipboard';
import {clearSelection, getSelectedLeafItems, getSelectedRootItems} from '../helper/selection'; import {clearSelection, getSelectedLeafItems, getSelectedRootItems} from '../helper/selection';
import {HANDLE_RATIO} from '../helper/math';
class ModeTools extends React.Component { class ModeTools extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'_getSelectedUncurvedPoints',
'_getSelectedUnpointedPoints',
'hasSelectedUncurvedPoints',
'hasSelectedUnpointedPoints',
'handleCopyToClipboard', '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 () { handleCopyToClipboard () {
const selectedItems = getSelectedRootItems(); const selectedItems = getSelectedRootItems();
if (selectedItems.length > 0) { if (selectedItems.length > 0) {
@ -50,8 +167,12 @@ class ModeTools extends React.Component {
render () { render () {
return ( return (
<ModeToolsComponent <ModeToolsComponent
hasSelectedUncurvedPoints={this.hasSelectedUncurvedPoints()}
hasSelectedUnpointedPoints={this.hasSelectedUnpointedPoints()}
onCopyToClipboard={this.handleCopyToClipboard} onCopyToClipboard={this.handleCopyToClipboard}
onCurvePoints={this.handleCurvePoints}
onPasteFromClipboard={this.handlePasteFromClipboard} onPasteFromClipboard={this.handlePasteFromClipboard}
onPointPoints={this.handlePointPoints}
/> />
); );
} }
@ -63,13 +184,17 @@ ModeTools.propTypes = {
incrementPasteOffset: PropTypes.func.isRequired, incrementPasteOffset: PropTypes.func.isRequired,
onUpdateSvg: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired,
pasteOffset: PropTypes.number, 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, setClipboardItems: PropTypes.func.isRequired,
setSelectedItems: PropTypes.func.isRequired setSelectedItems: PropTypes.func.isRequired
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
clipboardItems: state.scratchPaint.clipboard.items, clipboardItems: state.scratchPaint.clipboard.items,
pasteOffset: state.scratchPaint.clipboard.pasteOffset pasteOffset: state.scratchPaint.clipboard.pasteOffset,
selectedItems: state.scratchPaint.selectedItems
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
setClipboardItems: items => { setClipboardItems: items => {

View file

@ -1,5 +1,8 @@
import paper from '@scratch/paper'; 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 checkPointsClose = function (startPos, eventPoint, threshold) {
const xOff = Math.abs(startPos.x - eventPoint.x); const xOff = Math.abs(startPos.x - eventPoint.x);
const yOff = Math.abs(startPos.y - eventPoint.y); const yOff = Math.abs(startPos.y - eventPoint.y);
@ -95,6 +98,7 @@ const expandByOne = function (path) {
}; };
export { export {
HANDLE_RATIO,
checkPointsClose, checkPointsClose,
expandByOne, expandByOne,
getRandomInt, getRandomInt,

View file

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

View file

@ -1,6 +1,7 @@
import paper from '@scratch/paper'; import paper from '@scratch/paper';
import {snapDeltaToAngle} from '../math'; import {snapDeltaToAngle} from '../math';
import {clearSelection, getSelectedLeafItems} from '../selection'; import {clearSelection, getSelectedLeafItems} from '../selection';
import {HANDLE_RATIO} from '../math';
/** Subtool of ReshapeTool for moving control points. */ /** Subtool of ReshapeTool for moving control points. */
class PointTool { class PointTool {
@ -72,8 +73,8 @@ class PointTool {
hitProperties.hitResult.location.curve.length - hitProperties.hitResult.location.curveOffset; hitProperties.hitResult.location.curve.length - hitProperties.hitResult.location.curveOffset;
// Handle length based on curve length until next point // Handle length based on curve length until next point
let handleIn = hitProperties.hitResult.location.tangent.multiply(-beforeCurveLength / 2); let handleIn = hitProperties.hitResult.location.tangent.multiply(-beforeCurveLength * HANDLE_RATIO);
let handleOut = hitProperties.hitResult.location.tangent.multiply(afterCurveLength / 2); 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) // Don't let one handle overwhelm the other (results in path doubling back on itself weirdly)
if (handleIn.length > 3 * handleOut.length) { if (handleIn.length > 3 * handleOut.length) {
handleIn = handleIn.multiply(3 * handleOut.length / handleIn.length); handleIn = handleIn.multiply(3 * handleOut.length / handleIn.length);
@ -98,7 +99,7 @@ class PointTool {
if (beforeSegment && beforeSegment.handleOut) { if (beforeSegment && beforeSegment.handleOut) {
if (afterSegment) { if (afterSegment) {
beforeSegment.handleOut = beforeSegment.handleOut =
beforeSegment.handleOut.multiply(beforeCurveLength / 2 / beforeSegment.handleOut.length); beforeSegment.handleOut.multiply(beforeCurveLength * HANDLE_RATIO / beforeSegment.handleOut.length);
} else { } else {
beforeSegment.handleOut = null; beforeSegment.handleOut = null;
} }
@ -106,7 +107,7 @@ class PointTool {
if (afterSegment && afterSegment.handleIn) { if (afterSegment && afterSegment.handleIn) {
if (beforeSegment) { if (beforeSegment) {
afterSegment.handleIn = afterSegment.handleIn =
afterSegment.handleIn.multiply(afterCurveLength / 2 / afterSegment.handleIn.length); afterSegment.handleIn.multiply(afterCurveLength * HANDLE_RATIO / afterSegment.handleIn.length);
} else { } else {
afterSegment.handleIn = null; afterSegment.handleIn = null;
} }
@ -123,14 +124,15 @@ class PointTool {
if (beforeSegment && beforeSegment.handleOut) { if (beforeSegment && beforeSegment.handleOut) {
if (afterSegment) { if (afterSegment) {
beforeSegment.handleOut = beforeSegment.handleOut =
beforeSegment.handleOut.multiply(curveLength / 2 / beforeSegment.handleOut.length); beforeSegment.handleOut.multiply(curveLength * HANDLE_RATIO / beforeSegment.handleOut.length);
} else { } else {
beforeSegment.handleOut = null; beforeSegment.handleOut = null;
} }
} }
if (afterSegment && afterSegment.handleIn) { if (afterSegment && afterSegment.handleIn) {
if (beforeSegment) { if (beforeSegment) {
afterSegment.handleIn = afterSegment.handleIn.multiply(curveLength / 2 / afterSegment.handleIn.length); afterSegment.handleIn =
afterSegment.handleIn.multiply(curveLength * HANDLE_RATIO / afterSegment.handleIn.length);
} else { } else {
afterSegment.handleIn = null; 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}`); log.warn(`No selected items or wrong format provided: ${action.selectedItems}`);
return state; return state;
} }
// If they are not equal, return the new list of items. Else return old list // If they are both empty, no change
if (action.selectedItems.length !== state.length) { if (action.selectedItems.length === 0 && state.length === 0) {
return action.selectedItems; return state;
} }
// Shallow equality check (we may need to update this later for more granularity) return action.selectedItems;
for (let i = 0; i < action.selectedItems.length; i++) {
if (action.selectedItems[i] !== state[i]) {
return action.selectedItems;
}
}
return state;
default: default:
return state; return state;
} }