root/src/apps/icon-o-matic/transformable/PerspectiveBox.cpp
/*
 * Copyright 2006-2009, 2023, Haiku.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Stephan Aßmus <superstippi@gmx.de>
 *              Zardshard
 */

#include "PerspectiveBox.h"

#include <stdio.h>

#include <agg_trans_affine.h>
#include <agg_math.h>

#include <View.h>

#include "CanvasView.h"
#include "StateView.h"
#include "support.h"
#include "PerspectiveBoxStates.h"
#include "PerspectiveCommand.h"
#include "PerspectiveTransformer.h"


#define INSET 8.0


using std::nothrow;
using namespace PerspectiveBoxStates;


PerspectiveBox::PerspectiveBox(CanvasView* view,
                PerspectiveTransformer* parent)
        :
        Manipulator(NULL),

        fLeftTop(parent->LeftTop()),
        fRightTop(parent->RightTop()),
        fLeftBottom(parent->LeftBottom()),
        fRightBottom(parent->RightBottom()),

        fCurrentCommand(NULL),
        fCurrentState(NULL),

        fDragging(false),
        fMousePos(-10000.0, -10000.0),
        fModifiers(0),

        fPreviousBox(LONG_MAX, LONG_MAX, LONG_MIN, LONG_MIN),

        fCanvasView(view),
        fPerspective(parent),

        fDragLTState(new DragCornerState(this, &fLeftTop)),
        fDragRTState(new DragCornerState(this, &fRightTop)),
        fDragLBState(new DragCornerState(this, &fLeftBottom)),
        fDragRBState(new DragCornerState(this, &fRightBottom))
{
}


PerspectiveBox::~PerspectiveBox()
{
        _NotifyDeleted();

        delete fCurrentCommand;
        delete fDragLTState;
        delete fDragRTState;
        delete fDragLBState;
        delete fDragRBState;
}


void
PerspectiveBox::Draw(BView* into, BRect updateRect)
{
        // convert to canvas view coordinates
        BPoint lt = fLeftTop;
        BPoint rt = fRightTop;
        BPoint lb = fLeftBottom;
        BPoint rb = fRightBottom;

        fCanvasView->ConvertFromCanvas(&lt);
        fCanvasView->ConvertFromCanvas(&rt);
        fCanvasView->ConvertFromCanvas(&lb);
        fCanvasView->ConvertFromCanvas(&rb);

        into->SetDrawingMode(B_OP_COPY);
        into->SetHighColor(255, 255, 255, 255);
        into->SetLowColor(0, 0, 0, 255);

        _StrokeBWLine(into, lt, rt);
        _StrokeBWLine(into, rt, rb);
        _StrokeBWLine(into, rb, lb);
        _StrokeBWLine(into, lb, lt);

        _StrokeBWPoint(into, lt, 0.0);
        _StrokeBWPoint(into, rt, 90.0);
        _StrokeBWPoint(into, rb, 180.0);
        _StrokeBWPoint(into, lb, 270.0);
}


// #pragma mark -


bool
PerspectiveBox::MouseDown(BPoint where)
{
        fCanvasView->FilterMouse(&where);
        fCanvasView->ConvertToCanvas(&where);

        fDragging = true;
        if (fCurrentState) {
                fCurrentState->SetOrigin(where);

                delete fCurrentCommand;
                fCurrentCommand = new (nothrow) PerspectiveCommand(this, fPerspective,
                        fPerspective->LeftTop(), fPerspective->RightTop(),
                        fPerspective->LeftBottom(), fPerspective->RightBottom());
        }

        return true;
}


void
PerspectiveBox::MouseMoved(BPoint where)
{
        fCanvasView->FilterMouse(&where);
        fCanvasView->ConvertToCanvas(&where);

        if (fMousePos != where) {
                fMousePos = where;
                if (fCurrentState) {
                        fCurrentState->DragTo(fMousePos, fModifiers);
                        fCurrentState->UpdateViewCursor(fCanvasView, fMousePos);
                }
        }
}


Command*
PerspectiveBox::MouseUp()
{
        fDragging = false;
        return FinishTransaction();
}


bool
PerspectiveBox::MouseOver(BPoint where)
{
        fCanvasView->ConvertToCanvas(&where);

        fMousePos = where;
        fCurrentState = _DragStateFor(where, fCanvasView->ZoomLevel());

        if (fCurrentState) {
                fCurrentState->UpdateViewCursor(fCanvasView, fMousePos);
                return true;
        }

        return false;
}


// #pragma mark -


BRect
PerspectiveBox::Bounds()
{
        // convert from canvas view coordinates
        BPoint lt = fLeftTop;
        BPoint rt = fRightTop;
        BPoint lb = fLeftBottom;
        BPoint rb = fRightBottom;

        fCanvasView->ConvertFromCanvas(&lt);
        fCanvasView->ConvertFromCanvas(&rt);
        fCanvasView->ConvertFromCanvas(&lb);
        fCanvasView->ConvertFromCanvas(&rb);

        BRect bounds;
        bounds.left = min4(lt.x, rt.x, lb.x, rb.x);
        bounds.top = min4(lt.y, rt.y, lb.y, rb.y);
        bounds.right = max4(lt.x, rt.x, lb.x, rb.x);
        bounds.bottom = max4(lt.y, rt.y, lb.y, rb.y);
        return bounds;
}


BRect
PerspectiveBox::TrackingBounds(BView* withinView)
{
        return withinView->Bounds();
}


// #pragma mark -


void
PerspectiveBox::ModifiersChanged(uint32 modifiers)
{
        fModifiers = modifiers;
        if (fDragging && fCurrentState) {
                fCurrentState->DragTo(fMousePos, fModifiers);
        }
}


bool
PerspectiveBox::UpdateCursor()
{
        if (fCurrentState) {
                fCurrentState->UpdateViewCursor(fCanvasView, fMousePos);
                return true;
        }
        return false;
}


// #pragma mark -


void
PerspectiveBox::AttachedToView(BView* view)
{
        view->Invalidate(Bounds().InsetByCopy(-INSET, -INSET));
}


void
PerspectiveBox::DetachedFromView(BView* view)
{
        view->Invalidate(Bounds().InsetByCopy(-INSET, -INSET));
}


// pragma mark -


void
PerspectiveBox::ObjectChanged(const Observable* object)
{
}


// pragma mark -


void
PerspectiveBox::TransformTo(
        BPoint leftTop, BPoint rightTop, BPoint leftBottom, BPoint rightBottom)
{
        if (fLeftTop == leftTop
                && fRightTop == rightTop
                && fLeftBottom == leftBottom
                && fRightBottom == rightBottom)
                return;

        fLeftTop = leftTop;
        fRightTop = rightTop;
        fLeftBottom = leftBottom;
        fRightBottom = rightBottom;

        Update();
}


void
PerspectiveBox::Update(bool deep)
{
        BRect r = Bounds();
        BRect dirty(r | fPreviousBox);
        dirty.InsetBy(-INSET, -INSET);
        fCanvasView->Invalidate(dirty);
        fPreviousBox = r;

        if (deep)
                fPerspective->TransformTo(fLeftTop, fRightTop, fLeftBottom, fRightBottom);
}


Command*
PerspectiveBox::FinishTransaction()
{
        Command* command = fCurrentCommand;
        if (fCurrentCommand) {
                fCurrentCommand->SetNewPerspective(
                        fPerspective->LeftTop(), fPerspective->RightTop(),
                        fPerspective->LeftBottom(), fPerspective->RightBottom());
                fCurrentCommand = NULL;
        }
        return command;
}


// #pragma mark -


bool
PerspectiveBox::AddListener(PerspectiveBoxListener* listener)
{
        if (listener && !fListeners.HasItem((void*)listener))
                return fListeners.AddItem((void*)listener);
        return false;
}


bool
PerspectiveBox::RemoveListener(PerspectiveBoxListener* listener)
{
        return fListeners.RemoveItem((void*)listener);
}


// #pragma mark -


void
PerspectiveBox::_NotifyDeleted() const
{
        BList listeners(fListeners);
        int32 count = listeners.CountItems();
        for (int32 i = 0; i < count; i++) {
                PerspectiveBoxListener* listener
                        = (PerspectiveBoxListener*)listeners.ItemAtFast(i);
                listener->PerspectiveBoxDeleted(this);
        }
}


//! where is expected in canvas view coordinates
DragState*
PerspectiveBox::_DragStateFor(BPoint where, float canvasZoom)
{
        DragState* state = NULL;

        // convert to canvas zoom level
        //
        // the conversion is necessary, because the "hot regions"
        // around a point should be the same size no matter what
        // zoom level the canvas is displayed at
        float inset = INSET / canvasZoom;

        // check if the cursor is over the corners
        float dLT = point_point_distance(fLeftTop, where);
        float dRT = point_point_distance(fRightTop, where);
        float dLB = point_point_distance(fLeftBottom, where);
        float dRB = point_point_distance(fRightBottom, where);
        float d = min4(dLT, dRT, dLB, dRB);
        if (d < inset) {
                if (d == dLT)
                        state = fDragLTState;
                else if (d == dRT)
                        state = fDragRTState;
                else if (d == dLB)
                        state = fDragLBState;
                else if (d == dRB)
                        state = fDragRBState;
        }

        return state;
}


void
PerspectiveBox::_StrokeBWLine(BView* into, BPoint from, BPoint to) const
{
        // find out how to offset the second line optimally
        BPoint offset(0.0, 0.0);
        // first, do we have a more horizontal line or a more vertical line?
        float xDiff = to.x - from.x;
        float yDiff = to.y - from.y;
        if (fabs(xDiff) > fabs(yDiff)) {
                // horizontal
                if (xDiff > 0.0) {
                        offset.y = -1.0;
                } else {
                        offset.y = 1.0;
                }
        } else {
                // vertical
                if (yDiff < 0.0) {
                        offset.x = -1.0;
                } else {
                        offset.x = 1.0;
                }
        }
        // stroke two lines in high and low color of the view
        into->StrokeLine(from, to, B_SOLID_LOW);
        from += offset;
        to += offset;
        into->StrokeLine(from, to, B_SOLID_HIGH);
}


void
PerspectiveBox::_StrokeBWPoint(BView* into, BPoint point, double angle) const
{
        double x = point.x;
        double y = point.y;

        double x1 = x;
        double y1 = y - 5.0;

        double x2 = x - 5.0;
        double y2 = y - 5.0;

        double x3 = x - 5.0;
        double y3 = y;

        agg::trans_affine m;

        double xOffset = -x;
        double yOffset = -y;

        agg::trans_affine_rotation r(angle * M_PI / 180.0);

        r.transform(&xOffset, &yOffset);
        xOffset = x + xOffset;
        yOffset = y + yOffset;

        m.multiply(r);
        m.multiply(agg::trans_affine_translation(xOffset, yOffset));

        m.transform(&x, &y);
        m.transform(&x1, &y1);
        m.transform(&x2, &y2);
        m.transform(&x3, &y3);

        BPoint p[4];
        p[0] = BPoint(x, y);
        p[1] = BPoint(x1, y1);
        p[2] = BPoint(x2, y2);
        p[3] = BPoint(x3, y3);

        into->FillPolygon(p, 4, B_SOLID_HIGH);

        into->StrokeLine(p[0], p[1], B_SOLID_LOW);
        into->StrokeLine(p[1], p[2], B_SOLID_LOW);
        into->StrokeLine(p[2], p[3], B_SOLID_LOW);
        into->StrokeLine(p[3], p[0], B_SOLID_LOW);
}