view/paper-ext/Paper.Selection.js

/*
 * 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/>.
 */

paper.Selection = class {
    /**
     * Create a new paper.js selection.
     * @param {paper.Layer} layer - the layer to add the selection GUI to. Defaults to the active layer.
     * @param {paper.Item[]} items - the items to select.
     * @param {number} x - the amount the selection is translated on the x-axis
     * @param {number} y - the amount the selection is translated on the y-axis
     * @param {number} scaleX - the amount the selection is scaled on the x-axis
     * @param {number} scaleY - the amount the selection is scaled on the y-axis
     * @param {number} rotation - the amount the selection is rotated
     * @param {number} originX - the origin point of all transforms. Defaults to the center of selected items.
     * @param {number} originY - the origin point of all transforms. Defaults to the center of selected items.
     */
    constructor (args) {
        if(!args) args = {};

        this._lockScalingToAspectRatio = false;

        this._layer = args.layer || paper.project.activeLayer;
        this._items = args.items || [];
        this._transformation = {
            x: args.x || 0,
            y: args.y || 0,
            scaleX: args.scaleX || 1.0,
            scaleY: args.scaleY || 1.0,
            rotation: args.rotation || 0,
            originX: 0,
            originY: 0,
        };

        this._untransformedBounds = paper.Selection._getBoundsOfItems(this._items);

        // Origin/pivot point is set to the center of the bounds, unless one is given in args
        if(args.originX !== undefined) {
            this._transformation.originX = args.originX;
        } else {
            this._transformation.originX = this._untransformedBounds.center.x;
        }

        if(args.originY !== undefined) {
            this._transformation.originY = args.originY;
        } else {
            this._transformation.originY = this._untransformedBounds.center.y;
        }

        this._rebuild();
    }

    /**
     * The layer where the selection GUI was created.
     * @type {paper.Layer}
     */
    get layer () {
        return this._layer;
    }

    /**
     * The items in this selection.
     * @type {paper.Item[]}
     */
    get items () {
        return this._items;
    }

    set items (items) {
        this._destroy(false);
        this._items = items;
    }

    /**
     * The transformation applied to the selected items.
     * @type {object}
     */
    get transformation () {
        return JSON.parse(JSON.stringify(this._transformation));
    }

    set transformation (transformation) {
        this._destroy(true);
        this._transformation = transformation;
        this._create();
    }

    /**
     * Update the transformation with new values.
     * @param {object} newTransformation - an object containing new values for the transformation.
     */
    updateTransformation (newTransformation) {
        this.transformation = Object.assign(this.transformation, newTransformation);
    }

    /**
     * Toggles if scaling will preserve aspect ratio.
     * @type {boolean}
     */
    get lockScalingToAspectRatio () {
        return this._lockScalingToAspectRatio;
    }

    set lockScalingToAspectRatio (lockScalingToAspectRatio) {
        this._lockScalingToAspectRatio = lockScalingToAspectRatio;
    }

    /**
     * The absolute position of the top-left handle of the selection.
     * @type {paper.Point}
     */
    get position () {
        return this._untransformedBounds.topLeft.transform(this._matrix);
    }

    set position (position) {
        var d = position.subtract(this.position);
        this.updateTransformation({
            x: this.transformation.x + d.x,
            y: this.transformation.y + d.y,
        });
    }

    /**
     * The absolute position of the origin of the selection.
     * @type {paper.Point}
     */
    get origin () {
        return new paper.Point(
            this._transformation.originX,
            this._transformation.originY
        ).transform(this._matrix);
    }

    set origin (origin) {
        var d = origin.subtract(this.origin);
        this.updateTransformation({
            x: this.transformation.x + d.x,
            y: this.transformation.y + d.y,
        });
    }

    /**
     * The width of the selection.
     * @type {number}
     */
    get width () {
        return this._untransformedBounds.width * this.transformation.scaleX;
    }

    set width (width) {
        var d = this.width / width;
        this.updateTransformation({
            scaleX: this.transformation.scaleX / d,
        });
    }

    /**
     * The height of the selection.
     * @type {number}
     */
    get height () {
        return this._untransformedBounds.height * this.transformation.scaleY;
    }

    set height (height) {
        var d = this.height / height;
        this.updateTransformation({
            scaleY: this.transformation.scaleY / d,
        });
    }

    /**
     * The x-scale of the selection.
     * @type {number}
     */
    get scaleX () {
        return this.transformation.scaleX;
    }

    set scaleX (scaleX) {
        this.updateTransformation({
            scaleX: scaleX,
        });
    }

    /**
     * The y-scale of the selection.
     * @type {number}
     */
    get scaleY () {
        return this.transformation.scaleY;
    }

    set scaleY (scaleY) {
        this.updateTransformation({
            scaleY: scaleY,
        });
    }

    /**
     * The rotation of the selection.
     * @type {number}
     */
    get rotation () {
        return this.transformation.rotation;
    }

    set rotation (rotation) {
        this.updateTransformation({
            rotation: rotation,
        });
    }

    /**
     * Return a paper.js path attribute of the selection.
     * @param {string} attributeName - the name of the attribute
     */
    getPathAttribute (attributeName) {
        if(this.items.length === 0) {
            console.error('getPathAttribute(): Nothing in the selection!');
        }
        return this.items[0][attributeName];
    }

    /**
     * Update a paper.js path attribute of the selection.
     * @param {string} attributeName - the name of the attribute
     * @param {object} attributeValue - the value of the attribute to change
     */
    setPathAttrribute (attributeName, attributeValue) {
        this.items.forEach(item => {
            item[attributeName] = attributeValue;
        });
    }

    /**
     * The fill color of the selection.
     */
    get fillColor () {
        return this.getPathAttribute('fillColor');
    }

    set fillColor (fillColor) {
        this.setPathAttrribute('fillColor', fillColor);
    }

    /**
     * The stroke color of the selection.
     */
    get strokeColor () {
        return this.getPathAttribute('strokeColor');
    }

    set strokeColor (strokeColor) {
        this.setPathAttrribute('strokeColor', strokeColor);
    }

    /**
     * The stroke color of the selection.
     */
    get strokeWidth () {
        return this.getPathAttribute('strokeWidth');
    }

    set strokeWidth (strokeWidth) {
        this.setPathAttrribute('strokeWidth', strokeWidth);
    }

    /**
     * Flip the selection horizontally.
     */
    flipHorizontally () {
        // TODO replace
        /*
        this.updateTransformation({
            scaleX: -this.transformation.scaleX,
        });
        */
    }

    /**
     * Flip the selection vertically.
     */
    flipVertically () {
        // TODO replace
        /*
        this.updateTransformation({
            scaleY: -this.transformation.scaleY,
        });
        */
    }

    /**
     * Moves the selected items forwards.
     */
    moveForwards () {
        paper.Selection._sortItemsByLayer(this._items).forEach(items => {
            paper.Selection._sortItemsByZIndex(items).reverse().forEach(item => {
                if(item.nextSibling && this._items.indexOf(item.nextSibling) === -1) {
                    item.insertAbove(item.nextSibling);
                }
            });
        });
    }

    /**
     * Moves the selected items backwards.
     */
    moveBackwards () {
        paper.Selection._sortItemsByLayer(this._items).forEach(items => {
            paper.Selection._sortItemsByZIndex(items).forEach(item => {
                if(item.previousSibling && this._items.indexOf(item.previousSibling) === -1) {
                    item.insertBelow(item.previousSibling);
                }
            });
        });
    }

    /**
     * Brings the selected objects to the front.
     */
    bringToFront () {
        paper.Selection._sortItemsByLayer(this._items).forEach(items => {
            paper.Selection._sortItemsByZIndex(items).forEach(item => {
                item.bringToFront();
            });
        });
    }

    /**
     * Sends the selected objects to the back.
     */
    sendToBack () {
        paper.Selection._sortItemsByLayer(this._items).forEach(items => {
            paper.Selection._sortItemsByZIndex(items).reverse().forEach(item => {
                item.sendToBack();
            });
        });
    }

    /**
     * Nudge the selection by a specified amount.
     */
    nudge (x, y) {
        // TODO replace
        //this.position = this.position.add(new paper.Point(x, y));
    }

    /**
     * Move a handle and use the new handle position to scale the selection.
     * @param {string} handleName - the name of the handle to move
     * @param {paper.Point} position - the position to move the handle to
     */
    moveHandleAndScale (handleName, position) {
        /*
        var newHandlePosition = position;
        var currentHandlePosition = this._untransformedBounds[handleName];

        currentHandlePosition = currentHandlePosition.add(new paper.Point(this.transformation.x, this.transformation.y));

        newHandlePosition = newHandlePosition.subtract(this.origin);
        currentHandlePosition = currentHandlePosition.subtract(this.origin);

        newHandlePosition = newHandlePosition.rotate(-this.rotation, new paper.Point(0,0));

        var newScale = newHandlePosition.divide(currentHandlePosition);

        var lockYScale = handleName === 'leftCenter'
                      || handleName === 'rightCenter';
        var lockXScale = handleName === 'bottomCenter'
                      || handleName === 'topCenter';

        if(lockXScale) newScale.x = this.transformation.scaleX;
        if(lockYScale) newScale.y = this.transformation.scaleY;

        this.updateTransformation({
            scaleX: newScale.x,
            scaleY: this.lockScalingToAspectRatio ? newScale.x : newScale.y,
        });
        */
    }

    /**
     * Move a handle and use the new position of the handle to rotate the selection.
     * @param {string} handleName - the name of the handle to move
     * @param {paper.Point} position - the position to move the handle to
     */
    moveHandleAndRotate (handleName, position) {
        /*
        var newHandlePosition = position;
        var currentHandlePosition = this._untransformedBounds[handleName];

        currentHandlePosition = currentHandlePosition.transform(this._matrix);

        newHandlePosition = newHandlePosition.subtract(this.origin);
        currentHandlePosition = currentHandlePosition.subtract(this.origin);

        var angleDiff = newHandlePosition.angle - currentHandlePosition.angle;

        this.updateTransformation({
            rotation: this.transformation.rotation + angleDiff,
        });
        */
    }

    _rebuild () {
        if(this._gui) {
            this._gui.destroy();
        }

        this._gui = new paper.SelectionGUI({
            items: this._items,
            transformation: this._transformation,
            bounds: this._untransformedBounds,
        });

        // Don't add GUI to paper if nothing is selected...
        if(this._items.length > 0) {
            this._layer.addChild(this._gui.item);
        }
    }

    _destroy () {
        this._gui.destroy();
    }

    /* helper function: calculate the bounds of the smallest rectangle that contains all given items. */
    static _getBoundsOfItems (items) {
        if(items.length === 0)
            return new paper.Rectangle();

        var bounds = null;
        items.forEach(item => {
            bounds = bounds ? bounds.unite(item.bounds) : item.bounds;
        });

        return bounds;
    }

    /* helper function for ordering */
    static _sortItemsByLayer (items) {
        var layerLists = {};

        items.forEach(item => {
            // Create new list for the item's layer if it doesn't exist
            var layerID = item.layer.id;
            if(!layerLists[layerID]) {
                layerLists[layerID] = [];
            }

            // Add this item to its corresponding layer list
            layerLists[layerID].push(item);
        });

        // Convert id->array object to array of arrays
        var layerItemsArrays = [];
        for (var layerID in layerLists) {
            layerItemsArrays.push(layerLists[layerID])
        }
        return layerItemsArrays;
    }

    /* helper function for ordering */
    static _sortItemsByZIndex (items) {
        return items.sort(function (a,b) {
            return a.index - b.index;
        });
    }
};

paper.PaperScope.inject({
    Selection: paper.Selection,
});