/*
* Copyright 2020 WICKLETS LLC
*
* This file is part of Wick Engine.
*
* Wick Engine is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Wick Engine is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Wick Engine. If not, see <https://www.gnu.org/licenses/>.
*/
Wick.Tools.Cursor = class extends Wick.Tool {
/**
* Creates a cursor tool.
*/
constructor () {
super();
this.name = 'cursor';
this.SELECTION_TOLERANCE = 3;
this.CURSOR_DEFAULT = 'cursors/default.png';
this.CURSOR_SCALE_TOP_RIGHT_BOTTOM_LEFT = 'cursors/scale-top-right-bottom-left.png';
this.CURSOR_SCALE_TOP_LEFT_BOTTOM_RIGHT = 'cursors/scale-top-left-bottom-right.png';
this.CURSOR_SCALE_VERTICAL = 'cursors/scale-vertical.png';
this.CURSOR_SCALE_HORIZONTAL = 'cursors/scale-horizontal.png';
this.CURSOR_ROTATE_TOP = 'cursors/rotate-top-right.png';
this.CURSOR_ROTATE_RIGHT = 'cursors/rotate-bottom-right.png';
this.CURSOR_ROTATE_BOTTOM = 'cursors/rotate-bottom-left.png';
this.CURSOR_ROTATE_LEFT = 'cursors/rotate-top-left.png';
this.CURSOR_ROTATE_TOP_RIGHT = 'cursors/rotate-top-right.png';
this.CURSOR_ROTATE_TOP_LEFT = 'cursors/rotate-top-left.png';
this.CURSOR_ROTATE_BOTTOM_RIGHT = 'cursors/rotate-bottom-right.png';
this.CURSOR_ROTATE_BOTTOM_LEFT = 'cursors/rotate-bottom-left.png';
this.CURSOR_MOVE = 'cursors/move.png';
this.hitResult = new this.paper.HitResult();
this.selectionBox = new this.paper.SelectionBox(paper);
this.selectedItems = [];
this.currentCursorIcon = '';
}
/**
* Generate the current cursor.
* @type {string}
*/
get cursor () {
return 'url("'+this.currentCursorIcon+'") 32 32, auto';
}
onActivate (e) {
this.selectedItems = [];
}
onDeactivate (e) {
}
onMouseMove (e) {
super.onMouseMove(e);
// Find the thing that is currently under the cursor.
this.hitResult = this._updateHitResult(e);
// Update the image being used for the cursor
this._setCursor(this._getCursor());
}
onMouseDown (e) {
super.onMouseDown(e);
if(!e.modifiers) e.modifiers = {};
this.hitResult = this._updateHitResult(e);
if(this.hitResult.item && this.hitResult.item.data.isSelectionBoxGUI) {
// Clicked the selection box GUI, do nothing
} else if(this.hitResult.item && this._isItemSelected(this.hitResult.item)) {
// We clicked something that was already selected.
// Shift click: Deselect that item
if(e.modifiers.shift) {
this._deselectItem(this.hitResult.item);
this.fireEvent({eventName: 'canvasModified', actionName: 'cursorDeselect'});
}
} else if (this.hitResult.item && this.hitResult.type === 'fill') {
if(!e.modifiers.shift) {
// Shift click? Keep everything else selected.
this._clearSelection();
}
// Clicked an item: select that item
this._selectItem(this.hitResult.item);
this.fireEvent({eventName: 'canvasModified', actionName: 'cursorSelect'});
} else {
// Nothing was clicked, so clear the selection and start a new selection box
// (don't clear the selection if shift is held, though)
if(this._selection.numObjects > 0 && !e.modifiers.shift) {
this._clearSelection();
this.fireEvent({eventName: 'canvasModified', actionName: 'cursorClearSelect'});
}
this.selectionBox.start(e.point);
}
}
onDoubleClick (e) {
var selectedObject = this._selection.getSelectedObject();
if(selectedObject && selectedObject instanceof Wick.Clip) {
// Double clicked a Clip, set the focus to that Clip.
if (this.project.focusTimelineOfSelectedClip() ) {
this.fireEvent({eventName: 'canvasModified', actionName: 'cursorFocusTimelineSelected'});
}
} else if (selectedObject && (selectedObject instanceof Wick.Path) && (selectedObject.view.item instanceof paper.PointText)) {
// Double clicked text, switch to text tool and edit the text item.
// TODO
} else if (!selectedObject) {
// Double clicked the canvas, leave the current focus.
if (this.project.focusTimelineOfParentClip()) {
this.fireEvent({eventName: 'canvasModified', actionName: 'cursorFocusTimelineParent'});
}
}
}
onMouseDrag (e) {
if(!e.modifiers) e.modifiers = {};
this.__isDragging = true;
if(this.hitResult.item && this.hitResult.item.data.isSelectionBoxGUI) {
// Update selection drag
if(!this._widget.currentTransformation) {
this._widget.startTransformation(this.hitResult.item);
}
this._widget.updateTransformation(this.hitResult.item, e);
} else if (this.selectionBox.active) {
// Selection box is being used, update it with a new point
this.selectionBox.drag(e.point);
} else if(this.hitResult.item && this.hitResult.type === 'fill') {
// We're dragging the selection itself, so move the whole item.
if(!this._widget.currentTransformation) {
this._widget.startTransformation(this.hitResult.item);
}
this._widget.updateTransformation(this.hitResult.item, e);
} else {
this.__isDragging = false;
}
}
onMouseUp (e) {
if(!e.modifiers) e.modifiers = {};
if(this.selectionBox.active) {
// Finish selection box and select objects touching box (or inside box, if alt is held)
this.selectionBox.mode = e.modifiers.alt ? 'contains' : 'intersects';
this.selectionBox.end(e.point);
if(!e.modifiers.shift) {
this._selection.clear();
}
let selectables = this.selectionBox.items.filter(item => {
return item.data.wickUUID;
})
this._selectItems(selectables);
// Only modify the canvas if you actually selected something.
if (this.selectionBox.items.length > 0) {
this.fireEvent({eventName: 'canvasModified', actionName: 'cursorSelectMultiple'});
}
} else if (this._selection.numObjects > 0) {
if(this.__isDragging) {
this.__isDragging = false;
this.project.tryToAutoCreateTween();
this._widget.finishTransformation();
this.fireEvent({eventName: 'canvasModified', actionName: 'cursorDrag'});
}
}
}
_updateHitResult (e) {
var newHitResult = this.paper.project.hitTest(e.point, {
fill: true,
stroke: true,
curves: true,
segments: true,
tolerance: this.SELECTION_TOLERANCE,
match: (result => {
return result.item !== this.hoverPreview
&& !result.item.data.isBorder;
}),
});
if(!newHitResult) newHitResult = new this.paper.HitResult();
if(newHitResult.item && !newHitResult.item.data.isSelectionBoxGUI) {
// You can't select children of compound paths, you can only select the whole thing.
if (newHitResult.item.parent.className === 'CompoundPath') {
newHitResult.item = newHitResult.item.parent;
}
// You can't select individual children in a group, you can only select the whole thing.
if (newHitResult.item.parent.parent) {
newHitResult.type = 'fill';
while (newHitResult.item.parent.parent) {
newHitResult.item = newHitResult.item.parent;
}
}
// this.paper.js has two names for strokes+curves, we don't need that extra info
if(newHitResult.type === 'stroke') {
newHitResult.type = 'curve';
}
// Mousing over rasters acts the same as mousing over fills.
if(newHitResult.type === 'pixel') {
newHitResult.type = 'fill';
};
// Disable curve and segment selection. (this was moved to the PathCursor)
if(newHitResult.type === 'segment' || newHitResult.type === 'curve') {
newHitResult.type = 'fill';
};
}
return newHitResult;
}
_getCursor () {
if(!this.hitResult.item) {
return this.CURSOR_DEFAULT;
} else if (this.hitResult.item.data.isSelectionBoxGUI) {
// Don't show any custom cursor if the mouse is over the border, the border does nothing
if(this.hitResult.item.name === 'border') {
return this.CURSOR_DEFAULT;
}
// Calculate the angle in which the scale handle scales the selection.
// Use that angle to determine the cursor graphic to use.
// Here is a handy diagram showing the cursors that correspond to the angles:
// 315 0 45
// o-----o-----o
// | |
// | |
// 270 o o 90
// | |
// | |
// o-----o-----o
// 225 180 135
var baseAngle = {
topCenter: 0,
topRight: 45,
rightCenter: 90,
bottomRight: 135,
bottomCenter: 180,
bottomLeft: 225,
leftCenter: 270,
topLeft: 315,
}[this.hitResult.item.data.handleEdge];
var angle = baseAngle + this._widget.rotation;
// It makes angle math easier if we dont allow angles >360 or <0 degrees:
if(angle < 0) angle += 360;
if(angle > 360) angle -= 360;
// Round the angle to the nearest 45 degree interval.
var angleRoundedToNearest45 = Math.round(angle / 45) * 45;
angleRoundedToNearest45 = Math.round(angleRoundedToNearest45); // just incase of float weirdness
angleRoundedToNearest45 = ''+angleRoundedToNearest45; // convert to string
// Now we know which of eight directions the handle is pointing, so we choose the correct cursor
if (this.hitResult.item.data.handleType === 'scale') {
var cursorGraphicFromAngle = {
'0': this.CURSOR_SCALE_VERTICAL,
'45': this.CURSOR_SCALE_TOP_RIGHT_BOTTOM_LEFT,
'90': this.CURSOR_SCALE_HORIZONTAL,
'135': this.CURSOR_SCALE_TOP_LEFT_BOTTOM_RIGHT,
'180': this.CURSOR_SCALE_VERTICAL,
'225': this.CURSOR_SCALE_TOP_RIGHT_BOTTOM_LEFT,
'270': this.CURSOR_SCALE_HORIZONTAL,
'315': this.CURSOR_SCALE_TOP_LEFT_BOTTOM_RIGHT,
'360': this.CURSOR_SCALE_VERTICAL,
}[angleRoundedToNearest45];
return cursorGraphicFromAngle;
} else if (this.hitResult.item.data.handleType === 'rotation') {
var cursorGraphicFromAngle = {
'0': this.CURSOR_ROTATE_TOP,
'45': this.CURSOR_ROTATE_TOP_RIGHT,
'90': this.CURSOR_ROTATE_RIGHT,
'135': this.CURSOR_ROTATE_BOTTOM_RIGHT,
'180': this.CURSOR_ROTATE_BOTTOM,
'225': this.CURSOR_ROTATE_BOTTOM_LEFT,
'270': this.CURSOR_ROTATE_LEFT,
'315': this.CURSOR_ROTATE_TOP_LEFT,
'360': this.CURSOR_ROTATE_TOP,
}[angleRoundedToNearest45];
return cursorGraphicFromAngle;
}
} else {
if(this.hitResult.type === 'fill') {
return this.CURSOR_MOVE;
}
}
}
_setCursor (cursor) {
this.currentCursorIcon = cursor;
}
get _selection () {
return this.project.selection;
}
get _widget () {
return this._selection.view.widget;
}
_clearSelection () {
this._selection.clear();
}
_selectItem (item) {
var object = this._wickObjectFromPaperItem(item);
this._selection.select(object);
}
/**
* Select multiple items simultaneously.
* @param {object[]} items paper items
*/
_selectItems (items) {
let objects = [];
items.forEach(item => {
objects.push(this._wickObjectFromPaperItem(item));
});
this._selection.selectMultipleObjects(objects);
}
_deselectItem (item) {
var object = this._wickObjectFromPaperItem(item);
this._selection.deselect(object);
}
_isItemSelected (item) {
var object = this._wickObjectFromPaperItem(item);
return object.isSelected;
}
_wickObjectFromPaperItem (item) {
var uuid = item.data.wickUUID;
if(!uuid) {
console.error('WARNING: _wickObjectFromPaperItem: item had no wick UUID. did you try to select something that wasnt created by a wick view? is the view up-to-date?');
console.log(item);
}
return Wick.ObjectCache.getObjectByUUID(uuid);
}
}