mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-12 23:51:26 -05:00
Delete extra tools
This commit is contained in:
parent
87d9386906
commit
bda357bca6
4 changed files with 0 additions and 763 deletions
|
@ -1,177 +0,0 @@
|
||||||
import paper from '@scratch/paper';
|
|
||||||
import {getHoveredItem} from '../hover';
|
|
||||||
import {expandBy} from '../math';
|
|
||||||
|
|
||||||
class FillTool extends paper.Tool {
|
|
||||||
static get TOLERANCE () {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {function} setHoveredItem Callback to set the hovered item
|
|
||||||
* @param {function} clearHoveredItem Callback to clear the hovered item
|
|
||||||
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
|
||||||
*/
|
|
||||||
constructor (setHoveredItem, clearHoveredItem, onUpdateSvg) {
|
|
||||||
super();
|
|
||||||
this.setHoveredItem = setHoveredItem;
|
|
||||||
this.clearHoveredItem = clearHoveredItem;
|
|
||||||
this.onUpdateSvg = onUpdateSvg;
|
|
||||||
|
|
||||||
// We have to set these functions instead of just declaring them because
|
|
||||||
// paper.js tools hook up the listeners in the setter functions.
|
|
||||||
this.onMouseMove = this.handleMouseMove;
|
|
||||||
this.onMouseUp = this.handleMouseUp;
|
|
||||||
|
|
||||||
// Color to fill with
|
|
||||||
this.fillColor = null;
|
|
||||||
// The path that's being hovered over.
|
|
||||||
this.fillItem = null;
|
|
||||||
// If we're hovering over a hole in a compound path, we can't just recolor it. This is the
|
|
||||||
// added item that's the same shape as the hole that's drawn over the hole when we fill a hole.
|
|
||||||
this.addedFillItem = null;
|
|
||||||
this.fillItemOrigColor = null;
|
|
||||||
this.prevHoveredItemId = null;
|
|
||||||
}
|
|
||||||
getHitOptions () {
|
|
||||||
const isAlmostClosedPath = function (item) {
|
|
||||||
return item instanceof paper.Path && item.segments.length > 2 &&
|
|
||||||
item.lastSegment.point.getDistance(item.firstSegment.point) < 8;
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
segments: true,
|
|
||||||
stroke: true,
|
|
||||||
curves: true,
|
|
||||||
fill: true,
|
|
||||||
guide: false,
|
|
||||||
match: function (hitResult) {
|
|
||||||
return (hitResult.item instanceof paper.Path || hitResult.item instanceof paper.PointText) &&
|
|
||||||
(hitResult.item.hasFill() || hitResult.item.closed || isAlmostClosedPath(hitResult.item));
|
|
||||||
},
|
|
||||||
hitUnfilledPaths: true,
|
|
||||||
tolerance: FillTool.TOLERANCE / paper.view.zoom
|
|
||||||
};
|
|
||||||
}
|
|
||||||
setFillColor (fillColor) {
|
|
||||||
this.fillColor = fillColor;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* To be called when the hovered item changes. When the select tool hovers over a
|
|
||||||
* new item, it compares against this to see if a hover item change event needs to
|
|
||||||
* be fired.
|
|
||||||
* @param {paper.Item} prevHoveredItemId ID of the highlight item that indicates the mouse is
|
|
||||||
* over a given item currently
|
|
||||||
*/
|
|
||||||
setPrevHoveredItemId (prevHoveredItemId) {
|
|
||||||
this.prevHoveredItemId = prevHoveredItemId;
|
|
||||||
}
|
|
||||||
handleMouseMove (event) {
|
|
||||||
const hoveredItem = getHoveredItem(event, this.getHitOptions(), true /* subselect */);
|
|
||||||
if ((!hoveredItem && this.prevHoveredItemId) || // There is no longer a hovered item
|
|
||||||
(hoveredItem && !this.prevHoveredItemId) || // There is now a hovered item
|
|
||||||
(hoveredItem && this.prevHoveredItemId &&
|
|
||||||
hoveredItem.id !== this.prevHoveredItemId)) { // hovered item changed
|
|
||||||
this.setHoveredItem(hoveredItem ? hoveredItem.id : null);
|
|
||||||
}
|
|
||||||
const hitItem = hoveredItem ? hoveredItem.data.origItem : null;
|
|
||||||
// Still hitting the same thing
|
|
||||||
if ((!hitItem && !this.fillItem) || this.fillItem === hitItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.fillItem) {
|
|
||||||
if (this.addedFillItem) {
|
|
||||||
this.addedFillItem.remove();
|
|
||||||
this.addedFillItem = null;
|
|
||||||
} else {
|
|
||||||
this._setFillItemColor(this.fillItemOrigColor);
|
|
||||||
}
|
|
||||||
this.fillItemOrigColor = null;
|
|
||||||
this.fillItem = null;
|
|
||||||
}
|
|
||||||
if (hitItem) {
|
|
||||||
this.fillItem = hitItem;
|
|
||||||
this.fillItemOrigColor = hitItem.fillColor;
|
|
||||||
if (hitItem.parent instanceof paper.CompoundPath && hitItem.area < 0) { // hole
|
|
||||||
if (!this.fillColor) {
|
|
||||||
// Hole filled with transparent is no-op
|
|
||||||
this.fillItem = null;
|
|
||||||
this.fillItemOrigColor = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Make an item to fill the hole
|
|
||||||
this.addedFillItem = hitItem.clone();
|
|
||||||
this.addedFillItem.setClockwise(true);
|
|
||||||
this.addedFillItem.data.noHover = true;
|
|
||||||
this.addedFillItem.data.origItem = hitItem;
|
|
||||||
// This usually fixes it so there isn't a teeny tiny gap in between the fill and the outline
|
|
||||||
// when filling in a hole
|
|
||||||
expandBy(this.addedFillItem, .1);
|
|
||||||
this.addedFillItem.insertAbove(hitItem.parent);
|
|
||||||
} else if (this.fillItem.parent instanceof paper.CompoundPath) {
|
|
||||||
this.fillItemOrigColor = hitItem.parent.fillColor;
|
|
||||||
}
|
|
||||||
this._setFillItemColor(this.fillColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleMouseUp (event) {
|
|
||||||
if (event.event.button > 0) return; // only first mouse button
|
|
||||||
if (this.fillItem) {
|
|
||||||
// If the hole we're filling in is the same color as the parent, and parent has no outline, remove the hole
|
|
||||||
if (this.addedFillItem &&
|
|
||||||
this._noStroke(this.fillItem.parent) &&
|
|
||||||
this.addedFillItem.fillColor.type !== 'gradient' &&
|
|
||||||
this.fillItem.parent.fillColor.toCSS() === this.addedFillItem.fillColor.toCSS()) {
|
|
||||||
this.addedFillItem.remove();
|
|
||||||
this.addedFillItem = null;
|
|
||||||
let parent = this.fillItem.parent;
|
|
||||||
this.fillItem.remove();
|
|
||||||
parent = parent.reduce();
|
|
||||||
parent.fillColor = this.fillColor;
|
|
||||||
} else if (this.addedFillItem) {
|
|
||||||
// Fill in a hole.
|
|
||||||
this.addedFillItem.data.noHover = false;
|
|
||||||
} else if (!this.fillColor &&
|
|
||||||
this.fillItem.data &&
|
|
||||||
this.fillItem.data.origItem) {
|
|
||||||
// Filling a hole filler with transparent returns it to being gone
|
|
||||||
// instead of making a shape that's transparent
|
|
||||||
const group = this.fillItem.parent;
|
|
||||||
this.fillItem.remove();
|
|
||||||
if (!(group instanceof paper.Layer) && group.children.length === 1) {
|
|
||||||
group.reduce();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearHoveredItem();
|
|
||||||
this.fillItem = null;
|
|
||||||
this.addedFillItem = null;
|
|
||||||
this.fillItemOrigColor = null;
|
|
||||||
this.onUpdateSvg();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_noStroke (item) {
|
|
||||||
return !item.strokeColor ||
|
|
||||||
item.strokeColor.alpha === 0 ||
|
|
||||||
item.strokeWidth === 0;
|
|
||||||
}
|
|
||||||
_setFillItemColor (color) {
|
|
||||||
if (this.addedFillItem) {
|
|
||||||
this.addedFillItem.fillColor = color;
|
|
||||||
} else if (this.fillItem.parent instanceof paper.CompoundPath) {
|
|
||||||
this.fillItem.parent.fillColor = color;
|
|
||||||
} else {
|
|
||||||
this.fillItem.fillColor = color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deactivateTool () {
|
|
||||||
if (this.fillItem) {
|
|
||||||
this._setFillItemColor(this.fillItemOrigColor);
|
|
||||||
this.fillItemOrigColor = null;
|
|
||||||
this.fillItem = null;
|
|
||||||
}
|
|
||||||
this.clearHoveredItem();
|
|
||||||
this.setHoveredItem = null;
|
|
||||||
this.clearHoveredItem = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FillTool;
|
|
|
@ -1,133 +0,0 @@
|
||||||
import paper from '@scratch/paper';
|
|
||||||
import Modes from '../../lib/modes';
|
|
||||||
import {styleShape} from '../style-path';
|
|
||||||
import {clearSelection} from '../selection';
|
|
||||||
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
|
||||||
import NudgeTool from '../selection-tools/nudge-tool';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tool for drawing ovals.
|
|
||||||
*/
|
|
||||||
class OvalTool extends paper.Tool {
|
|
||||||
static get TOLERANCE () {
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {function} setSelectedItems Callback to set the set of selected items in the Redux state
|
|
||||||
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
|
||||||
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
|
||||||
*/
|
|
||||||
constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) {
|
|
||||||
super();
|
|
||||||
this.setSelectedItems = setSelectedItems;
|
|
||||||
this.clearSelectedItems = clearSelectedItems;
|
|
||||||
this.onUpdateSvg = onUpdateSvg;
|
|
||||||
this.boundingBoxTool = new BoundingBoxTool(Modes.OVAL, setSelectedItems, clearSelectedItems, onUpdateSvg);
|
|
||||||
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg);
|
|
||||||
|
|
||||||
// We have to set these functions instead of just declaring them because
|
|
||||||
// paper.js tools hook up the listeners in the setter functions.
|
|
||||||
this.onMouseDown = this.handleMouseDown;
|
|
||||||
this.onMouseDrag = this.handleMouseDrag;
|
|
||||||
this.onMouseUp = this.handleMouseUp;
|
|
||||||
this.onKeyUp = nudgeTool.onKeyUp;
|
|
||||||
this.onKeyDown = nudgeTool.onKeyDown;
|
|
||||||
|
|
||||||
this.oval = null;
|
|
||||||
this.colorState = null;
|
|
||||||
this.isBoundingBoxMode = null;
|
|
||||||
this.active = false;
|
|
||||||
}
|
|
||||||
getHitOptions () {
|
|
||||||
return {
|
|
||||||
segments: true,
|
|
||||||
stroke: true,
|
|
||||||
curves: true,
|
|
||||||
fill: true,
|
|
||||||
guide: false,
|
|
||||||
match: hitResult =>
|
|
||||||
(hitResult.item.data && hitResult.item.data.isHelperItem) ||
|
|
||||||
hitResult.item.selected, // Allow hits on bounding box and selected only
|
|
||||||
tolerance: OvalTool.TOLERANCE / paper.view.zoom
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Should be called if the selection changes to update the bounds of the bounding box.
|
|
||||||
* @param {Array<paper.Item>} selectedItems Array of selected items.
|
|
||||||
*/
|
|
||||||
onSelectionChanged (selectedItems) {
|
|
||||||
this.boundingBoxTool.onSelectionChanged(selectedItems);
|
|
||||||
}
|
|
||||||
setColorState (colorState) {
|
|
||||||
this.colorState = colorState;
|
|
||||||
}
|
|
||||||
handleMouseDown (event) {
|
|
||||||
if (event.event.button > 0) return; // only first mouse button
|
|
||||||
this.active = true;
|
|
||||||
|
|
||||||
if (this.boundingBoxTool.onMouseDown(event, false /* clone */, false /* multiselect */, this.getHitOptions())) {
|
|
||||||
this.isBoundingBoxMode = true;
|
|
||||||
} else {
|
|
||||||
this.isBoundingBoxMode = false;
|
|
||||||
clearSelection(this.clearSelectedItems);
|
|
||||||
this.oval = new paper.Shape.Ellipse({
|
|
||||||
point: event.downPoint,
|
|
||||||
size: 0
|
|
||||||
});
|
|
||||||
styleShape(this.oval, this.colorState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleMouseDrag (event) {
|
|
||||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
|
||||||
|
|
||||||
if (this.isBoundingBoxMode) {
|
|
||||||
this.boundingBoxTool.onMouseDrag(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const downPoint = new paper.Point(event.downPoint.x, event.downPoint.y);
|
|
||||||
const point = new paper.Point(event.point.x, event.point.y);
|
|
||||||
if (event.modifiers.shift) {
|
|
||||||
this.oval.size = new paper.Point(event.downPoint.x - event.point.x, event.downPoint.x - event.point.x);
|
|
||||||
} else {
|
|
||||||
this.oval.size = downPoint.subtract(point);
|
|
||||||
}
|
|
||||||
if (event.modifiers.alt) {
|
|
||||||
this.oval.position = downPoint;
|
|
||||||
} else {
|
|
||||||
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
handleMouseUp (event) {
|
|
||||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
|
||||||
|
|
||||||
if (this.isBoundingBoxMode) {
|
|
||||||
this.boundingBoxTool.onMouseUp(event);
|
|
||||||
this.isBoundingBoxMode = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.oval) {
|
|
||||||
if (Math.abs(this.oval.size.width * this.oval.size.height) < OvalTool.TOLERANCE / paper.view.zoom) {
|
|
||||||
// Tiny oval created unintentionally?
|
|
||||||
this.oval.remove();
|
|
||||||
this.oval = null;
|
|
||||||
} else {
|
|
||||||
const ovalPath = this.oval.toPath(true /* insert */);
|
|
||||||
this.oval.remove();
|
|
||||||
this.oval = null;
|
|
||||||
|
|
||||||
ovalPath.selected = true;
|
|
||||||
this.setSelectedItems();
|
|
||||||
this.onUpdateSvg();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.active = false;
|
|
||||||
}
|
|
||||||
deactivateTool () {
|
|
||||||
this.boundingBoxTool.removeBoundsPath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OvalTool;
|
|
|
@ -1,127 +0,0 @@
|
||||||
import paper from '@scratch/paper';
|
|
||||||
import Modes from '../../lib/modes';
|
|
||||||
import {styleShape} from '../style-path';
|
|
||||||
import {clearSelection} from '../selection';
|
|
||||||
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
|
||||||
import NudgeTool from '../selection-tools/nudge-tool';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tool for drawing rectangles.
|
|
||||||
*/
|
|
||||||
class RectTool extends paper.Tool {
|
|
||||||
static get TOLERANCE () {
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {function} setSelectedItems Callback to set the set of selected items in the Redux state
|
|
||||||
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
|
||||||
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
|
||||||
*/
|
|
||||||
constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) {
|
|
||||||
super();
|
|
||||||
this.setSelectedItems = setSelectedItems;
|
|
||||||
this.clearSelectedItems = clearSelectedItems;
|
|
||||||
this.onUpdateSvg = onUpdateSvg;
|
|
||||||
this.boundingBoxTool = new BoundingBoxTool(Modes.RECT, setSelectedItems, clearSelectedItems, onUpdateSvg);
|
|
||||||
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg);
|
|
||||||
|
|
||||||
// We have to set these functions instead of just declaring them because
|
|
||||||
// paper.js tools hook up the listeners in the setter functions.
|
|
||||||
this.onMouseDown = this.handleMouseDown;
|
|
||||||
this.onMouseDrag = this.handleMouseDrag;
|
|
||||||
this.onMouseUp = this.handleMouseUp;
|
|
||||||
this.onKeyUp = nudgeTool.onKeyUp;
|
|
||||||
this.onKeyDown = nudgeTool.onKeyDown;
|
|
||||||
|
|
||||||
this.rect = null;
|
|
||||||
this.colorState = null;
|
|
||||||
this.isBoundingBoxMode = null;
|
|
||||||
this.active = false;
|
|
||||||
}
|
|
||||||
getHitOptions () {
|
|
||||||
return {
|
|
||||||
segments: true,
|
|
||||||
stroke: true,
|
|
||||||
curves: true,
|
|
||||||
fill: true,
|
|
||||||
guide: false,
|
|
||||||
match: hitResult =>
|
|
||||||
(hitResult.item.data && hitResult.item.data.isHelperItem) ||
|
|
||||||
hitResult.item.selected, // Allow hits on bounding box and selected only
|
|
||||||
tolerance: RectTool.TOLERANCE / paper.view.zoom
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Should be called if the selection changes to update the bounds of the bounding box.
|
|
||||||
* @param {Array<paper.Item>} selectedItems Array of selected items.
|
|
||||||
*/
|
|
||||||
onSelectionChanged (selectedItems) {
|
|
||||||
this.boundingBoxTool.onSelectionChanged(selectedItems);
|
|
||||||
}
|
|
||||||
setColorState (colorState) {
|
|
||||||
this.colorState = colorState;
|
|
||||||
}
|
|
||||||
handleMouseDown (event) {
|
|
||||||
if (event.event.button > 0) return; // only first mouse button
|
|
||||||
this.active = true;
|
|
||||||
|
|
||||||
if (this.boundingBoxTool.onMouseDown(event, false /* clone */, false /* multiselect */, this.getHitOptions())) {
|
|
||||||
this.isBoundingBoxMode = true;
|
|
||||||
} else {
|
|
||||||
this.isBoundingBoxMode = false;
|
|
||||||
clearSelection(this.clearSelectedItems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleMouseDrag (event) {
|
|
||||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
|
||||||
|
|
||||||
if (this.isBoundingBoxMode) {
|
|
||||||
this.boundingBoxTool.onMouseDrag(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.rect) {
|
|
||||||
this.rect.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = new paper.Rectangle(event.downPoint, event.point);
|
|
||||||
if (event.modifiers.shift) {
|
|
||||||
rect.height = rect.width;
|
|
||||||
}
|
|
||||||
this.rect = new paper.Path.Rectangle(rect);
|
|
||||||
|
|
||||||
if (event.modifiers.alt) {
|
|
||||||
this.rect.position = event.downPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
styleShape(this.rect, this.colorState);
|
|
||||||
}
|
|
||||||
handleMouseUp (event) {
|
|
||||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
|
||||||
|
|
||||||
if (this.isBoundingBoxMode) {
|
|
||||||
this.boundingBoxTool.onMouseUp(event);
|
|
||||||
this.isBoundingBoxMode = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.rect) {
|
|
||||||
if (this.rect.area < RectTool.TOLERANCE / paper.view.zoom) {
|
|
||||||
// Tiny rectangle created unintentionally?
|
|
||||||
this.rect.remove();
|
|
||||||
this.rect = null;
|
|
||||||
} else {
|
|
||||||
this.rect.selected = true;
|
|
||||||
this.setSelectedItems();
|
|
||||||
this.onUpdateSvg();
|
|
||||||
this.rect = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.active = false;
|
|
||||||
}
|
|
||||||
deactivateTool () {
|
|
||||||
this.boundingBoxTool.removeBoundsPath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RectTool;
|
|
|
@ -1,326 +0,0 @@
|
||||||
import paper from '@scratch/paper';
|
|
||||||
import Modes from '../../lib/modes';
|
|
||||||
import {clearSelection} from '../selection';
|
|
||||||
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
|
||||||
import NudgeTool from '../selection-tools/nudge-tool';
|
|
||||||
import {hoverBounds} from '../guides';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tool for adding text. Text elements have limited editability; they can't be reshaped,
|
|
||||||
* drawn on or erased. This way they can preserve their ability to have the text edited.
|
|
||||||
*/
|
|
||||||
class TextTool extends paper.Tool {
|
|
||||||
static get TOLERANCE () {
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
static get TEXT_EDIT_MODE () {
|
|
||||||
return 'TEXT_EDIT_MODE';
|
|
||||||
}
|
|
||||||
static get SELECT_MODE () {
|
|
||||||
return 'SELECT_MODE';
|
|
||||||
}
|
|
||||||
/** Clicks registered within this amount of time are registered as double clicks */
|
|
||||||
static get DOUBLE_CLICK_MILLIS () {
|
|
||||||
return 250;
|
|
||||||
}
|
|
||||||
/** Typing with no pauses longer than this amount of type will count as 1 action */
|
|
||||||
static get TYPING_TIMEOUT_MILLIS () {
|
|
||||||
return 1000;
|
|
||||||
}
|
|
||||||
static get TEXT_PADDING () {
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {HTMLTextAreaElement} textAreaElement dom element for the editable text field
|
|
||||||
* @param {function} setSelectedItems Callback to set the set of selected items in the Redux state
|
|
||||||
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
|
||||||
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
|
||||||
* @param {!function} setTextEditTarget Call to set text editing target whenever text editing is active
|
|
||||||
*/
|
|
||||||
constructor (textAreaElement, setSelectedItems, clearSelectedItems, onUpdateSvg, setTextEditTarget) {
|
|
||||||
super();
|
|
||||||
this.element = textAreaElement;
|
|
||||||
this.setSelectedItems = setSelectedItems;
|
|
||||||
this.clearSelectedItems = clearSelectedItems;
|
|
||||||
this.onUpdateSvg = onUpdateSvg;
|
|
||||||
this.setTextEditTarget = setTextEditTarget;
|
|
||||||
this.boundingBoxTool = new BoundingBoxTool(Modes.TEXT, setSelectedItems, clearSelectedItems, onUpdateSvg);
|
|
||||||
this.nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg);
|
|
||||||
this.lastEvent = null;
|
|
||||||
|
|
||||||
// We have to set these functions instead of just declaring them because
|
|
||||||
// paper.js tools hook up the listeners in the setter functions.
|
|
||||||
this.onMouseDown = this.handleMouseDown;
|
|
||||||
this.onMouseDrag = this.handleMouseDrag;
|
|
||||||
this.onMouseUp = this.handleMouseUp;
|
|
||||||
this.onMouseMove = this.handleMouseMove;
|
|
||||||
this.onKeyUp = this.handleKeyUp;
|
|
||||||
this.onKeyDown = this.handleKeyDown;
|
|
||||||
|
|
||||||
this.textBox = null;
|
|
||||||
this.guide = null;
|
|
||||||
this.colorState = null;
|
|
||||||
this.mode = null;
|
|
||||||
this.active = false;
|
|
||||||
this.lastTypeEvent = null;
|
|
||||||
|
|
||||||
// If text selected and then activate this tool, switch to text edit mode for that text
|
|
||||||
// If double click on text while in select mode, does mode change to text mode? Text fully selected by default
|
|
||||||
}
|
|
||||||
getBoundingBoxHitOptions () {
|
|
||||||
return {
|
|
||||||
segments: true,
|
|
||||||
stroke: true,
|
|
||||||
curves: true,
|
|
||||||
fill: true,
|
|
||||||
guide: false,
|
|
||||||
match: hitResult =>
|
|
||||||
(hitResult.item.data && hitResult.item.data.isHelperItem) ||
|
|
||||||
hitResult.item.selected, // Allow hits on bounding box and selected only
|
|
||||||
tolerance: TextTool.TOLERANCE / paper.view.zoom
|
|
||||||
};
|
|
||||||
}
|
|
||||||
getTextEditHitOptions () {
|
|
||||||
return {
|
|
||||||
class: paper.PointText,
|
|
||||||
segments: true,
|
|
||||||
stroke: true,
|
|
||||||
curves: true,
|
|
||||||
fill: true,
|
|
||||||
guide: false,
|
|
||||||
match: hitResult => hitResult.item && !hitResult.item.selected, // Unselected only
|
|
||||||
tolerance: TextTool.TOLERANCE / paper.view.zoom
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Called when the selection changes to update the bounds of the bounding box.
|
|
||||||
* @param {Array<paper.Item>} selectedItems Array of selected items.
|
|
||||||
*/
|
|
||||||
onSelectionChanged (selectedItems) {
|
|
||||||
this.boundingBoxTool.onSelectionChanged(selectedItems);
|
|
||||||
}
|
|
||||||
// Allow other tools to cancel text edit mode
|
|
||||||
onTextEditCancelled () {
|
|
||||||
this.endTextEdit();
|
|
||||||
if (this.textBox) {
|
|
||||||
this.mode = TextTool.SELECT_MODE;
|
|
||||||
this.textBox.selected = true;
|
|
||||||
this.setSelectedItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Called when the view matrix changes
|
|
||||||
* @param {paper.Matrix} viewMtx applied to paper.view
|
|
||||||
*/
|
|
||||||
onViewBoundsChanged (viewMtx) {
|
|
||||||
if (this.mode !== TextTool.TEXT_EDIT_MODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const matrix = this.textBox.matrix;
|
|
||||||
this.element.style.transform =
|
|
||||||
`translate(0px, ${this.textBox.internalBounds.y}px)
|
|
||||||
matrix(${viewMtx.a}, ${viewMtx.b}, ${viewMtx.c}, ${viewMtx.d},
|
|
||||||
${viewMtx.tx}, ${viewMtx.ty})
|
|
||||||
matrix(${matrix.a}, ${matrix.b}, ${matrix.c}, ${matrix.d},
|
|
||||||
${matrix.tx}, ${matrix.ty})`;
|
|
||||||
}
|
|
||||||
setColorState (colorState) {
|
|
||||||
this.colorState = colorState;
|
|
||||||
}
|
|
||||||
handleMouseMove (event) {
|
|
||||||
const hitResults = paper.project.hitTestAll(event.point, this.getTextEditHitOptions());
|
|
||||||
if (hitResults.length) {
|
|
||||||
document.body.style.cursor = 'text';
|
|
||||||
} else {
|
|
||||||
document.body.style.cursor = 'auto';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleMouseDown (event) {
|
|
||||||
if (event.event.button > 0) return; // only first mouse button
|
|
||||||
this.active = true;
|
|
||||||
|
|
||||||
const lastMode = this.mode;
|
|
||||||
|
|
||||||
// Check if double clicked
|
|
||||||
let doubleClicked = false;
|
|
||||||
if (this.lastEvent) {
|
|
||||||
if ((event.event.timeStamp - this.lastEvent.event.timeStamp) < TextTool.DOUBLE_CLICK_MILLIS) {
|
|
||||||
doubleClicked = true;
|
|
||||||
} else {
|
|
||||||
doubleClicked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.lastEvent = event;
|
|
||||||
|
|
||||||
const doubleClickHitTest = paper.project.hitTest(event.point, this.getBoundingBoxHitOptions());
|
|
||||||
if (doubleClicked &&
|
|
||||||
this.mode === TextTool.SELECT_MODE &&
|
|
||||||
doubleClickHitTest) {
|
|
||||||
// Double click in select mode moves you to text edit mode
|
|
||||||
clearSelection(this.clearSelectedItems);
|
|
||||||
this.textBox = doubleClickHitTest.item;
|
|
||||||
this.beginTextEdit(this.textBox.content, this.textBox.matrix);
|
|
||||||
} else if (
|
|
||||||
this.boundingBoxTool.onMouseDown(
|
|
||||||
event, false /* clone */, false /* multiselect */, this.getBoundingBoxHitOptions())) {
|
|
||||||
// In select mode staying in select mode
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We clicked away from the item, so end the current mode
|
|
||||||
if (lastMode === TextTool.SELECT_MODE) {
|
|
||||||
clearSelection(this.clearSelectedItems);
|
|
||||||
this.mode = null;
|
|
||||||
} else if (lastMode === TextTool.TEXT_EDIT_MODE) {
|
|
||||||
this.endTextEdit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const hitResults = paper.project.hitTestAll(event.point, this.getTextEditHitOptions());
|
|
||||||
if (hitResults.length) {
|
|
||||||
// Clicking a different text item to begin text edit mode on that item
|
|
||||||
clearSelection(this.clearSelectedItems);
|
|
||||||
this.textBox = hitResults[0].item;
|
|
||||||
this.beginTextEdit(this.textBox.content, this.textBox.matrix);
|
|
||||||
} else if (lastMode === TextTool.TEXT_EDIT_MODE) {
|
|
||||||
// In text mode clicking away to begin select mode
|
|
||||||
if (this.textBox) {
|
|
||||||
this.mode = TextTool.SELECT_MODE;
|
|
||||||
this.textBox.selected = true;
|
|
||||||
this.setSelectedItems();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// In no mode or select mode clicking away to begin text edit mode
|
|
||||||
this.textBox = new paper.PointText({
|
|
||||||
point: event.point,
|
|
||||||
content: '',
|
|
||||||
font: 'Helvetica',
|
|
||||||
fontSize: 30,
|
|
||||||
fillColor: this.colorState.fillColor,
|
|
||||||
// Default leading for both the HTML text area and paper.PointText
|
|
||||||
// is 120%, but for some reason they are slightly off from each other.
|
|
||||||
// This value was obtained experimentally.
|
|
||||||
// (Don't round to 34.6, the text area will start to scroll.)
|
|
||||||
leading: 34.61
|
|
||||||
});
|
|
||||||
this.beginTextEdit(this.textBox.content, this.textBox.matrix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleMouseDrag (event) {
|
|
||||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
|
||||||
|
|
||||||
if (this.mode === TextTool.SELECT_MODE) {
|
|
||||||
this.boundingBoxTool.onMouseDrag(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleMouseUp (event) {
|
|
||||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
|
||||||
|
|
||||||
if (this.mode === TextTool.SELECT_MODE) {
|
|
||||||
this.boundingBoxTool.onMouseUp(event);
|
|
||||||
this.isBoundingBoxMode = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.active = false;
|
|
||||||
}
|
|
||||||
handleKeyUp (event) {
|
|
||||||
if (this.mode === TextTool.SELECT_MODE) {
|
|
||||||
this.nudgeTool.onKeyUp(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleKeyDown (event) {
|
|
||||||
if (event.event.target instanceof HTMLInputElement) {
|
|
||||||
// Ignore nudge if a text input field is focused
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mode === TextTool.SELECT_MODE) {
|
|
||||||
this.nudgeTool.onKeyUp(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleTextInput (event) {
|
|
||||||
// Save undo state if you paused typing for long enough.
|
|
||||||
if (this.lastTypeEvent && event.timeStamp - this.lastTypeEvent.timeStamp > TextTool.TYPING_TIMEOUT_MILLIS) {
|
|
||||||
this.onUpdateSvg();
|
|
||||||
}
|
|
||||||
this.lastTypeEvent = event;
|
|
||||||
if (this.mode === TextTool.TEXT_EDIT_MODE) {
|
|
||||||
this.textBox.content = this.element.value;
|
|
||||||
}
|
|
||||||
this.resizeGuide();
|
|
||||||
}
|
|
||||||
resizeGuide () {
|
|
||||||
if (this.guide) this.guide.remove();
|
|
||||||
this.guide = hoverBounds(this.textBox, TextTool.TEXT_PADDING);
|
|
||||||
this.guide.dashArray = [4, 4];
|
|
||||||
this.element.style.width = `${this.textBox.internalBounds.width}px`;
|
|
||||||
this.element.style.height = `${this.textBox.internalBounds.height}px`;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {string} initialText Text to initialize the text area with
|
|
||||||
* @param {paper.Matrix} matrix Transform matrix for the element. Defaults
|
|
||||||
* to the identity matrix.
|
|
||||||
*/
|
|
||||||
beginTextEdit (initialText, matrix) {
|
|
||||||
this.mode = TextTool.TEXT_EDIT_MODE;
|
|
||||||
this.setTextEditTarget(this.textBox.id);
|
|
||||||
|
|
||||||
const viewMtx = paper.view.matrix;
|
|
||||||
|
|
||||||
this.element.style.display = 'initial';
|
|
||||||
this.element.value = initialText ? initialText : '';
|
|
||||||
this.element.style.transformOrigin =
|
|
||||||
`${-this.textBox.internalBounds.x}px ${-this.textBox.internalBounds.y}px`;
|
|
||||||
this.element.style.transform =
|
|
||||||
`translate(0px, ${this.textBox.internalBounds.y}px)
|
|
||||||
matrix(${viewMtx.a}, ${viewMtx.b}, ${viewMtx.c}, ${viewMtx.d},
|
|
||||||
${viewMtx.tx}, ${viewMtx.ty})
|
|
||||||
matrix(${matrix.a}, ${matrix.b}, ${matrix.c}, ${matrix.d},
|
|
||||||
${matrix.tx}, ${matrix.ty})`;
|
|
||||||
this.element.focus({preventScroll: true});
|
|
||||||
this.eventListener = this.handleTextInput.bind(this);
|
|
||||||
this.element.addEventListener('input', this.eventListener);
|
|
||||||
this.resizeGuide();
|
|
||||||
}
|
|
||||||
endTextEdit () {
|
|
||||||
if (this.mode !== TextTool.TEXT_EDIT_MODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.mode = null;
|
|
||||||
|
|
||||||
// Remove invisible textboxes
|
|
||||||
if (this.textBox && this.textBox.content.trim() === '') {
|
|
||||||
this.textBox.remove();
|
|
||||||
this.textBox = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove guide
|
|
||||||
if (this.guide) {
|
|
||||||
this.guide.remove();
|
|
||||||
this.guide = null;
|
|
||||||
this.setTextEditTarget();
|
|
||||||
}
|
|
||||||
this.element.style.display = 'none';
|
|
||||||
if (this.eventListener) {
|
|
||||||
this.element.removeEventListener('input', this.eventListener);
|
|
||||||
this.eventListener = null;
|
|
||||||
}
|
|
||||||
this.lastTypeEvent = null;
|
|
||||||
|
|
||||||
// If you finished editing a textbox, save undo state
|
|
||||||
if (this.textBox && this.textBox.content.trim().length) {
|
|
||||||
this.onUpdateSvg();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deactivateTool () {
|
|
||||||
if (this.textBox && this.textBox.content.trim() === '') {
|
|
||||||
this.textBox.remove();
|
|
||||||
this.textBox = null;
|
|
||||||
}
|
|
||||||
this.endTextEdit();
|
|
||||||
this.boundingBoxTool.removeBoundsPath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TextTool;
|
|
Loading…
Reference in a new issue