mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-03-13 22:50:01 -04:00
Merge pull request #242 from fsih/toggleCurved
Toggle curved/pointed points in reshape tool
This commit is contained in:
commit
01d8ff736f
6 changed files with 168 additions and 29 deletions
|
@ -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))
|
||||
};
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -79,6 +79,7 @@ class HandleTool {
|
|||
}
|
||||
}
|
||||
if (moved) {
|
||||
this.setSelectedItems();
|
||||
this.onUpdateSvg();
|
||||
}
|
||||
this.selectedItems = [];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue