es.eucm.ead.editor.view.widgets.groupeditor.Handles.java Source code

Java tutorial

Introduction

Here is the source code for es.eucm.ead.editor.view.widgets.groupeditor.Handles.java

Source

/**
 * eAdventure is a research project of the
 *    e-UCM research group.
 *
 *    Copyright 2005-2014 e-UCM research group.
 *
 *    You can access a list of all the contributors to eAdventure at:
 *          http://e-adventure.e-ucm.es/contributors
 *
 *    e-UCM is a research group of the Department of Software Engineering
 *          and Artificial Intelligence at the Complutense University of Madrid
 *          (School of Computer Science).
 *
 *          CL Profesor Jose Garcia Santesmases 9,
 *          28040 Madrid (Madrid), Spain.
 *
 *          For more info please visit:  <http://e-adventure.e-ucm.es> or
 *          <http://www.e-ucm.es>
 *
 * ****************************************************************************
 *
 *  This file is part of eAdventure
 *
 *      eAdventure is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU Lesser General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 *
 *      eAdventure 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 Lesser General Public License for more details.
 *
 *      You should have received a copy of the GNU Lesser General Public License
 *      along with eAdventure.  If not, see <http://www.gnu.org/licenses/>.
 */
package es.eucm.ead.editor.view.widgets.groupeditor;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.Intersector;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;

/**
 * Contains all the logic to transform actors. It contains 10 handles: 8 handles
 * to resize, 1 handle to change the origin and 1 handle to rotate. Handles are
 * distributed as follows:
 * 
 * <pre>
 *           9
 *           |
 *     6 --- 7 --- 8
 *     |     |     |
 *     3 --- 4 --- 5
 *     |     |     |
 *     0 --- 1 --- 2
 * </pre>
 * 
 * <p>
 * </p>
 * Where 9 is the rotation point and 4 the origin point. Each number represent
 * its index in {@link #handles}</p>
 * <p>
 * All transformations are based on the position of this 10 handles.
 * </p>
 * <p>
 * The method {@link #readActorTransformation()} reads the transformation of an
 * actor ({@link #setInfluencedActor(Actor)}) and redistribute the handles to
 * match its transformation.
 * </p>
 * <p>
 * The method {@link #applyHandleTransformation()} performs the inverse
 * operation: applies the transformation represented by the handles to the given
 * actor.
 * </p>
 */
public class Handles extends Group {

    public final Vector2 tmp1 = new Vector2(), tmp2 = new Vector2(), tmp3 = new Vector2(), tmp4 = new Vector2(),
            tmp5 = new Vector2();

    public static final float ROTATION_STEP = 15.0f;

    public static final int ORIGIN_HANDLE_INDEX = 4;

    public static final int ROTATION_HANDLE_INDEX = 9;

    private int handleSquareSize;

    private int handleCircleSize;

    private int rotationHandleOffset;

    private ShapeRenderer shapeRenderer;

    private Modifier modifier;

    private Handle[] handles;

    private Actor influencedActor;

    private boolean keepAspectRatio = false;

    private boolean alwaysKeepAspectRatio = false;

    private float aspectRatio = 1.0f;

    public Handles(ShapeRenderer shapeRenderer, Modifier modifier, GroupEditorConfiguration config) {

        rotationHandleOffset = config.rotationHandleOffset;
        handleCircleSize = config.handleCircleSize;
        handleSquareSize = config.handleSquareSize;

        this.shapeRenderer = shapeRenderer;
        this.modifier = modifier;
        handles = new Handle[10];
        for (int i = 0; i < 9; i++) {
            if (i == ORIGIN_HANDLE_INDEX) {
                handles[i] = new OriginHandle(shapeRenderer);
            } else {
                handles[i] = new Handle(shapeRenderer);
            }
            handles[i].setVisible(i == 2 || config.drawHandles);
            addActor(handles[i]);
        }

        handles[ROTATION_HANDLE_INDEX] = new RotationHandle(shapeRenderer);
        handles[ROTATION_HANDLE_INDEX].setVisible(config.drawHandles);
        addActor(handles[ROTATION_HANDLE_INDEX]);

        /*
         * Set move constraints between handles. E.g., handle[0] is attached to
         * handle[2] in the x axis and to handle[6] in the y axis.
         * 
         * These constraints are based on that only 3 handles are required to
         * specify the transformation: 0 (origin), 2 (width) and 6 (height).
         * What the rest actually do is move one or more of these 3 handles.
         * 
         * So each point only need to specify its constraints with these 3 main
         * points.
         */
        handles[0].setAlignedX(handles[2]);
        handles[0].setAlignedY(handles[6]);

        handles[2].setAlignedX(handles[0]);

        handles[6].setAlignedY(handles[0]);

        handles[8].setAlignedX(handles[6]);
        handles[8].setAlignedY(handles[2]);

        handles[1].setAlignedX(handles[0], handles[2]);
        handles[3].setAlignedY(handles[0], handles[6]);

        handles[7].setAlignedX(handles[6]);
        handles[5].setAlignedY(handles[2]);
    }

    /**
     * Sets if the transformation must keep the aspect ratio.
     */
    public void setKeepAspectRatio(boolean keep) {
        this.keepAspectRatio = keep;
    }

    /**
     * Sets the actor that the handles will influence. If the actor is a group
     * with more than 1 child, the aspect ratio will be automatically kept, to
     * enforce affine transformations.
     */
    public void setInfluencedActor(Actor influencedActor) {
        this.influencedActor = influencedActor;
        if (influencedActor instanceof Group) {
            Group group = (Group) influencedActor;
            alwaysKeepAspectRatio = group.getChildren().size > 1;
        } else {
            alwaysKeepAspectRatio = false;
        }
        readActorTransformation();
        updateAspectRatio();
    }

    /**
     * Reads the transformation of the current influenced actor and sets it to
     * the handles
     */
    public void readActorTransformation() {
        if (influencedActor != null) {
            for (int i = 0; i <= 2; i++) {
                for (int j = 0; j <= 2; j++) {
                    if (i == 1 && j == 1) {
                        tmp1.set(influencedActor.getOriginX(), influencedActor.getOriginY());
                    } else {
                        float x = influencedActor.getWidth() / 2.0f * j;
                        float y = influencedActor.getHeight() / 2.0f * i;
                        tmp1.set(x, y);
                    }
                    influencedActor.localToParentCoordinates(tmp1);
                    Handle handle = handles[i * 3 + j];
                    handle.setX(tmp1.x);
                    handle.setY(tmp1.y);
                }
            }
            updateHandlesScale();
        }
    }

    /**
     * Updates the aspect ratio, reading the current influenced actor
     */
    public void updateAspectRatio() {
        aspectRatio = tmp1.set(handles[0].getX(), handles[0].getY()).sub(handles[2].getX(), handles[2].getY()).len()
                / tmp1.set(handles[0].getX(), handles[0].getY()).sub(handles[6].getX(), handles[6].getY()).len();
    }

    /**
     * Applies the current transformation represented by the handles to given
     * actor
     */
    public void applyHandleTransformation() {
        Vector2 o = tmp1.set(handles[0].getX(), handles[0].getY());
        Vector2 t = tmp2.set(handles[2].getX(), handles[2].getY());
        Vector2 n = tmp3.set(handles[6].getX(), handles[6].getY());
        modifier.applyTransformation(influencedActor, o, t, n);
        readActorTransformation();
    }

    /**
     * Updates the handles scale to keep them at the same stage-relative size
     */
    public void updateHandlesScale() {
        for (Handle handle : handles) {
            handle.stageToLocalCoordinates(tmp2.set(0, 0));
            if (handle instanceof OriginHandle || handle instanceof RotationHandle) {
                handle.stageToLocalCoordinates(tmp3.set(handleCircleSize, handleCircleSize));
            } else {
                handle.stageToLocalCoordinates(tmp3.set(handleSquareSize, handleSquareSize));
            }
            tmp3.sub(tmp2);
            handle.setRadius(tmp3.len());
        }

        // Set rotation handle
        Vector2 o = localToStageCoordinates(tmp1.set(handles[0].getX(), handles[0].getY()));
        Vector2 n = localToStageCoordinates(tmp2.set(handles[6].getX(), handles[6].getY())).sub(o).nor();

        Vector2 top = localToStageCoordinates(tmp3.set(handles[7].getX(), handles[7].getY()));

        top.add(n.scl(rotationHandleOffset));

        stageToLocalCoordinates(top);
        handles[ROTATION_HANDLE_INDEX].setX(top.x);
        handles[ROTATION_HANDLE_INDEX].setY(top.y);
    }

    @Override
    protected void drawChildren(Batch batch, float parentAlpha) {
        batch.end();
        Gdx.gl.glEnable(GL20.GL_BLEND);
        Gdx.gl.glBlendFunc(GL20.GL_ONE, GL20.GL_DST_COLOR);
        Gdx.gl.glBlendEquation(GL20.GL_FUNC_SUBTRACT);
        shapeRenderer.setProjectionMatrix(batch.getProjectionMatrix());
        shapeRenderer.setTransformMatrix(batch.getTransformMatrix());
        shapeRenderer.begin(ShapeType.Line);
        shapeRenderer.setColor(Color.WHITE);
        shapeRenderer.line(handles[0].getX(), handles[0].getY(), handles[2].getX(), handles[2].getY());
        shapeRenderer.line(handles[0].getX(), handles[0].getY(), handles[6].getX(), handles[6].getY());
        shapeRenderer.line(handles[2].getX(), handles[2].getY(), handles[8].getX(), handles[8].getY());
        shapeRenderer.line(handles[8].getX(), handles[8].getY(), handles[6].getX(), handles[6].getY());
        shapeRenderer.end();
        super.drawChildren(batch, parentAlpha);
        Gdx.gl.glBlendEquation(GL20.GL_FUNC_ADD);
        Gdx.gl.glDisable(GL20.GL_BLEND);
        batch.begin();
    }

    @Override
    public Actor hit(float x, float y, boolean touchable) {
        // Handles gorup can NEVER be hit
        Actor actor = super.hit(x, y, touchable);
        return actor == this ? null : actor;
    }

    public class Handle extends Group {

        private ShapeRenderer shapeRenderer;

        private float radius;

        private Handle[] alignedX;

        private Handle[] alignedY;

        public Handle(ShapeRenderer shapeRenderer) {
            this.shapeRenderer = shapeRenderer;
        }

        /**
         * Radius of the handle (use in drawing)
         */
        public void setRadius(float radius) {
            this.radius = radius;
        }

        /**
         * Sets which handles are aligned to this handle in the x axis. They
         * will be updated when the position of this handle changes to keep it
         * aligned.
         */
        public void setAlignedX(Handle... alignedX) {
            this.alignedX = alignedX;
        }

        /**
         * Sets which handles are aligned to this handle in the y axis. They
         * will be updated when the position of this handle changes to keep it
         * aligned.
         */
        public void setAlignedY(Handle... alignedY) {
            this.alignedY = alignedY;
        }

        /**
         * Updates the position of this handle, automatically updating the
         * handles attached to it
         * 
         * @param x
         *            new x for the handle
         * @param y
         *            new y for the handle
         */
        public void setPosition(float x, float y) {
            super.setPosition(x, y);
            float rotation = influencedActor.getRotation();
            updateAligned(alignedX, rotation, rotation + 90);
            updateAligned(alignedY, rotation + 90, rotation);
            if (keepAspectRatio || alwaysKeepAspectRatio) {
                keepAspectRatio(rotation);
            }
            applyHandleTransformation();
            readActorTransformation();
        }

        private void keepAspectRatio(float rotation) {
            /*
             * FIXME this can be done much better. I just don't know how. Yet.
             */
            Vector2 t = tmp2.set(handles[2].getX(), handles[2].getY()).sub(handles[0].getX(), handles[0].getY());
            Vector2 n = tmp3.set(handles[6].getX(), handles[6].getY()).sub(handles[0].getX(), handles[0].getY());

            if (handles[1] == this || handles[7] == this) {
                Vector2 center = tmp1.set(handles[0].getX(), handles[0].getY()).add(t.scl(0.5f));
                n.scl(aspectRatio).scl(0.5f).rotate90(1);
                handles[0].setX(center.x + n.x);
                handles[0].setY(center.y + n.y);
                handles[2].setX(center.x - n.x);
                handles[2].setY(center.y - n.y);
            } else if (handles[5] == this || handles[3] == this) {
                Vector2 center = tmp1.set(handles[0].getX(), handles[0].getY()).add(n.scl(0.5f));
                t.scl(1.f / aspectRatio).scl(0.5f).rotate90(1);
                handles[6].setX(center.x + t.x);
                handles[6].setY(center.y + t.y);
                handles[0].setX(center.x - t.x);
                handles[0].setY(center.y - t.y);
            } else if (handles[8] == this) {
                if (t.len() * aspectRatio >= n.len()) {
                    n.set(-t.y / aspectRatio, t.x / aspectRatio);
                } else {
                    t.set(n.y * aspectRatio, -n.x * aspectRatio);
                }
                handles[2].setX(handles[0].getX() + t.x);
                handles[2].setY(handles[0].getY() + t.y);
                handles[6].setX(handles[0].getX() + n.x);
                handles[6].setY(handles[0].getY() + n.y);
                handles[8].setX(handles[6].getX() + t.x);
                handles[8].setY(handles[6].getY() + t.y);
            } else if (handles[0] == this) {
                if (n.len() * aspectRatio >= t.len()) {
                    n.rotate90(1);
                    n.scl(aspectRatio);
                    n.add(handles[2].getX(), handles[2].getY());
                    handles[0].setX(n.x);
                    handles[0].setY(n.y);
                } else {
                    t.rotate90(-1);
                    t.scl(1.f / aspectRatio);
                    t.add(handles[6].getX(), handles[6].getY());
                    handles[0].setX(t.x);
                    handles[0].setY(t.y);
                }
            } else if (handles[2] == this) {
                if (n.len() * aspectRatio >= t.len()) {
                    n.rotate90(-1);
                    n.scl(aspectRatio);
                    n.add(handles[0].getX(), handles[0].getY());
                    handles[2].setX(n.x);
                    handles[2].setY(n.y);
                } else {
                    float right = handles[2].getX() + n.x;
                    float top = handles[2].getY() + n.y;
                    t.rotate90(-1);
                    t.scl(1.f / aspectRatio);
                    t.add(right, top);
                    handles[2].setX(t.x);
                    handles[2].setY(t.y);
                }
            } else if (handles[6] == this) {
                if (n.len() * aspectRatio >= t.len()) {
                    float right = handles[6].getX() + t.x;
                    float top = handles[6].getY() + t.y;
                    n.rotate90(1);
                    n.scl(aspectRatio);
                    n.add(right, top);
                    handles[6].setX(n.x);
                    handles[6].setY(n.y);
                } else {
                    t.rotate90(1);
                    t.scl(1.f / aspectRatio);
                    t.add(handles[0].getX(), handles[0].getY());
                    handles[6].setX(t.x);
                    handles[6].setY(t.y);
                }
            }
            updateAligned(alignedX, rotation, rotation + 90);
            updateAligned(alignedY, rotation + 90, rotation);
        }

        @Override
        public Actor hit(float x, float y, boolean touchable) {
            return tmp1.set(x, y).len() < radius ? this : null;
        }

        /**
         * Aligns all the given handles with this handle
         * 
         * @param aligned
         *            the handles to align
         * @param originAngle
         *            the angle of the line that pass for this handle
         * @param targetAngle
         *            the angle of the line that pass for the given handles
         *            (perpendicular to originAngle)
         */
        private void updateAligned(Handle[] aligned, float originAngle, float targetAngle) {
            if (aligned != null) {
                for (Handle handle : aligned) {
                    Vector2 p1 = tmp1.set(getX(), getY());
                    Vector2 p2 = tmp2.set(1, 0).rotate(originAngle).add(p1);

                    Vector2 q1 = tmp3.set(handle.getX(), handle.getY());
                    Vector2 q2 = tmp4.set(1, 0).rotate(targetAngle).add(q1);

                    Intersector.intersectLines(p1, p2, q1, q2, tmp5);
                    handle.setX(tmp5.x);
                    handle.setY(tmp5.y);
                }
            }
        }

        @Override
        protected void drawChildren(Batch batch, float parentAlpha) {
            shapeRenderer.setTransformMatrix(batch.getTransformMatrix());
            drawShape(shapeRenderer, radius);
        }

        protected void drawShape(ShapeRenderer shapeRenderer, float size) {
            shapeRenderer.begin(ShapeType.Filled);
            shapeRenderer.rect(-size / 2.0f, -size / 2.0f, size, size);
            shapeRenderer.end();
        }
    }

    public class OriginHandle extends Handle {

        public OriginHandle(ShapeRenderer shapeRenderer) {
            super(shapeRenderer);
        }

        @Override
        protected void drawShape(ShapeRenderer shapeRenderer, float size) {
            shapeRenderer.begin(ShapeType.Line);
            shapeRenderer.x(0, 0, size);
            shapeRenderer.end();
        }

        /**
         * Updates the origin of the given actor to match x and y
         */
        public void setPosition(float x, float y) {
            float rotation = influencedActor.getRotation();
            Vector2 o = tmp1.set(handles[0].getX(), handles[0].getY());
            Vector2 t = tmp2.set(handles[2].getX(), handles[2].getY());
            Vector2 n = tmp3.set(handles[6].getX(), handles[6].getY());
            Vector2 origin = tmp4.set(x, y).sub(o);

            Vector2 vt = t.sub(o);
            Vector2 vn = n.sub(o);

            vt.rotate(-rotation);
            vn.rotate(-rotation);
            origin.rotate(-rotation);
            influencedActor.setOrigin((origin.x / vt.x) * influencedActor.getWidth(),
                    (origin.y / vn.y) * influencedActor.getHeight());
            super.setPosition(x, y);
        }
    }

    public class RotationHandle extends Handle {

        private float startingAngle;

        private float originalRotation;

        public RotationHandle(ShapeRenderer shapeRenderer) {
            super(shapeRenderer);
        }

        protected void drawShape(ShapeRenderer shapeRenderer, float size) {
            shapeRenderer.begin(ShapeType.Filled);
            shapeRenderer.circle(0, 0, size);
            shapeRenderer.end();
        }

        /**
         * Initialize the rotation
         * 
         */
        public void startRotation() {
            originalRotation = influencedActor.getRotation();
            startingAngle = tmp1.set(getX(), getY())
                    .sub(handles[ORIGIN_HANDLE_INDEX].getX(), handles[ORIGIN_HANDLE_INDEX].getY()).angle();
        }

        /**
         * Sets the intended position for the handle and updates the rotation
         * 
         * @param x
         *            new position for the handle
         * @param y
         *            new position for the handle
         */
        public void setPosition(float x, float y) {
            float rotation = tmp1.set(x, y)
                    .sub(handles[ORIGIN_HANDLE_INDEX].getX(), handles[ORIGIN_HANDLE_INDEX].getY()).angle()
                    + originalRotation - startingAngle;

            float rotationStep = Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) ? ROTATION_STEP : 0.0f;

            influencedActor.setRotation(rotation - (rotationStep > 0.0f ? rotation % rotationStep : 0.0f));
            readActorTransformation();
        }

    }

    public Actor getInfluencedActor() {
        return influencedActor;
    }

}